TadaoYamaokaの開発日記

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

将棋AIの進捗 その20(自己対局による強化学習)

自己対局による強化学習を続けています。
現在、1サイクルあたり500万局を自己対局で生成するサイクルを17サイクル実行したところです。
教師ありでelmoで深さ8で生成した4.9億局面を事前学習したモデルを初期モデルとしています。
初期モデルは、収束前のLesserkaiには勝つがGPSfishには1度も勝てない程度のモデルです。

数サイクル回した時点では、初期モデルに対して弱くなっていましたが、17サイクル回したところで、やっと初期モデルに対して1手3秒の50回の対局で有意水準5%で強くなったことが確かめられました。
f:id:TadaoYamaoka:20180424235457p:plain:w350

しかし、GPSFishには1度も勝てていません。
1サイクルの実行にGPU3枚使って、3日かかっているので、レーティングを上げるのは気の長い作業になりそうです。

AlphaZeroの論文では、elmoのレーティングに達するのに110k stepsとなっています。
f:id:TadaoYamaoka:20180424234831p:plain

私のリソースでは、1 stepの実行に3日かかるので、1k stepsには333日、110k stepsには100年かかるため、AlphaZeroと同じレーティングの上昇傾向だと実現性がありません。
AlphaZeroと比較すると、

  • 特徴量に利き数と王手を使っていること
  • モデルサイズが小さいこと
  • 初期局面集を使っていること
  • 方策ネットワークの学習にREINFORCE Algorithmを使っていること
  • 価値ネットワークの学習にブートストラップ法を使っていること

という違いがあるので、収束が速くなっていればよいのですが、まだわかりません。

もう少し自分で検証して、間違いなくレーティングが上昇することが確かめられたらleela zeroのようにコミュニティの力を借りることを検討したいと思っています。

世界コンピュータ選手権は、今のままでは弱すぎるので、教師ありで学習したモデルを主に使うことになりそうです。
マルチGPUの1枚を強化学習したモデルにしてアンサンブルにすることも考えています。

Chainer4系がAnaconda3 4.2.0で動かない件

4/17にChainer 4.0.0がリリースされましたが、Anaconda3 4.2.0では以下のエラーがでて動かなくなっていました。

>>> import chainer
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "C:\Anaconda3\lib\site-packages\chainer\__init__.py", line 28, in <module>
    from chainer import training  # NOQA
  File "C:\Anaconda3\lib\site-packages\chainer\training\__init__.py", line 2, in <module>
    from chainer.training import extensions  # NOQA
  File "C:\Anaconda3\lib\site-packages\chainer\training\extensions\__init__.py", line 30, in <module>
    from chainer.training.extensions.variable_statistics_plot import VariableStatisticsPlot  # NOQA
  File "C:\Anaconda3\lib\site-packages\chainer\training\extensions\variable_statistics_plot.py", line 16, in <module>
    _plot_color = matplotlib.colors.to_rgba('#1f77b4')  # C0 color
AttributeError: module 'matplotlib.colors' has no attribute 'to_rgba'

原因は、chainerがmatplotlibのバージョン2系を前提にしているためです。

Anaconda3 4.2.0のmatplotlibのバージョンは、「pip freeze」で調べると、

matplotlib==1.5.3

となっていました。

回避策

とりあえずは、バージョンを指定してcupyとchainerをインストールすることで回避できます。

インストール済みのcupyとchainerをアンインストール
>pip uninstall cupy
>pip uninstall chainer 
バージョンを指定してインストール
>pip install cupy==2.4.0 --no-cache-dir
>pip install chainer==3.4.0 --no-cache-dir 

本格対処

Anacondaの最新版である5.1はmatplotが2系になっているので、Anaconda 5.1にすることが対処になります。
以前は、WindowsでPython3.6系のAnadondaを使用すると問題が起きましたが、現在は問題が解決されていました。

Anaconda3 4.2.0をアンインストール

プログラムの追加と削除から「Python 3.5.2 (Anaconda3 4.2.0 64-bit)」をアンインストールします。

Anadonda 5.1のダウンロード

