TadaoYamaokaの日記

山岡忠夫 Home で公開しているプログラムの開発ネタを中心に書いていきます。

将棋でディープラーニングする その4(ネットワーク構成の変更)

本日から世界コンピュータ将棋選手権が始まりましたね。
一次予選を参加者の方の生放送で見ていました。

今回からPonanza Chainer以外にもディープラーニングを取り入れて参加している方がちらほらいるようです。
こちらの方のアピール文章に、ネットワーク構成について記述がありました。
http://www2.computer-shogi.org/wcsc27/appeal/GANShogi/wcsc27.pdf

私が以前に試したネットワーク構成と似ていますが、フィルターサイズを複数種類用意しているようです。
また、出力を駒の移動先だけではなく、移動元も予測しているようです。

上記のアピール文書では一致率が50%程度になっているが、私の検証用ネットワークによる実験では3エポックで一致率が25%程度にしかなっていない。
入力特徴が最小限であること、学習のエポック数が少なくプラトー状態から抜け出ていない可能性があることが考えらる。
手番などの効果的な入力特徴を加えて、さらに学習することで50%程度の一致率まで上げられそうである。

さて、以前に将棋でディープラーニングを途中まで試した後しばらく放置していましたが、ニューラルネットワークの構成について気になる点があったので以下の通り変更を試してみました。

変更内容

前回DNNで実装した指し手を予測するモデルについて、持ち駒を値が2値の9×9の画像として入力していたが、全て同じ値の画像の畳み込みを行ってもすべて同じ値の結果になるため、計算に無駄がある。
また、エッジ部分ではパディングを行うため、悪影響がある。

そのため、持ち駒の入力の与え方を9×9ではなく、1×1の画像とし畳み込みの結果を9×9に引き延ばすように変更した。
画像を同じ値で引き延ばすのは、Chainerのunpooling_2dで行った。

このように変更した理由は、今後入力特徴に手番を加えたいが、手番の数値をOneHotエンコーディングすると、入力画像の枚数がかなり増えてしまうため省力化が必要と考えたためである。
効果の確認のためまずは持ち駒について変更を行った。

ネットワーク構成
class MyChain(Chain):
    def __init__(self):
        super(MyChain, self).__init__(
            l1_1=L.Convolution2D(in_channels = None, out_channels = k, ksize = 3, pad = 1),
            l1_2=L.Convolution2D(in_channels = None, out_channels = k, ksize = 1), # pieces_in_hand
            l2=L.Convolution2D(in_channels = k, out_channels = k, ksize = 3, pad = 1),
            l3=L.Convolution2D(in_channels = k, out_channels = k, ksize = 3, pad = 1),
            l4=L.Convolution2D(in_channels = k, out_channels = k, ksize = 3, pad = 1),
            l5=L.Convolution2D(in_channels = k, out_channels = k, ksize = 3, pad = 1),
            l6=L.Convolution2D(in_channels = k, out_channels = k, ksize = 3, pad = 1),
            l7=L.Convolution2D(in_channels = k, out_channels = k, ksize = 3, pad = 1),
            l8=L.Convolution2D(in_channels = k, out_channels = k, ksize = 3, pad = 1),
            l9=L.Convolution2D(in_channels = k, out_channels = k, ksize = 3, pad = 1),
            l10=L.Convolution2D(in_channels = k, out_channels = k, ksize = 3, pad = 1),
            l11=L.Convolution2D(in_channels = k, out_channels = k, ksize = 3, pad = 1),
            l12=L.Convolution2D(in_channels = k, out_channels = k, ksize = 3, pad = 1),
            l13=L.Convolution2D(in_channels = k, out_channels = len(shogi.PIECE_TYPES), ksize = 1, nobias = True),
            l13_2=L.Bias(shape=(9*9*len(shogi.PIECE_TYPES)))
        )

    def __call__(self, x1, x2):
        u1_1 = self.l1_1(x1)
        u1_2_1 = self.l1_2(x2)
        u1_2_2 = F.unpooling_2d(u1_2_1, 9, outsize=(9, 9))
        h2 = F.relu(self.l2(h1))
        h3 = F.relu(self.l3(h2))
        h4 = F.relu(self.l4(h3))
        h5 = F.relu(self.l5(h4))
        h6 = F.relu(self.l6(h5))
        h7 = F.relu(self.l7(h6))
        h8 = F.relu(self.l8(h7))
        h9 = F.relu(self.l9(h8))
        h10 = F.relu(self.l10(h9))
        h11 = F.relu(self.l11(h10))
        h12 = F.relu(self.l12(h11))
        h13 = self.l13(h12)
        return self.l13_2(F.reshape(h13, (len(h13.data), 9*9*len(shogi.PIECE_TYPES))))

