TadaoYamaokaの開発日記

個人開発しているスマホアプリや将棋AIの開発ネタを中心に書いていきます。

大規模言語モデルで将棋AIを作る その2(入力特徴量)

前回の続き。

今回は、入力特徴量の作成処理を実装し、ベースラインとして単純なTransformerモデルを学習させた。

入力特徴量

盤上の駒と駒の種類ごとの効き、効き数、持ち駒、王手をトークンに埋め込んで表現する。

盤上の駒は、各マスを1トークンに対応させて表現する。
駒の種類を単語とし、先手と後手の駒は別の単語とする。
駒がないマスは、ないことを示す単語を割り当てた方がよいかもしれないが、一旦なしとする。

駒の種類ごとの効きは、効きのあるマスのトークンにEmbeddingBagで埋め込む。
駒の種類ごとに別の単語とし、先手と後手の駒は別の単語とする。
効きのあるマスに、駒がある場合は、その駒の単語と、効きを表す単語の両方が1つのトークンに埋め込まれる。

効き数は、マスごとに効きのある駒の数を最大3までカウントする。
効き数は、それぞれ別の単語とする。先手と後手の利き数は別の単語とする。
効き数が2の場合、効き数1と効き数2の単語の両方がEmbeddingBagで1つのトークンに埋め込まれる。
スカラー値にしない理由は、効き数1と効き数2により、判断が全く変わる場合があるため、量として扱うには適していないためである。

持ち駒

持ち駒は、持ち駒の種類を1トークンに対応させて表現する。
先手と後手の持ち駒は別のトークンとする。

持ち駒の数は、各数値を別の単語とし、EmbeddingBagで1つのトークンに埋め込む。
持ち駒の数が2の場合、持ち駒1と持ち駒2の単語の両方が1つのトークンに埋め込まれる。
スカラー値にしない理由は、持ち駒の数1と持ち駒の数2により、判断が全く変わる場合があるため、量として扱うには適していないためである。

歩は最大8枚までカウントする。

また、駒の種類と、駒の数を別のトークンに分けて表現することも考えたが、トークン長が計算量に2乗で効いてくるため、1つのトークンに埋め込むことでトークン長を節約する。

王手

王手がかかっているかを1トークンに対応させて表現する。

トークン長

盤上のマスに対応したトークン: 81、持ち駒に対応したトークン: 7 × 2、王手:1 の合計で、96トークンになる。

出力

方策

方策の出力は、移動先の座標と移動方向の組み合わせから、あり得ない組み合わせを除いた1496通りのクラス分類とする。

価値

価値の出力は、勝率を表す[0, 1]のスカラー値とする。

位置エンコーダ

位置エンコーダは、位置ごとに学習可能なエンコーダとする。

Transformerモデル

ベースラインとして、PyTorchの標準のTransformerEncoderLayerとTransformerEncoderを使用したシンプルなモデルとした。

import torch
import torch.nn as nn
import torch.nn.functional as F

class PositionalEncoding(nn.Module):
    def __init__(self, d_model, max_len=96):
        super(PositionalEncoding, self).__init__()
        self.pos_encoding = nn.Parameter(torch.zeros(1, max_len, d_model))

    def forward(self, x):
        return x + self.pos_encoding[:, :x.size(1)]


class PolicyValueNetwork(nn.Module):
    def __init__(self, ntoken=96, d_model=256, nhead=8, dim_feedforward=256, num_layers=8, dropout=0.1):
        super(PolicyValueNetwork, self).__init__()
        self.encoder = nn.EmbeddingBag(2892, d_model, mode="sum", padding_idx=0)
        self.pos_encoder = PositionalEncoding(d_model, ntoken)
        transformer_layer = nn.TransformerEncoderLayer(d_model, nhead, dim_feedforward, dropout, activation="gelu", batch_first=True)
        self.transformer = nn.TransformerEncoder(transformer_layer, num_layers)
        self.policy = nn.Linear(d_model * ntoken, 1496)
        self.value_fc1 = nn.Linear(d_model * ntoken, 256, bias=False)
        self.value_norm = nn.BatchNorm1d(256)
        self.value_fc2 = nn.Linear(256, 1)
        self.ntoken = ntoken
        self.d_model = d_model
        

    def forward(self, src):
        x = self.encoder(src.type(torch.int64).view(-1, 35))
        x = x.view(-1, self.ntoken, self.d_model)
        x = self.pos_encoder(x)
        x = self.transformer(x)
        x = x.flatten(1)
        policy = self.policy(x)
        value = F.relu(self.value_norm(self.value_fc1(x)))
        value = self.value_fc2(value)
        return policy, value