Anaconda | Individual Edition
からAnadonda 5.1をダウンロードします。

Anadonda 5.1のインストール

インストールオプションのInstall for:で、「Just Me」か「All Users」を選択するところで、「Just Me」が推奨になっていますが、ディレクトリ階層が深くなるので「All Users」を選択しました(要Admin権限)。

Advanced Optionsで、「Add Anaconda to the system PATH environment variable」がデフォルトでオフになっていましたが、コマンドラインpythonコマンドを利用したいのでオンにしました。

pipをアップグレード
python -m pip install --upgrade pip
cupyインストール

以前はWindowsのPython3.6環境でcupyをインストールすると、UnicodeDecodeErrorが発生したが、pipの最新版では問題が改修されておりエラーがでなくなっていました。

>pip install cupy --no-cache-dir

インストール中に以下のエラーが出力されますが、無視しても問題ありません。

notebook 5.4.0 requires ipykernel, which is not installed.
jupyter 1.0.0 requires ipykernel, which is not installed.
jupyter-console 5.2.0 requires ipykernel, which is not installed.
ipywidgets 7.1.1 requires ipykernel>=4.5.1, which is not installed.
chainerインストール
>pip install chainer --no-cache-dir
MNISTサンプル実行

動作確認のためにMNISTサンプルを実行しました。

>git clone https://github.com/chainer/chainer.git
>cd chainer
>cd examples
>cd mnist
>python train_mnist.py -g 0

問題なく実行できました。

C:\Anaconda3\lib\site-packages\h5py\__init__.py:36: FutureWarning: Conversion of the second argument of issubdtype from `float` to `np.floating` is deprecated. In future, it will be treated as `np.float64 == np.dtype(float).type`.
  from ._conv import register_converters as _register_converters
GPU: 0
# unit: 1000
# Minibatch-size: 100
# epoch: 20

epoch       main/loss   validation/main/loss  main/accuracy  validation/main/accuracy  elapsed_time
1           0.191216    0.0942012             0.941517       0.9687                    11.5416
2           0.0722713   0.0799588             0.976866       0.9756                    14.6657
3           0.0487604   0.0722168             0.984582       0.9789                    17.4512
4           0.0362529   0.0650514             0.988115       0.9813                    20.3503
5           0.0274124   0.077619              0.990649       0.9808                    23.1016
6           0.0228435   0.0733697             0.992465       0.9821                    25.9697
7           0.0226351   0.0899091             0.992865       0.9786                    28.7722
8           0.0155136   0.0836356             0.994665       0.9809                    31.6092
9           0.0160265   0.106787              0.994949       0.9754                    34.4846
10          0.0164888   0.0899379             0.994832       0.979                     37.3887
11          0.0104961   0.122631              0.996966       0.9771                    40.3375
12          0.013743    0.0798696             0.995699       0.9823                    43.1569
13          0.0114804   0.121284              0.996465       0.9773                    46.0298
14          0.0146573   0.0919883             0.995599       0.9831                    48.8832
15          0.0115368   0.100213              0.996949       0.9808                    51.6212
16          0.00518552  0.105182              0.998316       0.9822                    54.3656
17          0.0141003   0.0964787             0.995815       0.9831                    57.1719
18          0.00718431  0.101884              0.998049       0.982                     59.9831
19          0.00747319  0.103948              0.997882       0.9816                    62.7022
20          0.00940562  0.126561              0.997032       0.9809                    65.5354

1行目に警告がでますが、h5pyがNumpyの廃止予定の型を使用しているためですが、特に問題ありません。

2018/8/27追記

以下の警告は、h5pyをアップグレードすることで抑止できます。

C:\Anaconda3\lib\site-packages\h5py\__init__.py:36: FutureWarning: Conversion of the second argument of issubdtype from `float` to `np.floating` is deprecated. In future, it will be treated as `np.float64 == np.dtype(float).type`.
  from ._conv import register_converters as _register_converters

h5pyは現在2.8.0がリリースされており、Anaconda 5.1のNumpyのバージョンに対応しています。

バージョン確認
>pip search h5py
h5py (2.8.0)          - Read and write HDF5 files from Python
  INSTALLED: 2.7.1
  LATEST:    2.8.0
