TadaoYamaokaの日記

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

Pythonで将棋の盤面をSVGで表示する

以前に作成したPython向けの高速な将棋ライブラリ「cshogi」に、盤面をSVGで表示する機能を追加した。

Jupyter Notebookで実行すると、以下のように盤面が画像で表示されるので視認しやすくなる。

from cshogi import *
board = Board()
board

987654321

直前の指し手も表示できる。

move = board.push_usi(b'7g7f')
board.to_svg(move)

987654321

持ち駒がある場合は以下のように表示される。

board.set_sfen(b'8l/1+B+P1G4/1K+B1+P1+Pp1/4Ppp2/3s1n2p/l2g2ks1/1P4+n1P/2r6/+p6+s1 b R2GSNL4Pnl3p 187')
board

987654321

Jupyter Nodebook

Google Colabで使い方がわかるJupyter Nodebookを公開したので参考にして欲しい。
colab.research.google.com

余談

先日将棋AIで学ぶディープラーニングのJupyter Notebookを公開したが、対局するには手順が多いため、Google Colabで完結して対局できるようにしたいと思っている。
そのための準備として、盤面の表示機能を追加した。
今後、(別ライブラリにするかもしれないが)usiエンジン同士の対局機能も追加する予定。

Windows上でPyTorchを使って強化学習を試す

以前はOpenAI GymはWindowsに対応してなかったが、以下のようにpipからインストールするだけで動くようになっている。
atariも問題なく動く。

pip install gym
pip install gym[atari]

ただし、env.render()で描画するとウィンドウの位置変更などができないという問題があった。これは後で書く別の方法で描画すれば回避できる。

とりあえず既にある強化学習の実装を試そうとして、ChainerRLを試してみたがマルチプロセス関係がWindowsでは動かなかったため、PyTorchでつくりながら学ぶ!深層強化学習で引用されていた以下のGitHubのコードを試してみた。
https://github.com/ikostrikov/pytorch-a2c-ppo-acktr-gail

インストール

GitHubのREADMEにある通り、baselinesとrequirements.txtから依存ライブラリのインストールが必要になる。

git clone https://github.com/ikostrikov/pytorch-a2c-ppo-acktr-gail.git

# Baselines for Atari preprocessing
git clone https://github.com/openai/baselines.git
cd baselines
pip install -e .

# Other requirements
cd ../pytorch-a2c-ppo-acktr-gail
pip install -r requirements.txt

実行

--num-processesを1にすればWindowsでもそのまま実行できる。

例)

python main.py --env-name "BreakoutNoFrameskip-v4" --num-processes 1

マルチプロセスで動かすには、envs.pyの91行目を以下の通り修正する。
修正前:

envs = ShmemVecEnv(envs, context='fork')

修正後:

envs = ShmemVecEnv(envs, context='spawn')

これで、Windowsでもマルチプロセスで動かせる。

ゲーム画面を描画する

学習を実行すると、コンソールにログしか表示されない。
高速に強化学習するには問題ないが、はじめのうちは学習している過程をゲーム画面で眺めたい。

ゲーム画面を表示するには、main.pyに以下のコードを追加する。
ファイルの先頭:

import cv2

120行目(obs, reward, done, infos = envs.step(action)の後)に追記:

            imgs = envs.get_images()
            cv2.imshow('img', imgs[0])
            cv2.waitKey(1)

envsは、OpenAI Gymのenvのマルチプロセス用のラッパーになっている。
get_images()で、マルチプロセスで実行しているすべてのエージェントのゲーム画面が取得できる。
0番目のエージェントの画面のみをOpenCVのimshowで描画する。
waiKey(1)がないと、ウィンドウに描画が更新されないため、1ミリ秒だけキー入力待ちをする処理を入れている。

f:id:TadaoYamaoka:20190815160821p:plain

この方法を使用して前述したenv.render()の問題を回避できる。
env.render()の代わりにenv.render(mode='rgb_array')で画像を取得して描画すればよい。

将棋AIの進捗 その33(末端ノードでの詰み探索)

dlshogiでは末端ノードで短手数(7手)の詰み探索を実施しているが、終盤ではゲーム木中に詰みの局面が多くなり、王手の合法手も多くなるため、探索速度が大幅に低下することがあった。
先日MCTSにAND/OR木を組み込む実装を行ったことで、MCTSで詰み探索を行うことができるようになったので、詰みの手数を短くしても良いのではないかと思って、5手に減らして強さを確認してみた。