学習結果

floodgateの2019年から2013年のR3800以上の棋譜から作成した8,872,952局面を訓練データ、2017年~2018年6月のfloodgateのR3500以上の棋譜からサンプリングした856,923局面を評価データとして、バッチサイズ1024で4エポック学習した。

比較のために、ResNet20ブロック256フィルタのモデルも同条件で学習した。

パラメータ数

TransformerとResNetのパラメータはそれぞれ以下の通り。

Transformer ResNet
46,990,553 24,363,714

Transformerのパラメータ数の内訳は以下の通り。

===============================================================================================
Layer (type:depth-idx)                        Output Shape              Param #
===============================================================================================
PolicyValueNetwork                            [1, 1496]                 --
├─EmbeddingBag: 1-1                           [96, 256]                 740,352
├─PositionalEncoding: 1-2                     [1, 96, 256]              24,576
├─TransformerEncoder: 1-3                     [1, 96, 256]              --
│    └─ModuleList: 2-1                        --                        --
│    │    └─TransformerEncoderLayer: 3-1      [1, 96, 256]              395,776
│    │    └─TransformerEncoderLayer: 3-2      [1, 96, 256]              395,776
│    │    └─TransformerEncoderLayer: 3-3      [1, 96, 256]              395,776
│    │    └─TransformerEncoderLayer: 3-4      [1, 96, 256]              395,776
│    │    └─TransformerEncoderLayer: 3-5      [1, 96, 256]              395,776
│    │    └─TransformerEncoderLayer: 3-6      [1, 96, 256]              395,776
│    │    └─TransformerEncoderLayer: 3-7      [1, 96, 256]              395,776
│    │    └─TransformerEncoderLayer: 3-8      [1, 96, 256]              395,776
├─Linear: 1-4                                 [1, 1496]                 36,767,192
├─Linear: 1-5                                 [1, 256]                  6,291,456
├─BatchNorm1d: 1-6                            [1, 256]                  512
├─Linear: 1-7                                 [1, 1]                    257
===============================================================================================
Total params: 46,990,553
Trainable params: 46,990,553
Non-trainable params: 0
Total mult-adds (Units.MEGABYTES): 114.13
===============================================================================================
Input size (MB): 0.00
Forward/backward pass size (MB): 0.41
Params size (MB): 175.30
Estimated Total Size (MB): 175.71
===============================================================================================
精度比較
Transformer ResNet
訓練損失 2.4075677 2.1040008
評価損失 2.9794911 2.5547059
方策正解率 0.3583973 0.4136466
価値正解率 0.6371848 0.6714451

Transformerよりも、ResNetの方が精度が高いという結果になった。

学習時間

TransformerとResNetの学習時間はそれぞれ以下の通り。

Transformer ResNet
1:26 1:37

(h:mm)

考察

Transformerは、ResNetに比べて帰納バイアスが弱いため、大量のデータがないと性能が上がらないことが知られている。
比較的少ないデータで実験したため、Transformerの精度が上がらなかった可能性がある。

また、方策の出力を全結合層にしたため、パラメータ数の半分くらいが全結合層になってしまっている。
トークンをチャンネル方向に連結して、1x1の畳み込みで、チャンネル方向に畳み込みを行うなどして効率化した方がよさそうである。

位置エンコーダも、将棋では絶対的な位置よりも相対的な位置関係が重要なため、Relative Position Representationsなどを使用した方がよさそうである。
Leela Chess Zeroでは、チェスの駒の関係をとらえたSmolgenという独自の位置エンコーダを実装している。こちらも参考にしたい。

まとめ

入力特徴量の作成処理を実装して、ベースラインとして単純なTransformerで学習を試した。
結果、Transformerよりも、ResNetの方が精度が高いという結果になった。
次回は、出力層の構成を見直して実験を行いたい。