アップグレード
>pip install -U h5py

【告知】技術書典4

4/22(日)に秋葉原で開催される技術書典4で、「ディープラーニングを使った将棋AIの作り方2~大規模学習、高速化編~」という本を出します。
場所は「き15」になります。

書籍「将棋AIで学ぶディープラーニング」の第13章の補足的な内容です。
頒布は紙の本のみです。

f:id:TadaoYamaoka:20180420000640p:plain
f:id:TadaoYamaoka:20180420000711p:plain

マイナビ出版様(い08)のところで書籍の割引販売もされるようですので、よろしければそちらもお願いいたします。

将棋AIの進捗 その19(初期局面集)

自己対局による強化学習を行う際に、対局の開始局面には、初期局面集を使用している。
AlphaZeroでは、固定手数まではノイズを加えルートノードの訪問回数に応じた確率で手を選択することで局面の多様性を確保している。
しかし、この方法ではモデルに依存した限られた序盤進行になってしまうため、初期局面集を使う方が良いと考えている。
実際のところは、どちらが良いかは検証してみないとはっきりはわからない。

他の将棋AIでも決まった方法があるわけではなさそうだ。
Aperyでは、初期局面集に1手ランダムムーブした局面から生成していて、やねうら王では20手までランダムムーブを行っているようだ。

第5回電王トーナメント版のdlshogiを学習したときは、elmo_for_learnで教師局面を生成した。
そのときは、Aperyのroots.hcpにネット集めた棋譜の85手までの局面を足して、重複局面を排除したものを使用した。
elmo_for_learnは、初期局面集からさらに6手ランダムムーブを行っていたので、1~6手の範囲で一様分布で選んだ手数だけランダムムーブするように変更した。
ランダムムーブしない初期局面を1回ずつ使用した局面も生成した。
AlphaZero方式で自己対局を行う際は、Aperyと同じ方法を使うようにしている。

elmo_for_learnで教師局面を生成すると、重複局面が4%近く生成される。
また、ディープラーニングでモデルを学習すると局面を使い切る前に収束する。
このことから、教師局面の多様性があまり確保できていないのではないかという疑いを持っている。

ディープラーニングは序盤は強いが、中終盤で悪手が増えるので、学習局面に中終盤のバリエーションがもっと増やしたいと考えている。
これを初期局面を増やすことで対策できないか試している。

最近の平岡さんのツイートで、初期局面をフィッシャーランダムで生成すると弱くなったという情報があった。


初期局面の多様性は重要だがランダムではかえって弱くなるようだ。
実際の対局で起こりそうな局面に近いことも重要なのかもしれない。

AlphaGoとイセドル九段の対局の第4局の敗北は、Policyの予測漏れに起因している。
これも中終盤の学習局面のバリエーション不足が原因になっている。
AlphaGo Masterでは弱点を克服しているが、どのように学習したかは明らかにされていない。(敵対的ネットワークを使ったとか使ってないとか)

ということで、初期局面集のバリエーションを増やすために、floodgateの2013年から2017年の5年分の棋譜をもとに、開始局面から棋譜上の評価値が1000点以内の手数までの局面を初期局面集に加えた。
重複局面を除くと、17,391,142局面が生成できた。
Aperyのroots.hcpは重複局面も含んでいるので、重複局面を除くと、45,945,496局面が含まれる。
それからすると少ない。Aperyは相当な数の棋譜を集めてroots.hcpを作成しているようだ。

Aperyのroots.hcpとfloodgateの棋譜から生成した局面集を合わせて重複局面を除くと、52,078,876局面になった。
6,133,380局面を増やすことできた。
効果があるか比較検証したいが、自己対局に時間がかかりすぎて比較検証はできそうにない。
検証は将来の課題として、勘を信じることにする。


初期局面集作成に使ったツール類を公開しています。
github.com

将棋AIの進捗 その18(スケーラビリティ)

AWSのp3.8xlargeインスタンスを試験的に借りてGPUを増やした場合の性能を測定しました。
Linuxだとマルチスレッドの性能がでないので、OSはWindowsです。

