Я покажу вам, как вы можете классифицировать каждого покемона как легендарного или нет, используя набор данных статистики покемонов.

Играли ли вы в Pokemon или нет, вы, вероятно, знаете, кто они такие. Лично я играл в нее большую часть своей жизни (и до сих пор иногда играю).

Если вы хоть немного знакомы с покемонами, то знаете, что ловля легендарных покемонов — это прекрасное чувство. Не только потому, что они обычно сильнее других покемонов, но и потому, что выглядят намного круче.

Однако задумывались ли вы когда-нибудь, что делает покемонов легендарными? Это их внешний вид? Или, может быть, их статистика? В этой статье я покажу, как можно создать прогностическую модель с помощью ИИ (а именно нейронной сети) для классификации покемонов на основе их статистика.

На момент написания этой статьи существует в общей сложности 905 покемонов (считая только уникальные виды), и 63 из них являются легендарными. Но как получить информацию о каждом из этих покемонов? Что ж, к счастью, люди уже объединили всю эту информацию для вас. Вы можете найти набор данных, который я использую здесь. Этот набор данных содержит много информации о каждом покемоне, но мы вернемся к этому позже. Это было взято с веб-сайта Kaggle, и, если вы не знакомы с ним, это очень хорошее место для поиска наборов данных для многих вещей.

Все файлы, использованные в этой статье, доступны здесь.

Шаг 1: Импорт и обработка набора данных

Итак, давайте начнем кодировать. Первое, что мы хотим сделать, это импортировать наши данные в Python, чтобы увидеть, с чем нам придется работать.

import pandas as pd

data = pd.read_csv('data.csv')
for col in data.columns:
    print(col)

Это должно напечатать столбцы (характеристики каждого покемона) нашего набора данных. Мы видим 51разные столбцы для каждого покемона, что содержит много информации, но мы не будем использовать большинство из них. Вы также можете видеть, что у нас есть в общей сложности 1045 «покемонов», но ранее я сказал, что их было только 905. Причина этого в том, что этот набор данных содержит покемонов, которые технически одинаковы, но имеют разные формы. или мега-эволюции (такие как Mewtwo и Mega Mewtwo). Я решил оставить их, потому что 905 записей не очень много для модели, поэтому чем больше, тем лучше.

(Далее мы проведем некоторую предварительную обработку, но если вы хотите пропустить это, вы можете скачать окончательный набор данных здесь)

