TadaoYamaokaの開発日記

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

将棋でディープラーニングする その42(ValueNetの出力をtanhにする)

将棋AIでは、評価関数をsigmoid関数で[0,1]の範囲で勝率にすることが行われている。
elmoの損失関数には、勝率の交差エントロピーと、浅い探索と深い探索の評価値から求めた勝率の交差エントロピーの和が使われている。

一方、AlphaGoでは報酬に[-1,1]が使用されており、ValueNetworkの出力にはtanhが使われている。
損失関数には、平均二乗誤差(MSE)が使用されている。

tanhを使用している理由は論文では特に説明されていないが、tanhの方が強い勾配が得られるそうだ。
machine learning - tanh activation function vs sigmoid activation function - Cross Validated

そこで、ValueNetworkの出力をtanhにして損失関数をMSEした場合と、出力をsigomidにして損失関数を交差エントロピーとした場合で比較してみた。
tanhの場合、ブートストラップ項(ValueNetworkの出力と深い探索の評価値の差)の損失関数もMSEとした。

tanhとsigmoidの比較

測定条件
  • 訓練データ:7000万局面
  • テストデータ:100万局面
  • 1000万局面ごとに評価
  • 学習率:0.01、0.001
  • モーメントパラメータ:0.9
  • ミニバッチサイズ:64
  • policyとvalueマルチタスク学習
  • policyは勝敗をアドバンテージにした強化学習
  • valueは勝敗をelmoの探索結果でブートストラップして学習

※評価関数からtanhへの変換はこの記事を参照

average train loss

f:id:TadaoYamaoka:20171106214552p:plain
※policyとvalueの損失の合計

test loss

f:id:TadaoYamaoka:20171106214621p:plain
※policyとvalueの損失の合計

test accuracy policy

f:id:TadaoYamaoka:20171106214637p:plain

test accuracy value

f:id:TadaoYamaoka:20171106214654p:plain

考察

tanhの場合、学習率が0.01の場合、発散して学習できなかった。
学習率を0.001として測定した。
sigmoidも併せて学習率0.01の他に0.001でも測定した。

train loss、test lossはtanhの方が、sigmoidの場合より大きくなっているが、値の範囲が[-1,1]になったことで、損失関数のスケールが2倍になっていることが影響している。

policyのtest accuracyは、tanhの方がsigmoidの場合より大きくなっている。
valueのtest accuracyは、tanhの方がsigmoidの場合より小さくなっている。
tanhの場合、policyの損失関数は交差エントロピーで、valueの損失関数がMSEなので、policyとvalueの損失の比率が、sigmoidのときと変わったことが影響していると思われる。

どちらが良いともはっきりと言えない結果だが、同じ学習率(0.001)では、valueのtest accuracyがほぼ同じなのに対して、policyのtest accuracyはtanhの方が良くなっている。

sigmoidとtanhどちらがよいかは、別のデータセットを使って別途検証してみたい。

将棋でディープラーニングする その41(モーメントありSGD)

AlphaGo Zeroニューラルネットワークの学習の最適化に使用されているモーメントありSGDを将棋AIで試してみた。

以前に、最適化手法を比較した際、Adamのような学習率を自動で調整する手法よりSGDの方が学習効率が高かった。
AlphaGo FanバージョンでもSGDが使われている。

AlphaGo Zeroでは、SGDにモーメントを付けた、モーメントありSGDが使われている。
そこで、SGDとモーメントありSGDの比較を行った。

SGDとモーメントありSGDの比較

測定条件
  • 訓練データ:7000万局面
  • テストデータ:100万局面
  • 1000万局面ごとに評価
  • 学習率:0.01
  • モーメントパラメータ:0.9
  • ミニバッチサイズ:64
  • policyとvalueマルチタスク学習
  • policyは勝敗をアドバンテージにした強化学習
  • valueは勝敗をelmoの探索結果でブートストラップして学習
average train loss

f:id:TadaoYamaoka:20171103094720p:plain
※policyとvalueの損失の合計

test loss