p3.8xlargeのマシンスペックは以下の通りです。

Tesla V100 GPUs 4
vCPUs 32
Main Memory 244GiB

GPUに割り当てる探索スレッド255として、GPUを増やしながら平手初期局面での探索速度(シミュレーション/秒)を測定しました。

GPU枚数 探索速度(シミュレーション/秒)
1 6344
2 7545
3 7757
4 8168

f:id:TadaoYamaoka:20180404230913p:plain
GPUを増やすほど、探索速度が上がっていますが線形には伸びていません。

スレッド数

GPU4枚で、GPUあたりの探索スレッドを変えて測定しました。

スレッド数 探索速度(シミュレーション/秒)
255 8168
192 8790
168 8923
128 8179

スレッド数が168のときに最大となりました。

スレッド数を変えてGPU枚数変更

スレッド数を168として、GPU枚数を変更しました。

GPU枚数 探索速度(シミュレーション/秒)
1 5839
2 9696
3 8921
4 8923

GPU2枚が最大となりました。

GPU2枚でスレッド数を変更

GPU2枚でスレッド数を変えて測定しました。

スレッド数 探索速度(シミュレーション/秒)
255 7545
192 9118
168 9696
128 7263

スレッド数168のときが最大となりました。

考察

GPU枚数とスレッド数を単純に増やしても探索速度は上がりませんでした。
GPU枚数とスレッド数のバランスが取れている時が探索速度が最大となっています。
スケーラビリティが頭打ちになった原因は、スレッド間でノード情報を共有するツリー並列化を行っているため、スレッド数が増えるほど競合が発生するため、効率が悪くなったためと推測できます。
ツリー並列化の方式では、GPUは2枚以上にしても探索速度の向上は期待できそうにないことがわかりました。

スケーラビリティを上げるには、マルチプロセスまたはマルチノードでルート並列化を行う必要がありそうです。

なお、AWSではなく2枚のGPU(Titan Vと1080Ti)にCore i9 10コアのPCだと、GPUあたりの探索スレッド数255で、10779シミュレーション/秒の速度がでています。
5月の大会はGPU2枚のPCでの参加の方向になりそうです。

将来的には、マルチノード構成を検討することにします(5月の大会には間に合いそうにない)。

2018/4/5 追記

自己対局をp3.8xlargeでGPU数のプロセスで実行した場合の生成速度は、7.61+8.85+9.08+6.56=32.1局面/秒となった。
GPU2枚のPCだと2プロセスで11.26+8.40=19.66局面/秒なので、AWSを使うと1.63倍の速度で生成できる。

LinuxとWindowsのマルチスレッド性能

将棋AIをAWSで動かそうとLinux対応しましたが、Linuxでマルチスレッドの性能がでないため、いろいろ実験してみました。

検証している将棋AIではGPUの計算が終わったら、待機中の複数の探索スレッドに通知する処理を行っています。
それを、以下のような処理で実装していましたが、Linux(Ubuntu 16.04)ではWindowsの1/10以下の探索速度になってしまいます。

探索スレッド側
while (uct_node[child_index].evaled == 0)
	this_thread::sleep_for(chrono::milliseconds(0));
GPUスレッド側
uct_node[index].evaled = 1;

探索スレッド数は255です。evaled はatomicです。

探索速度(シミュレーション/秒)の比較は、以下の通りです。

Windows 6305
Linux 561

sleep時間変更

はじめ、GPUまわりのコードを疑いましたが、GPUの処理を外しても変わらないため、マルチスレッドに起因する問題と推測し、探索スレッド側でビジーループしている箇所を0ミリ秒のsleepから1ミリ秒のsleepに変更してみました。
すると以下の通り少しだけ改善されました。

Windows 5190
Linux 1354

Windowsでは逆に探索速度が落ちています。

yieldに変更

今度は、0ミリ秒のsleepの代わりにyiledに変更してみました。

while (uct_node[child_index].evaled == 0)
	this_thread::yield();

今度はだいぶ改善されました。

Windows 6301
Linux 2258

