前回の続き。今回は、王様にとって安全な場所(升)を探します。
このソフト、実は、すでにたけわらべより強いんですけど…
計測してみると、前回の評価関数で、すでにたけわらべよりR70~R100ぐらい強いです。
たけわらべ、マシンによっては(かなりハイエンドPCでは)、floodgateでR3000近くあるようで、ギリギリ、プロレベルと言えるのだそうです。
ということは、それより+R70~+R100も強ければ、プロレベルであると言っても批判は殺到しませんね。(←何に怯えてるんだ)
亀仙人「この連載は、もうちょっとだけ続くんじゃ」
数十行程度で、プロ棋士レベルの将棋ソフトの評価関数を作るという連載の目的自体は達成しました。← 意図せず達成していました😅
わりと短いコードで評価関数が1から書けるというのがわかっていただけたかと思います。また、それと同時に、評価関数を設計する時に、どんな特徴量を持たせると効果が高いかが理解していただけたかと思います。
「将棋の形勢判断は簡単な計算式で導き出せる」というのは私が小学校に入学したころに漠然と思い描いた大予想です。それを実証するのには科学技術の進歩を待たなければならなかったので、実に40年もの歳月がかかってしまいました。
それに、今回の評価関数によってプロ棋士の棋力に匹敵する将棋ソフトが作れたとは言っても、最新の将棋ソフトとは棋力に大きな隔たりがあります。それは、まだこの連載では明らかにされていない大切な特徴量が欠けているからです。それは一体、何なのでしょうか。
ここからは、行数制限を取り払って、もう少しだけ強い評価関数を作っていきます。
王様の段ごとのボーナス
「居玉は避けよ」とよく言われますが、この連載の評価関数、居玉のまま戦いがちです。
どうせ入玉した時の王様の段に対して加点するので、まずは玉の段ごとのボーナスを求めて加算してみました。
optimizerによると、玉の段ごとのボーナスは以下のようになりました。1段目から3段目(入玉)はそれぞれ600点、400点、375点の価値があるということです。
1 2 3 4 5 6 7 |
// 玉の段ごとのBonus (9段目を0とする) //int king_rank_bonus[] = { 600,400,375,320,120,280,100,130,0 }; (中略) // 玉の段に対する加点(入玉対策) //score += color_of(pc) == BLACK ? king_rank_bonus[rank_of(sq)] : -king_rank_bonus[rank_of(Inv(sq))]; |
興味深いことに8段目の玉に130点の点数がつきました。9段目よりは寄せにくそうなので、それくらいつくのかな、というところですが。
実際に対局させると、初手58玉とか指してきます。
うーん。そうじゃないんだよな…。😅
確かに59と58とでは130点増えます。130点増えるので初手58玉!そりゃそうなるでしょう。59よりは58のほうが寄せにくいでしょうから、勝率も以前よりは悪くないです。そりゃoptimizerが8段目の玉に対して130点の点数をつけるのも無理はありません。
でも、そうじゃないんだよな…。😅
王様の升ごとのボーナス
そんなわけでして、王様の段ごとのテーブルでは初手58玉になってしまう(なりがち)という問題があることがわかりました。
きちんと玉を囲うソフトにするにはどうすれば良いでしょうか?
盤上で、王手がかかりにくい場所とか、囲いやすい場所が存在してしまうのは、初期陣形とか駒の特性とかから考えて、仕方がないことなので考えるのを諦めて、升ごとにどれだけボーナスを加算すれば良いのかoptimizerに尋ねてみました。
59の升のボーナスを0点として考えるとマイナスの升があったので、59の升を200点としました。
そのマイナスの升とは、どこなのか…。
王様には居玉よりまだ悪い場所がある
optimizerが返してきた、王様のいる升ごとのボーナスは以下のようになりました。
1 2 3 4 5 6 7 8 9 10 11 12 |
// 玉の升ごとにBonus(盤面の1段目9筋から9段目1筋の順) int king_pos_bonus[] = { 875, 655, 830, 680, 770, 815, 720, 945, 755, 605, 455, 610, 595, 730, 610, 600, 590, 615, 565, 640, 555, 525, 635, 565, 440, 600, 575, 520, 515, 580, 420, 640, 535, 565, 500, 510, 220, 355, 240, 375, 340, 335, 305, 275, 320, 500, 530, 560, 445, 510, 395, 455, 490, 410, 345, 275, 250, 355, 295, 280, 420, 235, 135, 335, 370, 385, 255, 295, 200, 265, 305, 305, 255, 225, 245, 295, 200, 320, 275, 70, 200 }; |
盤面、左上(91)が875点、右上(11)が755点、左下(99)が255点、右下(19)が200点というわけです。
29とか居玉(200点)より悪い点数(70点)がついてますけど、まあ、振り飛車にしているとは限らないので居飛車のままここに持って行けばそりゃひどいことになるわな、ぐらいの感じではあります。
これで実際に囲うの?
68の升が255点、78の升が385点なので、78の升をめがけて一目散に囲うようになりました。これでは、どう見ても棒銀の餌食です。しかし、前バージョンの評価関数のソフトは短い時間だと棒銀をしてきません。Oh..
そんなわけで、optimizerは勝率を最大化するパラメーターを発見してくれるのではありますが、その対戦相手がきちんと咎めにきてくれないと話にならない感じではあります。
かと言って、あまり強いソフトを対戦相手に持ってくるとほとんど勝てなくなってしまい勝率が最大化するところを発見しづらくなります。オールラウンダーで、どんな戦型も指しこなす大局観のとても優れた弱いソフトが必要です。(わりと難しい注文です)
とりあえず、玉をうまく囲うかどうかはともかく、入玉に対しては上のテーブルでそこそこうまく加点できているので、入玉模様の将棋での勝率は上がりました。
+R30ぐらい強くなりました。
王様は、58と88、どちらが安全か?
58は295点、88は370点なので88のほうが良いと出ました。これは、正しいのでしょうか?
下段玉(9段目の玉)が良くない位置であるというのは、
・将棋には前に利く駒が多いので下段玉は攻められやすい(つかまりやすい)
・王様の下側が壁(盤外)なのでそれ以上、下に逃げられない
からだと思われます。
であれば、88の玉も、背(左と下)を壁にしている分だけ58より少ない駒で捕まるはずです。これ自体は正しい(と思う)のですが、実際はその周りに城壁を築くことができます。前回書いたように、88の玉に対してその左上、左と左下、下、右下から遠方駒を離しての王手はできませんから、必然的に、それ以外(上、右上、右)からの攻撃となります。守る方角が3方向だけで済むので、そこに城壁を築くならば、城壁を築くための資源(駒)が58の時よりも節約できます。
つまり、88の玉は58より少ない駒で捕まるというのは正しい(と思う)のですが、城壁を築きやすいというメリットがあるので、一般的には88のほうが58より守りやすいと考えられます。(対して、58玉型は、守りよりは、王様の広さ、捕まりにくさを主張する囲いです。)
初心者は矢倉囲いなどで88玉に入城するほうが安全と思ってしまいますが、安全なのではなく、守りやすいだけであって、58よりも少ない駒で捕まりやすい升であることは忘れてはなりません。
この連載で作った将棋ソフトで遊んでみたい人へ
この連載で作ったソフトの実行ファイルは以下のところからダウンロードできます。
実行ファイル名に”Material”とついているのがそれで、MaterialLv1は単純な駒得のみの評価関数。そこからLv2,Lv3,…とLevelが上がるごとに強くなっていきます。良かったら遊んでみてくださいね。
https://github.com/yaneurao/YaneuraOu/releases
また、このLevelと連載の第何回目が対応するかについては以下のソースコードにあります。
https://github.com/yaneurao/YaneuraOu/blob/master/source/eval/material/evaluate_material.cpp
今回のソースコード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 |
// KKPEEテーブル。Eが3通りなのでKKPEE9と呼ぶ。 // 利きの価値を合算した値を求めるテーブル // [先手玉の升][後手玉の升][対象升][その升の先手の利きの数(最大2)][その升の後手の利きの数(最大2)][駒(先後区別あり)] // 81*81*81*3*3*size_of(int16_t)*32 = 306MB // 1つの升にある利きは、2つ以上の利きは同一視。 int16_t KKPEE[SQ_NB][SQ_NB][SQ_NB][3][3][PIECE_NB]; // ↑のテーブルに格納されている値の倍率 constexpr int FV_SCALE = 32; void init() { // 王様からの距離に応じたある升の利きの価値。 int our_effect_value[9]; int their_effect_value[9]; for (int d = 0; d < 9; ++d) { // 利きには、王様からの距離に反比例する価値がある。 our_effect_value[d] = 83 * 1024 / (d + 1); their_effect_value[d] = 92 * 1024 / (d + 1); } // 利きが1つの升にm個ある時に、our_effect_value(their_effect_value)の価値は何倍されるのか? // 利きは最大で10個のことがある。格納されている値は1024を1.0とみなす固定小数。 // optimizerの答えは、{ 0 , 1024/* == 1.0 */ , 1800, 2300 , 2900,3500,3900,4300,4650,5000,5300 } // 6365 - pow(0.8525,m-1)*5341 みたいな感じ? int multi_effect_value[11]; for (int m = 0; m < 11; ++m) multi_effect_value[m] = m == 0 ? 0 : int(6365 - std::pow(0.8525, m - 1) * 5341); // 利きを評価するテーブル // [自玉の位置][対象となる升][利きの数(0~10)] double our_effect_table [SQ_NB][SQ_NB][11]; double their_effect_table[SQ_NB][SQ_NB][11]; for(auto king_sq : SQ) for (auto sq : SQ) for(int m = 0 ; m < 3 ; ++m) // 利きの数 { // 筋と段でたくさん離れているほうの数をその距離とする。 int d = dist(sq, king_sq); our_effect_table [king_sq][sq][m] = double(multi_effect_value[m] * our_effect_value [d]) / (1024 * 1024); their_effect_table[king_sq][sq][m] = double(multi_effect_value[m] * their_effect_value[d]) / (1024 * 1024); } // 駒に味方/相手の利きがn個ある時の価値(この係数×駒の価値/4096が上乗せされる) int our_effect_to_our_piece[3] = { 0, 33 , 43 }; int their_effect_to_our_piece[3] = { 0, 113 , 122 }; // 玉の段ごとのBonus (9段目を0とする) //int king_rank_bonus[] = { 600,400,375,320,120,280,100,130,0 }; // 玉の升ごとにBonus(盤面の1段目9筋から9段目1筋の順) int king_pos_bonus[] = { 875, 655, 830, 680, 770, 815, 720, 945, 755, 605, 455, 610, 595, 730, 610, 600, 590, 615, 565, 640, 555, 525, 635, 565, 440, 600, 575, 520, 515, 580, 420, 640, 535, 565, 500, 510, 220, 355, 240, 375, 340, 335, 305, 275, 320, 500, 530, 560, 445, 510, 395, 455, 490, 410, 345, 275, 250, 355, 295, 280, 420, 235, 135, 335, 370, 385, 255, 295, 200, 265, 305, 305, 255, 225, 245, 295, 200, 320, 275, 70, 200 }; // ある升の利きの価値のテーブルの初期化。 // 対象升には駒がない時の価値をまず求める。(駒がある時の価値を計算する時にこの値を用いるため) // そのため、Pieceのループを一番外側にしてある。 for (Piece pc = NO_PIECE; pc < PIECE_NB; ++pc) // 駒(先後の区別あり) for (auto king_black : SQ) for (auto king_white : SQ) for (auto sq : SQ) for(int m1 = 0; m1<3;++m1) // 先手の利きの数 for (int m2 = 0; m2 <3; ++m2) // 後手の利きの数 { // いまから、先手から見たスコアを計算する。 double score = 0; // 対象升(sq)の利きの価値。 // これは双方の王様から対象升への距離に反比例する価値。 score += our_effect_table [ king_black ][ sq ][m1]; score -= their_effect_table[ king_black ][ sq ][m2]; score -= our_effect_table [Inv(king_white)][Inv(sq)][m2]; score += their_effect_table[Inv(king_white)][Inv(sq)][m1]; // 玉の8近傍に玉以外の味方の利きがなくて駒があるなら、ペナルティ(実は、加点。利きがなくとも駒があることはプラスであった) (+R15) // 玉の8近傍で玉以外の味方の利きがなくて空いている(or 相手の駒)なら、ペナルティ。(+R20) for (auto color : COLOR) // 両方の玉に対して { auto king_sq = color == BLACK ? king_black : king_white; // 玉の8近傍 if (dist(sq, king_sq) == 1) { int effect_us = color == BLACK ? m1 : m2; if (effect_us <= 1) // 自玉以外の利きがない。この時、そこが空きか敵駒なら、減点。さもなくば加点。 score -= double((pc == NO_PIECE || color_of(pc) != color) ? 11 : -20) * (color == BLACK ? 1 : -1); else // 自玉以外の利きがある。この時、そこが空きか敵駒なら、減点なし。さもなくば9点加点。 score -= double((pc == NO_PIECE || color_of(pc) != color) ? 0 : - 11 ) * (color == BLACK ? 1 : -1); } } if (pc == NO_PIECE) { // 1) 駒がない対象升 } else if (type_of(pc) == KING) { // 2) 玉がいる対象升 // 敵陣にいる玉の段に対する加点(入玉対策) //score += color_of(pc) == BLACK ? king_rank_bonus[rank_of(sq)] : -king_rank_bonus[rank_of(Inv(sq))]; // 玉のいる升に対する加点 // テーブル、わかりやすいように段の順番で並べたので、やねうら王はSquareは筋の順で並んでいるから、転置する。 score += color_of(pc) == BLACK ? king_pos_bonus[(FILE_9-file_of( sq )) + int(rank_of( sq )*9)] : -king_pos_bonus[(FILE_9-file_of(Inv(sq))) + int(rank_of(Inv(sq))*9)]; } else { // 3) 玉以外の駒がいる対象升 // 盤上の駒に対しては、その価値を1/10ほど減ずる。 // 玉に∞の価値があるので、PieceValueを求めてその何%かを加点しようとすると発散するから玉はここから除く。 double piece_value = PieceValue[pc]; score -= piece_value * 104 / 1024; // さらにこの駒に利きがある時は、その利きの価値を上乗せする。 // 味方の利き(紐がついてる) = 加点 // 相手の利き(質(しち)に入っている) = 減点 // piece_valueは、後手の駒だと負の値になっている。 // scoreはいま先手から見たスコアを計算しているのでこれで辻褄は合う。 int effect_us = color_of(pc) == BLACK ? m1 : m2; int effect_them = color_of(pc) == BLACK ? m2 : m1; score += piece_value * our_effect_to_our_piece [effect_us] / 4096; score -= piece_value * their_effect_to_our_piece[effect_them] / 4096; } // 先手から見たスコア KKPEE[king_black][king_white][sq][m1][m2][pc] = int16_t(score * FV_SCALE); } } // KKPEE9 評価関数本体(わずか8行) // 変数名短くするなどすれば1ツイート(140文字)に収まる。 Value compute_eval(const Position& pos) { Value score = VALUE_ZERO; for (auto sq : SQ) score += KKPEE[pos.king_square(BLACK)][pos.king_square(WHITE)][sq] [std::min(int(pos.board_effect[BLACK].effect(sq)),2)][std::min(int(pos.board_effect[WHITE].effect(sq)),2)][pos.piece_on(sq)]; // KKPEE配列はFV_SCALE倍されているのでこれで割ってから駒割を加算する。 score = score / FV_SCALE + pos.state()->materialValue; return pos.side_to_move() == BLACK ? score : -score; } |
次回予告
次回「将棋は上からの攻めと横からの攻め、どちらが受けにくい?」です。お楽しみに!
タケワレベはMaterialLv999だが、MaterialLv8より弱い(笑)
たけわらべは特別枠なのですw
てっきり連載を最後まで進めてから、読者課題でLv999のたけわらべを目指してみよう!みたいなシステムかと思ったら、たけわらべは本連載のレベルシステムとは別のユニークジョブだったんですねw
それにしてもほぼ盤上の評価だけでプロ棋士レベルになるとは驚きです。N枚目の手駒にM点とか盤外の項目も調整しないと届かないのかぁとか思ってました。
しかも、positionalな評価、ほとんどしてない状態でこれです。探索部が頑張ってるからではあるんですけど、さすがにもう少し書いてからでないと終われませんw
こんにちは。
特徴量が追加されてきましたが、これによって初期に決定した特徴量のパラメータが変わっていないか知りたいのですが、いかがでしょうか。
勿論すべての値を確認するのは大変と思うので、1~2つでも。
大きく変わるのであれば、順に何回かループを回す必要がありそうですが。
例えば、利きの価値は盤上の駒にのみ発生するので、このように盤上の駒によって発生する価値を上げていくと、盤上の駒と手駒との価値の差がなくなってくるので、手駒の価値をもっと上げないと!(or 盤上の駒の価値をもっと下げないと!)みたいな感じにはなりますね。ただ、微差なので正確な値を求めるのに3万対局ぐらい回さないといけなくて、それだけやってもレーティングにはほとんど寄与しないので、まあいいか…という感じでございます。
めちゃめちゃ面白いです!
typo:28→29?
>28とか居玉(200点)より悪い点数(70点)がついてます
ありがとうございます。修正しました!
こんなところまで研究に来てるリアル人間の棋士の方がいたら、王様を945のところにぶち込んだりする戦法をやってくれたりするかな?w
アメフトのタッチダウンじゃないんだからw