Если вы знакомы с нейронными сетями, вы, вероятно, знаете, что они работают только с числовыми значениями, вы не можете использовать строку в качестве входных данных (по крайней мере, без какой-либо предварительной обработки) и есть много столбцов, которые содержит строки, например столбец species. Поэтому, чтобы упростить задачу, мы будем использовать только некоторые столбцы и удалим большинство из них. Столбцы, которые мы будем использовать:

  • pokedex_number
  • имя
  • положение дел
  • высота_м
  • Вес (кг
  • hp
  • атака
  • защита
  • sp_attack
  • sp_defense
  • скорость

Остальные столбцы мы удалим. Для этого просто скопируйте и вставьте эту команду

data = data[['pokedex_number','name', 'status', 'height_m',  'weight_kg', 'total_points', 'hp', 'attack', 'defense',
'sp_attack', 'sp_defense', 'speed']]

Нам также нужно проверить, нет ли покемона со значением NaN, который в нашем случае только один. Так как это только один, давайте просто удалим его, набрав

data.dropna(inplace=True)
data.reset_index(drop=True, inplace=True)

Следующее, что нам нужно сделать, это отделить данные X от данных Y. Наши данные X будут состоять из функций, которые мы хотим придать нашей модели, в данном случае это будут все, начиная со столбца heigh_m. Наши данные Y будут классификацией покемонов (столбец status). Мы также пока сохраним столбец name, потому что он будет полезен.

X = data[['name','height_m', 'weight_kg', 'total_points', 'hp', 'attack', 'defense','sp_attack', 'sp_defense', 'speed']]
Y = data[['name','status']]

Давайте сделаем некоторую визуализацию данных, чтобы увидеть, какой тип классификации может получить покемон, используя matplotlib. Давайте сначала создадим словарь, содержащий количество для каждого класса, а затем нанесем его на график.

count = {}
for index, row in Y.iterrows():
    if row['status'] in count:
        count[row['status']] += 1
    else:
        count[row['status']] = 1

plt.pie(count.values(), labels=count.keys(), autopct='%1.1f%%', shadow=True)
plt.show()

Это должно дать нам что-то вроде этого

Мы видим, что большинство покемонов имеют статус Нормальный. Мы также видим, что в этом наборе данных проводится различие между легендарными и суб легендарнымипокемонами, но я считаю, что мы можем сгруппировать их в один класс, набрав Y = Y.replace('Sub Legendary', 'Legendary') . Кроме того, поскольку нас интересует только классификация покемонов как легендарных или нет, мы также можем преобразовать класс Мифический в Обычный.

Теперь у нас должен быть набор данных о покемонах, классифицированных между Легендарными (9,2%) и Обычными (90,8%).

Обычно с моделями машинного обучения вы делите данные на обучающие и тестовые данные, выбирая случайные выборки. Однако из-за небольшого количества легенд (всего 9,2%) выбор случайных выборок для обучающих данных может привести к тому, что обучающие данные будут без каких-либо легенд. Это означало бы, что модель не будет учиться должным образом. Чтобы обойти это, мы создадим набор данных для каждого класса.

X_normal = []
X_legendary = []

Y_normal = []
Y_legendary = []
for index, row in Y.iteritems():
    if row['status'] == 'Legendary':
        X_legendary.append(X.iloc[index])
        Y_legendary.append(Y.iloc[index])
    else:
        X_normal.append(X.iloc[index])
        Y_normal.append(Y.iloc[index])

На следующем шаге набор данных разделяется на наборы обучающий и тестовый. Данные обучения — это то, что будет использоваться для обучения модели, а тестовые данные будут использоваться для последующей оценки обученной модели. У вас также может быть набор validation, который используется для предотвращения переобучения, но сначала мы будем обучать модель без него.

В библиотеке sklearn есть очень простая функция, которая делает это автоматически. Однако мы не можем использовать, так как имеем дело с двумя разными наборами как для X, так и для Y. Поэтому нам нужно разделить данные на тест и обучить вручную. Мы сделаем это, создав список случайно выбранных покемонов из набора X_normal и X_legendary. Мы будем идентифицировать каждого покемона по имени, чтобы было проще.

После выбора имен мы просто перебираем наш набор X_normal и проверяем, присутствует ли текущий покемон (мы просто проверяем имена) в списке тестовых покемонов. Если это так, мы добавляем его в набор X_test, иначе — в набор X_train. Затем мы повторяем процесс, но на этот раз перебираем набор X_legendary. Затем повторите то же самое для Y_normal и Y_legendary.

Поскольку мы создаем окончательные наборы данных, теперь мы можем избавиться от тега имени, и нам также нужно преобразовать классы Обычный и Легендарный в числовые значения (0 и 1 соответственно).

Код этого процесса приведен ниже.

# Divide into train and test data

test_size = 0.33  # percentage of the data that is going to be used for testing
nr_legendary_test_samples = int(len(X_legendary) * test_size)
nr_normal_test_samples = int(len(X_normal) * test_size)

random_normal_samples = random.sample(X_normal, nr_normal_test_samples)
random_legendary_samples = random.sample(X_legendary, nr_legendary_test_samples)

normal_test_names = []
legendary_test_names = []
for sample in random_normal_samples:
    normal_test_names.append(sample['name'])

for sample in random_legendary_samples:
    legendary_test_names.append(sample['name'])

X_train = []
Y_train = []
X_test = []
Y_test = []

# append the normal pokemons
for pokemon in X_normal:
    values = np.delete(pokemon.values, 0)  # remove the name since it won't be used for training
    if pokemon.values[0] in normal_test_names:
        X_test.append(values)
    else:
        X_train.append(values)

# append the legendary pokemons
for pokemon in X_legendary:
    values = np.delete(pokemon.values, 0)  # remove the name since it won't be used for training
    if pokemon.values[0] in legendary_test_names:
        X_test.append(values)
    else:
        X_train.append(values)

# repeat the process for the Y set

# append the normal pokemons
for pokemon in Y_normal:
    values = np.delete(pokemon.values, 0)  # remove the name since it won't be used for training
    if pokemon.values[0] in normal_test_names:
        Y_test.append(0)
    else:
        Y_train.append(0)

# append the legendary pokemons
for pokemon in Y_legendary:
    values = np.delete(pokemon.values, 0)  # remove the name since it won't be used for training
    if pokemon.values[0] in legendary_test_names:
        Y_test.append(1)
    else:
        Y_train.append(1)

Это может показаться слишком сложным, но таким образом мы можем гарантировать, что и тренировочный набор, и тестовый набор имеют заданный процент легендарных предметов.

Последнее, что нам нужно сделать (обещаю, что это последнее), — перетасовать массивы. То, как мы строим оба набора данных, заставляет всех легендарных покемонов находиться в нижней части набора данных, поэтому это означает, что модель увидит только те примеры, которые находятся в конце набора данных. Это не идеально, они должны быть разбросаны по набору данных. К счастью, уже есть функция, которая делает это за нас.

from sklearn.utils import shuffle
X_train, Y_train = shuffle(X_train, Y_train)

При этом мы можем гарантировать, что оба массива перетасовываются и, что более важно, перетасовываются одинаково.

Опять же, если у вас возникли проблемы с этой частью, вы можете просто загрузить уже предварительно обработанные данные здесь.

Шаг 2: Настройка нейронной сети

Теперь мы можем приступить к созданию нашего классификатора. Для начала давайте создадим простую нейронную сеть с входным слоем, двумя скрытыми слоями и выходным слоем. Мы будем использовать TensorFlow и Keras для создания, обучения и тестирования нашей модели.

Создание модели

Входной слой будет иметь нейрон для каждого отдельного показателя в нашем X_train, всего 8 (вы можете подтвердить это, набрав print(len(X_train[0])) ).

Чтобы добавить слой, мы должны сначала инициализировать модель, которая будет иметь тип Sequential.

import tensorflow as tf
model = tf.keras.Sequential([
    tf.keras.layers.InputLayer(input_shape=(9,))
])

Итак, теперь у нас есть первый слой, давайте добавим два скрытых слоя и выходной слой.

model = tf.keras.Sequential([
    tf.keras.layers.InputLayer(input_shape=(9,)),
    tf.keras.layers.Dense(20, activation='relu'),
    tf.keras.layers.Dense(6, activation='relu'),
    tf.keras.layers.Dense(1, activation='sigmoid')
])

Количество слоев и единиц на слой являются несколько «случайными» числами, при выборе этих значений нет эмпирического правила, но мы можем попытаться оптимизировать их позже. Важным аспектом является то, что выходной слой имеет только 1 единицу с сигмоидой в качестве функции активации. Это работает, потому что мы имеем дело с проблемой двоичной классификации. Поскольку сигмоидограничит вывод от 0 до 1, мы можем предположить, что значения ближе к 0 классифицируются как 0 (Нормальный), а значения ближе к 1 классифицируются как 1. (Легендарный).

Обучите модель

Затем все, что нам нужно сделать, это скомпилировать модель и обучить.

model.compile(optimizer='adam', loss=tf.keras.losses.BinaryCrossentropy(), metrics=['accuracy'])
model.fit(X_train, Y_train, epochs=100)

Так просто! С этим кодом модель начнет обучаться, и вы должны увидеть улучшение точности и уменьшение потерь с течением времени.

Протестируйте и оцените модель

Наиболее распространенной метрикой, когда речь идет о проблемах классификации, является точность. Сначала нам нужно попросить модель сделать прогнозы на основе наших тестовых данных, а затем мы можем вычислить точность.

from sklearn.metrics import accuracy_score
predictions = model.predict(X_test)
accuracy = accuracy_score(Y_test, predictions.round())

Вы можете получить от меня другие результаты, но я получаю от 90% до 91%. Без всякого контекста это очень хороший результат, однако он вводит в заблуждение. Причина, по которой мы не должны сначала доверять этому результату, заключается в том, что показатель точности очень зависит от распределения классов.

Допустим, у нас есть тренировочный набор, в котором 99% точек данных относятся к одному классу, а остальные — к другому классу. Если мы обучим эту модель, она, вероятно, научится выводить только первый класс независимо от ввода, что не очень полезно. Но эта модель по-прежнему будет иметь точность 99%, потому что 99% образцов относятся к одному классу.

Ну, это именно то, что здесь происходит. Наш класс Legendary составляет всего около 9% от общего объема данных. Таким образом, первый шаг, чтобы проверить, научилась ли наша модель, — сравнить точность с базовым уровнем. Базовый уровень — это метрика, которая сообщает нам, какой была бы точность, если бы модель научилась предсказывать только класс с наивысшими точками данных (в нашем случае класс 0). Мы можем вычислить это, набрав

baseline = (len(Y_test) - sum(Y_test)) / len(Y_test)

С помощью этого кода мы подсчитываем, сколько у нас точек тестовых данных класса 0, и делим это число на общее количество точек. Это должно всегда обеспечивать одно и то же значение 90,96%. Если мы затем сравним нашу точность с базовым уровнем, мы увидим, что улучшения практически нет, что означает, что наша модель на самом деле не научилась.

Мы также можем подтвердить это, проанализировав еще две метрики: оценку f1 и матрицу путаницы.

from sklearn.metrics import f1_score, confusion_matrix
f1 = f1_score(Y_test, predictions.round())
conf_matrix = confusion_matrix(Y_test, predictions.round())
# print(f1) -> 0.0
# print(conf_matrix) -> [[310   2]
                         [ 31   0]]