f:id:TadaoYamaoka:20171103094740p:plain
※policyとvalueの損失の合計

test accuracy policy

f:id:TadaoYamaoka:20171103094758p:plain

test accuracy value

f:id:TadaoYamaoka:20171103094812p:plain

考察

比較結果から、モーメントありSGD(MomentumSGD)の方がSGDより、policy、valueともに学習効率が高いことがわかった。


電王トーナメント向けに35.8億局面学習したモデルでは3エポックの途中でtest accuracyが飽和したが、モーメントありにして3エポックをやり直したところ、SGDで飽和した値より学習が進むようになった。
現在、policyの一致率:46.1%、valueの一致率:78.2%になっており、約0.1%程改善している(学習継続中)。
SGDでは3エポックの途中で飽和した後さらに学習を続けると発散したが、モーメントありSGDでは発散していないので、間に合えば4エポック目の学習もできそうである。
※追記
とおもったが、やはり3エポック目の途中で飽和した後、発散した。

将棋でディープラーニングする その40(入力特徴に履歴追加)

その39からずいぶん期間が空きましたが、AlphaGo Zeroの論文を読んで試したいことができたので、AlphaGo Zeroの論文の方法が将棋AIに応用が可能か少しずつ試していこうと思います。

AlphaGo Zeroの特徴については、別の記事に記載していますので、参照してください。

AlphaGo Zeroでは、入力特徴は現局面を含む8手までの履歴局面の石の座標になっています。
入力特徴に履歴が必要な理由は、論文では囲碁にはコウがあるためと説明されています。

将棋にはコウはありませんが、駒の取り合いや千日手があるため、履歴の情報は有用と思われます。

前回までに作成したニューラルネットワーク(Wide ResNet、5ブロック、PolicyとValueの同時出力)の入力特徴に履歴を追加して試してみました。

入力特徴は、前回までと同様に駒の配置と持ち駒、王手がかかっているか、効き数の情報を8手まで持たせます。

学習局面は、elmo_for_learnで履歴も出力できるようにして探索の深さ8で生成しました。学習前に重複を排除してシャッフルしています。

※2017/11/3 データに誤りがあったので修正しました。

履歴あり/なしの比較結果

測定条件
  • 訓練データ:7000万局面
  • テストデータ:100万局面
  • 1000万局面ごとに評価
  • SGD(学習率:0.01)
  • ミニバッチサイズ:64
  • policyとvalueマルチタスク学習
  • policyは勝敗をアドバンテージにした強化学習
  • valueは勝敗をelmoの探索結果でブートストラップして学習
average train loss

f:id:TadaoYamaoka:20171103143117p:plain
※policyとvalueの損失の合計

test loss

f:id:TadaoYamaoka:20171103143137p:plain
※policyとvalueの損失の合計

test accuracy policy

f:id:TadaoYamaoka:20171103143210p:plain

test accuracy value

f:id:TadaoYamaoka:20171103143233p:plain

学習時間
時間/1000万局面
履歴あり 1:46:33
履歴なし 1:21:41

考察

予想に反して、履歴ありの方がpolicy、valueともにtest accuracyが小さくなっている。
将棋では囲碁のコウのように履歴の情報が重要になることが少ないことが関係しているかもしれない。
駒の取り合いや千日手となる局面においては精度がどう影響しているかは気になるが、今回の結果からは履歴は入れない方がよさそうである。

また、学習時間は、履歴を増やしたことで約1.3倍になった。

WindowsにChainer v3+CUDA9+cuDNN7をインストールする

Chainer v3(cupy v2)がCUDA9に対応したので、バージョンアップしました。
Chainer v3はcuDNNも最新のバージョン7に対応しているので、cuDNNも7にしました。

インストール手順は、以前のバージョンと同様です。

chainerをバージョンアップする際は、chainerとcupyをアンインストールしてからインストールします。

pip uninstall chainer
pip uninstall cupy
pip install cupy --no-cache-dir
pip install chainer --no-cache-dir

性能比較

CUDA8+cuDNN5.1と学習速度を比較しました。

