LSTMを用いた最強のポケモン生成

はじめに

以前読んだ論文に音象徴の機械学習による再現:最強のポケモンの生成というタイトルだけで面白い論文があった。
以前、この論文を参考に自分でアレンジしてゼミ発表に使ったので、それを紹介しようと思います!!

今回は以下の論文のアイディアを参考にして、深層学習(LSTM)のアクセントを加えつつ、同じ手法で最強のポケモンの生成を目指していきます。
論文では被験者アンケートで音の印象を定量化していましたが、そのようなデータは持ってないので、代わりに種族値を使用します。

音象徴の機械学習による再現:最強のポケモンの生成
三浦 智 ∗1 村田 真樹 ∗1 保田 祥 ∗2 宮部 真衣 ∗2 荒牧 英治 ∗2
∗1 鳥取大学大学院 *2 東京大学 言語処理学会 第18 回年次大会 発表論文集 (2012年 3月) 
http://luululu.com/paper/2012/C1-1.pdf

元論文の概要(3行)

・8人の被験者(ポケモンを知らない)に対して一対比較でポケモンの強弱を予想。
・これを学習データとしてSVMでモデルを生成。
・モデルを使い、ポケモンの名前を変えて強弱の判断を繰り返すことで最強のポケモンを生成。

データの前処理

使用データ

今回使用するデータは、第七世代までのポケモンのテーブルデータを使用しています。
ポケモンの名前は六文字までなので、それを超える名前が付与されているポケモン(ランドロス霊獣、ジガルデ10%等)の値を削除します。
また、今回はメガ進化ポケモンも対象外としました。

ポケモンのデータは以下のリンクから拝借。

import pandas as pd
status = pd.read_csv("pokemon_status.csv", encoding="shift_jis")
status

今回扱うcsvデータ↓
スクリーンショット 2020-11-07 17.54.24.png

これに前処理を加えていきます。

#余計なメガポケモンの削除
status = status[~status['図鑑番号'].str.contains('-')]

#名前の長さが7以上のポケモンを削除
status['len'] = status['ポケモン名'].map(lambda x: len(x))
de = status[status['len']>6]
status = status[status['len']<7]

#使用データだけのデータに
status = status.loc[:, ['ポケモン名','合計']]
status

今回使用するデータの完成!
スクリーンショット 2020-11-07 18.00.59.png

ここからLSTMに食わせるための前処理を続けていきます。

  • ポケモンの名前をTokenize(論文に倣ってポケモンの名前をモノグラムで入力)
  • ポケモンの種族値を正規化して0,1にラベリング

#tokenize
def function(name):
    n_gram = ''
    for n in name:
        n_gram = n_gram + n + ' '
    return n_gram

status['ポケモン名'] = status['ポケモン名'].map(function)
status

#種族値の正規化 & 0or1
from sklearn import preprocessing

def labeling(pred, p=0.5):
    if pred < p:
        pred_label = 0
    else:
        pred_label = 1
    
    return pred_label

status['合計'] = preprocessing.minmax_scale(status['合計'])
status['合計'] = status['合計'].map(labeling)
status

前処理後のデータ↓
スクリーンショット 2020-11-07 18.07.12.png

作成したデータをtrain、valに分類して保存。

from sklearn.model_selection import train_test_split

train_df, val_df = train_test_split(status, random_state=1234, test_size=0.2)
train_df.to_csv("./train_df.tsv", sep='\t')
val_df.to_csv("./val_df.tsv", sep='\t')

モデルの構築

pytorch信者なのでtorchでモデルを構築、軽くコードの紹介をします。
コード全文は以下リンクに置いているので、興味を持ってくれる方がいたらそちらを参考にしてみてください。

https://github.com/drop-ja/pokemon

from torchtext import data
import torchtext

batch_size = 4
max_len = 6

# tokenizeの方法
tokenizer = lambda x: x.split()

# ラベル情報等
TEXT = data.Field(sequential=True, tokenize=tokenizer, include_lengths=True, 
                 batch_first=True, fix_length=max_len)
LABEL = data.LabelField()

fields_train = [('id', None), ('name', TEXT), ('bs', LABEL)]

dataset_train, dataset_valid = data.TabularDataset.splits(
                             path = './',
                             format='TSV',
                             skip_header=True, 
                             train="train_df.tsv",
                             validation="val_df.tsv", 
                             fields=fields_train)
TEXT.build_vocab(dataset_train)
LABEL.build_vocab(dataset_train)

train_iter = data.BucketIterator(dataset=dataset_train, batch_size=batch_size, 
                                 sort_key=lambda x: len(x.name), repeat=False, shuffle=True)
val_iter = data.BucketIterator(dataset=dataset_valid, batch_size=1, 
                                 sort_key=lambda x: len(x.name), repeat=False, shuffle=False)

#モデルの定義
import torch
import torch.nn as nn
import torch.nn.init as init
import torch.optim as optim
import torch.nn.functional as F

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

