将棋AIの大会である電竜戦に新規参入者を増やそうとする試みとして準備しつつある1file matchですが,個人的に盛り上がってシリーズ化の勢いです。
1file match(仮) - 48's diary
1file match(仮)の参考資料 - 48's diary
1file match(仮)の参考資料2(数行でレートを1300以上上げる) - 48's diary
すると山岡さんが呼応するように1fileのサンプルを出して下さいました。
cshogiのサンプルプログラム(MinMax探索) - TadaoYamaokaの開発日記
ランダムプログラムとMin-Max法による探索のサンプルです。Min-Max法自体は1950年代からある手法で今更解説というまでもない気がしますので他に任せます。(私のスライドや動画もネット上のどこかにあるでしょう。)
Min-Max法には局面評価が必須になるのですが、将棋の場合は古典的に「駒得」という考え方があります。簡単に言うと将棋は取った相手の駒を自分の駒として使えるので、任意の局面で駒の総和が保存されますが自分と相手の駒の差が生じます。この差のことを駒得といいます。序盤においては歩一枚でも歩得と言われるほど繊細なものです。
森下卓九段の名言に「駒得は裏切らない」との言葉があるように、迷ったときに駒を得する方針で指していくことが失敗しづらいとされています。もちろん必ずしも正解であると言えないのが将棋の面白いところです。
上記山岡さんのプログラムを一行だけ引用しました。
value = sum([PIECE_VALUES[piece] for piece in board.pieces])
この一行で盤面の駒の価値を集計するものです。
board.piecesはcshogiの機能で盤面(board)上の駒(piece)を81マス分並べた配列が得られます。
for ○○ in ×× はpythonでよく使われる「リスト内包表記」ですね。
今回は81マス分のpieceです。このpieceは0が駒無しマス、1が先手の歩、2が先手の香車・・・そして31が後手の竜です。
PIECE_VALUES[piece]は各駒の価値で固定値で読み出し以前に初期化されています。
PIECE_VALUES[0] が 0 であることが重要です。
そしてこれらが大括弧で囲まれており、その値がList化されます。
sumの引数としてこのListが与えられますので盤面81マス分の駒の価値(PIECE_VALUES)の総和となるわけです。
もちろん普通のfor文で回して足していくことも可能ですが簡潔に書ける上、速度も速いことが知られています。
material_c = [0,90,315,405,495,855,990,540,0,
540,540,540,540,945,1395,0,0,
-90,-315,-405,-495,-855,-990,-540,0,
-540,-540,-540,-540,-945,-1395,0,0,]
material_h = [90,315,405,495,540,855,990,]
def eval(board): # 駒の価値
eval_mat = sum( material_c[p] for p in board.pieces if p > 0 )
pieces_in_hand = board.pieces_in_hand
eval_mat += sum( material_h[p] * (pieces_in_hand[0][p] - pieces_in_hand[1][p]) for p in range(7) )
if board.turn == cshogi.BLACK:
return eval_mat
else:
return -eval_mat
私の方は以前はnumpy.whereを使ってちょっと小技を利かせていたのですが、山岡さんのを参考に以上のように書き換えてみました。大きなデータだとnumpyの方が速いのですが本件くらいのサイズはこちらの方が高速でした。
material_h は持ち駒の価値ですが、この数値の由来はAperyが機械学習で算出したものと記憶しています。
material_c はこれを盤面用に先手後手と並べ替えた値であることは容易に分かると思います。山岡さんのPIECE_VALUESと同じ意味のものです。
次にこの一行ですが、ほぼ山岡さんと同じですが僅かに異なります。
eval_mat = sum( material_c[p] for p in board.pieces if p > 0 )
board.piecesの後ろにif文があります。81マスにはpが0の部分が大半なので、これを除外するイテレータとなります。要するに駒のあるマスしか数えないってことです。
material_c[0] に何が入っていても関係なくなります。
また、上記山岡さんのものと比較して大括弧が無いことが分かると思います。[]があるとリスト内包表記としてメモリ上にListが作成されます。無い場合は「ジェネレータ式」と呼ばれる表現になり、実態はジェネレータオブジェクトです。リスト内包表記はジェネレータ式をList化(実体化)したものと理解しましょう。
sumはListも引けますが、ジェネレータ式を引いた場合Listを実体化することなく合計だけを求めます。つまりListを実体化する分のメモリ利用も減りますし実行速度も上がります。(今回は僅かですが、大量のデータでは差が付きます)
詳しくはクラスメソッドの方がいい記事を書かれていますので御参照下さい。
Pythonのreduceと内包表記/ジェネレータ式を比較してみた | DevelopersIO
Pythonにもreduceあるんですね。知りませんでした。
eval_mat += sum( material_h[p] * (pieces_in_hand[0][p] - pieces_in_hand[1][p]) for p in range(7) )
こちらは持ち駒の集計ですね。
board.pieces_in_hand で得られる持ち駒の配列を一旦ローカルに入れてから集計しています。Pythonではオブジェクト参照のコストも馬鹿にならないので、こういったことをすることもあります。(Python本体のバージョンが上がったら無駄な行為となるのでしょう。)
最後に今まですべて先手側から見た評価でしたので、後手番の場合は符号を変えて返すことにします。
以上で駒得評価関数の実装サンプルでした。
短い中にも細かいテクニックがあり、それが効果的であることは手を動かして確認して頂けるとPython初級者にも身に付くと思います。
numpy.sumもジェネレータ式引けてベクトル演算できたら嬉しいなとか思いました。
---
追記:
あっという間に,山岡さんの実装も更新されてほぼ同じになってた。
実質contributorですね。
github.com
ちなみに随分昔に他の評価関数のPython実装も公開してある。
今よりコーディングが下手なのが分かるので上手に書き直すのもできそう。
github.com