比較対象として、将棋AIのResNet(5ブロック)の1000万局面の学習時間で比較しました。

CUDA8+cuDNN5.1 1:09:18
CUDA9+cuDNN7 1:07:16

※2回測定の平均
GPUは、GeForce 1080Ti

約3%学習速度が速くなりました。

なお、速度向上の要因がCUDA9によるものか、cuDNN6によるものか、cuDNN7によるものかは分かりません。

WindowsにAnaconda3でPython 3.6をインストールする

以前はChainerとTensorflowがPython3.6に対応していなかったため、Anaconda3のPython3.5系を使用していた。
現在は、ChainerもTensorflowもPython3.6に対応しているため、Python3.6に入れ替えを行った。

Anaconda | Individual Edition
から、Python 3.6 version(Anaconda3 5.0.1)をダウンロードして、インストールするだけだが、Python3.5系のAnaconda3 4.2.0との違いで戸惑った点を記載しておく。

スタートメニューに「Jupyter QTConsole」がなくなった

以前からWebベースのJupyer Notebookより、GUIで軽い「Jupyter QTConsole」を使用していた。
Anaconda3 5.0.1にしたところ、スタートメニューから「Jupyter QTConsole」がなくなってしまった。

対処

ファイル名を指定して実行から、「jupyter-qtconsole」を指定することで、起動できる。
f:id:TadaoYamaoka:20171027222241p:plain:w300

なお、裏でコマンドプロンプトが同時に起動するが気にしないようにする。

グラフがプロットできない

「Jupyter QTConsole」で、matplotlibでグラフを描画しようとしたら、

Out[2]: [<matplotlib.lines.Line2D at 0x1aa221a2550>]

とだけ表示されて、グラフが描画されない。

対処
%matplotlib

と入力すると、

Using matplotlib backend: Qt5Agg

と表示され、バックエンドが初期化されて描画できるようになる。

以下のようなスクリプトを実行すると、

import numpy as np
import matplotlib.pyplot as plt
x = np.arange(-3, 3, 0.1)
y = np.sin(x)
plt.plot(x, y)

別ウィンドウでグラフが表示される。
f:id:TadaoYamaoka:20171027215936p:plain:w300

cupyのインストールで、UnicodeDecodeErrorが発生

pipでcupyをインストールしようとすると以下のエラーが発生した。

>pip install cupy
Collecting cupy
  Using cached cupy-2.0.0.tar.gz
Exception:
Traceback (most recent call last):
  File "C:\Anaconda3\lib\site-packages\pip\compat\__init__.py", line 73, in console_to_str
    return s.decode(sys.__stdout__.encoding)
UnicodeDecodeError: 'utf-8' codec can't decode byte 0x83 in position 0: invalid start byte

(省略)
対処

以下のページを参考に、「C:\Anaconda3\lib\site-packages\pip\compat\__init__.py」を修正した。
Win + Python3.6で「pip install」を実行したときに「UnicodeDecodeError: 'utf-8' codec can't decode byte 0x83」と表示される。 - Qiita

修正後は正常にインストールできた。

※2018/4/23 追記
Anaconda 5.1で、pipをアップデートすると上記の問題が発生しなくなっていました。
Windows環境でもPython3.6が問題なく利用できるようになっています。

Chainerで計算グラフの可視化

Chainerの計算グラフの可視化機能を使ったことなかったので使ってみた。

将棋AIのPolicy networkとValue networkを結合したWide ResNetを可視化してみた。

dotファイル出力

Visualization of Computational Graph — Chainer 7.8.1 documentation
このページの説明通り、モデルを構築し順伝播を実行し、出力のVariableを使用して、.dotファイルを作成する。

import numpy as np
from chainer import Variable
from chainer import Chain
import chainer.functions as F
import chainer.links as L
import chainer.computational_graph as c

