TadaoYamaokaの開発日記

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

WSLでOpenAI Gymを動かしてVSCodeでデバッグする

OpenAI Gymのサンプルコードを調べたくてWSLで環境構築した際のメモです。

OpenAI GymはWindowsには対応していないため、Windowsで動かすにはWSL上のLinuxで動かす必要があります。
また、PythonコードをGUIデバッグしたい場合、Visual Studio Codeデバッグできると便利です。
その際、Windows上のVisual Studio CodeからWSLのPythonデバッグはできないため、WSLにVisual Studio Codeをインストールする必要があります。
WSL上でもX Window ServerをインストールすることでVisual Studio CodeGUIで実行できます。

以下、環境構築からサンプル実行までの手順です。

WSLのインストール

Microsoft StoreからUbuntu 18.04LTSをインストールする。

X Window Server

はじめXmingを試したが動作しなかったため、VcXsrvをインストールした。
https://sourceforge.net/projects/vcxsrv/

.bashrcの設定

Windows上のX Serverに接続できるように.bashrcに設定を追加する。

echo 'export DISPLAY=:0.0' >> .bashrc

sourceするかbashを再起動する。

VcXsrvの起動時オプション

VcXsrvを起動する際に、Native openglを有効にするとフルスクリーンでウィンドウ固定でしか起動しなかったため、オプションをオフにした。

Visual Studio Codeのインストール

公式ページの手順通りインストールする。
https://code.visualstudio.com/docs/setup/linux
Debian and Ubuntu based distributions

起動

VcXsrvを起動した状態で、

code

で起動する。

起動時に、以下のエラーが出力される場合、

/usr/share/code/bin/../code: error while loading shared libraries: libX11-xcb.so.1: cannot open shared object file: No such file or directory

以下のパッケージをインストールする。

sudo apt-get install gconf-service libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxss1 libxtst6 libappindicator1 libnss3 libasound2 libatk1.0-0 libc6 ca-certificates fonts-liberation lsb-release xdg-utils wget

参考:
How to fix puppetteer error ibX11-xcb.so.1 on Ubuntu | by Aran Cloverink Chananar | Medium

起動後、ExtentionsからPython拡張をインストールする。

Pythonインストール

Ubuntu 18.04ではPython3がデフォルトでインストールされている。
OpenAI Gymのサンプルを実行するため、追加でパッケージをインストールする。

sudo apt install python3-pip
pip3 install matplotlib
sudo apt-get install python-opengl
sudo apt-get install python3-tk

OpenAI Gymのインストール

pipからOpenAI Gymをインストールする。

pip3 install gym

Pytorchのインストール

PytorchのDQNのサンプルを動かしたかったので、Pytorchをインストールする。
インストール方法は公式の手順の通り。
WSLではCUDAは動作しないため、CUDAは選択しない。

pip3 install https://download.pytorch.org/whl/cpu/torch-1.1.0-cp36-cp36m-linux_x86_64.whl
pip3 install https://download.pytorch.org/whl/cpu/torchvision-0.3.0-cp36-cp36m-linux_x86_64.whl

サンプル実行

↓このサンプルを実行してデバッグする。
REINFORCEMENT LEARNING (DQN) TUTORIAL
Reinforcement Learning (DQN) Tutorial — PyTorch Tutorials 1.10.1+cu102 documentation

Visual Studio Codeで新規の.pyファイルを作成して、サンプルコードをコピーペーストする。
F5を押してデバッグ実行する。

問題なく実行できた。
f:id:TadaoYamaoka:20190701091608p:plain
f:id:TadaoYamaoka:20190630181428p:plain

あとは調べたい箇所にブレークポイントを設定して、変数の値などを調べればよい。

将棋AIの進捗 その30(NNキャッシュ)

先日、Leela Chess Zeroのソースを流用して、LRUキャッシュを実装したが、これを自己対局プログラムに組み込んだ。

はじめ、LRUキャッシュを1つにしてすべての探索スレッドで共有するようにしたが、ゲーム木の展開済みノードのNN計算結果が、他のスレッドの探索によって削除されることがあった。
キャッシュサイズを十分に大きくすればよいが、メモリ効率が悪いため、探索スレッドごとにNNキャッシュを保持するようにした。
それにより、必要なキャッシュサイズの見積もりが可能になる。