以下のような終盤の局面で、7手の場合と5手の場合で速度を比較すると、
f:id:TadaoYamaoka:20190813232004p:plain
position sfen 7nl/5kP2/3p2g1p/2p1gp3/p6sP/s1BGpN3/4nPSp1/1+r4R2/L1+p3K1L w GSNLPb6p 122

7手詰め 2783 nps
5手詰め 11757 nps

となり、7手と5手で探索速度に大きな差があることがわかった。

次に、5手詰めにした場合に、技巧2と1手3秒、やねうら王互角局面集を使用して100局対局して勝率を測定した。
結果は以下の通り。

勝敗数 勝率 信頼区間(95%)
7手詰め 38勝58負9分 41% 52.0%~32.2%
5手詰め 43勝51負6分 45% 55.8%~36.0%

勝率が上がっているが、100回対局では有意とは言えない差のため、対局数を増やして確認する必要がある。
少なくとも、5手詰めにしても弱くはなっていないことが確かめられた。
これがAND/OR木の効果なのかは、AND/OR木を外して別途確認する予定。
また、3手詰めにするとどうなるかも確認したい。


探索の工夫で、技巧2(CPU 4コア)にGPU1枚(2080Ti)で40%以上勝てるようになった。
今学習しているSENetのモデルがうまく学習できればもう少しで(GPU1枚で)技巧2を超えられそうである。

2019/8/14追記

末端ノードでの詰み探索を3手にした場合、以下の通りとなった。

探索速度
3手詰め 18914 nps
技巧2に対する勝率
勝敗数 勝率 信頼区間(95%)
3手詰め 29勝65負6分 30% 40.8%~22.4%

3手にした場合は、探索速度は5手詰めより1.6倍となっているが、勝率は明らかに低くなり弱くなっている。
末端ノードでの詰み探索の深さによる局面評価の精度と、探索速度による精度をバランスさせる必要があることがわかった。
実験からは、5手詰めが最も強くなりそうである。

ただし、現在7手詰めで探索速度が低下するのは、CPUがボトルネックになっているため、GPUに対するCPUのコア数を増やすことで対処できると考えている。

迷路を方策勾配法で解く

最近買った「つくりながら学ぶ!深層強化学習」という強化学習の本で、迷路を方策勾配法で解くという内容が記載されていたが、数式展開がなく自分で式を導出するのに苦労したのでメモを残しておく。

この本の迷路の問題の内容は、Webにも掲載されている。
第5回 ⽅策勾配法で迷路を攻略|Tech Book Zone Manatee

表形式のパラメータの更新則として以下の式が記載されている。
 \displaystyle
\theta_{s,a_j}=\theta_{s,a_j}+\eta\cdot\Delta\theta_{s,a_j} \\
\Delta\theta_{s,a_i}=(N(s,a_j)-p(s,a_j)N(s,a))/T
なお、書籍とWeb記事では、2つ目の式の符合が誤っている。
引用されている論文と式が違うので、まずここで躓いたのだが、正誤表に符合の誤りについて記載されていた。

さて、問題は2つ目の式の導出である。

以下では、方策勾配定理から書籍の式を導出するまでをできるだけ省略しないで行う。
方策勾配定理の近似式については引用されている論文これからの強化学習を参照。

式の導出

出発点となる方策勾配定理は、以下の式で表される。
 \displaystyle
\Delta\theta_{s,a} \sim \frac{1}{N} \sum_{i=1}^N \frac{1}{T} \sum_{t=1}^T \frac{\partial}{\partial \theta_{s_t,a_t}} \log p_{\theta_{s_t,a_t}}(a_t^i|s_t^i)z_T^i

ここで、添え字iはエピソードの番号を表しているが、1エピソードごとに更新するため、この問題では添え字iのシグマは削除できる。
報酬z_T^iは、ゴールにたどり着いた場合に、たどった経路すべてに割引なしで報酬1を与えるため、この式で常に1となる。
また、状態s_tと行動a_tは、1エピソードで同じ状態を通ることがあるため、状態sと行動状態aにまとめて記述する。

よって、式は以下のように記述できる。
 \displaystyle
\Delta\theta_{s,a} \sim \frac{1}{T} \frac{\partial}{\partial \theta_{s,a}} \log p_{\theta_{s,a}}(a|s)

ここで、方策p_{\theta_{s,a}}は、この問題ではSoftmax方策を採用しているため、
 \displaystyle