На данный момент f1_score равен 0, что очень плохо, и мы можем видеть по матрице путаницы, что модель вообще не предсказывает класс Legendary.

Есть пара вещей, которые мы можем сделать, чтобы исправить это. Первый — добавить вес каждому классу. Это заставит модель относиться к каждому классу с разной «важностью», а не относиться к ним одинаково.

class_weights = {
    0: 1,
    1: 10
}
model.compile(optimizer='adam', loss=tf.keras.losses.BinaryCrossentropy(), metrics=['accuracy'])
model.fit(X_train, Y_train, epochs=200, class_weight=class_weights)

С помощью этого кода я говорю модели обрабатывать класс 1 (легендарный) с важностью в 10 раз выше, чем у класса 0. Вы можете думать об этом так, как если бы 1 вход класса 1 вносил такой же вклад, как 10 входов класса 0. класс 0. Я также увеличил количество эпох до 200.

Если мы снова запустим нашу модель, мы должны получить что-то вроде этого

baseline: 0.9096209912536443
accuracy: 0.717201166180758
f1 score: 0.348993288590604
Confusion matrix: [[220  92]
                  [  5  26]]

Мы видим, что наша точность теперь намного ниже, но наша оценка f1 и матрица путаницы значительно улучшились. Однако теперь мы видим, что модель классифицирует множество обычных покемонов как легендарных, что является прямым следствием введения весов классов. Попробуем придать легендарному классу важность 2 вместо 10

