次世代の将棋思考エンジン、NNUE関数を学ぼう(その2.改造/学習編)

前回の記事からかなり時間が空いてしまいましたが、引き続きNNUE関数について議論していきます。
現在、まふ氏が作成したNNUE型の評価関数がQQR(KPPT型)とならび、将棋ソフトの評価関数で最強の座についています。学習パラメタなどについては、まふ氏の解説も参考になるでしょう。

ここではNNUE関数のネットワークの弄り方や、改造の大まかな指針について解説いたします。
NNUEはKPPTに比べ改造が加えやすい(おかげで開発者の計算資源がマッハで削れる)ので、工夫の入れようが多々あるでしょう。

qhapaq.hatenablog.com

【NNUE関数はやねうら王本家にマージされています】
NNUE関数がやねうら王にマージされたことで、やねうら王でNNUE関数の学習、対局が出来るようになりました。
今後の探索部の拡張や評価関数の解析機能などの追加機能はやねうら王で行われる可能性が高いため、NNUE関数はやねうら王上で扱うことをお勧めします。マージに伴い、本稿のファイル名、関数名もやねうら王基準に変更します。

【ネットワークサイズガチャのやり方】
ネットワークのサイズは以下のコードを書き換えることで変更できます。

【1. /source/eval/nnue/architecture.h を改造する】

// NNUE評価関数で用いる入力特徴量とネットワーク構造                
// 中略
// 入力特徴量とネットワーク構造が定義されたヘッダをincludeする 

// ここのファイル名を変える
#include "architectures/your_network.h"

// 元のファイルをコメントアウト
//#include "architectures/halfkp_256x2-32-32.h"
//#include "architectures/k-p_256x2-32-32.h"                                    
// 以下略

【2. eval/nnue/architectures/your_network.h でネットワークのサイズを定義する】

// architectures/halfkp_256x2-32-32.h をコピペした後に以下を書き換える
// kp -> 128x2 -> 32 -> 32 -> 1 のネットワークを構築
// 変換後の入力特徴量の次元数                                                                                                                  
constexpr IndexType kTransformedFeatureDimensions = 128;

namespace Layers {

// ネットワーク構造の定義                                                                                                                      
using InputLayer = InputSlice<kTransformedFeatureDimensions * 2>;
using HiddenLayer1 = ClippedReLU<AffineTransform<InputLayer, 32>>;
using HiddenLayer2 = ClippedReLU<AffineTransform<HiddenLayer1, 32>>;
using OutputLayer = AffineTransform<HiddenLayer2, 1>;

【2.5 (optional) . extra/config.h を編集する】

#if defined(YANEURAOU_2018_TNK_ENGINE)
#define ENGINE_NAME "YaneuraOu 2018 T.N.K."
#define EVAL_NNUE
# USE_BLAS を追加することでblasが使えるようになる(学習が高速になる。探索は高速にはならない)
#define USE_BLAS


【3. Makefileを編集する】

# デフォルトではKPPTになっているので、以下のコメントアウトを外す
YANEURAOU_EDITION = YANEURAOU_2018_TNK_ENGINE
# gccの場合、フラグにlblasを足さないといけない(筆者計算環境の場合)
ifeq ($(findstring g++,$(COMPILER)),g++)
        OPENMP   = -fopenmp -lblas
        OPENMP_LDFLAGS =
endif


【4. ビルドし直す】

make evallearn

【5. 学習させる】

./YaneuraOu-by-gcc
# SkipLoadingEvalをtrueにしないと評価関数のフォルダがないよと怒られる
# このオプションはEVAL_LEARNがonじゃないと動かない様子
setoption name SkipLoadingEval value true
setoption name Threads value 8                                                                                                                 
learn shuffled_sfen.bin ....

【ありがちな学習の失敗】
ネットワークの形を変えるとゼロから再学習になるため、それなりの計算資源が必要です。
例のごとくQhapaqチームにはそれを回す元気はないので、評価関数はあんまり強くなっていないです。

なお、よくある失敗としてtest_cross_entropy(ちゃんと教師データの外からvalidation用のデータを用意している)
は、デフォルトとさほど変わらない一で、move accuracyが落ちるパターンがあるようです。例えば私が用意した教師データに対し、tttakさんの評価関数では一致率が38%前後になるところ、小さいネットワークをゼロから学ばせると31%前後になります。

cross_entropyは据え置きにしながら手の一致率が落ちる理由は幾つか考えられますが、私自身はNNUEの学習にはランダムムーブによる局面の分散がKPPT以上に重要なのではないかと予想しています。

この予想を支持するデータ、理論は幾つかあるのですが、QhapaqはNNUEの実験に現状乗り遅れているので、秘密にしておきます。

【学習パラメタに関する議論】
詳しい解説はtttakさんの記事透さんの記事まふさんの記事をご覧頂きたいのですが、学習パラメタについてbatch_size_nnをどのぐらいにするか、教師をどのように作るか、lambdaをどうするか、newbob_decayを幾つにするかについて議論がされています。

画像認識の業界ではbatch_size_nnを大きくしすぎると精度が落ちることが報告されているようです。ただ、NNUE関数は通常のディープラーニングに比べてニューロンの数が少ないので、画像認識と同じ発想が使えるかは謎です。