/Gaussian_lambda

Primary LanguageC++GNU General Public License v3.0GPL-3.0

Gaussian_lambda

elmo式学習のlambda設定のGaussian化による学習の最適化

elmo式学習のlambda設定のGaussian化とは何か?

コンピューター将棋の強化学習ではelmo式学習の登場で、当時最強を誇っていたponanzaに引導を渡しました。
その後NNUEの学習でも採用され、コンピューターチェスのstockfish NNUEでもelmo式学習が使用されています。

elmo式学習の設定に存在するlambdaは、0から1の間の数値を設定可能で、0で教師の勝敗結果のみから学習し(Q-learning)、1で浅い探索の評価値を勝率変換したものから教師の深い探索の評価値を勝率変換したものを引いたものだけを学習します。(Rootstrap)

詰み寸前の局面では勝敗結果だけあれば十分で、僅か十数手先の探索結果など重視した場合、長手数の頓死の危険性もあるかもしれません。
逆に平手開始局面では、探索不可能な百数十手先の未来の勝敗結果はあまり影響を大きくすべきでは無いのではと疑問に思います。

例えば、教師データ生成時に序盤に正解の手を指していたとしても、中終盤などで読み抜けなどがあって負けた場合、序盤の正解の指し手が全て負けとして記録されてしまいます。 十数手程度の探索性能による勝敗結果が原因で、序盤の評価値が決定されて戦型が偏ってしまう懸念があります。(実際に巷の評価関数は既に居飛車の特定の戦形に偏ってしまっているようですが、勝敗結果が少なからず影響している気がするようなしないような…)

序盤に対する勝敗結果の影響については、Oracle DevelopersによるAlphaZero実装の解説記事でも言及されています。
Lessons From AlphaZero (part 4): Improving the Training Target
勝敗結果を使用しない場合のメリットの他にもデメリットとして、水平線効果に弱いことが挙げられていて、結論として探索結果と勝敗結果を両方混ぜた方が良いとなっています。

コンピューター将棋ソフトのYaneuraOuの学習ではlambda・lambda2の2段階設定が可能ですが、序盤と終盤はある程度狙った学習ができるのかもしれませんが、中盤は上手く学習しずらいのではないか、
さらにlambda2使用時のlambda_limit境界でlambdaが大きく変化するのは、学習方法の大きな変化による評価の間違いや読み抜け等が起きる事もあるのではないかと思い、 評価値の変化によってlambdaを滑らかに変動させるためにガウス関数で書き換え、 Gaussian_lambdaと名付けて実装しました。
(実装は2019年の6月頃に終えていたのですが、その後は学習の実験をずっと続けていました。)

ガウス関数と言えばベイズ最適化でも使用されていますが、Gaussian_lambdaはlambdaにガウス関数を掛け合わせただけのシンプルなものです。

Gaussian_lambda = lambda * exp(-x^2/2σ^2)

xに教師の深い探索の評価値を使用し、σには定数値をいれる(σ=標準偏差、σ^2=分散)。
評価値0で最大値(lambda設定値)になり、評価値が大きくなるにつれてlambdaが0に近づいていく。
分散を大きくすると、落ち幅が小さいなだらかな曲線になる。σ(標準偏差)の値が最も強さに影響します。
序盤と最終盤の設定はそれ程難しくは無いが、中盤の指し手がガウス曲線lambdaに影響される為、標準偏差σの決定が重要になります。

Gaussian_lambda(lambda0.5_σ600・800・1000)

一番最初にσにponanza定数に使われている600をなんとなく使ってみたが、lambdaの下げ幅が大きすぎるのか全然強くなりませんでした。
σ=600~1200位まで色々な数値をlambdaとの組み合わせ50パターン程学習を試しました。
色々な数値を試した結果、σ=900~1000辺りが良いという結論に達しました。

σ=900の時はまあまあmove accuracyが良かったが、test_cross_entropyがやや悪くなる。
σ=1000の方がmove accuracyが悪かったが、test_cross_entropyが良くなる。(move accuracy・test_cross_entropyは、YaneuraOuの学習logで使用されている数値です。)
特にσ=1000付近が最も強そうでしたが、膨大なパターンが試せず断定は出来ませんでした。

評価値無限大、即ち勝率100%の時の局面でlambdaは0が理想であるはずなので、lambdaと勝率の間には明確な関係があると仮定してσの値を決める事にしました。

統計学でのガウス関数のグラフでは、横軸に標準偏差σが使用されます。
Gaussian_lambdaをグラフにした場合、グラフの横軸が評価値xになり、縦軸がlambdaになります。
正規分布近似の信頼区間1σの確率は約0.6827ですが、x=0の時の確率が0.5からスタートする場合、信頼区間1σの確率は、0.6827/2 +0.5 ≒0.841 になります。

現在多くの将棋プログラムが、勝率=1/(1+exp(-評価値/600)) になっており、
評価値=-600*log((1/勝率)-1) である事から、
勝率84.1%の評価値≒999.4 になるので分かりやすいように切り上げてσ=1000に決定しました。
(この評価値算出のシグモイド関数の600というポナンザ定数を仮に300に変更した場合は、 σ=500に変更する必要があり、ポナンザ定数=360の時はσ=600に変更する必要があります。)