dlshogiの自己対局は、探索とNN計算の直列化を行っているため、必要なキャッシュサイズは、シミュレーション回数×バッチサイズとなる。

Leela Chess Zeroの実装では、キャッシュヒットした場合も順番を先頭にしないようになっていたため、上記の構成にしてもキャッシュが削除されることがあった。
そのため、キャッシュヒットした場合に、キャッシュ内の順番を先頭にもってきてゲーム木の展開済みノードのNN計算結果が削除されないようにした。
(Leela Chess Zeroではこの制御を行っておらず、どのように対処しているか気になっているがソースを読み切れていない。)

また、スレッドごとにNNキャッシュを保持するようになるため、排他制御のコードは削除した。

NNキャッシュの効果

NNキャッシュの実装前後で、局面生成速度は、以下の通りとなった。

NNキャッシュなし 41.50 nodes/sec
NNキャッシュあり 64.92 nodes/sec

NNキャッシュにより局面生成速度が、1.56倍になった。

余談

Leela Chess Zeroのソースを調べていたら、SE Netや、価値のブートストラップが実装されていて、dlshogiで行っているようなことは次々と試されているようだ。
issueで議論も活発に行われているので参考になる。
αβ探索の将棋ソフトは、Stockfishを参考に開発が行われているが、MCTSではlc0を参考にする流れになってきそうだ。

2019/6/25 追記

バックアップする際に親ノードをキーにしてキャッシュを調べていたバグがあり、修正して再測定したところ、以下の通りとなった。

NNキャッシュあり 47.46 nodes/sec

キャッシュなしの1.14倍程度であまり早くなっていない。
どれくらいキャッシュにヒットしているか調べたところ、49.2%ヒットしていた。
キャッシュヒット率から2倍くらいの生成速度になってもよいはずなので、遅すぎる。

ボトルネックを調べるためにVisual Studioのプロファイラで調査した。
f:id:TadaoYamaoka:20190625090030p:plain
2スレッドで1GPUを利用しているため、CPUによる探索の処理(Playout)とGPUによる推論処理(EvalNode)がバランスしている場合に、GPUの利用効率が高くなる。
プロファイラで調べた結果、CPU処理の方に時間がかかっており、GPUに待ちが発生していた。

次に、CPUによる探索処理を調べると以下のようになっていた。
f:id:TadaoYamaoka:20190625085943p:plain
末端ノードでの詰み探索処理(mateMoveInOddPly)にかなりの時間が費やされていた。

NNキャッシュにヒットした場合には詰み探索も実施済みのため、詰み探索を行う必要はないが行っていた。
これがキャッシュを導入しても速くならない原因だったようだ。

NNキャッシュヒットした場合に詰み探索をしないように修正

NNキャッシュにヒットした場合は、詰み探索を行わないように修正して再測定した結果、以下の通りとなった。

NNキャッシュあり 72.06 nodes/sec

キャッシュなしの1.74倍になっており、キャッシュの効果が表れている。
プロファイラの結果は以下の通りになった。
f:id:TadaoYamaoka:20190625091147p:plain
CPUの探索処理とGPUの推論処理がほぼバランスしており、GPUが効率的に使用されている。

NNの計算結果をキャッシュする

自己対局でノードの再利用しないようにしたが、NNの計算結果は再利用した方が効率がよいため、キャッシュの仕組みを導入したい。

並列で実行しているゲームすべてについて1ゲーム分のNNの結果を保持するにはメモリ容量が不足するため、使用されなくなった局面(古い手番で探索した局面)を削除する仕組みが必要になる。
その仕組みとして、単純な方法としては、ゲームごとに開始局面からの手数を記録しておき、局面の探索開始前に古い手数の局面を削除するようなことが考えられる。
しかし、一部のエントリを削除する場合、エントリの線形探索やハッシュエントリの再配置が必要になるため効率が良くない。

LRUキャッシュ

古いエントリを効率よく削除するデータ構造として、Least Recently Used(LRU) Cacheという仕組みがある。
オペレーティングシステムの教科書とかでよく出てくる。

C++のLRU Cacheの実装には、検索すればサンプルコードがいくつも出てくる。
ほとんどは、mapかunordered_mapと、リンクリストを使って実装されている。
boostでもlru_cache.hppに実装されている。
ただし、並列実行する自己対局で使用するには、スレッドセーフな実装が必要になる。


