Introduction

I am most interested in the work of the connectionists and neural networks. In this report, I will attempt to answer the following question:

Given an image of a Pokemon, can a convolutional neural network model be used to classify the Pokemon by type?

I had this inspiration from a dataset I recently found on Kaggle, shown here:

https://www.kaggle.com/vishalsubbiah/pokemon-images-and-types

Background

I refer the reader to the following Wikipedia article for more information on Pokemon:

https://en.wikipedia.org/wiki/Pok%C3%A9mon_(video_game_series)

For the purposes of context for this report, Pokemon is shorthand for Pocket Monsters, and it is a Japanese video game series developed for Nintendo gaming systems. In it, the player goes on an adventure where they assemble a team of 6 creatures, train them up to become strong, and compete for the recognition of becoming the most powerful trainer in the game world. Players in Pokemon compete by battling them against each other. As of this writing, there are 890 unique Pokemon. The Kaggle dataset mentioned above contains only 809 Pokemon, and was not updated for the additional 81 Pokemon introduced in Pokemon Sword and Shield in November 2019.

Each Pokemon has a primary and possibly a secondary type. Not all Pokemon have a seconday type. For the purposes of simplicity, we will only use the primary type of a Pokemon as a class label. There are 18 unique types of Pokemon, which each type having its own strengths and weaknesses in battle with respect to other types. Examples of types include Fire, Water, Grass, Ground, or Electric. We will investigate in this report the ability of a CNN to distinguish Pokemon by type based on their appearance in images.

Experiments

First we load in the data:

In [1]:
import numpy as np
import cv2
import pandas as pd
import matplotlib.pyplot as plt
import tensorflow as tf
import scipy
import os
from sklearn.preprocessing import LabelEncoder
from sklearn.preprocessing import OneHotEncoder
from sklearn.model_selection import train_test_split
In [2]:
os.chdir('C:/Users/mkell/Dropbox/Spring 2020/Artificial Intelligence/pokemon-type-classifier/pokemon-classifier')
In [3]:
pokemon=pd.read_csv('pokemon.csv')
print(np.unique(pokemon['Type1'], return_counts=True))
pokemon=pokemon.sort_values('Name')
pokemon=pokemon.reset_index(drop=True)
pokemon
(array(['Bug', 'Dark', 'Dragon', 'Electric', 'Fairy', 'Fighting', 'Fire',
       'Flying', 'Ghost', 'Grass', 'Ground', 'Ice', 'Normal', 'Poison',
       'Psychic', 'Rock', 'Steel', 'Water'], dtype=object), array([ 72,  29,  27,  40,  18,  29,  53,   3,  27,  78,  32,  23, 105,
        34,  53,  46,  26, 114], dtype=int64))
Out[3]:
Name Type1 Type2
0 abomasnow Grass Ice
1 abra Psychic NaN
2 absol Dark NaN
3 accelgor Bug NaN
4 aegislash-blade Steel Ghost
... ... ... ...
804 zoroark Dark NaN
805 zorua Dark NaN
806 zubat Poison Flying
807 zweilous Dark Dragon
808 zygarde-50 Dragon Ground

809 rows × 3 columns

This is the original dataset of 809 Pokemon. We will use the column 'Type1' for labeling. Next we load in the images:

In [4]:
images=np.empty((len(os.listdir('images/original_images')), 120, 120, 3))
count=0

for root, dirs, files in os.walk('images/original_images'):
    for i, file in enumerate(files):        
        path = os.path.join(root, file) 
        img=cv2.imread(path)        
        images[count] = img        
        count=count+1        
        #print("Loaded file "+str(count)+ " of "+str(len(os.listdir('images/images')))+ " ")              
In [5]:
images.shape
Out[5]:
(809, 120, 120, 3)

Some Pokemon types are intuitive and some are not. For example, the first and third Pokemon in the original dataset are Ice-type Pokemon, which is supported by their white, snowy appearance. However, the second Pokemon is Psychic-type, which is not immediately evident from its appearance. The difficulty of this task for humans is present because of this fact. Thus, we hope to see how difficult this task is for a neural network.

