ねね将棋miniを開発しました
第28回世界コンピュータ将棋選手権まで1か月を切りました。開発者の皆様いかがお過ごしでしょうか。
開発がうまくいかないので現実逃避として、将棋におけるディープラーニング評価関数の雰囲気を知ってもらうため、「ねね将棋mini」というアプリを開発しました。
アプリとはいっても、インストールは不要でWebブラウザ上で動作します。こちらからご覧ください。https://select766.github.io/neneshogi_mini/ 1MB程度のロードが発生しますが、最新のWebブラウザが載っていればスマホでも動作します。
概要
ディープラーニングを使うと、局面を受け取って次の適切な指し手を予測するDNNモデル(ディープニューラルネットワーク)を学習することができます。 DNNの表現力は大きく、先の展開を探索しなくても既存の将棋AIの指し手を40%程度の確率で当てることができます。 そこで、ねね将棋のために学習したDNNで(探索なしに)指し手を選択し、自己対局するアプリが「ねね将棋mini」です。 ディープラーニングの棋風といえるほどのものがあるかはわかりませんが、探索がないのにうまい感じに寄せたりするのは見ていて面白いですよ。
この記事では、ねね将棋miniを構成する技術要素を解説します。
主な要素は次の通りです。
- DNN
- DNNのランタイムフレームワーク
- 将棋盤の表現
- DNNの入力生成
DNN
局面(盤上の駒配置、持ち駒、王手かどうか等)を入力とし、各指し手の確率を出力するモデルです。構造としてはConvolutional Neural Networkです。
以下の棋譜を利用させていただきました。 depth10で作った110億局面の教師データ、期間限定で公開します
ねね将棋miniでは複雑さの違う2種類のモデルを提供していて、"small1"では35%、"large1"では40%程度の確率で学習元の棋譜の指し手を当てられる(その指し手の確率が最大となる)ようです。 "small1"だと圧縮した状態で1MBを切るサイズとなっています。
DNNのランタイムフレームワーク
ねね将棋miniでは、生成済みの棋譜をダウンロードするのではなく、Webブラウザ上でDNNを実行し、1手ずつ指し手を生成しています。 初公開バージョンはAI同士の自動対局のみなので生成済み棋譜のダウンロードでもよいのですが、今後人間との対局機能を追加したいと考えています。 その際にはユーザの指し手に呼応してDNNの実行をする必要があります。 DNNの実行が可能なサーバを準備するとなると無料のものはほぼなく、AWS等にお金を払い続けなければなりません。 そこで、アプリを実行するユーザ側の端末上でDNNを実行することにしました。この方式であれば、サーバからは静的なモデルファイルを配布するだけでよくgithub pagesにて無料で公開することができました。 ディープラーニング関連のことをやろうとするとNVIDIA社のGPUを用意し、CUDAのセットアップをして云々という面倒な環境構築が頭をよぎりますが、 Webブラウザの機種非依存の標準機能だけを用いて動作するようになっているため簡単に試すことができます。
DNNの学習自体はChainerフレームワークで行ったのですが、WebDNNというフレームワークによりWebブラウザ上で動作する形に変換することができます。 計算の最適化、モデルの圧縮機能等も搭載されています。今回は使用しませんでしたが、GPUを利用する機能も含まれています。 Chainerモデルの変換は特に難しいことはなく、本体部分は10行程度のpythonコードで書けました。 https://github.com/select766/neneshogi_mini/blob/master/model_convert/model_convert.py
既存のモデルクラスを読み込み、学習したパラメータをセットします。
def load_model(model_config): model = Model(**model_config["options"]) chainer.serializers.load_npz(model_config["path"], model) return model
ダミーのデータを流し、計算グラフを取り出します。move, valueというのは方策と状態価値関数(評価値)の2つを出力とするモデルだからです。 train=Falseとし、batch normalizationを無効にしていないとおかしくなるので注意。
def get_graph(model): dummy_input = np.zeros((1, 86, 9, 9), dtype=np.float32) x = chainer.Variable(dummy_input) with chainer.using_config("train", False): # disable batch normalization move, value = model.forward(x) graph = ChainerConverter().convert([x], [move, value]) return graph
ブラウザに読み込ませるデータを生成します。ブラウザ上の計算環境としてWebGPU, WebGL, WebAssembly等が選べます。試してみたところWebAssemblyで十分な速度が出たのでこれを使いました。
exec_info = generate_descriptor("webassembly", graph, constant_encoder_name="eightbit") output_dir = f"../docs/webdnn_model/{model_config['name']}" exec_info.save(output_dir)
特に変わったところもないモデルなので、すんなりと変換ができました。
将棋盤の表現
JavaScriptで駒の配置や合法手の列挙等が必要なので、Shogi.jsというライブラリを利用しました。盤面の状態はシンプルに配列で表現されていて使い始めるのは簡単でした。
駒の動ける範囲の生成まではあるのですが、王手放置のチェックが実装されていませんでした。探索の指し手生成としては非効率ですが、速度は今回問題にならないため簡単な実装をしました。アイデアは、指した後に手番でなくなる側が王手をかけられていれば王手放置(開き王手を含む)ということです。手番でない側の駒の動きを生成し、その中に相手玉の位置が入っていれば王手放置、という実装になっています。
if (piece.color !== color) { let moves = this.getMovesFrom(i, j); for (let k = 0; k < moves.length; k++) { let move = moves[k]; if (move.to.x === kingPos.x && move.to.y === kingPos.y) { return true; } } }
全体をつなげてUIをくっつける
将棋盤とDNNをくっつけ、最後にユーザーインターフェースを実装して出来上がりです。
DNNの入力は86×9×9のテンソルになっています。テンソル上のインデックスをT(c,y,x)としたとき、座標(2, 3)に手番側の飛車があればT(6, 2, 3)=1というように値をセットします。
WebDNNの入力は6966(=86×9×9)要素の配列となっており、直接値をセットしようとするとT[6 * (9 * 9) + 2 * 9 + 3] = 1
のようにインデックス計算が煩雑になります。
そこで、ndarrayというライブラリを用いて抽象化しました。先ほどの値のセットは次のようにわかりやすくなりました。
let array = ndarray(raw_array, [86, 9, 9]); array.set(6, 2, 3, 1);
DNNの出力も、入力と同じような状況です。合法手それぞれの確率を取り出すことができるので、それに従ってランダムに手を選ぶようにしました。 最大確率の手を指すのが一番強いはずですが、それだと毎回同じ進行になってしまうため、ある程度展開がばらけつつ不自然にもならないようなパラメータを選択しました。
UI部分は極力シンプルに実装しました。10×10のテーブルに駒の文字を入れて盤面の表現にしました。後手番の駒の文字は上下逆にする必要がありますが、画像としては用意せずにCSSで文字を回転させました。
今後
やはり対人戦ができたほうが面白いと思います。そんなに複雑ではないので近日中に実装できればと思います。
このデモに組み込まれているものより強いモデルをお持ちの方、リポジトリをforkして自分だけのデモを作ってみてはいかがでしょうか。