TadaoYamaokaの開発日記

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

将棋でディープラーニングする その27(対局できるようにする)

以前の対局できるバージョンは、別プロセスのUSIエンジンを補助的に使用して詰みの探索を行っていたが、Pythonから呼び出せるC++のモジュールに変更した。

elmo_for_learnのソースを流用してPythonから使用できるようにした。
cppshogiというモジュールにしている。

対局中の各局面で深さ6で評価値を求めて3000を超えた場合は、探索の結果で指すようにした。

今回は、policy networkの出力からBoltzmann分布に従う確率に応じて手を選択するようにした。
これにより指し手が固定化されなくなる。
Boltzmann分布の計算は、policy networkの出力を合法手でフィルターして、残ったロジットについて、expf(logits[i] * beta)を計算した。
本来は分布の合計が1になるように正規化するが、C++のdiscrete_distributionを使うと合計が1になっていなくても、確率分布に応じたインデックスを返却できるため、計算を簡略化した。

elmo_for_learnで生成した2千万局面で学習したSL policy networkを使って、Lesserkaiと対局させて勝つことがあるのを確認した。(勝率は低い)
f:id:TadaoYamaoka:20170529220205p:plain

elmoの教師データを使うと、floodgateの棋譜に比べて学習の進みが遅い。
f:id:TadaoYamaoka:20170529220646p:plain

まだ学習が進んでいるので、教師データを1千万ずつ生成して学習させてみる予定。

RL policy networkはまだcppshogiに対応できていないので、後日修正予定。

github.com

将棋でディープラーニングする その26(学習の高速化)

前回の日記で書いたC++でミニバッチデータを作成する処理を組み込んで、バリューネットワークの学習の速度が改善されたか確認を行った。

測定条件

  • 学習データはelmo_for_learnで生成した100万局面
  • ミニバッチサイズ32
  • 1エポック

測定結果

Python(変更前) 0:30:08
C++(変更後) 0:11:05

学習時間が37%に短縮できた。

elmoと同じ50億局面を学習するには、38.5日かかる見積もりとなる。(前回は105日の見積もり)

SL policy networkで測定

今までSL policy networkは、CSA形式の棋譜データを使用していたが、elmoで生成した教師データで学習するように変更した。
こちらもC++化による効果を確認した。

50000イテレーションの時間を比較した。(使用する学習データは異なる)

Python(変更前) 0:24:50
C++(変更後) 0:16:32

学習時間が67%に短縮された。
SL policy networkでは棋譜データからミニバッチデータを作成していたのでバリューネットワークのようにハフマン符号のデコードがなかったため、改善効果は比較的少なめである。
それでも十分速くなっている。

精度の評価

精度については以下のようになった。

train loss test accuracy
Python(変更前) 2.49 0.36
C++(変更後) 2.76 0.31

elmoの学習データを使用する精度が落ちているように見える。
これは、棋譜には序盤に重複データが多く含まれているが、elmoの学習データは異なる局面から生成しているので重複が少ないためと思われる。

1000万局面を学習

elmo_for_learnで生成した1000万局面を学習させてみた。
f:id:TadaoYamaoka:20170528233243p:plain

train loss 2.27
test accuracy 0.36
時間 1:41:08

train lossは下がり続けており、学習局面を増やせばまだ精度が上げられそうだ。

elmoと同じ50億局面を学習するには、35.1日かかる見積もりとなる。


今まで詰みの判定に外部のUSIエンジンを使用していたが、C++のライブラリを使用するようにしたので、詰みの判定処理もC++のライブラリで行うようにしたい。
今回は学習用途だったため探索の処理をコメントアウトしたが、詰みの探索ができるようにして、USIエンジンに頼らず対局できるようにする予定。

github.com

将棋でディープラーニングする その25(C++でミニバッチ作成)

先日の日記で、elmoの教師データを使用してバリューネットワークの学習を行ったところ、elmoの教師データはハフマン符号で圧縮されているため、デコードする処理に時間がかかるという問題があることがわかった。

そこで、デコード部分をC++で実装することで高速化できないか試した。
PythonからC++のモジュールを呼び出すとオーバーヘッドがあるため、できるだけまとめて処理できた方が良い。
そのため、1回の呼び出しでミニバッチデータの作成までをまとめて行うようにした。