Note that the original dataset contains only 809 images in 18 classes. This is hardly enough data with which to train a model. Nevertheless, we will try and train a model on this small dataset to see what happens.

Next we preprocess the data:

In [6]:
#create labels
image_labels=np.array(pokemon['Type1'])
In [7]:
#normalize data
images/=255
images=images.astype('float32')
In [8]:
# integer encode
label_encoder = LabelEncoder()
image_labels = label_encoder.fit_transform(image_labels)
# one hot encode
onehot_encoder = OneHotEncoder(sparse=False)
image_labels = image_labels.reshape(len(image_labels), 1)
image_labels = onehot_encoder.fit_transform(image_labels)
image_labels = np.asarray(image_labels)
In [9]:
#split data into train/test sets
train_data, test_data, train_labels, test_labels=train_test_split(images, image_labels, test_size=0.3, shuffle=True)
train_data, val_data, train_labels, val_labels=train_test_split(train_data, train_labels, test_size=0.1, shuffle=True)

Next we define the model. Our input images are of size (120, 120, 3), as they are 120x120 RGB images. We use a Conv-Pool-Conv-Pool format for the network, doubling the number of filters in each convolutional layer. Once we have reduced the output to 512 1x1 images, we flatten the convolutional output, we use 3 Dense layers at the end of the network with 256, 128, and 64 nodes before pssing the output to our final softmax layer of 18 classes. All layers of the neural network except the final output layer have a Rectified Linear Unit, or ReLU activation function.

These choices for model architecture were based on past convolutional neural network designs in the field. For an optimizer, we use Adam, or adaptive gradient descent with momentum. This is the most widely accepted optimizer for convolutional neural networks in the literature. We use a learning rate of 0.0001 for the network. This was determined through trial and error of training the network. We train the network with 63% of the 809 Pokemon, validate it on 7% of the 809 Pokemon, and test it on 30% of the 809 Pokemon.

In [10]:
model=tf.keras.models.Sequential()
model.add(tf.keras.layers.Conv2D(filters=32, kernel_size=(5, 5), activation='relu', input_shape=(120, 120, 3)))
model.add(tf.keras.layers.MaxPooling2D(pool_size=(2, 2), strides=2))
model.add(tf.keras.layers.Conv2D(filters=64, kernel_size=(3, 3), activation='relu'))
model.add(tf.keras.layers.MaxPooling2D(pool_size=(2, 2), strides=2))
model.add(tf.keras.layers.Conv2D(filters=128, kernel_size=(5, 5), activation='relu'))
model.add(tf.keras.layers.MaxPooling2D(pool_size=(2, 2), strides=2))
model.add(tf.keras.layers.Conv2D(filters=256, kernel_size=(3, 3), activation='relu'))
model.add(tf.keras.layers.MaxPooling2D(pool_size=(2, 2), strides=2))
model.add(tf.keras.layers.Conv2D(filters=512, kernel_size=(3, 3), activation='relu'))
model.add(tf.keras.layers.MaxPooling2D(pool_size=(2, 2), strides=2))
model.add(tf.keras.layers.Flatten())
model.add(tf.keras.layers.Dense(256, activation='relu'))
model.add(tf.keras.layers.Dense(128, activation='relu'))
model.add(tf.keras.layers.Dense(64, activation='relu'))
model.add(tf.keras.layers.Dense(18, activation='softmax'))

adam=tf.keras.optimizers.Adam(lr=10**-4)