FEATURES1_NUM=62
FEATURES2_NUM=57
MAX_MOVE_LABEL_NUM=101
k = 192
dropout_ratio = 0.1
fcl = 256 # fully connected layers
class PolicyValueNetwork(Chain):
    def __init__(self):
        super(PolicyValueNetwork, self).__init__(
            l1_1_1=L.Convolution2D(in_channels = FEATURES1_NUM, out_channels = k, ksize = 3, pad = 1, nobias = True),
            l1_1_2=L.Convolution2D(in_channels = FEATURES1_NUM, out_channels = k, ksize = 1, pad = 0, nobias = True),
            l1_2=L.Convolution2D(in_channels = FEATURES2_NUM, out_channels = k, ksize = 1, nobias = True), # pieces_in_hand
            l2=L.Convolution2D(in_channels = k, out_channels = k, ksize = 3, pad = 1, nobias = True),
            l3=L.Convolution2D(in_channels = k, out_channels = k, ksize = 3, pad = 1, nobias = True),
            l4=L.Convolution2D(in_channels = k, out_channels = k, ksize = 3, pad = 1, nobias = True),
            l5=L.Convolution2D(in_channels = k, out_channels = k, ksize = 3, pad = 1, nobias = True),
            l6=L.Convolution2D(in_channels = k, out_channels = k, ksize = 3, pad = 1, nobias = True),
            l7=L.Convolution2D(in_channels = k, out_channels = k, ksize = 3, pad = 1, nobias = True),
            l8=L.Convolution2D(in_channels = k, out_channels = k, ksize = 3, pad = 1, nobias = True),
            l9=L.Convolution2D(in_channels = k, out_channels = k, ksize = 3, pad = 1, nobias = True),
            l10=L.Convolution2D(in_channels = k, out_channels = k, ksize = 3, pad = 1, nobias = True),
            l11=L.Convolution2D(in_channels = k, out_channels = k, ksize = 3, pad = 1, nobias = True),
            l12=L.Convolution2D(in_channels = k, out_channels = MAX_MOVE_LABEL_NUM, ksize = 1, nobias = True),
            l12_2=L.Bias(shape=(9*9*MAX_MOVE_LABEL_NUM)),
            # value network
            l12_v=L.Convolution2D(in_channels = k, out_channels = MAX_MOVE_LABEL_NUM, ksize = 1, nobias = True),
            l12_2_v=L.Bias(shape=(9*9*MAX_MOVE_LABEL_NUM)),
            l13=L.Linear(9*9*MAX_MOVE_LABEL_NUM, fcl),
            l14=L.Linear(fcl, 1),
            norm1=L.BatchNormalization(k),
            norm2=L.BatchNormalization(k),
            norm3=L.BatchNormalization(k),
            norm4=L.BatchNormalization(k),
            norm5=L.BatchNormalization(k),
            norm6=L.BatchNormalization(k),
            norm7=L.BatchNormalization(k),
            norm8=L.BatchNormalization(k),
            norm9=L.BatchNormalization(k),
            norm10=L.BatchNormalization(k)
        )

    def __call__(self, x1, x2):
        u1_1_1 = self.l1_1_1(x1)
        u1_1_2 = self.l1_1_2(x1)
        u1_2 = self.l1_2(x2)
        u1 = u1_1_1 + u1_1_2 + u1_2
        # Residual block
        h1 = F.relu(self.norm1(u1))
        h2 = F.dropout(F.relu(self.norm2(self.l2(h1))), ratio=dropout_ratio)
        u3 = self.l3(h2) + u1
        # Residual block
        h3 = F.relu(self.norm3(u3))
        h4 = F.dropout(F.relu(self.norm4(self.l4(h3))), ratio=dropout_ratio)
        u5 = self.l5(h4) + u3
        # Residual block
        h5 = F.relu(self.norm5(u5))
        h6 = F.dropout(F.relu(self.norm6(self.l6(h5))), ratio=dropout_ratio)
        u7 = self.l7(h6) + u5
        # Residual block
        h7 = F.relu(self.norm7(u7))
        h8 = F.dropout(F.relu(self.norm8(self.l8(h7))), ratio=dropout_ratio)
        u9 = self.l9(h8) + u7
        # Residual block
        h9 = F.relu(self.norm9(u9))
        h10 = F.dropout(F.relu(self.norm10(self.l10(h9))), ratio=dropout_ratio)
        u11 = self.l11(h10) + u9
        # output
        h12 = self.l12(u11)
        h12_1 = self.l12_2(F.reshape(h12, (len(h12.data), 9*9*MAX_MOVE_LABEL_NUM)))
        # value network
        h12_v = self.l12_v(u11)
        h12_2 = F.relu(self.l12_2_v(F.reshape(h12_v, (len(h12_v.data), 9*9*MAX_MOVE_LABEL_NUM))))
        h13 = F.relu(self.l13(h12_2))
        return h12_1, self.l14(h13)