Python側実装

Python側では、elmoの教師データのデータ構造であるHuffmanCodedPosAndEvalをNumpyの構造体として定義する。

HuffmanCodedPosAndEval = np.dtype([
    ('hcp', np.uint8, 32),
    ('eval', np.int16),
    ('bestMove16', np.uint16),
    ('gameResult', np.uint8),
    ('dummy', np.uint8),
    ])

ファイルから読み込む際は、

hcpevec = np.fromfile(args.file, dtype=HuffmanCodedPosAndEval)

のようにすることで一度で全件読み込める。

C++側の実装

Boost.Pythonを使うことで、Pythonのモジュールが実装できる。
また、Boost.Numpyを使うことで、Python側からnumpyのオブジェクトを受け取ることができる。
(詳細は、先日の日記参照)

入力は、HuffmanCodedPosAndEval型のnumpyの配列を受け取るようにする。
出力は、引数で受け取ったnumpyのオブジェクトに設定して返すようにする。
出力のnumpyのオブジェクトは、Python側でnp.emptyで事前に領域を確保しておく。

C++側では、ndarray.get_data()でメモリ領域にポインタでアクセスできる。
numpyのオブジェクトは、shapeによらずメモリ上では連続した領域になっているので、C++側ではshapeに合わせてポインタ操作を行う。

elmoの教師データのデコードは、elmoのソースコード(および派生元のApery)をそのまま流用した。
王手を入力特徴に加えているので、王手のチェックを高速で行えるように盤面管理にelmoのPositionクラスを利用した。
探索部分は不要なためコメントアウトしている。

座標系が今まで使用していたpython-shogiとは異なるが、今後はelmoのソースを流用していこうと思うので、座標系の変換は行わずそのままとした。
そのため、いままで学習したモデルは使用できなくなる。

座標系の違いについては以前の日記を参照。
駒の順番もelmoに合わせた。
python-shogiは、歩(PAWN)、香(LANCE)、桂(KNIGHT)、銀(SILVER)、金(GOLD)、角(BISHOP)、飛(ROOK)、玉(KING)の順だが、
elmo(Apery)では、歩(PAWN)、香(LANCE)、桂(KNIGHT)、銀(SILVER)、角(BISHOP)、飛(ROOK)、金(GOLD)、玉(KING)の順になる。

Python側から利用

C++で作成したソースは、DLLとしてビルドする。
拡張子を、.pydにすることで、Pythonからimportして、Pythonのモジュールと同等に使用できる。

import hcp_decoder

hcpevec = np.fromfile(args.file, dtype=HuffmanCodedPosAndEval)

features1 = np.empty((len(hcpevec), 2, 14, 81), dtype=np.float32)
features2 = np.empty((len(hcpevec), 2 * MAX_PIECES_IN_HAND_SUM + 1, 81), dtype=np.float32)

result = np.empty(len(hcpevec), dtype=np.float32)
move = np.empty(len(hcpevec), dtype=np.int32)
value = np.empty(len(hcpevec), dtype=np.float32)

hcp_decoder.decode_with_value(hcpevec, features1, features2, value, move, result)

デバッグについて

C++を使うとデバッグが難しくなるが、Visual StudioPython Tools for Visual Studioを使い、デバッグの設定で、Enable native code debuggingをチェックしていると、PythonのコードとC++のコードをシームレスにデバッグできる。

処理時間測定

elmoで生成した教師データ1万局面を使い、Pythonでデコードした場合と、C++でデコードしてミニバッチデータの作成まで行った場合で処理時間の比較を行った。
結果は以下の通りとなった。

Python 7.186秒
C++ 0.111秒

約64.7倍速くなった。
Pythonではデコードのみでミニバッチデータの作成までは行っていないので、実際はこれ以上に差がある。

C++にすることで圧倒的に速くなることが確認できたので、python-shogiからelmoから流用したソースに置き換える予定。


C++側の教師データの読み込み処理のソースをGitHubに公開しました。
github.com

