将棋AIと言えば、探索と評価関数で構成されています。探索と評価関数が車輪の両輪のようなもので、その2つがうまく合わさって初めて強い将棋AIが完成します。
ところが、やねうら王系の最新の評価関数であるNNUEは浅い層からなるニューラルネットを採用しているので、新たに将棋ソフトを作ろうと始めた人がいきなりNNUEの実装をやろうとすると普通、挫折します。
そもそも評価関数が完成してもパラメーターが多すぎるので機械学習によってチューニングせねばなりません。機械学習を行うには、教師データが必要で、将棋AIの場合、自己対局により教師データを生成してそれを用いるのですが、そのためには大量の計算資源が必要になります。あと強化学習をするためのコードを書かないといけません。機械学習の理論を学ばなければなりません。
そのように、ちょっと中高校生ぐらいの人がご家庭のPCで趣味で将棋ソフトを作ろうと思った時に、いきなりNNUEを実装して、機械学習のコードを書いて…みたいなのは、わりとハードルが高いということになります。
NNUEの一つ前に流行ったKPPT型は、Bonanzaで採用されていた3駒関係(KPP型)に手番の評価を追加したもので、こちらはわりと実装は楽ですが、それでも機械学習は必要になりますし、他の人が作った評価関数ファイルを使うにしても、駒の通し番号(BonaPiece)は、やねうら王と同じ定義にしないといけませんし、あまりお手軽とは言い難いです。
そこで、実装が容易で、移植が容易で、かつ、そこそこ強い評価関数を提案し、自作してみようというわけです。ここで簡単で強い評価関数を自作することによって、どのような特徴量が将棋の盤面評価のために重要であるかという知見が得られますし、新たな評価関数を設計するときの指針にもなります。
目標としては、
・50行程度の評価関数で、人間(プロ棋士)と同じ程度の強さに
※ Ryzen Threadripper 3990Xを使用して、持ち時間15分のときの棋力。
・実装が容易・移植が容易
・機械学習は用いない(パラメーターのチューニングはして良いものとする)
・人間にとって意味を理解しやすい
・汎用性が高い
ということです。
要するに機械学習を使わずにプロレベルの強さにするということで、それだけでもハードルが高いのに(もしかすると前人未到?)、それをたった50行程度の評価関数で行わないといけなく、パラメーター数も手で調整可能な範囲(50個ぐらいまで?)で、それでいて人間に理解しやすく、汎用性が高い(盤面サイズを9×9ではなく、6×5や13×13など他のサイズにしてもそこそこうまく機能する)という、大変、欲張りな企画ですね。
自分で書いてて、「おいマジかよ…」と思わなくもないですが、さっそくやっていきましょう。
駒得評価関数
まず、駒得だけの評価関数を考えます。
将棋は、敵の玉の行き場所を無くせば勝ちのゲームです。その行き場所を無くすためには、敵玉とその周辺8近傍に利きを発生させる必要があります。利きを発生させるには駒が盤上になくてはなりません。つまりは、そのための駒が必要なゲームなのです。言うまでもありませんね。
やねうら王では、局面を1手進めたり戻したりした時に駒割を差分計算していて、Positionクラスから辿れるStateInfoというクラスが駒割を持っています。
そこでやねうら王を用いる場合は、これを返す関数を書くだけで良いです。
やねうら王を駒得評価関数を使うようにコンパイルするとして(Makefileで一発でできます。Makefileを見てください)、evaluate.cppにその時の評価関数を書くところがあるので、とりあえずそこを書き換えていきます。
// 備考 : 近いうちに、この駒得評価関数本体は、evaluate.cppから、eval/Material/に移動させます。
1 2 3 4 |
Value compute_eval(const Position& pos) { auto score = pos.state()->materialValue; return pos.side_to_move() == BLACK ? score : -score; } |
pos.state()->materialValueが駒割です。compute_eval()は手番側から見た評価値を返す関数なので、BLACK(先手)でなければ符号を反転させて返します。
駒割には、Aperyで定義されていた駒点をそのまま使っています。これは、KPPT型の評価関数を読み込むために必要だったのでこうしてあります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// Apery(WCSC26)の駒割り enum { PawnValue = 90, LanceValue = 315, KnightValue = 405, SilverValue = 495, GoldValue = 540, BishopValue = 855, RookValue = 990, ProPawnValue = 540, ProLanceValue = 540, ProKnightValue = 540, ProSilverValue = 540, HorseValue = 945, DragonValue = 1395, KingValue = 15000, }; |
はい、これで、駒得だけを評価する評価関数のエンジンができました。やねうら王の探索部を用いる場合、AWSの最上位インスタンス(c5.24xlarge or c5.metal)を用いればこれでR2300前後ぐらいです。
ただ、持ち時間を増やしてもほぼ強くなりません。ソフトの指す序盤の形が悪すぎて、将棋ウォーズ5段ぐらいの人は何回かやると作戦勝ちにする手順を見つけてしまいます。
これは、序盤はどの指し手であっても駒得にはならなず、評価値が0である局面が続くので、指し手が生成した指し手の順番(端歩を突くだとか、変な筋の歩を突くだとか)になりやすく、それが緩手になりやすいためです。
駒得評価関数 + 手駒評価 / R2350
手駒の価値と盤面の駒の価値は異なります。手駒はどこにでも打てますからね。
そこで、盤面の駒の価値を1/10ほど割り引いてやりましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
Value compute_eval(const Position& pos) { auto score = pos.state()->materialValue; for(auto sq:SQ) { auto pc = pos.piece_on(sq); // この升に駒がなければ次の升へ if (pc == NO_PIECE) continue; // 駒の価値。 // 後手の駒ならマイナスになるが、 // いま計算しようとしているのは先手から見た評価値なので // これで辻褄が合う。 auto piece_value = PieceValue[pc]; score -= piece_value * 104 / 1024; } return pos.side_to_move() == BLACK ? score : -score; } |
1/10引けば良いのですが、CPUにとって割り算は非常に遅い演算なのでこれを避けるためにちょっと工夫しています。割り算は、足し算・引き算の100倍ぐらい遅く、掛け算は、足し算・引き算の3倍ぐらいの遅さです。なので、割り算は掛け算に変形したいわけです。そこで、上式では1024倍してあります。1024での除算は、1024が2の10乗なので(いまどきのC++のコンパイラであれば)ビットシフトで行うコードが生成されます。なので1024での割り算は、生成されるコードは割り算ではありません。
あと102 / 1024 ではなく 104 / 1024 となっているのは、104にしたほうが強かったからです。このようなパラメーターの最適化は、やねうら王では(半)自動で行うためのチューニングフレームワークがあるのですが、まあ、そういうのを自作していない人は、普通に3000局ほど自己対局させて、勝ち越したほうの値に調整していく、ぐらいでよろしいかと思います。
さて、このように手駒と盤上の駒の価値を変えたことによって、先手の飛車先の歩の交換を後手(ソフト)は、拒否するようになります。相手に歩を手駒にされると評価値が下がりますからね。
わずかなコードの違いですが、こういう少ししたことが棋風に大きな影響を与えていきます。実際、上の改良だけで、+R50ぐらい稼げます。将棋って面白いですね!
この連載にとても期待しています!
期待に1000%ぐらい、お応えできる内容となるはずです!(`・ω・´)b
ボナンザ以前の将棋AIプログラマー達の苦労を開陳するのかと思いましたが、そうではなくて安心しました。
続投を期待しております。
まあ、当時は計算資源が乏しく、自己対局自体がままなりませんでしたし、評価関数のどこが要所なのかという理解自体が浅かったので当時の開発者の人たちが苦労されたであろうことは容易に想像がつきます。現代は、他のところからアイデアをたくさんもらうことが出来ますし、連続自己対局フレームワーク、パラメーター自動最適化フレームワーク、CIツールによる自動化、当時の数百倍の計算資源等、何もかも状況が異なりますので…。
PawnValue = 100 こそが評価値の基準であり定義ということになっていませんでしたっけ?
当初はそうだったけど、その後に有名無実化したのでしょうか?
出力するときにUSI/UCIプロトコルでは、PawnValue = 100とみなして正規化して出力する必要があります。この単位はcp あるいは centipawn(1/100 ポーン)と呼ばれます。ところがこれを用いて評価関数を設計すると将棋の場合、評価値がときどきINT16_MAX = 2^15-1(=32768)を超えてしまいます。そこでAperyはKPPT型の評価関数を実装するときに、これをダウンスケールするために0.9で割ったのです。
「よくわかるコンピューター将棋の歴史」← みたいな本があれば載ってそう。
おおーこれは豆知識!
ま、またスレッド無視をやってしまった
駒得だけを評価というのは、評価関数が何もない状態から
何手か先を読んで駒を取る/取られるだけを計算してR2300になる(序盤は除く)ということでしょうか。
だとしたらすごいと思います!
それだけやねうら王の探索部の精度が良いということでしょうか。
仮に20年前の知識で作った探索部を使用した場合、どのぐらいレーティングが下がると考えられますか。
20年前の探索部 + 駒得評価関数 + 現代のPC だとしたら、R1000、あるかないかぐらいでしょうか…。
楽しみです よろしくお願いいたします。
将棋好きです。