TadaoYamaokaの開発日記

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

高速なPythonの将棋ライブラリを作る

python-shogiは、Pythonで扱える非常に役立つ将棋ライブラリですが、速度が遅いのが用途によっては欠点になります。
公式サイトにも記述されていますが、速度よりもシンプルに抽象的に扱えることが目的となっています。

しかし、機械学習の用途に使用しようとする速度の遅さがネックになります。
そこでPythonからもできるだけ高速に動作する将棋ライブラリを作成することにしました。

python-shogiの内部では、盤面はビットボードで表現されていますが、Pythonのビット演算は非常に遅くボトルネックとなっています。
ビット演算部分をC++で開発して、Pythonから呼び出せるようにすることで速度の改善が見込まれます。
C++で将棋ライブラリを一から作成するのもロマンがありますが、ほとんど既存のライブラリをまねるだけになるため、C++部分にAperyのソースコードを使用させてもらい、AperyをPythonから呼び出せるようにする形で作成することにします。

C++Pythonのブリッジの方法には、Boost.PythonやPyBind11やcythonなどがありますが、cythonを使うことにしました。
理由は、cythonではインストール時に自動でビルドすることが可能で、Google Colabのようなクラウドでのインストールも容易になるためです。

C++Pythonバインディング

cythonでC++のクラスや関数を呼び出すことは比較的容易で、拡張子が、.pyxのファイルを作成して、Pythonとほぼ同じ構文で

cdef extern from "cshogi.h":
	cdef cppclass __Board:
		__Board() except +
		__Board(const string& sfen) except +
		bool set_hcp(const char* hcp)

のように、extern宣言を行うだけでPythonで定義したクラスとほぼ同様に扱うことができます。
型の変換も自動で行われるため、ほとんど気にすることはありません。
ただし、ポインタで配列を渡す場合などは、少しトリックが必要になります。

Pythonからポインタを渡すには、cythonのメモリビュー機能を使って、Numpyのndarrayからポインタに変換します。

def func(data)
    cdef char[::1] = data
    c_func(&data[0])

参考:Typed Memoryviews — Cython 3.0.0a10 documentation

Aperyから借用したソース

Aperyのソースコードから盤面管理と合法手生成の部分を残して、それ以外は削除しました。
PositionクラスはSearcherと密結合になっていましたが、Searcherが不要になるように修正を行いました。

Aperyにある局面を圧縮形式(HuffmanCodedPos)で保存、読み込みする機能も、Pythonから使用できるようにしています。
これにより、機械学習での局面の保存、読み込みが速くなります。

hcpでの保存、読み込みの例
import cshogi
import numpy as np
board = cshogi.Board()
# save
hcp = np.empty(1, dtype=cshogi.HuffmanCodedPos)
board.to_hcp(hcp[0]['hcp'])

# load
board.set_hcp(hcp[0]['hcp'])

独自に追加した機能

独自に追加した機能としては、python-shogiと同じようにCSAファイルの読み込みが行える機能を用意しました。

また、やねうら王の学習局面データ形式(PackedSfen)も読み込めるようにしました。

インストール

GitHubからcloneして、pipでインストールします。

git clone https://github.com/TadaoYamaoka/cshogi.git
cd cshogi
pip install -e .

直接GitHubからインストールすることもできます。

pip install git+https://github.com/TadaoYamaoka/cshogi --no-cache-dir

インストールパッケージ名前は、cshogiで、importする際のモジュール名も、cshogiになります。
※2019/2/28 インストールパッケージ名をモジュール名と合わせました。

使用例

盤を作成して、開始局面で合法手を生成して表示し、1手指す処理の例です。

import cshogi
board = cshogi.Board()
# legal moves
for move in board.legal_moves:
    print(cshogi.move_to_usi(move))
# move
move = board.push_usi('7g7f')
# print board
print(board)

注意点

文字列の扱い

文字列はすべてバイト列で扱います。
Python3では、文字列リテラルにbをプレフィックスとして付けます。

※2019/8/25 引数の文字列をバイト文字列から標準の文字列型に修正しました。

指し手の扱い

指し手は、数値で表します。
python-shogiでは、指し手はMoveクラスとして扱い、便利なメソッドが用意されていますが、速度重視でクラスにしていません。
その代わり、いくつかのヘルパーメソッドを用意しています。

  • move_to(move)
  • move_from(move)
  • move_is_promotion(move)
  • move_is_drop(move)
  • move_to_usi(move)
  • move_to_csa(move)

合法手チェック

速度を重視して着手を適用する際、合法手チェックを行っていません。
誤ったmoveを渡すと盤面データが壊れて、その後アクセス違反などでプログラムが異常終了する場合があります。

undo処理

python-shogiと異なり、局面を戻す際に、moveが必要になります。

# undo move
board.pop(move)
座標系

座標系は、Aperyに準拠しています。python-shogiとは異なるので注意が必要です。詳細は、こちらの記事を参照してください。

駒の扱い

駒に対応する数値もAperyに準拠しています。python-shogiとは異なるので注意が必要です。
詳細は、cshogi.pyxのPIECES、PIECE_TYPESの定義を確認してください。

速度比較

python-shogiと速度を比較しました。
比較には、機械学習を想定して、CSA形式の棋譜を読み込んで、棋譜を再生して、局面をリストに格納する処理を使用しました。
python-shogiでは、ビットボードをコピーしてリストに保存し、cshogiでは、hcpe形式でリストに格納します。
比較用コード:cshogi/read_csa.py at master · TadaoYamaoka/cshogi · GitHub

棋譜100ファイルを処理した時間の比較
python-shogi 0.6229 秒
cshogi 0.0629 秒

cshogiの方が、9.9倍高速です。

今後の予定

Google ColabでPythonオンリーでAlpha Zeroと同じ強化学習ができるようにすることを目標にしています。
そのために必要な機能拡充を行っていく予定です。

技術書典6に当選したので、強化学習をテーマに本を出す予定です。
本にするには、読者の実行環境を考慮する必要がありますが、誰でも同じように実行できるGoogle Colabが最適だと思います。
作ったのは、そのための準備でもあります。

今のところ使用方法とか説明がないので、使い方の詳細はソースを読む必要があります。
余裕ができたら使い方の説明も作成したいと思います。

補足

おそらくまだバグが残っています。
プルリクもお待ちしています。

github.com

2019/2/11 追記

Google Colabで使用する方法

現状PyPIに登録していないため、Google Colabで使用するには、GitHubからcloneしてローカルのファイルでインストールする必要がある。
そのためには、ノードブックで以下のコードを実行する。

!git clone https://github.com/TadaoYamaoka/cshogi.git
%cd cshogi
!pip install -e .
%cd ..

これで、cshogiをimportして使用できるようになる。

2019/2/16 追記

直接GitHubからインストールすることもできる。

!pip install git+https://github.com/TadaoYamaoka/cshogi --no-cache-dir
アンインストール

ランタイムをクリアすれば、インストールの状態もクリアされるが、個別にアンインストールしたい場合は、

!pip uninstall cshogi

を実行すればよい。ロード済みのモジュールはそのまま使用される。再インストールして再読み込みさせるにはランタイムの再起動が必要になる。