model.compile(optimizer=adam, loss='categorical_crossentropy', metrics=['accuracy'])
model.summary()
Model: "sequential"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
conv2d (Conv2D)              (None, 116, 116, 32)      2432      
_________________________________________________________________
max_pooling2d (MaxPooling2D) (None, 58, 58, 32)        0         
_________________________________________________________________
conv2d_1 (Conv2D)            (None, 56, 56, 64)        18496     
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 28, 28, 64)        0         
_________________________________________________________________
conv2d_2 (Conv2D)            (None, 24, 24, 128)       204928    
_________________________________________________________________
max_pooling2d_2 (MaxPooling2 (None, 12, 12, 128)       0         
_________________________________________________________________
conv2d_3 (Conv2D)            (None, 10, 10, 256)       295168    
_________________________________________________________________
max_pooling2d_3 (MaxPooling2 (None, 5, 5, 256)         0         
_________________________________________________________________
conv2d_4 (Conv2D)            (None, 3, 3, 512)         1180160   
_________________________________________________________________
max_pooling2d_4 (MaxPooling2 (None, 1, 1, 512)         0         
_________________________________________________________________
flatten (Flatten)            (None, 512)               0         
_________________________________________________________________
dense (Dense)                (None, 256)               131328    
_________________________________________________________________
dense_1 (Dense)              (None, 128)               32896     
_________________________________________________________________
dense_2 (Dense)              (None, 64)                8256      
_________________________________________________________________
dense_3 (Dense)              (None, 18)                1170      
=================================================================
Total params: 1,874,834
Trainable params: 1,874,834
Non-trainable params: 0
_________________________________________________________________
In [11]:
mc=tf.keras.callbacks.ModelCheckpoint('best_pokemon_model_original.hdf5', monitor='val_loss', save_best_only=True)

hist=model.fit(train_data, train_labels, batch_size=1, epochs=30, verbose=1, callbacks=[mc], 
               validation_data=(val_data, val_labels))
Train on 509 samples, validate on 57 samples
Epoch 1/50
509/509 [==============================] - 5s 11ms/sample - loss: 2.7904 - accuracy: 0.1159 - val_loss: 2.7313 - val_accuracy: 0.1404
Epoch 2/50
509/509 [==============================] - 3s 5ms/sample - loss: 2.7432 - accuracy: 0.1375 - val_loss: 2.7033 - val_accuracy: 0.1404
Epoch 3/50
509/509 [==============================] - 3s 5ms/sample - loss: 2.7061 - accuracy: 0.1297 - val_loss: 2.7465 - val_accuracy: 0.0526
Epoch 4/50
509/509 [==============================] - 2s 5ms/sample - loss: 2.6936 - accuracy: 0.1316 - val_loss: 2.7210 - val_accuracy: 0.0526
Epoch 5/50
509/509 [==============================] - 3s 5ms/sample - loss: 2.6623 - accuracy: 0.1493 - val_loss: 2.6691 - val_accuracy: 0.1404
Epoch 6/50
509/509 [==============================] - 3s 5ms/sample - loss: 2.6150 - accuracy: 0.1768 - val_loss: 2.6646 - val_accuracy: 0.1754
Epoch 7/50
509/509 [==============================] - 3s 5ms/sample - loss: 2.5806 - accuracy: 0.1709 - val_loss: 2.5917 - val_accuracy: 0.1930
Epoch 8/50
509/509 [==============================] - 3s 5ms/sample - loss: 2.4846 - accuracy: 0.2122 - val_loss: 2.5936 - val_accuracy: 0.2105
Epoch 9/50
509/509 [==============================] - 3s 5ms/sample - loss: 2.3682 - accuracy: 0.2397 - val_loss: 2.6653 - val_accuracy: 0.1404
Epoch 10/50
509/509 [==============================] - 3s 5ms/sample - loss: 2.2358 - accuracy: 0.2908 - val_loss: 2.6678 - val_accuracy: 0.1404
Epoch 11/50
509/509 [==============================] - 3s 5ms/sample - loss: 2.0515 - accuracy: 0.3360 - val_loss: 2.7091 - val_accuracy: 0.2105
Epoch 12/50
509/509 [==============================] - 3s 5ms/sample - loss: 1.8368 - accuracy: 0.3969 - val_loss: 2.8849 - val_accuracy: 0.0877
Epoch 13/50
509/509 [==============================] - 3s 5ms/sample - loss: 1.5850 - accuracy: 0.4951 - val_loss: 3.2488 - val_accuracy: 0.1404
Epoch 14/50
509/509 [==============================] - 2s 5ms/sample - loss: 1.3344 - accuracy: 0.5776 - val_loss: 3.8088 - val_accuracy: 0.0877
Epoch 15/50
509/509 [==============================] - 3s 5ms/sample - loss: 1.0043 - accuracy: 0.6857 - val_loss: 4.2210 - val_accuracy: 0.0526
Epoch 16/50
509/509 [==============================] - 3s 5ms/sample - loss: 0.8055 - accuracy: 0.7485 - val_loss: 4.1426 - val_accuracy: 0.0877
Epoch 17/50
509/509 [==============================] - 2s 5ms/sample - loss: 0.5908 - accuracy: 0.8173 - val_loss: 4.8775 - val_accuracy: 0.1053
Epoch 18/50
509/509 [==============================] - 3s 5ms/sample - loss: 0.4581 - accuracy: 0.8546 - val_loss: 4.9178 - val_accuracy: 0.1053
Epoch 19/50
509/509 [==============================] - 3s 5ms/sample - loss: 0.3612 - accuracy: 0.9018 - val_loss: 5.0595 - val_accuracy: 0.1053
Epoch 20/50
509/509 [==============================] - 3s 5ms/sample - loss: 0.4014 - accuracy: 0.8625 - val_loss: 7.2022 - val_accuracy: 0.1053
Epoch 21/50
509/509 [==============================] - 3s 5ms/sample - loss: 0.2903 - accuracy: 0.9214 - val_loss: 5.6238 - val_accuracy: 0.1053
Epoch 22/50
509/509 [==============================] - 3s 5ms/sample - loss: 0.2541 - accuracy: 0.9371 - val_loss: 6.0118 - val_accuracy: 0.0877
Epoch 23/50
509/509 [==============================] - 3s 5ms/sample - loss: 0.3070 - accuracy: 0.9234 - val_loss: 6.1755 - val_accuracy: 0.1053
Epoch 24/50
509/509 [==============================] - 3s 5ms/sample - loss: 0.1261 - accuracy: 0.9627 - val_loss: 7.6448 - val_accuracy: 0.0702
Epoch 25/50
509/509 [==============================] - 3s 5ms/sample - loss: 0.1147 - accuracy: 0.9686 - val_loss: 8.5017 - val_accuracy: 0.0526
Epoch 26/50
509/509 [==============================] - 3s 5ms/sample - loss: 0.3075 - accuracy: 0.9214 - val_loss: 5.9130 - val_accuracy: 0.1053
Epoch 27/50
509/509 [==============================] - 2s 5ms/sample - loss: 0.2575 - accuracy: 0.9293 - val_loss: 6.4049 - val_accuracy: 0.0702
Epoch 28/50
509/509 [==============================] - 3s 5ms/sample - loss: 0.1029 - accuracy: 0.9705 - val_loss: 7.6685 - val_accuracy: 0.1053
Epoch 29/50
509/509 [==============================] - 3s 5ms/sample - loss: 0.1791 - accuracy: 0.9411 - val_loss: 8.2560 - val_accuracy: 0.1053
Epoch 30/50
509/509 [==============================] - 3s 5ms/sample - loss: 0.2425 - accuracy: 0.9352 - val_loss: 6.9606 - val_accuracy: 0.0702
Epoch 31/50
509/509 [==============================] - 3s 5ms/sample - loss: 0.0815 - accuracy: 0.9804 - val_loss: 7.9533 - val_accuracy: 0.1053
Epoch 32/50
509/509 [==============================] - 3s 5ms/sample - loss: 0.1421 - accuracy: 0.9646 - val_loss: 6.7992 - val_accuracy: 0.0702
Epoch 33/50
509/509 [==============================] - 3s 5ms/sample - loss: 0.1469 - accuracy: 0.9587 - val_loss: 7.2109 - val_accuracy: 0.0877
Epoch 34/50
509/509 [==============================] - 3s 5ms/sample - loss: 0.0561 - accuracy: 0.9804 - val_loss: 7.9610 - val_accuracy: 0.1404
Epoch 35/50
509/509 [==============================] - 3s 5ms/sample - loss: 0.1181 - accuracy: 0.9705 - val_loss: 7.4330 - val_accuracy: 0.0526
Epoch 36/50
509/509 [==============================] - 3s 5ms/sample - loss: 0.0165 - accuracy: 0.9980 - val_loss: 8.9522 - val_accuracy: 0.0526
Epoch 37/50
509/509 [==============================] - 3s 5ms/sample - loss: 0.0059 - accuracy: 1.0000 - val_loss: 9.4915 - val_accuracy: 0.0526
Epoch 38/50
509/509 [==============================] - 3s 5ms/sample - loss: 0.0207 - accuracy: 0.9941 - val_loss: 10.3578 - val_accuracy: 0.0702
Epoch 39/50
509/509 [==============================] - 3s 5ms/sample - loss: 0.0024 - accuracy: 1.0000 - val_loss: 10.6215 - val_accuracy: 0.0702
Epoch 40/50
509/509 [==============================] - 3s 5ms/sample - loss: 6.7247e-04 - accuracy: 1.0000 - val_loss: 10.8836 - val_accuracy: 0.0351
Epoch 41/50
509/509 [==============================] - 3s 5ms/sample - loss: 3.4454e-04 - accuracy: 1.0000 - val_loss: 11.2257 - val_accuracy: 0.0526
Epoch 42/50
509/509 [==============================] - 3s 5ms/sample - loss: 2.2661e-04 - accuracy: 1.0000 - val_loss: 11.4404 - val_accuracy: 0.0351
Epoch 43/50
509/509 [==============================] - 3s 5ms/sample - loss: 1.5895e-04 - accuracy: 1.0000 - val_loss: 11.6694 - val_accuracy: 0.0526
Epoch 44/50
509/509 [==============================] - 3s 5ms/sample - loss: 1.1021e-04 - accuracy: 1.0000 - val_loss: 11.9421 - val_accuracy: 0.0351
Epoch 45/50
509/509 [==============================] - 3s 5ms/sample - loss: 8.5771e-05 - accuracy: 1.0000 - val_loss: 12.1437 - val_accuracy: 0.0351
Epoch 46/50
509/509 [==============================] - 3s 5ms/sample - loss: 5.9806e-05 - accuracy: 1.0000 - val_loss: 12.3870 - val_accuracy: 0.0351
Epoch 47/50
509/509 [==============================] - 3s 5ms/sample - loss: 4.4914e-05 - accuracy: 1.0000 - val_loss: 12.5721 - val_accuracy: 0.0526
Epoch 48/50
509/509 [==============================] - 3s 5ms/sample - loss: 3.4917e-05 - accuracy: 1.0000 - val_loss: 12.8199 - val_accuracy: 0.0351
Epoch 49/50
509/509 [==============================] - 3s 5ms/sample - loss: 2.6081e-05 - accuracy: 1.0000 - val_loss: 13.0631 - val_accuracy: 0.0351
Epoch 50/50
509/509 [==============================] - 3s 5ms/sample - loss: 2.0865e-05 - accuracy: 1.0000 - val_loss: 13.4131 - val_accuracy: 0.0351
In [12]:
model=tf.keras.models.load_model('best_pokemon_model_original.hdf5')
test_results=model.evaluate(test_data, test_labels, verbose=0)
test_results
Out[12]:
[2.724155082624145, 0.12757201]

Discussion

Given that we have 18 classes of Pokemon type, if a network were randomly guessing, it would achieve an accuracy of approximately 1/18=0.055. Achieving a test accuracy of 23% thus means the network is doing better than randomly guessing, though still has fairly low accuracy. This supports the conclusion that there are noticeable, yet inconsistent patterns in Pokemon appearance that signify type.

There is also the possibility that a dataset of 809 instances is two small to properly train a model. It is possible to artificially inflate the dataset by making copies of the existing images or making rotated or reflected copies of the images. However, this will likely lead to overfitting, and it will be difficult to generalize what the network has learned to new Pokemon when they are released, as there is a large amount of variety in Pokemon design. Future work will investigate how rotations or reflections of these images affect the network.

References