なお、C++側のソースのライセンスはelmoのソースを流用しているのでGPLが適用されるようになります。

将棋でディープラーニングする その24(歩の持ち駒の上限)

前回の日記でバリューネットワークの学習時間を見積もったところ、elmoと同じ50億局面を学習するには3.5ヶ月かかる見積もりになったので、高速化を行う必要性を感じている。

ミニバッチデータの加工をPythonで行っている部分をC++に書き換えることでかなり高速化ができそうである。
それについては別途記事を書く予定。

今回は、入力特徴の歩の持ち駒について見直しを行う。
歩の持ち駒は最大で18枚になるが、実践で歩の持ち駒が18枚になることはまずあり得ない。
竜王戦棋譜3744局から、歩の持ち駒の数について調べた。
f:id:TadaoYamaoka:20170527204036p:plain:w400
1局中の歩の持ち駒の最大枚数は、ほぼ10枚以内に収まっている。

また、1枚持っているのと、2枚持っているのでは、指し手が変わると思われるが、9枚と10枚ではほとんど影響ないと思われる。

そこで、入力特徴の歩の持ち駒に上限を設けた場合に、精度と学習時間にどのような影響があるか調べた。

はじめ竜王戦棋譜で調べたが局数が少なすぎるのかほとんど差がみられなかった。

f:id:TadaoYamaoka:20170527204844p:plain
8枚が上限のときが若干test accuracyが良いが、誤差の範囲。
また、歩以外の持ち駒全てなしにしてもほとんど変わらなかった。
確かに盤面にない駒は持ち駒になっているので、盤面の情報に持ち駒の情報も内包しているが、学習データの少なさによるものと思われる。

floodgateの棋譜で再測定

そこで、floodgateの棋譜14,230局を使って、測定をやり直した。
f:id:TadaoYamaoka:20170527205523p:plain
歩以外の持ち駒もなしにすると、精度が落ちることが観測できた。
しかし、歩の持ち駒の枚数による違いはほとんどない。

学習時間は以下のようになった。
f:id:TadaoYamaoka:20170527205904p:plain
持ち駒の上限が増えるほど、学習時間も増える傾向がある。
歩の持ち駒の上限が2と18では、約2分の違いがある。

以上の測定結果から、歩の持ち駒の上限は制限しても精度にはあまり影響がないことがわかった。

上限を何枚にするか決め手にかけるが、18枚は無駄があるので、上限はとりえあず8枚とする。

PythonからC++を呼び出してnumpyを使う

将棋でディープラーニングを試しているが、Pythonで入力データの加工を行うと処理速度が問題になっている。

そこで、PythonからC++で作成したモジュールを呼び出して、その中でnumpyのオブジェクトの加工を行いたい。
PythonからC++の呼び出しはオーバーヘッドがあるため、頻繁に呼び出すとかえって遅くなる。
そのため、ミニバッチのデータをまとめて作成するようにする予定。

以下の手順で、PythonC++のモジュールを使うことができた。

環境

Boostのダウンロード

Boostの1.63以降でBoost.Numpyが標準で利用できるようになっている。
公式サイトから最新の1.64.0をダウンロードした。
ダウンロードしたファイルを任意のディレクトリに展開する。
以下、C:\boost_1_64_0に展開したものとして記述する。

Boost.Pythonのビルドを有効にする

ホームディレクトリC:\Users\xxxxに「user-config.jam」を作成し、以下の通り編集する。

using python : 3.5 : C:\\Anaconda3\\python ;

※C:\\Anaconda3のパスは環境に合わせて編集する。

Boostのビルド

cd /d C:\boost_1_64_0
bootstrap.bat
b2 toolset=msvc threading=multi variant=debug,release link=static runtime-link=static address-model=64 --stagedir=stage/x64 -j 8

64ビットのスタティックリンクライブラリを生成するオプションを指定している。
ダイナミックリンクライブラリでは、numpyのdtypeがグローバル変数に定義されているため、DLLの内の変数がリンクできないため、np::dtype::get_builtin()などを使うとリンクエラーとなる。

stage\x64\libに.libが生成される。

python関連の.libファイルの名前を以下の通り変更する。