効果確認

変更前後で、前回プロの棋譜から学習した結果と比較したところ、以下の通りとなった。

422852局面を3エポック学習した結果
学習時間 train loss test accuracy
変更前 0:37:48 3.578415275 0.24140625
変更後 0:37:51 3.35968709 0.27265625

※train lossとtest accuracyは最後20イテレーション(100ミニバッチごと)の平均

計算量は減っているはずだが計算時間はわずかに増えている。
1×1の畳み込みの計算と、unpooling_2dの計算で2ステップ必要になるため実行時間に影響したと思われる。

train lossは減って、test accuracyが増えているので、パディングの悪影響が減らせたかもしれない。

変更その2

1×1の畳み込みの計算と、unpooling_2dの計算で2ステップ必要になるのが計算時間のロスになるので、入力は9×9として、フィルターサイズを1×1としてみた。
このようにすることで、多少計算時間を改善して、パディングの悪影響を抑えることができる。

ネットワーク構成
class MyChain(Chain):
    def __init__(self):
        super(MyChain, self).__init__(
            l1_1=L.Convolution2D(in_channels = None, out_channels = k, ksize = 3, pad = 1),
            l1_2=L.Convolution2D(in_channels = None, out_channels = k, ksize = 1), # pieces_in_hand
            l2=L.Convolution2D(in_channels = k, out_channels = k, ksize = 3, pad = 1),
            l3=L.Convolution2D(in_channels = k, out_channels = k, ksize = 3, pad = 1),
            l4=L.Convolution2D(in_channels = k, out_channels = k, ksize = 3, pad = 1),
            l5=L.Convolution2D(in_channels = k, out_channels = k, ksize = 3, pad = 1),
            l6=L.Convolution2D(in_channels = k, out_channels = k, ksize = 3, pad = 1),
            l7=L.Convolution2D(in_channels = k, out_channels = k, ksize = 3, pad = 1),
            l8=L.Convolution2D(in_channels = k, out_channels = k, ksize = 3, pad = 1),
            l9=L.Convolution2D(in_channels = k, out_channels = k, ksize = 3, pad = 1),
            l10=L.Convolution2D(in_channels = k, out_channels = k, ksize = 3, pad = 1),
            l11=L.Convolution2D(in_channels = k, out_channels = k, ksize = 3, pad = 1),
            l12=L.Convolution2D(in_channels = k, out_channels = k, ksize = 3, pad = 1),
            l13=L.Convolution2D(in_channels = k, out_channels = len(shogi.PIECE_TYPES), ksize = 1, nobias = True),
            l13_2=L.Bias(shape=(9*9*len(shogi.PIECE_TYPES)))
        )

    def __call__(self, x1, x2):
        u1_1 = self.l1_1(x1)
        u1_2 = self.l1_2(x2)
        h1 = F.relu(u1_1 + u1_2)
        h2 = F.relu(self.l2(h1))
        h3 = F.relu(self.l3(h2))
        h4 = F.relu(self.l4(h3))
        h5 = F.relu(self.l5(h4))
        h6 = F.relu(self.l6(h5))
        h7 = F.relu(self.l7(h6))
        h8 = F.relu(self.l8(h7))
        h9 = F.relu(self.l9(h8))
        h10 = F.relu(self.l10(h9))
        h11 = F.relu(self.l11(h10))
        h12 = F.relu(self.l12(h11))
        h13 = self.l13(h12)
        return self.l13_2(F.reshape(h13, (len(h13.data), 9*9*len(shogi.PIECE_TYPES))))

効果確認

学習時間 train loss test accuracy
変更前 0:37:48 3.578415275 0.24140625
変更後(unpooling_2d) 0:37:51 3.35968709 0.27265625
変更後(ksize=1) 0:36:36 3.346870267 0.26328125

計算時間は少しだけ短くなっている。
unpoolingを行った場合より、train lossは減って、test accuracyも減っているが、行っていることは同じであるため、誤差の範囲と思われる。
変更前にくらべたらパディングの悪影響が減らせていると思われる。

以上の実験より、持ち駒の入力層の畳み込みは9×9の2値画像として入力し、カーネルサイズ1とすることで、計算時間を短縮できパディングの悪影響を減らすことができる。

手番の特徴を加える場合も、同じようにカーネルサイズを1とした畳み込みにするのがよいと考えられる。
手番は1~200以上の値をとるが、入力特徴の枚数が増えすぎると計算時間も増えるため、序盤、中盤、終盤を数分割するくらいがよいと思う。
AlpahGoの論文でも手番の特徴は8枚となっている。
8枚をどのような手番の範囲に割り振っているかは残念ながら論文には記述されていない。

GitHubのソースにも反映しました。
github.com