Windowsは0ミリ秒のスリープと変わりません。
Linuxはだいぶ改善されましたが、Windowsの半分以下です。

condition_variableに変更

通知の処理を、ビジーループからcondition_variableに変更しました。

探索スレッド側
std::unique_lock<std::mutex> lk(mtx);
while (uct_node[child_index].evaled == 0)
	cond.wait(lk);
GPUスレッド側
uct_node[index].evaled = 1;
cond.notify_all();

探索速度は、以下の通りとなりました。

Windows 1823
Linux 2769

Linuxは今までで一番よい値になりましたが、やはりWindowsのビジーループの場合の半分以下です。
Windowsはcondition_variableを使うと、ビジーループの場合の29%まで落ち込みました。

考察

以上の実験結果から、Linuxではビジーループよりもcondition_variableの方がマルチスレッドの性能がよさそうです。
しかし、Windowsでビジーループする場合の半分以下の性能しかでません。
Windowsでは、condition_variableを使うと、ビジーループよりも性能が低下します。

LinuxWindowsと同等のマルチスレッドの性能を出す方法は今のところ不明です。
AWSを借りる場合、料金は上がりますが、Windowsを借りることになりそうです・・・。
どなたかLinuxでもマルチスレッドの性能について知見をお持ちの方がいたらアドバイスいただけると嬉しいです。

将棋AIの進捗 その17(AWS対応を検討)

世界コンピュータ選手権の参加者のマシンスペックをみると、マシンスペック高すぎです( ゚Д゚)

GPUを2枚詰んだ個人のPCで参加しようと思っていましたが、GPU8枚とかで来られたらモデルと探索の性能ではどうにもならなそうです。
モンテカルロ木探索は並列化の効果が高く、私の実験でも、GPUを2枚にするだけで、GPSfishに対して勝利が40%から75%(R+261)に上がっています。
8枚になると単純計算で、R+2088です(実際は線形には伸びませんが)。

ということで、AWSでp3.16xlargeを借りて参加することを検討しています。

AWSで参加することを考慮して、cuDNN対応したコードをLinuxで動かせるようにしました。
しかし、Ubuntu 16.04 LTSをCore i9で動かすと、マルチスレッドの性能がでず、探索速度がWindowsの半分になります。
GPUの推論処理をコメントアウトしても変わらないので、Ubuntu 16.04 LTSのカーネルが古いためCore i9に対応していないためと予想しています。
AWSXeonなので、性能劣化しないと思っていますが、事前に実験しておく必要がありそうです。
GPUインスタンスは高いので実験するにもお財布が痛みます・・・


あと、AWSを使う場合は、参加者の皆さんはどのように実行されているのでしょうか。
現地にノートPCを持ち込んで、ノートPC上でsshコマンドをバッチファイルにして、将棋所に登録すればよいと思っていますが、どういう方法がメジャーなのでしょうか。
ノートPCを経由するので多少のラグはありますが、大会のサーバがLAN内にあると、AWSから直接というわけにもいかないと思うので、この方法になるのではないかと思っています。

2018/4/3 追記

AWSを試験的に使ってみたのですが、AWSでも遅かったので原因を調べたところ、Linuxで遅くなる原因が判明しました。
ニューラルネットワークの実行を待機する処理を

	while (uct_node[current].evaled == 0)
		this_thread::sleep_for(chrono::milliseconds(0));

のように記述していましたが、これがWindowsだとスレッドの実行をスイッチしてくれますが、LinuxだとCPUを100%消費してスレッドが切り替わらないためでした。

	while (uct_node[current].evaled == 0)
		this_thread::sleep_for(chrono::milliseconds(1));

にすると、速くなったのですが、1ミリ秒のスリープが無駄なのでスピンロックするのはやめて、condition_variableを使った実装に修正する予定です。

2018/4/4 追記

Twitterでの開発者同士のやり取りで以下の方法で可能ということなので、情報を載せておきます。
SSHのコマンドをバッチファイルに記述することで、リモートでUSIエンジンを動かせるようです。
http://ai65536.hatenablog.com/entry/2015/03/31/190904ai65536.hatenablog.com