libboost_python3-vc140-mt-s-1_64.lib → libboost_python-vc140-mt-s-1_64.lib
libboost_python3-vc140-mt-sgd-1_64.lib → libboost_python-vc140-mt-sgd-1_64.lib
libboost_numpy3-vc140-mt-s-1_64.lib → libboost_numpy-vc140-mt-s-1_64.lib
libboost_numpy3-vc140-mt-sgd-1_64.lib → libboost_numpy-vc140-mt-sgd-1_64.lib

※2017/10/27 追記
Version 1.65.1では名前の変更は不要になっていました。

C++のDLLプロジェクト作成

Visual Studio 2015でWin32のDLLプロジェクトを作成する。
アクティブな構成をx64/Releaseにする。

プロジェクトのプロパティでインクルードディレクトリに以下を追加する。

  • C:\Anaconda3\include
  • C:\boost_1_64_0

ライブラリディレクトリに以下を追加する。

  • C:\Anaconda3\libs
  • C:\boost_1_64_0\stage\x64\lib

C/C++->コード生成のランタイムライブラリをマルチスレッド(/MT)に変更する。
boostをスタティックリンクライブラリとして生成しているので合わせる必要がある。

C++のコード作成

以下のようなコードを作成する。
(コードはこちらのサイトを参考にした。)

mymod1.cpp
#define BOOST_PYTHON_STATIC_LIB
#define BOOST_NUMPY_STATIC_LIB
#include <boost/python/numpy.hpp>
#include <stdexcept>
#include <algorithm>

namespace p = boost::python;
namespace np = boost::python::numpy;

/* 2倍にする */
void mult_two(np::ndarray a) {
	int nd = a.get_nd();
	if (nd != 1)
		throw std::runtime_error("a must be 1-dimensional");
	size_t N = a.shape(0);
	if (a.get_dtype() != np::dtype::get_builtin<float>())
		throw std::runtime_error("a must be float32 array");
	float *p = reinterpret_cast<float *>(a.get_data());
	std::transform(p, p + N, p, [](float x) { return 2 * x; });
}

BOOST_PYTHON_MODULE(mymod1) {
	Py_Initialize();
	np::initialize();
	p::def("mult_two", mult_two);
}

スタティックリンクをするには、ヘッダーを読み込む前に以下の定義が必要
#define BOOST_PYTHON_STATIC_LIB
#define BOOST_NUMPY_STATIC_LIB

ビルドする。

プロジェクトのx64/Releaseに.dllが作成される。

Pythonコード作成

以下のようなコードを作成する。

mymod.py
import mymod1
import numpy as np

if __name__ == '__main__':
    a = np.array([1,2,3], dtype=np.float32)
    mymod1.mult_two(a)
    print(a)

上記でビルドした.dllファイルをpythonのコードと同じディレクトリにコピーし、ファイル名をmymod1.pydに変更する。
拡張子は.pydである必要がある。

Visual Studioのビルドイベント->ビルド後のイベントで、

copy $(TargetPath) $(SolutionDir)\PythonApplication1\mymod1.pyd

など設定すればよい。

Pythonのコードを実行する。

python mymod.py
実行結果
[ 2.  4.  6.]

将棋でディープラーニングする その23(バリューネットワークの実装)

前々回の日記に書いたバリューネットワークの実装を行った。

elmoで生成した教師データのフォーマットで教師データを読み込むようにした。

前々回の日記で書いたAlphaGoの手法を参考にして、ネットワーク構成は、SL policy networkの出力層に全結合層を繋げてtanhで勝率を表すスカラー値を出力するようにした。
AlphaGoの論文では入力に手番の色の加えているが、とりあえずSL policy networkと同じにした。
教師データは勝敗データ(-1, 1)、損失関数は平均2乗誤差とした。

教師データ

elmo_for_learnで100万局面を生成した。
(elmoの教師データの生成については以前の日記を参照)

学習結果

ミニバッチサイズを32として、SGDで1エポック学習させた結果は以下の通り。
f:id:TadaoYamaoka:20170524234930p:plain
1エポック後のテストデータ1万局面のtest lossは、0.9058882594108582となった。

train lossは順調に下がっているので、学習は成功している。