Leela Chess Zeroでも、NNのキャッシュの仕組みとしてLeast Recently Used(LRU) Cacheが使用されている。
スレッドセーフで、ゾブリストハッシュと組み合わせて使用できるように実装されている。
テンプレートで汎用的に実装されているため、将棋AIでもそのまま流用できそうなので、利用させてもらうことにする。

Leela Chess ZeroのLRU Cacheは以下のようにして使用する。

#include <iostream>
#include <vector>
#include <random>
#include "LruCache.h"

constexpr int cache_size = 8;

int main() {
	LruCache<int, float> cache(cache_size);

	std::mt19937 rnd;
	std::vector<int> key(10);
	for (int i = 0; i < 10; i++)
		key[i] = rnd();

	for (int i = 0; i < 10; i++)
		cache.Insert(key[i], std::move(std::make_unique<float>(float(i))));

	for (int i = 0; i < 10; i++) {
		LruCacheLock<int, float> cache_lock(&cache, key[i]);
		auto v = *cache_lock;
		std::cout << i << "\t";
		if (v == nullptr)
			std::cout << "null" << std::endl;
		else
			std::cout << *v << std::endl;
	}
}

※LruCache.hが、Leela Chess Zeroのcache.hを流用したソース

LRUキャッシュを作成するには、LruCacheのテンプレート引数の1つ目にキーの型、2つ目に格納する値の型を指定する。コンストラクタの引数にはキャッシュサイズを指定する。

Insertメソッドにキーと値を設定して、エントリを追加する。
値は、unique_ptrにする必要がある。

値を検索して使用する場合は、LookupAndPinメソッドで値を取り出して使用し、使用が終わった後Unpinで値を戻す。
LruCacheLockを使うとスコープ範囲内で、LookupAndPinとUnpinの呼び出しを自動化できる。

実行結果

上記コードの実行結果は以下のようになる。

0       null
1       null
2       2
3       3
4       4
5       5
6       6
7       7
8       8
9       9

キャッシュサイズが8で、10エントリを追加しており、古いエントリから削除されていることが確認できる。

将棋AIの実験ノート(入力特徴量の数値の表現方法)

AlphaZeroでは入力特徴量として持ち駒の枚数、手数をそのまま数値として与えている。

一方、Alpha Goでは、呼吸点などの数は、ont-hotエンコーディングして与えている。
例)上限が3で2の場合、010。

dlshogiでは、持ち駒の枚数、利きの数を、数値の分だけ1にして与えている。
例)上限が3で2の場合、110。

入力特徴量の数値の表現方法として、どれがよいか比較してみた。

測定方法

以前に測定したSENetと同じネットワークで、数値の表現方法のみ変更して比較する。

測定条件

  1. 現在のdlshogiと同じ表現方法(数値の数だけ1にする)。
  2. 数値をそのまま与える(AlphaZero方式)。※上限で割って正規化
  3. one-hotエンコーディングする(AlphaGo方式)