p_{\theta_{s,a}}(s|a) = \frac{e^{\theta_{s,a}}}{\sum_j e^{\theta_{s,a_j}}}
で与えらえる。


ここから、対数の公式と微分の公式を使って式を展開する。
 \displaystyle
\begin{eqnarray}
\Delta\theta_{s,a} &\sim& \frac{1}{T} \frac{\partial}{\partial \theta_{s,a}} \log p_{\theta_{s,a}}(a|s) \\
&=& \frac{1}{T} \frac{\partial}{\partial \theta_{s,a}} \log \frac{e^{\theta_{s,a}}}{\sum_j e^{\theta_{s,a_j}}} \\
&=& \frac{1}{T} \frac{\partial}{\partial \theta_{s,a}} (\log e^{\theta_{s,a}} - \log \sum_j e^{\theta_{s,a_j}}) \\
&=& \frac{1}{T} \frac{\partial}{\partial \theta_{s,a}} (\theta_{s,a} - \log \sum_j e^{\theta_{s,a_j}}) \\
&=& \frac{1}{T} (N_1 - \frac{\frac{\partial}{\partial \theta_{s,a}} \sum_j e^{\theta_{s,a_j}}}{\sum_j e^{\theta_{s,a_j}}}) \\
&=& \frac{1}{T} (N_1 - N_0 \frac{e^{\theta_{s,a}}}{\sum_j e^{\theta_{s,a_j}}})
\end{eqnarray}

ここで、N_1は、当該エピソードで状態sで行動aをとった回数、N_0は、当該エピソードで状態sを通った回数を表す。

式の展開で4行目から5行目への展開は、微分の連鎖則を使用している。
つまり、
 \displaystyle
\begin{eqnarray}
\frac{\partial}{\partial x} \log f(x) &=& \frac{\partial \log f(x)}{\partial f(x)} \frac{\partial f(x)}{\partial x} \\
&=& \frac{1}{f(x)} \frac{\partial f(x)}{\partial x}
\end{eqnarray}
を利用した。

式の5行目から6行目で、
 \displaystyle
\frac{\partial}{\partial \theta_{s,a}} \theta_{s,a} = N_1
となっているが、これは、1つのエピソードで状態sで行動aをとることが複数あるため、偏微分の結果は1だが、N_1回足す必要があるためである。

また、式の6行目から7行目で、
 \displaystyle
\frac{\partial}{\partial \theta_{s,a}} \sum_j e^{\theta_{s,a_j}} = N_0 e^{\theta_{s,a}}
となっているが、これは、1つのエピソードで状態sを複数とることがあり、行動a以外を取った場合も\sum_jの中に含まれているため、N_0回足す必要があるためである。
(補足:指数の微分が指数になる部分の式の展開は省略している。)


さらに、式を展開すると、Softmaxの定義から、
 \displaystyle
\begin{eqnarray}
\Delta\theta_{s,a} &\sim& \frac{1}{T} \frac{\partial}{\partial \theta_{s,a}} \log p_{\theta_{s,a}}(a|s) \\
&=& \frac{1}{T} (N_1 - N_0 \frac{e^{\theta_{s,a}}}{\sum_j e^{\theta_{s,a_j}}}) \\
&=& \frac{1}{T} (N_1 - N_0 p_{\theta_{s,a}}(a|s))
\end{eqnarray}
となり、書籍の式が導出できた。

SocketでC#とPythonを連携する

先日、gRPCでC#Pythonを連携する方法について書いたが、Pythonで受信したProtobufのデータを処理すると実行速度に問題があることがわかった。
速度が必要なケースでは、Protobufの使用はあきらめた方がよさそうだ。

今回は、シンプルにSocket通信でデータを渡してPythonで処理する方法について記述する。
Socketでデータを送受信する場合、データの中身はただのバイナリになるため、構造化されたデータを送受信する場合は、データのシリアライズとデシリアライズの処理を記述する必要がある。
データが固定長であれば、処理しやすいが、可変長の場合は、送受信のデータにサイズを含めるなどの処理が必要になる。

C#側(クライアント)

サンプルコード:
SocketSample/Program.cs at master · TadaoYamaoka/SocketSample · GitHub

以下のようにしてSocketでサーバに接続する。

TcpClient tcp = new TcpClient("127.0.0.1", 50007);
NetworkStream ns = tcp.GetStream();

送信するデータは、BitConverter.GetBytesでbyte配列に変換して送信する。
通信回数を抑えるため、MemoryStreamに格納してまとめて送信する。