accuracy: 0.8046647230320699
f1 score: 0.38532110091743116
Confusion matrix: [[255  57]
                  [ 10  21]]

Мы видим, что точность и оценка f1 улучшились, но классификация легендарных покемонов была затронута, и теперь она не так эффективна.

Следующее, что мы можем сделать, это попытаться автоматически найти лучшую архитектуру и параметры для нашей нейронной сети. Мы можем варьировать несколько вещей: вес класса 1 и фактическую архитектуру нейронной сети (количество слоев и единиц на слой). Мы можем добиться этого, обучая модель каждой конфигурации и отслеживая лучшую из них, оценивая оценку f1 для каждой из них.

possible_weights = np.linspace(1, 4, 15)
possible_hidden_layers = [[5], [10], [20], [5, 4], [10, 5], [10, 10], [20, 6], [20, 4], [20, 10, 5]]
best_f1_score = -1.0

for weight in possible_weights:
    for config in possible_hidden_layers:
        model = tf.keras.Sequential()
        model.add(tf.keras.layers.InputLayer(input_shape=(len(X_train[0]),)))
        for nr_layer in range(len(config)):
            model.add(tf.keras.layers.Dense(config[nr_layer], activation='relu'))

        model.add(tf.keras.layers.Dense(1, activation='sigmoid'))

        class_weights = {
            0: 1,
            1: weight
        }
        model.compile(optimizer='adam', loss=tf.keras.losses.BinaryCrossentropy(), metrics=['accuracy'])
        model.fit(X_train, Y_train, epochs=200, class_weight=class_weights, verbose=0)

        predictions = model.predict(X_test)
        f1 = f1_score(Y_test, predictions.round())
        if f1 > best_f1_score:
            best_f1_score = f1
            best_weight = weight
            best_config = config
            model.save('best_model')
print('Best f1 score:', best_f1_score)
print('Best weight:', best_weight)
print("Best config:", best_config)

Имейте в виду, что это занимает некоторое время. Если вы не хотите запускать это самостоятельно, я уже нашел лучшее решение (по крайней мере, для этих значений), которое

Best f1 score: 0.625
Best weight: 3.357142857142857
Best config: [10, 5]
accuracy: 0.9125364431486881
f1 score: 0.625
Confusion matrix: [[288  24]
                   [  6  25]]

Вероятно, мы можем сделать еще несколько оптимизаций, но я оставлю это в качестве домашнего задания :).

Меня беспокоит одна вещь: мы не можем видеть, какие покемоны были классифицированы неправильно. Вы можете попытаться создать набор данных, который также включает идентификатор или имя покемона, но сейчас я хочу оценить классификацию данного покемона. Для этого мы создадим функцию с именем classify.

def classify(name):
    stats = list(np.squeeze(X.loc[X['name'] == name].drop(columns=['name']).to_numpy()))
    _prediction = best_model.predict([stats]).round()
    return 'Legendary' if _prediction == 1 else 'Normal'


print('Classification of Rayquaza: {0}\nTrue classification: {1}'.format(classify('Rayquaza'), Y.loc[Y['name'] == 'Rayquaza']['status'].values[0]))

>>>Classification of Rayquaza: Legendary
   True classification: Legendary

Мы видим, что теперь мы можем использовать имя покемона, чтобы получить его предсказанную и реальную классификацию.

Я попробовал этот код с некоторыми легендарными покемонами, которые использовались для тестирования, и, например, модель не удалась.

Classification of Latias: Normal
True classification: Legendary

Теперь вы можете использовать эту функцию для тестирования модели с любым заданным именем покемона!

И это почти все. Весь используемый здесь код доступен здесь. Дайте знать, если у вас появятся вопросы!