Gaussian_lambda(σ1000_lambda0.6・0.5・0.4)

σ=1000が正解の値なのかはわかりませんが、実験でも一番良い結果だったのでこれで良しとします。もしかしたら全く違う値が正解の可能性も無いとは言えないですが、もう実験するのがきつくてモチベーションが続かないので、誰かが正解の値を見つけるまではこのままで行きます。上の計算結果は、自分を納得させる為のこじつけみたいなものです。

こんな事するより進行度をlambdaに利用すれば?とか言われてしまいそうですが、コードの改造がごく僅かで簡単だったのでこの手法を実装しました。進行度を利用したものを誰か作ってくれるかも?(ちなみにtttakさんが技巧2をYaneuraOuの教師データで学習する際に進行度をlambdaに利用してたようです。https://github.com/tttak/Gikou/releases/tag/Gikou2_20171214)
ただし、進行度を利用したlambdaで学習したものが強くなるかどうかはよく分からない為、期待しすぎない方が良いかもしれません。 ガウス関数のような自然できれいな曲線になるのかも不明なので。仮にやるとしたらlambda*(1-進行度)とかになる?

評価値=勝率に連動してlambdaがガウス関数の曲線で変化していくのは特に悪いことでは無いような気がするのでまあ良いんではないでしょうか。 コードの改造も凄く楽だったし。

今回の場合は、YaneuraOuのlearner.cppのlambdaが入った更新式のlambdaをGaussian_lambdaに置き換えるだけでOKです。(改変後のsourceはlearner.cppの1120行目~112行目と1148行目~1152行目。
YaneuraOu v6.02に対応していますが、それ以降は更新していません。実はv6.50が出た時期ぐらいに最新バージョンに更新して学習させてみた所、うまく学習出来なかったのでそれ以降v6.02を学習に使用していました。 (その後、v5.33に変更しました。)
v6.02に更新する前は、v5.32を使用してしていて、v6.02にはただ何となく更新していたのですが、その後nodchipさんの実験により、v5.33で学習した方が強いらしい事が分かっており、強さも確認せずに更新するのは危険だと感じました。)

この学習法はNNUEに限らず、elmo式であればKPPTの学習等にも使用可能なので汎用性が高いと思います。
ディープラーニングの学習でも使用は可能なのではないかと思います。
dlshogiの学習では、Value Network の学習の損失関数は、勝敗を教師データとした交差エントロピーと、探索結果の評価値を教師データとした交差エントロピーの和としているので、この部分の割合をGaussian_lambdaのように変化させる事は可能であると思われます。

学習時のlambda設定

元々のノーマルなlambdaは、数値を大きくするとmove accuracyが良くなりtest_cross_entropyが悪くなる傾向があります。
逆にlambdaを小さくすると、move accuracyが悪くなりtest_cross_entropyが良くなる傾向があります。
Gaussian_lambdaはこれらの中間のバランスの取れた学習結果になります。

Gaussian_lambdaではlambda設定の基準値は0.4を最低値として、教師データの質によってlambdaを上げていくのが良いと思います。
例えばdepth10の教師で0.5位とか、depth20の教師なら0.6あたりとか。ただし、絶対的に正しいわけではないので、毎回教師データ毎に様々な数値を試すべきだとは思います。
とは言え、depthが深くなるほど評価値の信頼性も上がるので、深いdepthほどlambdaを大きくしていく指針で良いんじゃないでしょうか。

教師データが大量にに用意できない場合はlambdaを大きめにした方が良いと思います。教師データが少ないと勝敗結果の影響力が大きくなりすぎ、序盤の学習を難しくします。
教師データを十分大量に用意可能ならば、勝敗結果を使用するデメリットを薄めることが可能なので 、lambdaを小さくするのも有りなのかなと思います。どれだけ大量に必要なのかは分かりませんが。

終盤は自動的にGaussian_lambdaが小さくなるのであまり気にする必要はありません。いかに序盤の学習をlambda設定でコントロールするかが焦点になります。
ただし、lambdaをあまり大きくしすぎると終盤力に影響し、頓死の確率が上がるので程々の値にするか、lambda2・lambda_limitが併用可能なので序盤だけ大きくするという方法もあるかもしれません。

lambda2・lambda_limitを併用する場合、中終盤に大きな変化をさせるのは危険なので、もし使用するならば序盤のせいぜい250点程度がlambda_limitの限界ではないかと考えたりしましたが、 実際はどうなのかは分かりません。
最近の勝敗結果偏重な他の将棋ソフトとは違う、lambdaを大きくして教師データの勝敗結果の影響を少なくした序盤の指し手で、違った読み筋で差す評価関数を作りたいんですが、lambda_limitをどう設定するのが最善かはまだ結論を出せていません。

今のところ、勝率60%程度の評価値でlambda_limitにするのが良さそうだと感じています。
勝率変換にポナンザ定数600を使用している場合は、勝率60%の評価値240をlambda_limitに設定するのをお勧めします。 (現状FV_SCALE=24、ポナンザ定数360、Gaussian_lambdaのσ=600、lambda_limit=140で実験を繰り返しています。)