var ms = new MemoryStream();
int size = 4 + 4 + 4 * a.b.Length;
ms.Write(BitConverter.GetBytes(size), 0, 4);
// ...
ns.Write(ms.ToArray(), 0, 4 + size);

応答の受信は、Readで行う。byte配列で受け取るため、BitConverter.ToInt32などで数値など変換する。

byte[] int_bytes = new byte[4];
ns.Read(int_bytes, 0, 4);
size = BitConverter.ToInt32(int_bytes, 0);

最後に接続をクローズする。

ns.Close();
tcp.Close();

Python側(サーバー)

サンプルコード:
SocketSample/SocketServer.py at master · TadaoYamaoka/SocketSample · GitHub

以下のように接続待ち→接続を行う。

s.bind(('127.0.0.1', 50007))
s.listen(1)
conn, addr = s.accept()

受信は、recvを使用するとバイト列が返る。recv_intoを使うとバッファに直接書き込む。バッファにはnumpyのndarrayが使用できる。

size = int.from_bytes(conn.recv(4, socket.MSG_WAITALL), sys.byteorder)
# ...
data = np.empty(size, dtype=np.uint8)
conn.recv_into(data, size, socket.MSG_WAITALL)

応答の送信は、sendallで行う。numpyのndarrayは直接引数に渡せる。

res = np.array([1, 2, 3])
conn.sendall(len(res).to_bytes(4, sys.byteorder))
conn.sendall(res)

シリアライズとデータ加工をCythonで行う

バイト列をデシリアライズして、機械学習で使用するためにデータ加工を行うような場合は、実行速度をできるだけ高速化したい。
そのような場合、Cythonを使うことでデシリアライズとデータ加工をC++で記述できる。
サンプルコード:
SocketSample/Decoder.pyx at master · TadaoYamaoka/SocketSample · GitHub
SocketSample/Decoder_impl.cpp at master · TadaoYamaoka/SocketSample · GitHub
データの受け渡しをnumpyのndarrayにすることで、余分なメモリコピーを抑えることができる。

LinuxでC#からC++で作成した共有ライブラリを呼び出す

cmakeでC++の共有ライブラリプロジェクトを作成する

CMakeLists.txt
cmake_minimum_required(VERSION 3.8)
project(SampleDll)

enable_language(CXX)
add_library(SampleDll SHARED
  sample_dll.cpp
)


sample_dll.cppに、C++で処理を記述する。
Windowsでも使えるように、ディレクティブを使用している。

sample_dll.cpp
#include <iostream>

#ifdef _MSC_VER
#define DLL_EXPORT __declspec(dllexport)
#else
#define DLL_EXPORT
#endif

extern "C" {
	DLL_EXPORT DLL_EXPORT int Add(int n1, int n2);
}

DLL_EXPORT int Add(int n1, int n2)
{
	std::cout << n1 << "," << n2 << std::endl;
	return n1 + n2;
}

ビルドする

mkdir build
cd build
cmake ..
make

ビルドに成功するとlibSampleDll.soが作成される。

共有ライブラリを使用可能にする

Ubuntuの共有ライブラリの格納パスは、/etc/ld.so.conf.d/libc.confに以下の通り設定されている。

# libc default configuration
/usr/local/lib

したがって、共有ライブラリを/usr/local/libにコピーする。

cp libSampleDll.so /usr/local/lib/

ただし、このままではロードできない。
キャッシュの更新が必要なため、ldconfigコマンドを実行する。

ldconfig

以上で、共有ライブラリが使用可能となる。

.NET CoreでC#のプロジェクトを作成する

dotnet new console

共有ライブラリを呼び出すコードを記述する。

Program.cs
using System;
using System.Runtime.InteropServices;

namespace Sample
{
    class Program
    {
        [DllImport("SampleDll")]
        extern static int Add(int n1, int n2);

        static void Main(string[] args)
        {
            int a = Add(1, 2);
            Console.WriteLine(a);
        }
    }
}

共有ライブラリの名前には接頭辞の「lib」と、拡張子の「.so」は含める必要はない。

実行する

dotnet run

成功すれば以下の通り表示される。

1,2
3


GitHubソースコードを公開しました。
GitHub - TadaoYamaoka/csnative

また、Google Colabでノートブックを公開しました。
https://colab.research.google.com/drive/15Yr-JeYuUJUa08jsDPOI3BUOBMxX3Rh-

WindowsでPyTorchをC++(Visual C++)で動かす

