機械学習（LSTM）を用いてファイアーエムブレム最強／最弱の名前を決定する

　ファイアーエムブレムシリーズの最新作となるエンゲージが 2023 年 1 月 20 日に発売されました。私はシリーズ全体を通じたキャラクターの[成長率解析](https://github.com/hibit-at/FE_growth_rate)を行っているので、早速データを更新しました。しかし、ただ散布図を出すだけではつまらないので、もっと突っ込んだ解析をしてみたくなりました。

　それは、キャラクターの名前から性別や成長率を予測できるかどうか、という問題です。

　幸い、ポケモンで似たような研究[1][2]をやっているのを見かけました。これらの研究では、ポケモンの名前から種族値やタイプを予測する機械学習モデルが開発されています。これらの研究成果を参考に、ファイアーエムブレムのキャラクターに対しても同様の解析を行っていきます。

　以後、データは私が用意した [csv](https://github.com/hibit-at/FE_growth_rate/blob/master/growth_rate.csv) を用います。また、機械学習のフレームワークは PyTorch を使います。

# 名前をベクター化する

　全部カタカナ、最大 6 文字までなので、6 文字を最大長としたパディング処理を行い one hot vector に変換します。

```python
import csv
import os
import torch
import torch.nn as nn
import torch.optim as optim
import numpy as np
import matplotlib.pyplot as plt
from torch.utils.data import Dataset, DataLoader, random_split

# カタカナ文字セット
katakana = [chr(i) for i in range(ord("ァ"), ord("ヺ")+1)]
katakana.append('ー')

# 文字列をカタカナのインデックスに変換
def name_to_index(name):
    indices = []
    for char in name:
        index = katakana.index(char)
        indices.append(index)
    return indices

# インデックスをワンホットエンコーディングに変換
def one_hot_encoding(indices, max_length=6, num_classes=len(katakana)):
    encoded = np.zeros((max_length, num_classes))
    for i, idx in enumerate(indices):
        if i < max_length:
            encoded[i, idx] = 1.0
    return encoded

# 文字列からテンソルに一気に変換する関数
def name_to_tensor(word):
    vector = one_hot_encoding(name_to_index(word))
    return torch.Tensor(vector)
```

# 名前→性別を学習する

## データセット作成

　まずは名前－性別のデータセットを作ります。

```python

class MyDataset(Dataset):
    def __init__(self, file_path):
        self.data = []
        with open(file_path, encoding='utf-8') as csvfile:
            reader = csv.reader(csvfile)
            # タイトル行をスキップする
            next(reader)
            for row in reader:
                name = row[0].split('（')[0]
                tensor = name_to_tensor(name)
                # 'Male'の場合は1、それ以外の場合は0をセットする
                sex = int(row[2] == 'Male')
                self.data.append((name, tensor, sex))

    def __len__(self):
        return len(self.data)

    def __getitem__(self, index):
        name, tensor, sex = self.data[index]
        # nameを補助的な情報、tensorを入力、sexを出力とするタプルを返す
        return name, tensor, sex
```

## 学習ネットワーク

　学習ネットワークを定義します。名前なので前後の関係を見るため、時系列解析の一種である LSTM を用いています。出力は性別（0,1）なので最後はシグモイドで活性化しています。

```python
class LSTMNet(nn.Module):
    def __init__(self, input_size, hidden_size, num_layers, output_size):
        super(LSTMNet, self).__init__()
        self.hidden_size = hidden_size
        self.num_layers = num_layers
        self.lstm = nn.LSTM(input_size, hidden_size,
                            num_layers, batch_first=True)
        self.fc = nn.Linear(hidden_size, output_size)

    def forward(self, x):
        h0 = torch.zeros(self.num_layers, x.size(
            0), self.hidden_size).to(x.device)
        c0 = torch.zeros(self.num_layers, x.size(
            0), self.hidden_size).to(x.device)
        out, _ = self.lstm(x, (h0, c0))
        out = self.fc(out[:, -1, :])
        out = torch.sigmoid(out)
        return out
```

## ハイパーパラメータとか

　ハイパーパラメータや諸々の設定をします。

```python
file_path = 'growth_rate.csv'
input_size = 91  # 名前の最大長
hidden_size = 128  # 隠れ層のユニット数
num_layers = 3  # LSTMのレイヤー数
output_size = 1  # 出力層のユニット数
lr = 0.001  # 学習率

## データセットとデータローダーを作成する
dataset = MyDataset(file_path)
train_size = len(dataset) - 32
test_size = 32
generator = torch.Generator().manual_seed(0)
train_dataset, test_dataset = random_split(dataset, [train_size, test_size], generator=generator)
train_dataloader = DataLoader(train_dataset, batch_size=128, shuffle=False)
test_dataloader = DataLoader(test_dataset, batch_size=32, shuffle=False)
net = LSTMNet(input_size, hidden_size, num_layers, output_size)

FILE_PATH = "./name_to_sex.pth"
```

## 学習（関数）

　学習を回す関数を定義します。

```python
def train():
    criterion = nn.BCELoss()
    optimizer = optim.Adam(net.parameters(), lr=lr)

    # ロスの履歴を保存するためのリスト
    train_loss_history = []

    # モデルを学習する
    num_epochs = 200
    for epoch in range(num_epochs):
        for i, (name, inputs, targets) in enumerate(train_dataloader):
            optimizer.zero_grad()
            outputs = net(inputs)
            loss = criterion(outputs.squeeze(), targets.float())
            loss.backward()
            optimizer.step()
            if i % 100 == 0:
                log = []
                log.append('Epoch [{}/{}]'.format(epoch+1,num_epochs))
                log.append('Step [{}/{}]'.format(i+1,len(dataset)))
                log.append('Loss : {:.4f}'.format(loss.item()))
                print(', '.join(log))

            # ロスを履歴に追加する
            train_loss_history.append(loss.item())
                    
    torch.save(net.state_dict(), FILE_PATH)

    # ロスのグラフを描画して保存する
    fig, ax = plt.subplots()
    ax.plot(train_loss_history)
    ax.set_xlabel('iteration')
    ax.set_ylabel('loss')
    ax.set_title('Training Loss')
    fig.savefig('train_loss_name_to_sex.png')
```

## 学習（本番）

　Go

```python
if __name__ == '__main__':
    train()
```

```python
Epoch [1/200], Step [1/629], Loss : 0.7105
Epoch [2/200], Step [1/629], Loss : 0.6749
Epoch [3/200], Step [1/629], Loss : 0.6390
Epoch [4/200], Step [1/629], Loss : 0.6355
Epoch [5/200], Step [1/629], Loss : 0.6436
...
## （以下略）
```

　ロスの推移です。

![train_loss_name_to_sex.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/288125/1a3e24cc-1b41-358f-ef0f-6c11bc95b259.png)

## 評価（関数）

　評価用の関数を定義します。

```python
def eval():
    if os.path.exists(FILE_PATH):
        net.load_state_dict(torch.load(FILE_PATH))
        print("Saved weight loaded.")
        for i, (names, inputs, targets) in  enumerate(test_dataloader):
            predicts = net(inputs)
            for name,predict in zip(names,predicts):
                print(f'{name} {predict.item():.4f}')
    else:
        print("No saved weight.")
```

## 評価（本番）

　Go

```python
if __name__ == '__main__':
    # train()
    eval()
```

　評価用に、学習に使用しないデータを 32 個だけ分離しておきましたので、これに学習モデルを適用させて性能を評価します。1 が男、0 が女になります。

```python
アズール 1.0000 #OK
シルヴァン 0.9996 #OK
ジャファル 0.9995 #OK
レテ 0.0003 #OK
メリン 0.0000 #OK
レンハ 0.9968 #OK
ローレンツ 0.9991 #OK
アンナ 0.0001 #OK
セーバー 0.9906 #OK
ザガロ 0.9996 #OK
ガイツ 0.9996 #OK
ユキムラ 0.9979 #OK
エルレーン 0.0018 #NG!!
ランス 0.9982 #OK
アイク 0.9992 #OK
コノモール 0.9994 #OK
ムワリム 0.9050 #OK
リベラ 0.0000 #NG!!
ケセルダ 0.9999 #OK
リシテア 0.0000 #OK
ミタマ 0.0000 #OK
ジャンヌ 0.0000 #OK
ララム 0.9976 #NG!!
イングリット 0.9993 #NG!! 
ダロス 0.9995 #OK
ユミナ 0.0001 #OK
クリフ 0.9737 #OK
ナンナ 0.0002 #OK
ルーナ 0.0000 #OK
ヨハルヴァ 0.9996 #OK
フィーナ 0.0000 #OK
ヒナタ 0.0000 #OK
```

　結果、正答率は $28/32 = 0.875%$ とそれなりに高い結果を得られました。誤答してしまった答えも「リベラ」とか「エルレーン」はさもありなん（何も知らない人間がみても間違えそう）、という感じです。ほぼ問題ない精度といってよいでしょう。「ララム」が男判定されたのは謎ですが。

## テスト
　
　FEがまったく関係ないテストも行います。私のハンドルネーム「ヒビット」を男らしくした「ヒビトス」、女らしくした「ヒビリア」でテストしてみます。

```python
def test(name):
    if os.path.exists(FILE_PATH):
        net.load_state_dict(torch.load(FILE_PATH))
        tensor = name_to_tensor(name)
        tensor = tensor.view(1,6,91)
        print(tensor.shape)
        predict = net(tensor)
        print(f'{name} {predict.item():.4f}')
    else:
        print("No saved weight.")
```

```python
if __name__ == '__main__':
    # train()
    # eval()
    test('ヒビトス') # 0.996
    test('ヒビリア') # 0.000
```

## 全ソースコード

　https://github.com/hibit-at/FE_growth_rate/blob/master/name_to_sex_analysis.py

# 名前→パラメータを学習する

## データセット作成

　出力の形が変わるので少し変えています。

```python
class MyDataset(Dataset):
    def __init__(self, file_path):
        self.data = []
        with open(file_path, encoding='utf-8') as csvfile:
            reader = csv.reader(csvfile)
            # タイトル行をスキップする
            next(reader)
            for row in reader:
                name = row[0].split('（')[0]
                tensor = name_to_tensor(name)
                params = [min(float(row[i])/100, 1.0)
                          for i in range(3, 11)]  # row[3]からrow[10]までをfloatに変換
                params = torch.tensor(params)  # Tensorに変換
                self.data.append((name, tensor, params))

    def __len__(self):
        return len(self.data)

    def __getitem__(self, index):
        name, tensor, params = self.data[index]
        # nameを補助的な情報、tensorを入力、params出力とするタプルを返す
        return name, tensor, params
```

## 学習ネットワーク

　同じく。

```python
class LSTMNet(nn.Module):
    def __init__(self, input_size, hidden_size, num_layers, output_size):
        super(LSTMNet, self).__init__()
        self.hidden_size = hidden_size
        self.num_layers = num_layers
        self.lstm = nn.LSTM(input_size, hidden_size,
                            num_layers, batch_first=True)
        self.fc1 = nn.Linear(hidden_size, hidden_size)
        self.fc2 = nn.Linear(hidden_size, hidden_size)
        self.fc3 = nn.Linear(hidden_size, output_size)

    def forward(self, x):
        h0 = torch.zeros(self.num_layers, x.size(
            0), self.hidden_size).to(x.device)
        c0 = torch.zeros(self.num_layers, x.size(
            0), self.hidden_size).to(x.device)
        out, _ = self.lstm(x, (h0, c0))
        out = F.relu(self.fc1(out[:, -1, :]))
        out = F.relu(self.fc2(out))
        out = self.fc3(out)
        return out
```

## ハイパーパラメータとか

```python
file_path = 'growth_rate.csv'
input_size = 91  # 名前の最大長
hidden_size = 64  # 隠れ層のユニット数
num_layers = 3  # LSTMのレイヤー数
output_size = 8  # 出力層のユニット数
lr = 0.001  # 学習率

# データセットとデータローダーを作成する
dataset = MyDataset(file_path)
train_size = len(dataset) - 32
test_size = 32
generator = torch.Generator().manual_seed(0)
train_dataset, test_dataset = random_split(
    dataset, [train_size, test_size], generator=generator)
train_dataloader = DataLoader(train_dataset, batch_size=128, shuffle=False)
test_dataloader = DataLoader(test_dataset, batch_size=32, shuffle=False)
net = LSTMNet(input_size, hidden_size, num_layers, output_size)

FILE_PATH = "./name_to_params.pth"
```

## 学習（関数）

　今度はパラメータという連続的な量を解析するので、シグモイドはいれずに、ロスもクロスエントロピーではなく単なる誤差（MSE）で計測します。

```python
def train():
    criterion = nn.MSELoss()
    optimizer = optim.RMSprop(net.parameters(), lr=lr)

    # ロスの履歴を保存するためのリスト
    train_loss_history = []

    # モデルを学習する
    num_epochs = 1000
    for epoch in range(num_epochs):
        for i, (name, inputs, targets) in enumerate(train_dataloader):
            optimizer.zero_grad()
            outputs = net(inputs)
            loss = criterion(outputs.squeeze(), targets.float())
            loss.backward()
            optimizer.step()
            if i % 100 == 0:
                log = []
                log.append('Epoch [{}/{}]'.format(epoch+1,num_epochs))
                log.append('Step [{}/{}]'.format(i+1,len(dataset)))
                log.append('Loss : {:.4f}'.format(loss.item()))
                print(', '.join(log))
            # ロスを履歴に追加する
            train_loss_history.append(loss.item())

    torch.save(net.state_dict(), FILE_PATH)

    # ロスのグラフを描画して保存する
    fig, ax = plt.subplots()
    ax.plot(train_loss_history)
    ax.set_xlabel('iteration')
    ax.set_ylabel('loss')
    ax.set_title('Training Loss')
    fig.savefig('train_loss_name_to_params.png')
```

## 学習（本番）

```python
Epoch [1/1000], Step [1/629], Loss : 0.2005
Epoch [2/1000], Step [1/629], Loss : 0.0537
Epoch [3/1000], Step [1/629], Loss : 0.0305
Epoch [4/1000], Step [1/629], Loss : 0.0298
Epoch [5/1000], Step [1/629], Loss : 0.0302
...
# 以下略
```

　ロスの推移です。

![train_loss_name_to_params.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/288125/9fd61703-2376-2592-f030-6bc74c842e0e.png)


## 評価（関数）

```python
def eval():
    if os.path.exists(FILE_PATH):
        net.load_state_dict(torch.load(FILE_PATH))
        print("Saved weight loaded.")
        for i, (names, inputs, targets) in enumerate(test_dataloader):
            predicts = net(inputs)
            for name, predict in zip(names, predicts):
                predict = predict.tolist()
                predict = {k: int(v*100)
                    for k, v in zip(['HP', 'POW', 'MAG', 'TEC', 'SPD', 'LUC', 'DEF', 'RES'], predict)}
                print(f'{name} {predict}')
    else:
        print("No saved weight.")
```

## 評価（本番）

```python
アズール {'HP': 65, 'POW': 20, 'MAG': 35, 'TEC': 27, 'SPD': 54, 'LUC': 31, 'DEF': 12, 'RES': 8}
シルヴァン {'HP': 100, 'POW': 39, 'MAG': 18, 'TEC': 34, 'SPD': 37, 'LUC': 40, 'DEF': 25, 'RES': 8}
ジャファル {'HP': 90, 'POW': 54, 'MAG': 44, 'TEC': 29, 'SPD': 49, 'LUC': 25, 'DEF': 38, 'RES': 24}
レテ {'HP': 36, 'POW': 27, 'MAG': 36, 'TEC': 43, 'SPD': 42, 'LUC': 33, 'DEF': 31, 'RES': 27}
メリン {'HP': 44, 'POW': 32, 'MAG': 46, 'TEC': 42, 'SPD': 54, 'LUC': 36, 'DEF': 15, 'RES': 29}
レンハ {'HP': 83, 'POW': 40, 'MAG': 38, 'TEC': 45, 'SPD': 33, 'LUC': 11, 'DEF': 22, 'RES': 24}
ローレンツ {'HP': 41, 'POW': 17, 'MAG': 26, 'TEC': 24, 'SPD': 15, 'LUC': 53, 'DEF': 25, 'RES': 28}
アンナ {'HP': 36, 'POW': 30, 'MAG': 40, 'TEC': 36, 'SPD': 40, 'LUC': 63, 'DEF': 25, 'RES': 38}
セーバー {'HP': 83, 'POW': 40, 'MAG': 27, 'TEC': 42, 'SPD': 34, 'LUC': 38, 'DEF': 18, 'RES': 3}
ザガロ {'HP': 42, 'POW': 39, 'MAG': 13, 'TEC': 36, 'SPD': 35, 'LUC': 31, 'DEF': 27, 'RES': 13}
ガイツ {'HP': 27, 'POW': 42, 'MAG': 5, 'TEC': 37, 'SPD': 51, 'LUC': 55, 'DEF': 24, 'RES': 8}
ユキムラ {'HP': 58, 'POW': 29, 'MAG': 23, 'TEC': 39, 'SPD': 58, 'LUC': 20, 'DEF': 28, 'RES': 37}
エルレーン {'HP': 27, 'POW': 33, 'MAG': 59, 'TEC': 41, 'SPD': 36, 'LUC': 35, 'DEF': 19, 'RES': 46}
ランス {'HP': 43, 'POW': 24, 'MAG': 33, 'TEC': 43, 'SPD': 45, 'LUC': 29, 'DEF': 25, 'RES': 31}
アイク {'HP': 67, 'POW': 36, 'MAG': -7, 'TEC': 48, 'SPD': 33, 'LUC': 33, 'DEF': 26, 'RES': 2}
コノモール {'HP': 41, 'POW': 65, 'MAG': 18, 'TEC': 42, 'SPD': 31, 'LUC': 42, 'DEF': 27, 'RES': 18}
ムワリム {'HP': 55, 'POW': 25, 'MAG': 28, 'TEC': 35, 'SPD': 27, 'LUC': 35, 'DEF': 29, 'RES': 3}
リベラ {'HP': 48, 'POW': 41, 'MAG': 95, 'TEC': 21, 'SPD': 14, 'LUC': 46, 'DEF': 1, 'RES': 29}
ケセルダ {'HP': 62, 'POW': 31, 'MAG': 31, 'TEC': 46, 'SPD': 26, 'LUC': 53, 'DEF': 20, 'RES': 14}
リシテア {'HP': 44, 'POW': 58, 'MAG': 60, 'TEC': 30, 'SPD': 40, 'LUC': 40, 'DEF': 7, 'RES': 27}
ミタマ {'HP': 33, 'POW': 40, 'MAG': -2, 'TEC': 41, 'SPD': 33, 'LUC': 45, 'DEF': 36, 'RES': 8}
ジャンヌ {'HP': 43, 'POW': 59, 'MAG': 7, 'TEC': 46, 'SPD': 65, 'LUC': 63, 'DEF': 28, 'RES': 7}
ララム {'HP': 53, 'POW': 45, 'MAG': 30, 'TEC': 35, 'SPD': 29, 'LUC': 29, 'DEF': 36, 'RES': 27}
イングリット {'HP': 89, 'POW': 57, 'MAG': 56, 'TEC': 24, 'SPD': 28, 'LUC': 33, 'DEF': 36, 'RES': 5}
ダロス {'HP': 33, 'POW': 15, 'MAG': 13, 'TEC': 18, 'SPD': 27, 'LUC': 37, 'DEF': 23, 'RES': 2}
ユミナ {'HP': 33, 'POW': 39, 'MAG': 6, 'TEC': 19, 'SPD': 24, 'LUC': 15, 'DEF': 19, 'RES': 12}
クリフ {'HP': 41, 'POW': 40, 'MAG': 23, 'TEC': 39, 'SPD': 45, 'LUC': 51, 'DEF': 43, 'RES': 8}
ナンナ {'HP': 43, 'POW': 36, 'MAG': 12, 'TEC': 17, 'SPD': 29, 'LUC': 49, 'DEF': 15, 'RES': 44}
ルーナ {'HP': 35, 'POW': 31, 'MAG': 66, 'TEC': 41, 'SPD': 54, 'LUC': 41, 'DEF': 12, 'RES': 43}
ヨハルヴァ {'HP': 76, 'POW': 44, 'MAG': 5, 'TEC': 42, 'SPD': 24, 'LUC': 33, 'DEF': 53, 'RES': 19}
フィーナ {'HP': 65, 'POW': 50, 'MAG': 42, 'TEC': 35, 'SPD': 57, 'LUC': 2, 'DEF': 15, 'RES': 28}
ヒナタ {'HP': 56, 'POW': 22, 'MAG': 39, 'TEC': 43, 'SPD': 76, 'LUC': 51, 'DEF': 25, 'RES': 8}
```

　こちらは性別と違って、あまり精度はよくなさそうです……。男っぽい名前を持つユニットは力・防御重視になるかと思いきや、そうとは言い切れない感じ。

## テスト（関数）

```python
def test(name):
    if os.path.exists(FILE_PATH):
        net.load_state_dict(torch.load(FILE_PATH))
        tensor = name_to_tensor(name)
        tensor = tensor.view(1, 6, 91)
        predict = net(tensor)
        predict = predict.tolist()[0]
        predict = {k: int(v*100)
            for k, v in zip(['HP', 'POW', 'MAG', 'TEC', 'SPD', 'LUC', 'DEF', 'RES'], predict)}
        print(f'{name} {predict}')
    else:
        print("No saved weight.")
```

## テスト（本番）

```python
if __name__ == '__main__':
    # train()
    # eval()
    test('ヒビトス')
    test('ヒビリア')
```

```python
ヒビトス {'HP': 84, 'POW': 38, 'MAG': 46, 'TEC': 34, 'SPD': 39, 'LUC': 28, 'DEF': 21, 'RES': 17}
ヒビリア {'HP': 61, 'POW': 31, 'MAG': 22, 'TEC': 48, 'SPD': 53, 'LUC': 37, 'DEF': 26, 'RES': 28}
```

　ヒビトス（男）は HP と魔力（！）が多め、ヒビリア（女）は技と速さが多めという形になりました。これは概ねイメージ通りです。

## 全ソースコード

https://github.com/hibit-at/FE_growth_rate/blob/master/name_to_params_analysis.py

# 最強は誰？

　カタカナ $91$ 通りで最大 $6$ 文字なので、$91^6$ 通りの名前が考えられますが、これは全探索するには長すぎる量です。よって、以下のようなヒューリスティック解法を考えました。

1. $dp[i][(\text{score},\text{word})]$ $i$ 文字目までのスコアで最大のスコアとその時の言葉、という配列を考える
1. $i$ 文字目の最良の結果は確定しているとして、$i+3$ 文字目までを全探索する（$91^3$ 通り）
1. 探索の過程で、既存のスコアより上回ればそれで置き換える
1. これを$i=0$ から $i=5$ まで行うことにより、$6$ 文字目までの結果が確定する

　これによって計算量を $6 \times 91^3$ （現実的なライン）まで抑えることができます。

　関数を組みます。上で現実的なラインと書きましたが、それでも時間がかかるので、GPU で計算を行うことにより高速化しています。

```python
def heuristic_gpu():
    class Monoid: # scoreとwordを比較するための独自クラス
        def __init__(self, word: str) -> None:
            self.score = api_score(word)
            self.word = word

        def chmax(self, score, word):
            if self.score < score:
                self.score = score
                self.word = word

        def chmin(self, score, word):
            if self.score > score:
                self.score = score
                self.word = word

        def __repr__(self) -> str:
            return f'{self.score:.1f} "{self.word}"'

    # dpテーブルをモノイドで初期化
    dp = [Monoid('') for _ in range(9)]
    print(dp)

    # モデルの読み込みとGPUへの転送
    net.load_state_dict(torch.load(FILE_PATH))
    net.to('cuda:0')

    for i in range(6):
        tensors = []
        words = []
        katakana_list = list(katakana) + ['']

        # 3文字目までのありえるすべてのカタカナを生成
        for a in katakana_list:
            for b in katakana_list:
                for c in katakana_list:
                    word = dp[i].word + a + b + c
                    words.append(word)
                    tensors.append(name_to_tensor(word))

        tensors = torch.stack(tensors).to('cuda:0')
        dataloader = DataLoader(tensors, batch_size=32, shuffle=False)

        for i, batch_tensors in enumerate(dataloader):
            outputs = net(batch_tensors)
            scores = [sum(output.cpu().tolist()) * 100 for output in outputs]

            for score, word in zip(scores, words[i * 32:(i + 1) * 32]):
                dp[len(word)].chmax(score, word)
                # dp[len(word)].chmin(score, word) 最弱を比較したい時はこっち

        print(dp)
```

　結果が以下のようになります。

## 最強

```python:chmax
[324.2 "", 324.2 "", 324.2 "", 324.2 "", 324.2 "", 324.2 "", 324.2 "", 324.2 "", 324.2 ""]
[324.2 "", 419.4 "ァ", 625.3 "ファ", 635.7 "ゲペァ", 324.2 "", 324.2 "", 324.2 "", 324.2 "", 324.2 ""]
[324.2 "", 419.4 "ァ", 625.3 "ファ", 635.7 "ゲペァ", 534.9 "ァヌヌヘ", 324.2 "", 324.2 "", 324.2 "", 324.2 ""]
[324.2 "", 419.4 "ァ", 625.3 "ファ", 635.7 "ゲペァ", 644.1 "ファヂァ", 646.3 "ファヂァト", 324.2 "", 324.2 "", 324.2 ""]
[324.2 "", 419.4 "ァ", 625.3 "ファ", 635.7 "ゲペァ", 644.1 "ファヂァ", 646.6 "ゲペァャァ", 648.2 "ゲペァゼソソ", 324.2 "", 324.2 ""]
[324.2 "", 419.4 "ァ", 625.3 "ファ", 635.7 "ゲペァ", 644.1 "ファヂァ", 646.6 "ゲペァャァ", 648.2 "ゲペァゼソソ", 647.5 "ファヂァトャァ", 324.2 ""]
[324.2 "", 419.4 "ァ", 625.3 "ファ", 635.7 "ゲペァ", 644.1 "ファヂァ", 646.6 "ゲペァャァ", 648.2 "ゲペァゼソソ", 647.5 "ファヂァトャァ", 647.5 "ゲペァャァヴァァ"]
```

　最強は **ゲペァゼソソ** さんになりました。

```python
ゲペァゼソソ 0.8663 # 男寄り
ゲペァゼソソ {'HP': 100, 'POW': 92, 'MAG': 100, 'TEC': 91, 'SPD': 68, 'LUC': 103, 'DEF': 31, 'RES': 59}
```

## 最弱

```python:chmin
[324.2 "", 324.2 "", 324.2 "", 324.2 "", 324.2 "", 324.2 "", 324.2 "", 324.2 "", 324.2 ""]
[324.2 "", 137.5 "ゲ", -4.2 "ヌッ", -20.0 "ヌドホ", 324.2 "", 324.2 "", 324.2 "", 324.2 "", 324.2 ""]
[324.2 "", 137.5 "ゲ", -4.2 "ヌッ", -20.0 "ヌドホ", 26.4 "ゲヌヌヤ", 324.2 "", 324.2 "", 324.2 "", 324.2 ""]
[324.2 "", 137.5 "ゲ", -4.2 "ヌッ", -20.0 "ヌドホ", -19.1 "ヌッエナ", -21.3 "ヌッェヤン", 324.2 "", 324.2 "", 324.2 ""]
[324.2 "", 137.5 "ゲ", -4.2 "ヌッ", -20.0 "ヌドホ", -20.4 "ヌドホー", -23.1 "ヌドホピン", -25.7 "ヌドホーニン", 324.2 "", 324.2 ""]
[324.2 "", 137.5 "ゲ", -4.2 "ヌッ", -20.0 "ヌドホ", -20.4 "ヌドホー", -23.1 "ヌドホピン", -25.7 "ヌドホーニン", -25.7 "ヌドホーニンァ", 324.2 ""]
[324.2 "", 137.5 "ゲ", -4.2 "ヌッ", -20.0 "ヌドホ", -20.4 "ヌドホー", -23.1 "ヌドホピン", -25.7 "ヌドホーニン", -25.7 "ヌドホーニンァ", -25.2 "ヌドホピンソァァ"]
```

　最弱は **ヌドホーニン** さんになりました。

```python
ヌドホーニン 0.9915 # かなり男寄り
ヌドホーニン {'HP': -11, 'POW': 0, 'MAG': -3, 'TEC': -11, 'SPD': -13, 'LUC': 1, 'DEF': 0, 'RES': 13}
```

# 参考文献

1. 三浦智, et al. 音象徴の機械学習による再現: 最強のポケモンの生成. 言語処理学会第 18 回年次大会発表論文集, 2012, 65-68.（https://sociocom.jp/mednlp/PAPER/2012/C1-1.pdf）
1. LSTMを用いた最強のポケモン生成, https://is-ai.jp/?p=333, 2023年3月28日閲覧