class LSTMPVClassifier(nn.Module):
    def __init__(self, vocab_size, embedding_dim, lstm_hidden_size,  mlp_hidden_size, output_size):
        super(LSTMPVClassifier, self).__init__()
        self.lstm_hidden_size = lstm_hidden_size
   
        self.embed = nn.Embedding(vocab_size, embedding_dim, padding_idx=1)
        self.lstm = nn.LSTM(embedding_dim, self.lstm_hidden_size, batch_first=True, 
                        num_layers=1, bidirectional=False, dropout=0.0)
    
        self.fc1 = nn.Linear(self.lstm_hidden_size, mlp_hidden_size)
        self.fc2 = nn.Linear(mlp_hidden_size, output_size)
    
    def forward(self, x):
        b_size = x.size(0) # バッチサイズ
        seq_len  = x.size(1) # ポケモン名の長さ

        x = self.embed(x)
        h0 = torch.zeros(1, b_size, self.lstm_hidden_size).to(device)
        c0 = torch.zeros(1, b_size, self.lstm_hidden_size).to(device)
  
        lstm_output_seq, (h_n, c_n) = self.lstm(x, (h0, c0))
   
        out = torch.relu(self.fc1(lstm_output_seq))
        out = torch.sigmoid(self.fc2(out)) 

        return out

結果

上記のモデルで10Epoch回した結果がこちら。
'合計'は正解ラベル、'予測値'はモデルにて予測した結果を返しています。

スクリーンショット 2020-11-07 18.19.30.png

上記の結果のF値をいかに示します。
適当にモデルを組んだ割には結構いい値が返ってきています。

スクリーンショット 2020-11-07 18.21.24.png

生成したモデルで遊んでみる

適当に名前を入れて遊んでみました。
研究室メンバーの名前を入れてランキング化したりして、これを使って遊ぶだけで結構盛り上がりました。

スクリーンショット 2020-11-07 18.25.41.png

以下コードです。

#変換後の数値をチェック
def to_dataset(list_obj, pri=True):
    index = pd.DataFrame(list_obj)
    index[0] = index[0].map(function)
    index.to_csv('./test.tsv', sep='\t')
    
    fields_test = [('id', None), ('name', TEXT)]
    dataset_test = data.TabularDataset(path='./test.tsv',
                             format='TSV', skip_header=True, fields=fields_test)
    
    test_iter = data.BucketIterator(dataset=dataset_test, batch_size=1, 
                                 sort_key=lambda x: len(x.name), repeat=False, shuffle=False)
    
    batch = next(iter(test_iter))
    
    if pri:
        print(batch.name)
    
    return test_iter

list_obj = ['デンシケトル', 'ガギグゲゴ', 'デンシレンジ', 'フライパン', 'ジサボケ', 'ポケモン']
test_iter = to_dataset(list_obj)
def result_show(test_iter, pri=True):
    test_predicted = []
    
    for batch in test_iter:
        text = batch.name[0]
        text = text.to(device)
        outputs = eval_net(text)
        outputs = outputs[:, -1]
        tmp_pred_label = outputs.to('cpu').detach().numpy().copy()
        test_predicted.extend(tmp_pred_label[0])
    
    if pri:
        print(test_predicted)
    
    return test_predicted
        
result = result_show(test_iter)

df = pd.DataFrame(list_obj, columns=['名前'])
df['予測値'] = result
df['0,1ラベル'] = labeling(df['予測値'])
df

最強のポケモンを生成

やっと今回の主題です。

最強のポケモン生成手法は論文に倣い同じ手法で生成します。

論文の手法

  • 適当にサンプルを選定
  • ランダムに一文字置き換える
  • 置き換えた名前とモデルを用いて比較
  • 上記3工程を50回ループ

論文では"パラセクト"、"ニドクイン"から生成していたのでそれに倣います。
以下コード。

def generate_pokemon(string):
    history = []
    score = []
    history.append(string)
    
    for i in range(50):
        changed_string = change_name(string, 1)
        cd_result = result_show(to_dataset([string, changed_string], False), False)
        
        #最初だけ追加
        if i ==0:
            score.append(cd_result[0])
            
        if cd_result[0] > cd_result[1]:
            score.append(cd_result[0])
        else:
            string = changed_string
            score.append(cd_result[1])
            
        history.append(string)
        
    cd_df = pd.DataFrame(history, columns=['名前'])
    cd_df['予測値'] = score
    
    return string, cd_df


string = 'パラセクト'
saikyou, port = generate_pokemon(string)

print('最強の名前 : ', saikyou)
pd.DataFrame(port)

パラセクトからの生成結果(過程10/50)
スクリーンショット 2020-11-07 18.35.14.png

最終的な生成結果は「エギネオオ」になりました。
ランダムで文字を交換しているので、回すたび結果が変わるのが面白く何回も回てしまいました。
こうやって結果を確認しながら動かしていくのは楽しいです。

また今回はポケモンを使用しましたが、ラーメン店の名前と食べログ評価とかでやっても面白そうですね。

おすすめの記事