条件1は、以前の測定結果を使用(2500万局面を初期値から学習を14回行った平均
条件2と3は、同じ局面を初期値から学習を5回行った平均

測定結果

条件 訓練損失 (合計) テスト損失 (Policy) テスト損失 (Value) テスト損失 (評価値)
dlshogi方式 1.516394064 1.029514836 0.598087899 0.648289729
数値で入力 1.52924292 1.0321482 0.59702086 0.648769538
one-hotエンコーディング 1.68249998 1.08649676 0.69094248 0.707457938
テストaccuracy
条件 テスト accuracy (Policy) テスト accuracy (Value)
dlshogi方式 0.369727066 0.661483297
数値で入力 0.368871708 0.660558768
one-hotエンコーディング 0.35960994 0.569358166
テストデータの局面を推論した際の平均エントロピー
条件 エントロピー (Policy) エントロピー (Value)
dlshogi方式 2.11130705 0.631885641
数値で入力 2.11478008 0.629217708
one-hotエンコーディング 2.233401 0.626962894

考察

現在のdlshogiの方式がpolicyが一番精度が高くなり、valueは数値でそのまま入力した場合とほぼ同じという結果になった。
one-hotエンコーディングした場合は、policy、valueともに大きく精度が落ちている。

この結果からは、数値の分だけ1にする方式(dlshogi方式)が一番よさそうだ。
dlshogiでこの方式にした理由は、たとえば持ち駒の枚数の場合、1枚持っている場合と2枚持っている場合で、1枚目を持っているという状態は共通だが、one-hotエンコーディングすると1枚のときに1だった特徴面が0になるためニューラルネットワークにとってわかりにくいのでないかと思ったからだ。
ニューラルネットワークの気持ちになって考えた( ˙-˙ )

AlphaZeroが数値をそのまま入力するようにした理由は特に論文に書かれていなかったが、one-hotエンコーディングとの比較で結果が良かったからかもしれない。
また、AlphaZeroでも数値の分だけ1にする方式の方がよいかもしれない。

将棋AIの実験ノート(自己対局時のノード再利用)

以前に自己対局時にノードを再利用することで一時的に精度が上がったが、その後学習を続けるとpolicyのテスト損失が上昇する現象が起きた。

ノード再利用とバッチサイズの変更(1024→2048)を同時に行ったので、どちらが原因で発生したのかわからなかったため、バッチサイズを戻してノード再利用の有無で比較を行った。

比較のために以前と同じサイクル(173サイクル目から15サイクル)だけ、自己対局と学習のサイクルを回した(1か月以上かかった・・・)。

測定条件

  1. バッチサイズ2048 + ノード再利用あり (b2048_reuse) ※以前の条件
  2. バッチサイズ1024 + ノード再利用あり (b1024_reuse)
  3. バッチサイズ1024 + ノード再利用なし (b1024_noreuse)

測定結果

訓練損失(policy+value+eval)

f:id:TadaoYamaoka:20190608153648p:plain

テスト損失
  • policy

f:id:TadaoYamaoka:20190608153806p:plain

f:id:TadaoYamaoka:20190608153820p:plain

  • eval(評価値)

f:id:TadaoYamaoka:20190608153904p:plain

テストaccuracy
  • policy

f:id:TadaoYamaoka:20190608153928p:plain

f:id:TadaoYamaoka:20190608153939p:plain

テストデータを推論した際のエントロピー
  • policy

f:id:TadaoYamaoka:20190608154015p:plain

f:id:TadaoYamaoka:20190608154027p:plain

考察

ノード再利用あり

ノード再利用を行った場合、バッチサイズによらずほぼ同じ傾向になっている。
ノード再利用を行うと、valueのテスト損失が下がるが、policyのテスト損失はしばらく横ばい後上昇している。
評価値のテスト損失は上昇し続けており、勝率予測が0か1に近い方に偏る傾向が出ている。
テストデータを推論した際のエントロピーは、ノード再利用をするとpolicy、valueともに下降を続けており、方策と勝率予測が決定論的になっていることがわかる。

ノード再利用なし

ノード再利用を行わないと、policy、valueのテスト損失はともに徐々に下降しており学習が進んでいる。評価値のテスト損失もほぼ一定である。
テストデータを推論した際のエントロピーは、policyはわずかに上昇しており、強化学習により新しい手を学習できている。


この結果から、ノード再利用は行わない方が良いことがわかった。


AlphaZeroでは、ノード再利用を行っているため、学習方法の違い(方策を分布ではなく指し手を学習、正則化の有無)が影響しているものと思う。
もしかしたらAlphaZeroでもノード再利用を行わない方がよいのかもしれない(論文では特にノード再利用については説明されていない)。

Leela Chess Zeroではノード再利用しないことを決めたということだが、その理由についてはGitHubのissueに書かれていた。
Port tree re-use over from Leela Zero · Issue #37 · glinscott/leela-chess · GitHub

There is a problem with tree reuse and Dirichlet noise. Suppose we reuse 790 visits from the previous tree. We add Dirichlet noise, but now there are only 10 visits left that use the noised values. I think it might be better to not do tree-reuse for self play games.

ノード再利用するとディリクレノイズを加えてもノイズの効果がなくなる(新しい手を試さなくなる)というのが理由のようだ。

追記

187サイクル学習したモデルで、技巧2とやねうら王互角局面集を使用して100回対局した際の勝率は以下の通りとなった。

条件 勝敗数 勝率 信頼区間(95%)
b2048_reuse 21勝68負11分 23% 0.333938203~0.159823214
b1024_reuse 27勝71負2分 27% 0.371162243~0.196794269
b1024_noreuse 29勝60負11分 32% 0.428659671~0.237438285

ノード再利用なしで自己対局を行った場合が、(有意差があるとは言えないが)一番勝率が高くなっている。

なお、信頼区間の計算は、ここを参考にさせてもらった。

2019/6/9 追記

ノード再利用ありで強化学習したモデルとノード再利用なしで強化学習したモデル同士で対局させてみた。

勝敗数 勝率 信頼区間(95%)
55勝42負3分 56% 0.661201007~0.467714044

※ノード再利用なし基準

(有意差があるとは言えないが)ノード再利用なしの方がわずかに強くなっている。

将棋AIの実験ノート(初期値とシャッフルの影響)

各種条件を変更して比較実験を行っているが、初期値とシャッフルにランダム性があるので、測定結果は毎回ばらつきがある。
ランダムシードを固定すると結果が同じになるが、そのシード値での比較にしかならないので、固定しないで測定している。
複数回測定して統計的に比較した方がよいが、実験に時間がかかりすぎるのでできていない。

毎回行わないにしても、一つの条件で複数回測定して、初期値とシャッフルによる結果のばらつき具合を確認しておきたい。
そこで、先日のSEResNetのモデルの学習を初期値から2500万局面の学習を14回(2日で測定できた分)測定して、Pandasで統計量を表示させてみた。

訓練損失

train loss average
count 14
mean 1.516394
std 0.001418
min 1.513539
25% 1.515517
50% 1.516475
75% 1.517473
max 1.518854

テスト損失

test loss(policy) test loss(value) test loss(eval) test loss(sum)
count 14 14 14 14
mean 1.029515 0.598088 0.64829 1.644319
std 0.004993 0.00286 0.002039 0.005775
min 1.023603 0.593676 0.64615 1.63495
25% 1.025503 0.596493 0.64685 1.640151
50% 1.02841 0.597624 0.647721 1.644488
75% 1.032528 0.599781 0.648773 1.649289
max 1.038524 0.602845 0.653469 1.653205

テストaccuracy

test accuracy(policy) test accuracy(value)
count 14 14
mean 0.369727 0.661483
std 0.00206 0.005618
min 0.366104 0.650109
25% 0.368686 0.6581
50% 0.37043 0.662966
75% 0.371367 0.664878
max 0.3721 0.669561

テスト局面を推論した際のエントロピー

test entropy(policy) test entropy(value)
count 14 14
mean 2.111307 0.631886
std 0.038889 0.00333
min 2.022084 0.623964
25% 2.094391 0.630838
50% 2.11782 0.632153
75% 2.13492 0.633946
max 2.159858 0.6374

考察

訓練損失のばらつきはテスト損失に比べて小さい。
policyのテスト損失(交差エントロピー)の標準偏差は、value、evalに比べて大きく、0.015の範囲でばらついている。
テスト局面を推論した際のエントロピーはばらつきが大きく約0.14の範囲でばらついている。
比較結果が今回調べたばらつきの範囲以内の場合は、誤差の範囲と言えそう。


2500万局面の学習には3時間くらいかかっているので、学習局面を減らして測定回数を増やして、t検定とかで判断する方がよいかもしれない。
(論文書くわけではないのでそこまでする気はあまりないけど)そのうち試してみたい。

Jupyter Qt ConsoleでPandasを見やすくする

個人的な備忘録です。

Pythonを対話的に使う際、Jupyter Qt Consoleを主に使っている。
デフォルトだとウィンドウサイズの幅が81文字分しかないため、Pandasでhead()やdescribe()をすると、途中で改行されて見づらい。

Jupyter Qt Consoleのウィンドウの幅を大きくする

起動時のオプションに

--ConsoleWidget.console_width=150

のようにして幅を指定する。

Windowsの場合、jupyter-qtconsole.exeのショートカットを作成してリンク先にオプションを追加しておくとよい。
ついでに、Windowsの場合コマンドプロンプトが表示されて邪魔なので、実行時の大きさを最小化にしておくとよい。
f:id:TadaoYamaoka:20190602220242p:plain

Pandasの折り返し位置を変更する

ユーザディレクトリ(C:\Users\anyone)の.ipython\profile_default\startupに任意の名前でPythonスクリプトファイルを作成し、

import pandas as pd
pd.set_option("display.width", 150)

のように記述して保存する。

これで折り返し位置が150になるので見やすくなる。