WindowsでPyTorchをC++のサンプル(MNIST)をVisual Studio 2017でビルドして動かす手順のメモです。

LibTorchのインストール

https://pytorch.orgのQUICK START LOCALLYから、Stable/Windows/LibTorch/C++/10.0(CUDAのバージョンが10.0の場合)を選んでlibtorch-win-shared-with-deps-latest.zipをダウンロードする。
適当な場所に解凍する(以下、C:\に解凍したとして説明)。

環境変数PATHに

C:\libtorch

を追加する。

最も単純なサンプルをビルドして実行

Installing C++ Distributions of PyTorch — PyTorch master documentation
の説明の通り、適当な場所に

  • CMakeLists.txt
  • example-app.cpp

を作成して、上記のページの内容の通り内容を記述する。

ビルド

スタートメニューから「VS 2017 用 x64_x86 Cross Tools コマンド プロンプト」を起動する。
サンプルコードを配置したディレクトリに移動して、以下のコマンドを実行する。

mkdir build
cd build
cmake -G "Visual Studio 15 2017 Win64" -DCMAKE_PREFIX_PATH=C:\libtorch ..

cmakeのオプションは、Visual Studio 2017とLibTorchを解凍した場所に合わせている。
(cmakeはVisual Studioのインストール時のオプションで選択すればインストールされる。)

buildディレクトリに「example-app.sln」が作成されるので、エクスプローラから開いて、Visual Studio 2017でビルドして、スタートアッププロジェクトを「example-app」に設定して実行する。
成功すれば、以下のように表示される。

 0.3171  0.7950  0.6067
 0.1094  0.7421  0.8496
[ Variable[CPUType]{2,3} ]

もしくは、msbuildでビルドする。
ソリューションの構成が「RelWithDebInfo」の場合の例)

msbuild example-app.sln /t:build /p:Configuration=RelWithDebInfo;Platform="x64"

MNISTサンプルをビルドして実行

The C++ Frontend — PyTorch master documentation
の説明の通り、サンプルのGitHubレポジトリのcppブランチを取得する。

git clone -b cpp https://github.com/goldsborough/examples.git

以下の通り実行する。

cd examples\cpp\mnist
mkdir build
cd build
cmake -G "Visual Studio 15 2017 Win64" -DCMAKE_PREFIX_PATH=C:\libtorch ..

buildディレクトリにmnist.slnが作成されるので、エクスプローラから開いてビルドする。
スタートアッププロジェクトを「mnist」に設定して実行する。
ソリューションの構成が「Debug」の場合、
register_module("conv1", conv1);
の箇所で、vectorライブラリ内で例外が発生して実行できなかった。
ソリューションの構成を「Release」または「RelWithDebInfo」にした場合は実行できた。
成功すれば、以下のように表示される。

CUDA available! Training on GPU.
Train Epoch: 1 [59584/60000] Loss: 0.4143
Test set: Average loss: 0.1984 | Accuracy: 0.939
Train Epoch: 2 [59584/60000] Loss: 0.1886
Test set: Average loss: 0.1356 | Accuracy: 0.959
Train Epoch: 3 [59584/60000] Loss: 0.1666
Test set: Average loss: 0.0983 | Accuracy: 0.969
Train Epoch: 4 [59584/60000] Loss: 0.1346
Test set: Average loss: 0.0870 | Accuracy: 0.973
Train Epoch: 5 [59584/60000] Loss: 0.1822
Test set: Average loss: 0.0770 | Accuracy: 0.975
Train Epoch: 6 [59584/60000] Loss: 0.0635
Test set: Average loss: 0.0696 | Accuracy: 0.978
Train Epoch: 7 [59584/60000] Loss: 0.1375
Test set: Average loss: 0.0676 | Accuracy: 0.978
Train Epoch: 8 [59584/60000] Loss: 0.0289
Test set: Average loss: 0.0625 | Accuracy: 0.980
Train Epoch: 9 [59584/60000] Loss: 0.0218
Test set: Average loss: 0.0553 | Accuracy: 0.982
Train Epoch: 10 [59584/60000] Loss: 0.0773
Test set: Average loss: 0.0530 | Accuracy: 0.983

Google Colabでビルドして実行する方法

Linuxの場合は、公式の説明の通り実行できる。
Google Colabのノートブックを公開したので参考にしてほしい。
https://colab.research.google.com/drive/1F8kSS1VLTAi0zVocIf7kCCMhSgM732PW