学習時間は、0:30:08かかった。
elmoと同じ50億局面学習すると、105日(3.5ヶ月)かかる。

バッチサイズを4倍の128にすると、学習時間は0:23:58になった。(1エポック後のtrain lossは0.898と少し悪化)
少し速くなっているが、4倍にはなっていない。
ミニバッチデータを作成する際のハフマン符号のデコードにCPU時間がかかっていると思われる。
この部分をマルチスレッドで処理するなど高速化の工夫が必要そうだ。

ネットワークのパラメータは初期値から学習したが、出力層以外はSL/RL policy networkと同一なため、転移学習が可能である。
転移学習した場合についても次回検証を行いたい。

GitHubにソースを公開しました。
github.com

将棋でディープラーニングする その22(評価値と勝率の関係)

前回の日記で書いたようにバリューネットワークの学習データとして、elmoの教師データを使用する予定である。

elmoの教師データは自己対戦の勝敗だけでなく、深さ6で探索した評価値も同時に出力される。
そこで、学習がうまくいっているかの検証用として、elmoの評価値から推測される勝率を使用したい。

シグモイド関数で近似

elmoは評価値から勝率への変換に、ロジスティック(シグモイド)関数を使用している。
その際、評価値を600で割ってスケーリングしている。
\displaystyle
\frac{1}{1 + \exp(\frac{-a}{600})}
グラフにすると以下のようになる。
f:id:TadaoYamaoka:20170524214751p:plain

教師データの生成に使用しているelmoの評価関数は、この式で最適化されているので、実際に生成されたデータも近い分布になるはずである。

そこで、10万局面を生成し、勝率をプロットしてみた。
その結果、以下のようになった。
f:id:TadaoYamaoka:20170524215115p:plain

評価値100ずつの範囲で勝敗の平均値をプロットしている。
緑の線は、回帰で近似したシグモイド曲線である。
回帰の結果、シグモイド関数のスケーリングのパラメータは、0.00132566 = 1/754.3となった。

elmoは損失関数に、浅い探索と深い探索の評価値の交差エントロピー正則化項として加えているので、元の600から差がでていると思われる。

双曲線関数で近似

AlphaGoの論文では、バリューネットワークの出力層は双曲線関数(tanh)が使われている。
tanhは、グラフの形はシグモイド関数と似ているが、値の範囲が-1~1となる関数である。
負けを-1、勝ちを1で表す場合に適している。
(elmoでは、負けを0、勝ちを1で表しているのでシグモイド関数が適している。)

elmoで生成したデータの勝敗を-1と1にして、tanhで近似した場合についても調べた。
その結果、以下のようになった。
f:id:TadaoYamaoka:20170524220123p:plain

緑の線は回帰で近似したtanh曲線である。
回帰の結果、スケーリングのパラメータは、0.00067492 = 1/1481.7となった。

このスケーリングのパラメータを使用して、バリューネットワークの出力の妥当性を検証したい。

調査に使用したソースコードGitHubに公開した。
github.com
winrate.pyが勝率調査用のコードである。
回帰にはChainerを使用した。

2017/6/19 追記

上記の勝率をプロットしたグラフで、評価値3000の個所で、一つだけ下に外れた点がある。
これはelmo_for_learnでは400手が勝負が付かない場合に、結果をDrawとして出力しており、集計の際Drawを負けとしていたためである。
Drawを含むデータだと、勝ちの局面を正しく判断できなくなる可能性があるので、Drawは出力しない方が良いかもしれない。

Drawを除いて、深さ8で生成した10局面のデータで、集計し直すとシグモイド関数のグラフは以下のようになった。
f:id:TadaoYamaoka:20170619161931p:plain
近似曲線のスケーリングパラメータは、0.0013989=714.847380084352となった。

2017/11/24 追記

tanhの係数も引き分けなしで再測定を行った。
100万局で再計測したので、sigmoidの値も掲載しておく。

tanh

f:id:TadaoYamaoka:20171124174201p:plain
1485.7957922263165 = 1/0.00067304

sigmoid

f:id:TadaoYamaoka:20171124174317p:plain
756.0864962951762 = 1/0.0013226