前回の続き。
今回は、利きの価値についてさらに突っ込んで考えていきます。
玉のいる升で利きの価値はどう変動するか
これまで玉がどこにいてもそこからd升離れた升にある利きの価値は一定であると仮定して考えてきました。これは果たして正しいのでしょうか。
例えば、99にいる先手玉を1升離れて包囲するには、3升で済みますが、59にいる玉ですと5升必要になります。58にいる玉ですと8升必要になります。
このように包囲するのに必要な升の数が、玉の位置によって異なります。
「もしかして、玉のいる升をd升離れたところで盤上で包囲する升の数を数えて、それで割ってやればより正確な利きの価値が算出できるのでは?」
とあなたは考えることでしょう。
しかし残念ながらこの仮説は実は全く正しくないのです。このことを私はいまから2つの方法で示します。
1つ目は、まずこのコードを書いて、optimizerでパラメーターを最適化し、それが元の評価関数より弱くなってしまうことによってです。
2つ目は、その解釈だと前回記事の内容に反するということによってです。
盤上の升sqにいる玉をd升離れて包囲するコードを書いてみよう
「盤上の升sqにいる玉をd升離れて包囲するのに何升必要か」というのは、計算で出せるのですが条件式が複雑で下手に書くとソースコードが汚くなりバグの原因にもなります。
テーブル自体は事前に計算しておけば良いので、愚直にsqに玉を置いたときにd升離れたところに一辺が(2*d+1)升の正方形を置いて、その辺のうち盤内に収まっている升の数を数え上げたほうが楽です。
// d升離れたところに配置するのは「一辺が(2d+1)升の正方形」なので、辺の長さは盤外に出てる分まで含めると、この4倍である (8d+4) から4つの頂点である4升分を引いて 8*d 升となります。ここから盤外にある分を省いた升の数をいま求めようとしています。
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 |
// sqの升にいる王様からの距離に応じたある升の利きの価値。 int our_effect_value [9][SQ_NB]; int their_effect_value[9][SQ_NB]; for (auto king_sq : SQ) for (int d = 0; d < 9; ++d) { int sum = 0; for(int r = -d ; r <= d ; ++r) for (int f = -d; f <= d; ++f) { // 辺でないならカウントしない if (!(r == -d || r == d || f == -d || f == d)) continue; File f2 = File(file_of(king_sq) + f); Rank r2 = Rank(rank_of(king_sq) + r); // File,Rankはintのenumなので、マイナスの値を取れることは保証されている。 if (f2 < FILE_1 || f2 > FILE_9 || r2 < RANK_1 || r2 > RANK_9) continue; ++sum; } // SQ_22 に対して d = 8など、sum = 0になりうるが、SQ_22に対して d = 8になる場所に駒を配置できないため // この時のentryは実際には参照されない。とりあえず、sum = 1として計算してゼロ割を回避する。 if (sum == 0) sum = 1; our_effect_value [d][king_sq] = PARAM_OUR_EFFECT_VALUE / sum; their_effect_value[d][king_sq] = PARAM_THEIR_EFFECT_VALUE / sum; } } |
これで強くなる?
optimizerでPARAM_OUR_EFFECT_VALUE,PARAM_THEIR_EFFECT_VALUEの値を決めてみましたが、駒得評価関数+R100程度になりました。前回の評価関数からすると -R100 程度ですね。つまりは弱くなりました。
弱くなることは実は事前に予期できた
これで弱くなるのは何故なのか?というところなのですが、私にはこの結果になることは事前に予測できていました。
これから、その理由を示します。
前回記事の内容から、自分の王様に味方の利きがある時に68。1升離れたところだと、その496/1024 ≒ 48.4% つまり半分程度の価値になる(半分程度の価値はある)ということです。
ところが今回の方法ですと1升離れた升の数は、王様が盤面の四隅にいるときに最小値3、そこ以外で盤面の辺にいるときに5、それら以外の升にいるときは8となります。“均す”(単純平均ではなく実戦で現れやすい局面での平均)と、6升ぐらいでしょうか。
つまり、今回のモデルですと、1升離れたときの利きの価値は、(王様に利きがある時の)1/6と計算してしまいます。これは、上の48.4%をかなり下回る数値です。
このようにoptimizerからの答えから外れるような値に設定すれば弱くなって当然ですし、そのようなモデルはそれがいかにもっともらしく思えたとしても誤っている(ことが極めて多い)ということです。
詰ますのに必要な利きの数は玉の場所に(あまり)依存しない
99の先手玉の1升離れた3升すべてに相手の利きがある状況と、59の玉の左上・真上・右上の3升に利きがある状況とで、どちらが逃れやすいかということです。わずかに59のほうがマシかもしれませんが、59のほうはそこを包囲する5升のうちの3升だけしか利きがないから5/3倍ぐらい逃げやすいぞ、とはならないでしょう。
つまりは、玉がどの升にいようと、そこまで大きく利きの価値が変わるわけではないと考えられます。(変わることは変わります。そこをうまくモデル化してやる必要がありますが、今回の記事の範囲ではないので割愛します。)
今回のモデルに悪い点はもうひとつあって、例えば、98の玉を8升離れて包囲するのに必要な升の数は、1筋の升(9升)になります。1筋って後手の香を11におけば(1筋に駒が他になければ)9升中8升も利きますが、これが99から1升離れた3升に利きがある状態と同じ価値があるわけないですよね。
このように、王様からd升離れた升の利きの価値を、“盤上で”王様をd升離れて包囲するのに必要な升の数に反比例すると主張するのは相当な無理があるのです。
我々は、すでに得られているoptimizerからの答えや、いまわかっている事実に反しないモデルを組み立てていかなければなりません。
optimizerは現代のチート兵器
20年ほど前は、評価関数パラメーターの調整に機械学習が導入されておらず、評価関数を手で設計していた古き良き時代でありました。
その頃は、自己対局もいまのように並列化して1台のPCで並列120対局のようなことができる時代ではなかったので、改良前のプログラムと100局ぐらい対局させて勝ち越していれば「採用!」というような感じで開発していました。
それからすると、手軽に実装して、「ここのパラメーターの最適値教えて?」と機械に尋ねると、10分ほどで(今回のケースですと0.1秒対局×40並列対局×Xeon機8台で1万対局)最適なパラメーターを教えてくれる現代とは事情が全く異なるのです。
また、現代であれば、1手1秒の3000対局ぐらいすぐにこなせるので、R5やR10のような小さな改良を拾うことも容易です。当時は計測技術自体がそこまでなかったので、そんな小さな改良を積み重ねることは到底できませんでした。
戦国時代のドラマを観て、「こんなん核兵器があれば一発やん」みたいなことを言うのが無粋なのと同様、現代の視点から当時の技術を軽々に語るべきではありません。むしろ、そのような限られた計算資源のなかでプログラムを改良してこれた先人たちの知恵に対して敬意を払うべきだと思います。電子顕微鏡がない時代に原子説や分子説を発表したドルトンやアボガドロ同様に。
まあ、optimizerの存在が強力無比であることは言うまでもありません。20年前には、「利きの価値」のようなものを推定することは非常に難しかったことでしょう。(私は当時の開発者が利きの価値をどのように推定していたのか、推定できていたのかを知りません。)
現代では、利きの価値を簡単に推定できるので、それを利用して短いコードで効率的な評価関数が手軽に書けるというわけです。今回の連載で書いているソースコードが極めて短いのに、高い効果を発揮できているのは、このoptimizerあってこそなのです。
optimizerを活用して、どれだけ短いコードでどれだけ価値の高いコード(強い評価関数)が作れるかというのがこの連載の見どころの一つでしょう。
次回予告
次回「1つの升に利きが複数あるとき、そんなに価値が減るの?なんで?!」です。お楽しみに!(予告の内容とは、異なる内容の場合がございます。予めご了承願います。)