model = PolicyValueNetwork()

features1 = np.empty((1, FEATURES1_NUM, 9, 9), dtype=np.float32)
features2 = np.empty((1, FEATURES2_NUM, 9, 9), dtype=np.float32)
x1 = Variable(features1)
x2 = Variable(features2)

y1, y2 = model(x1, x2)

g = c.build_computational_graph([y1, y2])
with open('graph.dot', 'w') as o:
    o.write(g.dump())

dotファイルの可視化

dotファイルは、Graphvizを使って可視化する。

Graphvizをインストール後、binディレクトリをPATHに追加する。
以下のコマンドでdotをpngに変換する。

dot -Tpng graph.dot -o graph.png
生成されたpngファイル

f:id:TadaoYamaoka:20171025215710p:plain

灰色の横長の8角形が値、青の四角がFunctionを示している。
値は、畳み込みのパラメータはW:、バイアスはb:が頭に付いているので区別できる。

GUIツール

また、pngに変換しなくても、Graphvizに含まれるgveditというGUIツールを使うと、dotファイルを開いて表示できる。
f:id:TadaoYamaoka:20171025220435p:plain


TensorBoardと比べると見やすいとは言えないが、コーディングしたネットワークに誤りがないかの確認にはなると思う。

AlphaGo Zeroの論文を読む その5(ドメイン知識)

前回までで、実装に必要な内容についてほぼ解説しました。

今回は、補足的な内容です。

ドメイン知識

論文には以下の一文が記載されている。

Our primary contribution is to demonstrate that superhuman performance can be achieved without human domain knowledge.

「私たちの主な貢献は、人間の知識なしに超人的なパフォーマンスが達成できることを示すことです。」

人間の知識を用いないということが、この技術が囲碁に特化しない汎用的な技術であることを示している。
それを明確にするために、使用したドメイン知識を列挙している。

使用したドメイン知識

  1. 囲碁のルール:
    • シミュレーションの終端状態でのスコア付け
    • ゲームの終了条件
    • 各局面での合法手
  2. MCTSシミュレーション中にTromp-Taylorスコアリング(曖昧さの無いルール)を使用
  3. 19×19のボードであること
  4. 回転と反転しても囲碁のルールが不変であること

以上の点を超えるドメイン知識は使用していない。
以前のAlphaGoでは、rollout policyやtree policyにドメイン知識やヒューリスティックを利用していたが、rollout policyやtree policyは使用していない。

合法手は一切除外していない。
従来のプログラムでは合法手でも駄目を詰めるといった無駄な手を除外していたが、そのようなことはしていない。

ニューラルネットワークアーキテクチャは、画像認識の最新技術に基づいており、それに応じて訓練用ハイパーパラメータを選択した。
MCTS探索パラメータは、予備実験で訓練されたニューラルネットワークを使って自己対局を行い、ガウス過程により最適化した。

感想

技術の汎用性を示すために、あえてゼロから訓練を行っているように感じました。
部分的にドメイン知識を利用した方が強くなる可能性もあると思いますが、単に強くすることだけが目的ではないのでしょう。
逆に考えれば、囲碁の知識も有効活用すれば、強くすることに関しては上回る可能性はあると思います。
ただ、AlpaGo Zeroは以前のAlphaGoより強くなっているので、下手な囲碁の知識は入れない方が良いとは思います。


今回で終了します。