diff --git a/src/Makefile b/src/Makefile index 41c2aff6..8a02ce1c 100644 --- a/src/Makefile +++ b/src/Makefile @@ -38,9 +38,24 @@ PGOBENCH = ./$(EXE) bench ### Source and object files SRCS = benchmark.cpp bitbase.cpp bitboard.cpp endgame.cpp evaluate.cpp main.cpp \ material.cpp misc.cpp movegen.cpp movepick.cpp pawns.cpp position.cpp psqt.cpp \ - search.cpp thread.cpp timeman.cpp tt.cpp uci.cpp ucioption.cpp tune.cpp syzygy/tbprobe.cpp + search.cpp thread.cpp timeman.cpp tt.cpp uci.cpp ucioption.cpp tune.cpp syzygy/tbprobe.cpp \ + eval/evaluate_mir_inv_tools.cpp \ + eval/nnue/evaluate_nnue.cpp \ + eval/nnue/evaluate_nnue_learner.cpp \ + eval/nnue/features/half_kp.cpp \ + eval/nnue/features/half_relative_kp.cpp \ + eval/nnue/features/k.cpp \ + eval/nnue/features/p.cpp \ + eval/nnue/features/castling_right.cpp \ + eval/nnue/features/enpassant.cpp \ + eval/nnue/nnue_test_command.cpp \ + extra/sfen_packer.cpp \ + learn/gensfen2019.cpp \ + learn/learner.cpp \ + learn/learning_tools.cpp \ + learn/multi_think.cpp -OBJS = $(notdir $(SRCS:.cpp=.o)) +OBJS = $(SRCS:.cpp=.o) VPATH = syzygy @@ -81,6 +96,7 @@ bits = 32 prefetch = no popcnt = no sse = no +avx2 = no pext = no ### 2.2 Architecture specific @@ -119,12 +135,22 @@ ifeq ($(ARCH),x86-64-modern) sse = yes endif +ifeq ($(ARCH),x86-64-avx2) + arch = x86_64 + bits = 64 + prefetch = yes + popcnt = yes + sse = yes + avx2 = yes +endif + ifeq ($(ARCH),x86-64-bmi2) arch = x86_64 bits = 64 prefetch = yes popcnt = yes sse = yes + avx2 = yes pext = yes endif @@ -151,8 +177,8 @@ endif ### 3.1 Selecting compiler (default = gcc) -CXXFLAGS += -Wall -Wcast-qual -fno-exceptions -std=c++11 $(EXTRACXXFLAGS) -DEPENDFLAGS += -std=c++11 +CXXFLAGS += -Wall -Wcast-qual -fno-exceptions -std=c++17 $(EXTRACXXFLAGS) +DEPENDFLAGS += -std=c++17 LDFLAGS += $(EXTRALDFLAGS) ifeq ($(COMP),) @@ -232,6 +258,28 @@ ifeq ($(COMP),clang) endif endif +ifeq ($(COMP),msys2) + comp=gcc + CXX=g++ + CXXFLAGS += -pedantic -Wextra -Wshadow + + ifeq ($(ARCH),armv7) + ifeq ($(OS),Android) + CXXFLAGS += -m$(bits) + LDFLAGS += -m$(bits) + endif + else + CXXFLAGS += -m$(bits) + LDFLAGS += -m$(bits) + endif + + ifneq ($(KERNEL),Darwin) + LDFLAGS += -Wl,--no-as-needed + endif + + LDFLAGS += -static -Wl,-s +endif + ifeq ($(comp),icc) profile_make = icc-profile-make profile_use = icc-profile-use @@ -320,19 +368,26 @@ endif ### 3.6 popcnt ifeq ($(popcnt),yes) ifeq ($(arch),ppc64) - CXXFLAGS += -DUSE_POPCNT + CXXFLAGS += -DUSE_POPCNT -DUSE_SSE2 else ifeq ($(comp),icc) - CXXFLAGS += -msse3 -DUSE_POPCNT + CXXFLAGS += -msse3 -DUSE_POPCNT -DUSE_SSE2 else - CXXFLAGS += -msse3 -mpopcnt -DUSE_POPCNT + CXXFLAGS += -msse3 -mpopcnt -DUSE_POPCNT -DUSE_SSE2 + endif +endif + +ifeq ($(avx2),yes) + CXXFLAGS += -DUSE_AVX2 + ifeq ($(comp),$(filter $(comp),gcc clang mingw msys2)) + CXXFLAGS += -mavx2 endif endif ### 3.7 pext ifeq ($(pext),yes) CXXFLAGS += -DUSE_PEXT - ifeq ($(comp),$(filter $(comp),gcc clang mingw)) - CXXFLAGS += -msse4 -mbmi2 + ifeq ($(comp),$(filter $(comp),gcc clang mingw msys2)) + CXXFLAGS += -mbmi2 endif endif @@ -341,7 +396,7 @@ endif ### needs access to the optimization flags. ifeq ($(optimize),yes) ifeq ($(debug), no) - ifeq ($(comp),$(filter $(comp),gcc clang)) + ifeq ($(comp),$(filter $(comp),gcc clang msys2)) CXXFLAGS += -flto LDFLAGS += $(CXXFLAGS) endif @@ -384,6 +439,7 @@ help: @echo "Supported archs:" @echo "" @echo "x86-64-bmi2 > x86 64-bit with pext support (also enables SSE4)" + @echo "x86-64-avx2 > x86 64-bit with avx2 support (also enables SSE4)" @echo "x86-64-modern > x86 64-bit with popcnt support (also enables SSE3)" @echo "x86-64 > x86 64-bit generic" @echo "x86-32 > x86 32-bit (also enables SSE)" @@ -400,6 +456,7 @@ help: @echo "mingw > Gnu compiler with MinGW under Windows" @echo "clang > LLVM Clang compiler" @echo "icc > Intel compiler" + @echo "msys2 > MSYS2" @echo "" @echo "Simple examples. If you don't know what to do, you likely want to run: " @echo "" @@ -449,7 +506,7 @@ clean: objclean profileclean # clean binaries and objects objclean: - @rm -f $(EXE) *.o ./syzygy/*.o + @rm -f $(EXE) *.o ./syzygy/*.o ./learn/*.o ./extra/*.o ./eval/*.o ./eval/nnue/*.o ./eval/nnue/features/*.o # clean auxiliary profiling files profileclean: @@ -479,6 +536,7 @@ config-sanity: @echo "prefetch: '$(prefetch)'" @echo "popcnt: '$(popcnt)'" @echo "sse: '$(sse)'" + @echo "avx2: '$(avx2)'" @echo "pext: '$(pext)'" @echo "" @echo "Flags:" @@ -539,8 +597,20 @@ icc-profile-use: EXTRACXXFLAGS='-prof_use -prof_dir ./profdir' \ all +nnue: config-sanity + $(MAKE) CXXFLAGS='$(CXXFLAGS) -DEVAL_NNUE -DUSE_EVAL_HASH -DENABLE_TEST_CMD -fopenmp' LDFLAGS='$(LDFLAGS) -fopenmp' build + +nnue-gen-sfen-from-original-eval: config-sanity + $(MAKE) CXXFLAGS='$(CXXFLAGS) -DEVAL_LEARN -DUSE_EVAL_HASH -DENABLE_TEST_CMD -fopenmp' LDFLAGS='$(LDFLAGS) -fopenmp' build + +nnue-learn: config-sanity + $(MAKE) CXXFLAGS='$(CXXFLAGS) -DEVAL_LEARN -DEVAL_NNUE -DUSE_EVAL_HASH -DENABLE_TEST_CMD -fopenmp' LDFLAGS='$(LDFLAGS) -fopenmp' build + +nnue-learn-use-blas: config-sanity + $(MAKE) CXXFLAGS='$(CXXFLAGS) -DEVAL_LEARN -DEVAL_NNUE -DUSE_EVAL_HASH -DENABLE_TEST_CMD -DUSE_BLAS -I/mingw64/include/OpenBLAS -fopenmp' LDFLAGS='$(LDFLAGS) -lopenblas -fopenmp' build + .depend: - -@$(CXX) $(DEPENDFLAGS) -MM $(SRCS) > $@ 2> /dev/null + -@$(CXX) $(DEPENDFLAGS) -MM $(OBJS:.o=.cpp) > $@ 2> /dev/null -include .depend diff --git a/src/eval/evaluate_common.h b/src/eval/evaluate_common.h new file mode 100644 index 00000000..5d5d05b1 --- /dev/null +++ b/src/eval/evaluate_common.h @@ -0,0 +1,82 @@ +#ifndef _EVALUATE_COMMON_H_ +#define _EVALUATE_COMMON_H_ + +// いまどきの手番つき評価関数(EVAL_KPPTとEVAL_KPP_KKPT)の共用header的なもの。 + +#if defined(EVAL_NNUE) || defined(EVAL_LEARN) +#include + +// KKファイル名 +#define KK_BIN "KK_synthesized.bin" + +// KKPファイル名 +#define KKP_BIN "KKP_synthesized.bin" + +// KPPファイル名 +#define KPP_BIN "KPP_synthesized.bin" + +namespace Eval +{ + +#if defined(USE_EVAL_HASH) + // prefetchする関数 + void prefetch_evalhash(const Key key); +#endif + + // 評価関数のそれぞれのパラメーターに対して関数fを適用してくれるoperator。 + // パラメーターの分析などに用いる。 + // typeは調査対象を表す。 + // type = -1 : KK,KKP,KPPすべて + // type = 0 : KK のみ + // type = 1 : KKPのみ + // type = 2 : KPPのみ + void foreach_eval_param(std::functionf, int type = -1); + + // -------------------------- + // 学習用 + // -------------------------- + +#if defined(EVAL_LEARN) + // 学習のときの勾配配列の初期化 + // 学習率を引数に渡しておく。0.0なら、defaultの値を採用する。 + // update_weights()のepochが、eta_epochまでetaから徐々にeta2に変化する。 + // eta2_epoch以降は、eta2から徐々にeta3に変化する。 + void init_grad(double eta1, uint64_t eta_epoch, double eta2, uint64_t eta2_epoch, double eta3); + + // 現在の局面で出現している特徴すべてに対して、勾配の差分値を勾配配列に加算する。 + // freeze[0] : kkは学習させないフラグ + // freeze[1] : kkpは学習させないフラグ + // freeze[2] : kppは学習させないフラグ + // freeze[3] : kpppは学習させないフラグ + void add_grad(Position& pos, Color rootColor, double delt_grad, const std::array& freeze); + + // 現在の勾配をもとにSGDかAdaGradか何かする。 + // epoch : 世代カウンター(0から始まる) + // freeze[0] : kkは学習させないフラグ + // freeze[1] : kkpは学習させないフラグ + // freeze[2] : kppは学習させないフラグ + // freeze[3] : kpppは学習させないフラグ + void update_weights(uint64_t epoch, const std::array& freeze); + + // 評価関数パラメーターをファイルに保存する。 + // ファイルの末尾につける拡張子を指定できる。 + void save_eval(std::string suffix); + + // 現在のetaを取得する。 + double get_eta(); + + // -- 学習に関連したコマンド + + // KKを正規化する関数。元の評価関数と完全に等価にはならないので注意。 + // kkp,kppの値をなるべくゼロに近づけることで、学習中に出現しなかった特徴因子の値(ゼロになっている)が + // 妥当であることを保証しようという考え。 + void regularize_kk(); + +#endif + + +} + +#endif // defined(EVAL_NNUE) || defined(EVAL_LEARN) + +#endif // _EVALUATE_KPPT_COMMON_H_ \ No newline at end of file diff --git a/src/eval/evaluate_mir_inv_tools.cpp b/src/eval/evaluate_mir_inv_tools.cpp new file mode 100644 index 00000000..56a0a63e --- /dev/null +++ b/src/eval/evaluate_mir_inv_tools.cpp @@ -0,0 +1,190 @@ +#if defined(EVAL_NNUE) || defined(EVAL_LEARN) + +#include "evaluate_mir_inv_tools.h" + +namespace Eval +{ + + // --- tables + + // あるBonaPieceを相手側から見たときの値 + // BONA_PIECE_INITが-1なので符号型で持つ必要がある。 + // KPPTを拡張しても当面、BonaPieceが2^15を超えることはないのでint16_tで良しとする。 + int16_t inv_piece_[Eval::fe_end]; + + // 盤面上のあるBonaPieceをミラーした位置にあるものを返す。 + int16_t mir_piece_[Eval::fe_end]; + + + // --- methods + + // あるBonaPieceを相手側から見たときの値を返す + Eval::BonaPiece inv_piece(Eval::BonaPiece p) { return (Eval::BonaPiece)inv_piece_[p]; } + + // 盤面上のあるBonaPieceをミラーした位置にあるものを返す。 + Eval::BonaPiece mir_piece(Eval::BonaPiece p) { return (Eval::BonaPiece)mir_piece_[p]; } + + std::function mir_piece_init_function; + + void init_mir_inv_tables() + { + // mirrorとinverseのテーブルの初期化。 + + // 初期化は1回に限る。 + static bool first = true; + if (!first) return; + first = false; + + // fとeとの交換 + int t[] = { + f_pawn , e_pawn , + f_knight , e_knight , + f_bishop , e_bishop , + f_rook , e_rook , + f_queen , e_queen , + }; + + // 未初期化の値を突っ込んでおく。 + for (BonaPiece p = BONA_PIECE_ZERO; p < fe_end; ++p) + { + inv_piece_[p] = BONA_PIECE_NOT_INIT; + + // mirrorは手駒に対しては機能しない。元の値を返すだけ。 + mir_piece_[p] = (p < f_pawn) ? p : BONA_PIECE_NOT_INIT; + } + + for (BonaPiece p = BONA_PIECE_ZERO; p < fe_end; ++p) + { + for (int i = 0; i < 32 /* t.size() */; i += 2) + { + if (t[i] <= p && p < t[i + 1]) + { + Square sq = (Square)(p - t[i]); + + // 見つかった!! + BonaPiece q = (p < fe_hand_end) ? BonaPiece(sq + t[i + 1]) : (BonaPiece)(Inv(sq) + t[i + 1]); + inv_piece_[p] = q; + inv_piece_[q] = p; + + /* + ちょっとトリッキーだが、pに関して盤上の駒は + p >= fe_hand_end + のとき。 + + このpに対して、nを整数として(上のコードのiは偶数しかとらない)、 + a) t[2n + 0] <= p < t[2n + 1] のときは先手の駒 + b) t[2n + 1] <= p < t[2n + 2] のときは後手の駒 +  である。 + + ゆえに、a)の範囲にあるpをq = Inv(p-t[2n+0]) + t[2n+1] とすると180度回転させた升にある後手の駒となる。 + そこでpとqをswapさせてinv_piece[ ]を初期化してある。 + */ + + // 手駒に関してはmirrorなど存在しない。 + if (p < fe_hand_end) + continue; + + BonaPiece r1 = (BonaPiece)(Mir(sq) + t[i]); + mir_piece_[p] = r1; + mir_piece_[r1] = p; + + BonaPiece p2 = (BonaPiece)(sq + t[i + 1]); + BonaPiece r2 = (BonaPiece)(Mir(sq) + t[i + 1]); + mir_piece_[p2] = r2; + mir_piece_[r2] = p2; + + break; + } + } + } + + if (mir_piece_init_function) + mir_piece_init_function(); + + for (BonaPiece p = BONA_PIECE_ZERO; p < fe_end; ++p) + { + // 未初期化のままになっている。上のテーブルの初期化コードがおかしい。 + assert(mir_piece_[p] != BONA_PIECE_NOT_INIT && mir_piece_[p] < fe_end); + assert(inv_piece_[p] != BONA_PIECE_NOT_INIT && inv_piece_[p] < fe_end); + + // mirとinvは、2回適用したら元の座標に戻る。 + assert(mir_piece_[mir_piece_[p]] == p); + assert(inv_piece_[inv_piece_[p]] == p); + + // mir->inv->mir->invは元の場所でなければならない。 + assert(p == inv_piece(mir_piece(inv_piece(mir_piece(p))))); + + // inv->mir->inv->mirは元の場所でなければならない。 + assert(p == mir_piece(inv_piece(mir_piece(inv_piece(p))))); + } + +#if 0 + // 評価関数のミラーをしても大丈夫であるかの事前検証 + // 値を書き込んだときにassertionがあるので、ミラーしてダメである場合、 + // そのassertに引っかかるはず。 + + // AperyのWCSC26の評価関数、kppのp1==0とかp1==20(後手の0枚目の歩)とかの + // ところにゴミが入っていて、これを回避しないとassertに引っかかる。 + + std::unordered_set s; + vector a = { + f_hand_pawn - 1,e_hand_pawn - 1, + f_hand_lance - 1, e_hand_lance - 1, + f_hand_knight - 1, e_hand_knight - 1, + f_hand_silver - 1, e_hand_silver - 1, + f_hand_gold - 1, e_hand_gold - 1, + f_hand_bishop - 1, e_hand_bishop - 1, + f_hand_rook - 1, e_hand_rook - 1, + }; + for (auto b : a) + s.insert((BonaPiece)b); + + // さらに出現しない升の盤上の歩、香、桂も除外(Aperyはここにもゴミが入っている) + for (Rank r = RANK_1; r <= RANK_2; ++r) + for (File f = FILE_1; f <= FILE_9; ++f) + { + if (r == RANK_1) + { + // 1段目の歩 + BonaPiece b1 = BonaPiece(f_pawn + (f | r)); + s.insert(b1); + s.insert(inv_piece[b1]); + + // 1段目の香 + BonaPiece b2 = BonaPiece(f_lance + (f | r)); + s.insert(b2); + s.insert(inv_piece[b2]); + } + + // 1,2段目の桂 + BonaPiece b = BonaPiece(f_knight + (f | r)); + s.insert(b); + s.insert(inv_piece[b]); + } + + cout << "\nchecking kpp_write().."; + for (auto sq : SQ) + { + cout << sq << ' '; + for (BonaPiece p1 = BONA_PIECE_ZERO; p1 < fe_end; ++p1) + for (BonaPiece p2 = BONA_PIECE_ZERO; p2 < fe_end; ++p2) + if (!s.count(p1) && !s.count(p2)) + kpp_write(sq, p1, p2, kpp[sq][p1][p2]); + } + cout << "\nchecking kkp_write().."; + + for (auto sq1 : SQ) + { + cout << sq1 << ' '; + for (auto sq2 : SQ) + for (BonaPiece p1 = BONA_PIECE_ZERO; p1 < fe_end; ++p1) + if (!s.count(p1)) + kkp_write(sq1, sq2, p1, kkp[sq1][sq2][p1]); + } + cout << "..done!" << endl; +#endif + } + +} + +#endif // defined(EVAL_NNUE) || defined(EVAL_LEARN) diff --git a/src/eval/evaluate_mir_inv_tools.h b/src/eval/evaluate_mir_inv_tools.h new file mode 100644 index 00000000..8d6378ec --- /dev/null +++ b/src/eval/evaluate_mir_inv_tools.h @@ -0,0 +1,47 @@ +#ifndef _EVALUATE_MIR_INV_TOOLS_ +#define _EVALUATE_MIR_INV_TOOLS_ + +#if defined(EVAL_NNUE) || defined(EVAL_LEARN) + +// BonaPieceのmirror(左右反転)やinverse(盤上の180度回転)させた駒を得るためのツール類。 + +#include "../types.h" +#include "../evaluate.h" +#include + +namespace Eval +{ + // ------------------------------------------------- + // tables + // ------------------------------------------------- + + // --- BonaPieceに対してMirrorとInverseを提供する。 + + // これらの配列は、init()かinit_mir_inv_tables();を呼び出すと初期化される。 + // このテーブルのみを評価関数のほうから使いたいときは、評価関数の初期化のときに + // init_mir_inv_tables()を呼び出すと良い。 + // これらの配列は、以下のKK/KKP/KPPクラスから参照される。 + + // あるBonaPieceを相手側から見たときの値を返す + extern Eval::BonaPiece inv_piece(Eval::BonaPiece p); + + // 盤面上のあるBonaPieceをミラーした位置にあるものを返す。 + extern Eval::BonaPiece mir_piece(Eval::BonaPiece p); + + + // mir_piece/inv_pieceの初期化のときに呼び出されるcallback + // fe_endをユーザー側で拡張するときに用いる。 + // この初期化のときに必要なのでinv_piece_とinv_piece_を公開している。 + // mir_piece_init_functionが呼び出されたタイミングで、fe_old_endまでは + // これらのテーブルの初期化が完了していることが保証されている。 + extern std::function mir_piece_init_function; + extern int16_t mir_piece_[Eval::fe_end]; + extern int16_t inv_piece_[Eval::fe_end]; + + // この関数を明示的に呼び出すか、init()を呼び出すかしたときに、上のテーブルが初期化される。 + extern void init_mir_inv_tables(); +} + +#endif // defined(EVAL_NNUE) || defined(EVAL_LEARN) + +#endif diff --git a/src/eval/nnue/architectures/halfkp-cr-ep_256x2-32-32.h b/src/eval/nnue/architectures/halfkp-cr-ep_256x2-32-32.h new file mode 100644 index 00000000..7063f334 --- /dev/null +++ b/src/eval/nnue/architectures/halfkp-cr-ep_256x2-32-32.h @@ -0,0 +1,38 @@ +// NNUE]֐ŗp͓ʂƃlbg[N\̒` + +#include "../features/feature_set.h" +#include "../features/half_kp.h" +#include "../features/castling_right.h" +#include "../features/enpassant.h" + +#include "../layers/input_slice.h" +#include "../layers/affine_transform.h" +#include "../layers/clipped_relu.h" + +namespace Eval { + + namespace NNUE { + + // ]֐ŗp͓ + using RawFeatures = Features::FeatureSet< + Features::HalfKP, Features::CastlingRight, + Features::EnPassant>; + + // ϊ͓̓ʂ̎ + constexpr IndexType kTransformedFeatureDimensions = 256; + + namespace Layers { + + // lbg[N\̒` + using InputLayer = InputSlice; + using HiddenLayer1 = ClippedReLU>; + using HiddenLayer2 = ClippedReLU>; + using OutputLayer = AffineTransform; + + } // namespace Layers + + using Network = Layers::OutputLayer; + + } // namespace NNUE + +} // namespace Eval diff --git a/src/eval/nnue/architectures/halfkp_256x2-32-32.h b/src/eval/nnue/architectures/halfkp_256x2-32-32.h new file mode 100644 index 00000000..9b25ee54 --- /dev/null +++ b/src/eval/nnue/architectures/halfkp_256x2-32-32.h @@ -0,0 +1,35 @@ +// NNUE評価関数で用いる入力特徴量とネットワーク構造の定義 + +#include "../features/feature_set.h" +#include "../features/half_kp.h" + +#include "../layers/input_slice.h" +#include "../layers/affine_transform.h" +#include "../layers/clipped_relu.h" + +namespace Eval { + +namespace NNUE { + +// 評価関数で用いる入力特徴量 +using RawFeatures = Features::FeatureSet< + Features::HalfKP>; + +// 変換後の入力特徴量の次元数 +constexpr IndexType kTransformedFeatureDimensions = 256; + +namespace Layers { + +// ネットワーク構造の定義 +using InputLayer = InputSlice; +using HiddenLayer1 = ClippedReLU>; +using HiddenLayer2 = ClippedReLU>; +using OutputLayer = AffineTransform; + +} // namespace Layers + +using Network = Layers::OutputLayer; + +} // namespace NNUE + +} // namespace Eval diff --git a/src/eval/nnue/architectures/k-p-cr-ep_256x2-32-32.h b/src/eval/nnue/architectures/k-p-cr-ep_256x2-32-32.h new file mode 100644 index 00000000..17871169 --- /dev/null +++ b/src/eval/nnue/architectures/k-p-cr-ep_256x2-32-32.h @@ -0,0 +1,38 @@ +// NNUE]֐ŗp͓ʂƃlbg[N\̒` + +#include "../features/feature_set.h" +#include "../features/k.h" +#include "../features/p.h" +#include "../features/castling_right.h" +#include "../features/enpassant.h" + +#include "../layers/input_slice.h" +#include "../layers/affine_transform.h" +#include "../layers/clipped_relu.h" + +namespace Eval { + + namespace NNUE { + + // ]֐ŗp͓ + using RawFeatures = Features::FeatureSet; + + // ϊ͓̓ʂ̎ + constexpr IndexType kTransformedFeatureDimensions = 256; + + namespace Layers { + + // lbg[N\̒` + using InputLayer = InputSlice; + using HiddenLayer1 = ClippedReLU>; + using HiddenLayer2 = ClippedReLU>; + using OutputLayer = AffineTransform; + + } // namespace Layers + + using Network = Layers::OutputLayer; + + } // namespace NNUE + +} // namespace Eval diff --git a/src/eval/nnue/architectures/k-p-cr_256x2-32-32.h b/src/eval/nnue/architectures/k-p-cr_256x2-32-32.h new file mode 100644 index 00000000..9ce7ecf1 --- /dev/null +++ b/src/eval/nnue/architectures/k-p-cr_256x2-32-32.h @@ -0,0 +1,37 @@ +// NNUE]֐ŗp͓ʂƃlbg[N\̒` + +#include "../features/feature_set.h" +#include "../features/k.h" +#include "../features/p.h" +#include "../features/castling_right.h" + +#include "../layers/input_slice.h" +#include "../layers/affine_transform.h" +#include "../layers/clipped_relu.h" + +namespace Eval { + + namespace NNUE { + + // ]֐ŗp͓ + using RawFeatures = Features::FeatureSet; + + // ϊ͓̓ʂ̎ + constexpr IndexType kTransformedFeatureDimensions = 256; + + namespace Layers { + + // lbg[N\̒` + using InputLayer = InputSlice; + using HiddenLayer1 = ClippedReLU>; + using HiddenLayer2 = ClippedReLU>; + using OutputLayer = AffineTransform; + + } // namespace Layers + + using Network = Layers::OutputLayer; + + } // namespace NNUE + +} // namespace Eval diff --git a/src/eval/nnue/architectures/k-p_256x2-32-32.h b/src/eval/nnue/architectures/k-p_256x2-32-32.h new file mode 100644 index 00000000..b77aeaa6 --- /dev/null +++ b/src/eval/nnue/architectures/k-p_256x2-32-32.h @@ -0,0 +1,35 @@ +// NNUE評価関数で用いる入力特徴量とネットワーク構造の定義 + +#include "../features/feature_set.h" +#include "../features/k.h" +#include "../features/p.h" + +#include "../layers/input_slice.h" +#include "../layers/affine_transform.h" +#include "../layers/clipped_relu.h" + +namespace Eval { + +namespace NNUE { + +// 評価関数で用いる入力特徴量 +using RawFeatures = Features::FeatureSet; + +// 変換後の入力特徴量の次元数 +constexpr IndexType kTransformedFeatureDimensions = 256; + +namespace Layers { + +// ネットワーク構造の定義 +using InputLayer = InputSlice; +using HiddenLayer1 = ClippedReLU>; +using HiddenLayer2 = ClippedReLU>; +using OutputLayer = AffineTransform; + +} // namespace Layers + +using Network = Layers::OutputLayer; + +} // namespace NNUE + +} // namespace Eval diff --git a/src/eval/nnue/evaluate_nnue.cpp b/src/eval/nnue/evaluate_nnue.cpp new file mode 100644 index 00000000..ce478783 --- /dev/null +++ b/src/eval/nnue/evaluate_nnue.cpp @@ -0,0 +1,322 @@ +// NNUE評価関数の計算に関するコード + +#if defined(EVAL_NNUE) + +#include +#include + +#include "../../evaluate.h" +#include "../../position.h" +#include "../../misc.h" +#include "../../uci.h" + +#include "evaluate_nnue.h" + +namespace Eval { + +namespace NNUE { + +// 入力特徴量変換器 +AlignedPtr feature_transformer; + +// 評価関数 +AlignedPtr network; + +// 評価関数ファイル名 +const char* const kFileName = "nn.bin"; + +// 評価関数の構造を表す文字列を取得する +std::string GetArchitectureString() { + return "Features=" + FeatureTransformer::GetStructureString() + + ",Network=" + Network::GetStructureString(); +} + +namespace { + +namespace Detail { + +// 評価関数パラメータを初期化する +template +void Initialize(AlignedPtr& pointer) { + pointer.reset(reinterpret_cast(aligned_malloc(sizeof(T), alignof(T)))); + std::memset(pointer.get(), 0, sizeof(T)); +} + +// 評価関数パラメータを読み込む +template +bool ReadParameters(std::istream& stream, const AlignedPtr& pointer) { + std::uint32_t header; + stream.read(reinterpret_cast(&header), sizeof(header)); + if (!stream || header != T::GetHashValue()) return false; + return pointer->ReadParameters(stream); +} + +// 評価関数パラメータを書き込む +template +bool WriteParameters(std::ostream& stream, const AlignedPtr& pointer) { + constexpr std::uint32_t header = T::GetHashValue(); + stream.write(reinterpret_cast(&header), sizeof(header)); + return pointer->WriteParameters(stream); +} + +} // namespace Detail + +// 評価関数パラメータを初期化する +void Initialize() { + Detail::Initialize(feature_transformer); + Detail::Initialize(network); +} + +} // namespace + +// ヘッダを読み込む +bool ReadHeader(std::istream& stream, + std::uint32_t* hash_value, std::string* architecture) { + std::uint32_t version, size; + stream.read(reinterpret_cast(&version), sizeof(version)); + stream.read(reinterpret_cast(hash_value), sizeof(*hash_value)); + stream.read(reinterpret_cast(&size), sizeof(size)); + if (!stream || version != kVersion) return false; + architecture->resize(size); + stream.read(&(*architecture)[0], size); + return !stream.fail(); +} + +// ヘッダを書き込む +bool WriteHeader(std::ostream& stream, + std::uint32_t hash_value, const std::string& architecture) { + stream.write(reinterpret_cast(&kVersion), sizeof(kVersion)); + stream.write(reinterpret_cast(&hash_value), sizeof(hash_value)); + const std::uint32_t size = static_cast(architecture.size()); + stream.write(reinterpret_cast(&size), sizeof(size)); + stream.write(architecture.data(), size); + return !stream.fail(); +} + +// 評価関数パラメータを読み込む +bool ReadParameters(std::istream& stream) { + std::uint32_t hash_value; + std::string architecture; + if (!ReadHeader(stream, &hash_value, &architecture)) return false; + if (hash_value != kHashValue) return false; + if (!Detail::ReadParameters(stream, feature_transformer)) return false; + if (!Detail::ReadParameters(stream, network)) return false; + return stream && stream.peek() == std::ios::traits_type::eof(); +} + +// 評価関数パラメータを書き込む +bool WriteParameters(std::ostream& stream) { + if (!WriteHeader(stream, kHashValue, GetArchitectureString())) return false; + if (!Detail::WriteParameters(stream, feature_transformer)) return false; + if (!Detail::WriteParameters(stream, network)) return false; + return !stream.fail(); +} + +// 差分計算ができるなら進める +static void UpdateAccumulatorIfPossible(const Position& pos) { + feature_transformer->UpdateAccumulatorIfPossible(pos); +} + +// 評価値を計算する +static Value ComputeScore(const Position& pos, bool refresh = false) { + auto& accumulator = pos.state()->accumulator; + if (!refresh && accumulator.computed_score) { + return accumulator.score; + } + + alignas(kCacheLineSize) TransformedFeatureType + transformed_features[FeatureTransformer::kBufferSize]; + feature_transformer->Transform(pos, transformed_features, refresh); + alignas(kCacheLineSize) char buffer[Network::kBufferSize]; + const auto output = network->Propagate(transformed_features, buffer); + + // VALUE_MAX_EVALより大きな値が返ってくるとaspiration searchがfail highして + // 探索が終わらなくなるのでVALUE_MAX_EVAL以下であることを保証すべき。 + + // この現象が起きても、対局時に秒固定などだとそこで探索が打ち切られるので、 + // 1つ前のiterationのときの最善手がbestmoveとして指されるので見かけ上、 + // 問題ない。このVALUE_MAX_EVALが返ってくるような状況は、ほぼ詰みの局面であり、 + // そのような詰みの局面が出現するのは終盤で形勢に大差がついていることが多いので + // 勝敗にはあまり影響しない。 + + // しかし、教師生成時などdepth固定で探索するときに探索から戻ってこなくなるので + // そのスレッドの計算時間を無駄にする。またdepth固定対局でtime-outするようになる。 + + auto score = static_cast(output[0] / FV_SCALE); + + // 1) ここ、下手にclipすると学習時には影響があるような気もするが…。 + // 2) accumulator.scoreは、差分計算の時に用いないので書き換えて問題ない。 + score = Math::clamp(score , -VALUE_MAX_EVAL , VALUE_MAX_EVAL); + + accumulator.score = score; + accumulator.computed_score = true; + return accumulator.score; +} + +} // namespace NNUE + +#if defined(USE_EVAL_HASH) +// HashTableに評価値を保存するために利用するクラス +struct alignas(16) ScoreKeyValue { +#if defined(USE_SSE2) + ScoreKeyValue() = default; + ScoreKeyValue(const ScoreKeyValue& other) { + static_assert(sizeof(ScoreKeyValue) == sizeof(__m128i), + "sizeof(ScoreKeyValue) should be equal to sizeof(__m128i)"); + _mm_store_si128(&as_m128i, other.as_m128i); + } + ScoreKeyValue& operator=(const ScoreKeyValue& other) { + _mm_store_si128(&as_m128i, other.as_m128i); + return *this; + } +#endif + + // evaluate hashでatomicに操作できる必要があるのでそのための操作子 + void encode() { +#if defined(USE_SSE2) + // ScoreKeyValue は atomic にコピーされるので key が合っていればデータも合っている。 +#else + key ^= score; +#endif + } + // decode()はencode()の逆変換だが、xorなので逆変換も同じ変換。 + void decode() { encode(); } + + union { + struct { + std::uint64_t key; + std::uint64_t score; + }; +#if defined(USE_SSE2) + __m128i as_m128i; +#endif + }; +}; + +// シンプルなHashTableの実装。 +// Sizeは2のべき乗。 +template +struct HashTable { + HashTable() { clear(); } + T* operator [] (const Key k) { return entries_ + (static_cast(k) & (Size - 1)); } + void clear() { memset(entries_, 0, sizeof(T)*Size); } + + // Size が 2のべき乗であることのチェック + static_assert((Size & (Size - 1)) == 0, ""); + + private: + T entries_[Size]; +}; + +// evaluateしたものを保存しておくHashTable(俗にいうehash) + +#if !defined(USE_LARGE_EVAL_HASH) +// 134MB(魔女のAVX2以外の時の設定) +struct EvaluateHashTable : HashTable {}; +#else +// prefetch有りなら大きいほうが良いのでは…。 +// → あまり変わらないし、メモリもったいないのでデフォルトでは↑の設定で良いか…。 +// 1GB(魔女のAVX2の時の設定) +struct EvaluateHashTable : HashTable {}; +#endif + +EvaluateHashTable g_evalTable; + +// prefetchする関数も用意しておく。 +void prefetch_evalhash(const Key key) { + constexpr auto mask = ~((uint64_t)0x1f); + prefetch((void*)((uint64_t)g_evalTable[key] & mask)); +} +#endif + +// 評価関数ファイルを読み込む +// benchコマンドなどでOptionsを保存して復元するのでこのときEvalDirが変更されたことになって、 +// 評価関数の再読込の必要があるというフラグを立てるため、この関数は2度呼び出されることがある。 +void load_eval() { + NNUE::Initialize(); + + if (!Options["SkipLoadingEval"]) + { + const std::string dir_name = Options["EvalDir"]; + const std::string file_name = Path::Combine(dir_name, NNUE::kFileName); + //{ + // std::ofstream stream(file_name, std::ios::binary); + // NNUE::WriteParameters(stream); + //} + std::ifstream stream(file_name, std::ios::binary); + const bool result = NNUE::ReadParameters(stream); + +// ASSERT(result); + if (!result) + { + // 読み込みエラーのとき終了してくれないと困る。 + std::cout << "Error! : failed to read " << NNUE::kFileName << std::endl; + my_exit(); + } + } +} + +// 初期化 +void init() { +} + +// 評価関数。差分計算ではなく全計算する。 +// Position::set()で一度だけ呼び出される。(以降は差分計算) +// 手番側から見た評価値を返すので注意。(他の評価関数とは設計がこの点において異なる) +// なので、この関数の最適化は頑張らない。 +Value compute_eval(const Position& pos) { + return NNUE::ComputeScore(pos, true); +} + +// 評価関数 +Value evaluate(const Position& pos) { + const auto& accumulator = pos.state()->accumulator; + if (accumulator.computed_score) { + return accumulator.score; + } + +#if defined(USE_GLOBAL_OPTIONS) + // GlobalOptionsでeval hashを用いない設定になっているなら + // eval hashへの照会をskipする。 + if (!GlobalOptions.use_eval_hash) { + ASSERT_LV5(pos.state()->materialValue == Eval::material(pos)); + return NNUE::ComputeScore(pos); + } +#endif + +#if defined(USE_EVAL_HASH) + // evaluate hash tableにはあるかも。 + const Key key = pos.key(); + ScoreKeyValue entry = *g_evalTable[key]; + entry.decode(); + if (entry.key == key) { + // あった! + return Value(entry.score); + } +#endif + + Value score = NNUE::ComputeScore(pos); +#if defined(USE_EVAL_HASH) + // せっかく計算したのでevaluate hash tableに保存しておく。 + entry.key = key; + entry.score = score; + entry.encode(); + *g_evalTable[key] = entry; +#endif + + return score; +} + +// 差分計算ができるなら進める +void evaluate_with_no_return(const Position& pos) { + NNUE::UpdateAccumulatorIfPossible(pos); +} + +// 現在の局面の評価値の内訳を表示する +void print_eval_stat(Position& /*pos*/) { + std::cout << "--- EVAL STAT: not implemented" << std::endl; +} + +} // namespace Eval + +#endif // defined(EVAL_NNUE) diff --git a/src/eval/nnue/evaluate_nnue.h b/src/eval/nnue/evaluate_nnue.h new file mode 100644 index 00000000..a95f2bd9 --- /dev/null +++ b/src/eval/nnue/evaluate_nnue.h @@ -0,0 +1,64 @@ +// NNUE評価関数で用いるheader + +#ifndef _EVALUATE_NNUE_H_ +#define _EVALUATE_NNUE_H_ + +#if defined(EVAL_NNUE) + +#include "nnue_feature_transformer.h" +#include "nnue_architecture.h" + +#include + +namespace Eval { + +namespace NNUE { + +// 評価関数の構造のハッシュ値 +constexpr std::uint32_t kHashValue = + FeatureTransformer::GetHashValue() ^ Network::GetHashValue(); + +// メモリ領域の解放を自動化するためのデリータ +template +struct AlignedDeleter { + void operator()(T* ptr) const { + ptr->~T(); + aligned_free(ptr); + } +}; +template +using AlignedPtr = std::unique_ptr>; + +// 入力特徴量変換器 +extern AlignedPtr feature_transformer; + +// 評価関数 +extern AlignedPtr network; + +// 評価関数ファイル名 +extern const char* const kFileName; + +// 評価関数の構造を表す文字列を取得する +std::string GetArchitectureString(); + +// ヘッダを読み込む +bool ReadHeader(std::istream& stream, + std::uint32_t* hash_value, std::string* architecture); + +// ヘッダを書き込む +bool WriteHeader(std::ostream& stream, + std::uint32_t hash_value, const std::string& architecture); + +// 評価関数パラメータを読み込む +bool ReadParameters(std::istream& stream); + +// 評価関数パラメータを書き込む +bool WriteParameters(std::ostream& stream); + +} // namespace NNUE + +} // namespace Eval + +#endif // defined(EVAL_NNUE) + +#endif diff --git a/src/eval/nnue/evaluate_nnue_learner.cpp b/src/eval/nnue/evaluate_nnue_learner.cpp new file mode 100644 index 00000000..636f90e1 --- /dev/null +++ b/src/eval/nnue/evaluate_nnue_learner.cpp @@ -0,0 +1,231 @@ +// NNUE評価関数の学習時用のコード + +#if defined(EVAL_LEARN) && defined(EVAL_NNUE) + +#include +#include + +#include "../../learn/learn.h" +#include "../../learn/learning_tools.h" + +#include "../../position.h" +#include "../../uci.h" +#include "../../misc.h" +#include "../../thread_win32_osx.h" + +#include "../evaluate_common.h" + +#include "evaluate_nnue.h" +#include "evaluate_nnue_learner.h" +#include "trainer/features/factorizer_feature_set.h" +#include "trainer/features/factorizer_half_kp.h" +#include "trainer/trainer_feature_transformer.h" +#include "trainer/trainer_input_slice.h" +#include "trainer/trainer_affine_transform.h" +#include "trainer/trainer_clipped_relu.h" +#include "trainer/trainer_sum.h" + +namespace Eval { + +namespace NNUE { + +namespace { + +// 学習データ +std::vector examples; + +// examplesの排他制御をするMutex +std::mutex examples_mutex; + +// ミニバッチのサンプル数 +uint64_t batch_size; + +// 乱数生成器 +std::mt19937 rng; + +// 学習器 +std::shared_ptr> trainer; + +// 学習率のスケール +double global_learning_rate_scale; + +// 学習率のスケールを取得する +double GetGlobalLearningRateScale() { + return global_learning_rate_scale; +} + +// ハイパーパラメータなどのオプションを学習器に伝える +void SendMessages(std::vector messages) { + for (auto& message : messages) { + trainer->SendMessage(&message); + assert(message.num_receivers > 0); + } +} + +} // namespace + +// 学習の初期化を行う +void InitializeTraining(double eta1, uint64_t eta1_epoch, + double eta2, uint64_t eta2_epoch, double eta3) { + std::cout << "Initializing NN training for " + << GetArchitectureString() << std::endl; + + assert(feature_transformer); + assert(network); + trainer = Trainer::Create(network.get(), feature_transformer.get()); + + if (Options["SkipLoadingEval"]) { + trainer->Initialize(rng); + } + + global_learning_rate_scale = 1.0; + EvalLearningTools::Weight::init_eta(eta1, eta2, eta3, eta1_epoch, eta2_epoch); +} + +// ミニバッチのサンプル数を設定する +void SetBatchSize(uint64_t size) { + assert(size > 0); + batch_size = size; +} + +// 学習率のスケールを設定する +void SetGlobalLearningRateScale(double scale) { + global_learning_rate_scale = scale; +} + +// ハイパーパラメータなどのオプションを設定する +void SetOptions(const std::string& options) { + std::vector messages; + for (const auto& option : Split(options, ',')) { + const auto fields = Split(option, '='); + assert(fields.size() == 1 || fields.size() == 2); + if (fields.size() == 1) { + messages.emplace_back(fields[0]); + } else { + messages.emplace_back(fields[0], fields[1]); + } + } + SendMessages(std::move(messages)); +} + +// 学習用評価関数パラメータをファイルから読み直す +void RestoreParameters(const std::string& dir_name) { + const std::string file_name = Path::Combine(dir_name, NNUE::kFileName); + std::ifstream stream(file_name, std::ios::binary); + bool result = ReadParameters(stream); + assert(result); + + SendMessages({{"reset"}}); +} + +// 学習データを1サンプル追加する +void AddExample(Position& pos, Color rootColor, + const Learner::PackedSfenValue& psv, double weight) { + Example example; + if (rootColor == pos.side_to_move()) { + example.sign = 1; + } else { + example.sign = -1; + } + example.psv = psv; + example.weight = weight; + + Features::IndexList active_indices[2]; + for (const auto trigger : kRefreshTriggers) { + RawFeatures::AppendActiveIndices(pos, trigger, active_indices); + } + if (pos.side_to_move() != WHITE) { + active_indices[0].swap(active_indices[1]); + } + for (const auto color : Colors) { + std::vector training_features; + for (const auto base_index : active_indices[color]) { + static_assert(Features::Factorizer::GetDimensions() < + (1 << TrainingFeature::kIndexBits), ""); + Features::Factorizer::AppendTrainingFeatures( + base_index, &training_features); + } + std::sort(training_features.begin(), training_features.end()); + + auto& unique_features = example.training_features[color]; + for (const auto& feature : training_features) { + if (!unique_features.empty() && + feature.GetIndex() == unique_features.back().GetIndex()) { + unique_features.back() += feature; + } else { + unique_features.push_back(feature); + } + } + } + + std::lock_guard lock(examples_mutex); + examples.push_back(std::move(example)); +} + +// 評価関数パラメーターを更新する +void UpdateParameters(uint64_t epoch) { + assert(batch_size > 0); + + EvalLearningTools::Weight::calc_eta(epoch); + const auto learning_rate = static_cast( + get_eta() / batch_size); + + std::lock_guard lock(examples_mutex); + std::shuffle(examples.begin(), examples.end(), rng); + while (examples.size() >= batch_size) { + std::vector batch(examples.end() - batch_size, examples.end()); + examples.resize(examples.size() - batch_size); + + const auto network_output = trainer->Propagate(batch); + + std::vector gradients(batch.size()); + for (std::size_t b = 0; b < batch.size(); ++b) { + const auto shallow = static_cast(Round( + batch[b].sign * network_output[b] * kPonanzaConstant)); + const auto& psv = batch[b].psv; + const double gradient = batch[b].sign * Learner::calc_grad(shallow, psv); + gradients[b] = static_cast(gradient * batch[b].weight); + } + + trainer->Backpropagate(gradients.data(), learning_rate); + } + SendMessages({{"quantize_parameters"}}); +} + +// 学習に問題が生じていないかチェックする +void CheckHealth() { + SendMessages({{"check_health"}}); +} + +} // namespace NNUE + +// 評価関数パラメーターをファイルに保存する +void save_eval(std::string dir_name) { + auto eval_dir = Path::Combine(Options["EvalSaveDir"], dir_name); + std::cout << "save_eval() start. folder = " << eval_dir << std::endl; + + // すでにこのフォルダがあるならmkdir()に失敗するが、 + // 別にそれは構わない。なければ作って欲しいだけ。 + // また、EvalSaveDirまでのフォルダは掘ってあるものとする。 + Dependency::mkdir(eval_dir); + + if (Options["SkipLoadingEval"] && NNUE::trainer) { + NNUE::SendMessages({{"clear_unobserved_feature_weights"}}); + } + + const std::string file_name = Path::Combine(eval_dir, NNUE::kFileName); + std::ofstream stream(file_name, std::ios::binary); + const bool result = NNUE::WriteParameters(stream); + assert(result); + + std::cout << "save_eval() finished. folder = " << eval_dir << std::endl; +} + +// 現在のetaを取得する +double get_eta() { + return NNUE::GetGlobalLearningRateScale() * EvalLearningTools::Weight::eta; +} + +} // namespace Eval + +#endif // defined(EVAL_LEARN) && defined(EVAL_NNUE) diff --git a/src/eval/nnue/evaluate_nnue_learner.h b/src/eval/nnue/evaluate_nnue_learner.h new file mode 100644 index 00000000..e2e68738 --- /dev/null +++ b/src/eval/nnue/evaluate_nnue_learner.h @@ -0,0 +1,46 @@ +// NNUE評価関数の学習で用いるインターフェイス + +#ifndef _EVALUATE_NNUE_LEARNER_H_ +#define _EVALUATE_NNUE_LEARNER_H_ + +#if defined(EVAL_LEARN) && defined(EVAL_NNUE) + +#include "../../learn/learn.h" + +namespace Eval { + +namespace NNUE { + +// 学習の初期化を行う +void InitializeTraining(double eta1, uint64_t eta1_epoch, + double eta2, uint64_t eta2_epoch, double eta3); + +// ミニバッチのサンプル数を設定する +void SetBatchSize(uint64_t size); + +// 学習率のスケールを設定する +void SetGlobalLearningRateScale(double scale); + +// ハイパーパラメータなどのオプションを設定する +void SetOptions(const std::string& options); + +// 学習用評価関数パラメータをファイルから読み直す +void RestoreParameters(const std::string& dir_name); + +// 学習データを1サンプル追加する +void AddExample(Position& pos, Color rootColor, + const Learner::PackedSfenValue& psv, double weight); + +// 評価関数パラメータを更新する +void UpdateParameters(uint64_t epoch); + +// 学習に問題が生じていないかチェックする +void CheckHealth(); + +} // namespace NNUE + +} // namespace Eval + +#endif // defined(EVAL_LEARN) && defined(EVAL_NNUE) + +#endif diff --git a/src/eval/nnue/features/castling_right.cpp b/src/eval/nnue/features/castling_right.cpp new file mode 100644 index 00000000..30e46e23 --- /dev/null +++ b/src/eval/nnue/features/castling_right.cpp @@ -0,0 +1,73 @@ +// NNUE評価関数の入力特徴量Kの定義 + +#if defined(EVAL_NNUE) + +#include "castling_right.h" +#include "index_list.h" + +namespace Eval { + + namespace NNUE { + + namespace Features { + + // 特徴量のうち、値が1であるインデックスのリストを取得する + void CastlingRight::AppendActiveIndices( + const Position& pos, Color perspective, IndexList* active) { + // コンパイラの警告を回避するため、配列サイズが小さい場合は何もしない + if (RawFeatures::kMaxActiveDimensions < kMaxActiveDimensions) return; + + int castling_rights = pos.state()->castlingRights; + int relative_castling_rights; + if (perspective == WHITE) { + relative_castling_rights = castling_rights; + } + else { + // Invert the perspective. + relative_castling_rights = ((castling_rights & 3) << 2) + & ((castling_rights >> 2) & 3); + } + + for (int i = 0; i < kDimensions; ++i) { + if (relative_castling_rights & (i << 1)) { + active->push_back(i); + } + } + } + + // 特徴量のうち、一手前から値が変化したインデックスのリストを取得する + void CastlingRight::AppendChangedIndices( + const Position& pos, Color perspective, + IndexList* removed, IndexList* added) { + + int previous_castling_rights = pos.state()->previous->castlingRights; + int current_castling_rights = pos.state()->castlingRights; + int relative_previous_castling_rights; + int relative_current_castling_rights; + if (perspective == WHITE) { + relative_previous_castling_rights = previous_castling_rights; + relative_current_castling_rights = current_castling_rights; + } + else { + // Invert the perspective. + relative_previous_castling_rights = ((previous_castling_rights & 3) << 2) + & ((previous_castling_rights >> 2) & 3); + relative_current_castling_rights = ((current_castling_rights & 3) << 2) + & ((current_castling_rights >> 2) & 3); + } + + for (int i = 0; i < kDimensions; ++i) { + if ((relative_previous_castling_rights & (i << 1)) && + (relative_current_castling_rights & (i << 1)) == 0) { + removed->push_back(i); + } + } + } + + } // namespace Features + + } // namespace NNUE + +} // namespace Eval + +#endif // defined(EVAL_NNUE) diff --git a/src/eval/nnue/features/castling_right.h b/src/eval/nnue/features/castling_right.h new file mode 100644 index 00000000..1384865f --- /dev/null +++ b/src/eval/nnue/features/castling_right.h @@ -0,0 +1,48 @@ +// NNUE]֐͓̓K̒` + +#ifndef _NNUE_FEATURES_CASTLING_RIGHT_H_ +#define _NNUE_FEATURES_CASTLING_RIGHT_H_ + +#if defined(EVAL_NNUE) + +#include "../../../evaluate.h" +#include "features_common.h" + +namespace Eval { + + namespace NNUE { + + namespace Features { + + // KFʂ̈ʒu + class CastlingRight { + public: + // ʖ + static constexpr const char* kName = "CastlingRight"; + // ]֐t@CɖߍރnbVl + static constexpr std::uint32_t kHashValue = 0x913968AAu; + // ʂ̎ + static constexpr IndexType kDimensions = 4; + // ʂ̂Aɒl1ƂȂCfbNX̐̍ől + static constexpr IndexType kMaxActiveDimensions = 4; + // vZ̑ɑSvZs^C~O + static constexpr TriggerEvent kRefreshTrigger = TriggerEvent::kNone; + + // ʂ̂Al1łCfbNX̃Xg擾 + static void AppendActiveIndices(const Position& pos, Color perspective, + IndexList* active); + + // ʂ̂AOlωCfbNX̃Xg擾 + static void AppendChangedIndices(const Position& pos, Color perspective, + IndexList* removed, IndexList* added); + }; + + } // namespace Features + + } // namespace NNUE + +} // namespace Eval + +#endif // defined(EVAL_NNUE) + +#endif diff --git a/src/eval/nnue/features/enpassant.cpp b/src/eval/nnue/features/enpassant.cpp new file mode 100644 index 00000000..523fd966 --- /dev/null +++ b/src/eval/nnue/features/enpassant.cpp @@ -0,0 +1,47 @@ +// NNUE]֐͓̓K̒` + +#if defined(EVAL_NNUE) + +#include "enpassant.h" +#include "index_list.h" + +namespace Eval { + + namespace NNUE { + + namespace Features { + + // ʂ̂Al1łCfbNX̃Xg擾 + void EnPassant::AppendActiveIndices( + const Position& pos, Color perspective, IndexList* active) { + // RpČx邽߁AzTCYꍇ͉Ȃ + if (RawFeatures::kMaxActiveDimensions < kMaxActiveDimensions) return; + + auto epSquare = pos.state()->epSquare; + if (epSquare == SQ_NONE) { + return; + } + + if (perspective == BLACK) { + epSquare = Inv(epSquare); + } + + auto file = file_of(epSquare); + active->push_back(file); + } + + // ʂ̂AOlωCfbNX̃Xg擾 + void EnPassant::AppendChangedIndices( + const Position& pos, Color perspective, + IndexList* removed, IndexList* added) { + // Not implemented. + assert(false); + } + + } // namespace Features + + } // namespace NNUE + +} // namespace Eval + +#endif // defined(EVAL_NNUE) diff --git a/src/eval/nnue/features/enpassant.h b/src/eval/nnue/features/enpassant.h new file mode 100644 index 00000000..fe827584 --- /dev/null +++ b/src/eval/nnue/features/enpassant.h @@ -0,0 +1,48 @@ +// NNUE]֐͓̓K̒` + +#ifndef _NNUE_FEATURES_ENPASSANT_H_ +#define _NNUE_FEATURES_ENPASSANT_H_ + +#if defined(EVAL_NNUE) + +#include "../../../evaluate.h" +#include "features_common.h" + +namespace Eval { + + namespace NNUE { + + namespace Features { + + // KFʂ̈ʒu + class EnPassant { + public: + // ʖ + static constexpr const char* kName = "EnPassant"; + // ]֐t@CɖߍރnbVl + static constexpr std::uint32_t kHashValue = 0x02924F91u; + // ʂ̎ + static constexpr IndexType kDimensions = 8; + // ʂ̂Aɒl1ƂȂCfbNX̐̍ől + static constexpr IndexType kMaxActiveDimensions = 1; + // vZ̑ɑSvZs^C~O + static constexpr TriggerEvent kRefreshTrigger = TriggerEvent::kAnyPieceMoved; + + // ʂ̂Al1łCfbNX̃Xg擾 + static void AppendActiveIndices(const Position& pos, Color perspective, + IndexList* active); + + // ʂ̂AOlωCfbNX̃Xg擾 + static void AppendChangedIndices(const Position& pos, Color perspective, + IndexList* removed, IndexList* added); + }; + + } // namespace Features + + } // namespace NNUE + +} // namespace Eval + +#endif // defined(EVAL_NNUE) + +#endif diff --git a/src/eval/nnue/features/feature_set.h b/src/eval/nnue/features/feature_set.h new file mode 100644 index 00000000..919be65d --- /dev/null +++ b/src/eval/nnue/features/feature_set.h @@ -0,0 +1,249 @@ +// NNUE評価関数の入力特徴量セットを表すクラステンプレート + +#ifndef _NNUE_FEATURE_SET_H_ +#define _NNUE_FEATURE_SET_H_ + +#if defined(EVAL_NNUE) + +#include "features_common.h" +#include + +namespace Eval { + +namespace NNUE { + +namespace Features { + +// 値のリストを表すクラステンプレート +template +struct CompileTimeList; +template +struct CompileTimeList { + static constexpr bool Contains(T value) { + return value == First || CompileTimeList::Contains(value); + } + static constexpr std::array + kValues = {{First, Remaining...}}; +}; +template +constexpr std::array + CompileTimeList::kValues; +template +struct CompileTimeList { + static constexpr bool Contains(T /*value*/) { + return false; + } + static constexpr std::array kValues = {{}}; +}; + +// リストの先頭への追加を行うクラステンプレート +template +struct AppendToList; +template +struct AppendToList, AnotherValue> { + using Result = CompileTimeList; +}; + +// ソートされた重複のないリストへの追加を行うクラステンプレート +template +struct InsertToSet; +template +struct InsertToSet, AnotherValue> { + using Result = std::conditional_t< + CompileTimeList::Contains(AnotherValue), + CompileTimeList, + std::conditional_t<(AnotherValue < First), + CompileTimeList, + typename AppendToList, AnotherValue>::Result, + First>::Result>>; +}; +template +struct InsertToSet, Value> { + using Result = CompileTimeList; +}; + +// 特徴量セットの基底クラス +template +class FeatureSetBase { + public: + // 特徴量のうち、値が1であるインデックスのリストを取得する + template + static void AppendActiveIndices( + const Position& pos, TriggerEvent trigger, IndexListType active[2]) { + for (const auto perspective : Colors) { + Derived::CollectActiveIndices( + pos, trigger, perspective, &active[perspective]); + } + } + + // 特徴量のうち、一手前から値が変化したインデックスのリストを取得する + template + static void AppendChangedIndices( + const PositionType& pos, TriggerEvent trigger, + IndexListType removed[2], IndexListType added[2], bool reset[2]) { + const auto& dp = pos.state()->dirtyPiece; + if (dp.dirty_num == 0) return; + + for (const auto perspective : Colors) { + reset[perspective] = false; + switch (trigger) { + case TriggerEvent::kNone: + break; + case TriggerEvent::kFriendKingMoved: + reset[perspective] = + dp.pieceNo[0] == PIECE_NUMBER_KING + perspective; + break; + case TriggerEvent::kEnemyKingMoved: + reset[perspective] = + dp.pieceNo[0] == PIECE_NUMBER_KING + ~perspective; + break; + case TriggerEvent::kAnyKingMoved: + reset[perspective] = dp.pieceNo[0] >= PIECE_NUMBER_KING; + break; + case TriggerEvent::kAnyPieceMoved: + reset[perspective] = true; + break; + default: + assert(false); + break; + } + if (reset[perspective]) { + Derived::CollectActiveIndices( + pos, trigger, perspective, &added[perspective]); + } else { + Derived::CollectChangedIndices( + pos, trigger, perspective, + &removed[perspective], &added[perspective]); + } + } + } +}; + +// 特徴量セットを表すクラステンプレート +// 実行時の計算量を線形にするために、内部の処理はテンプレート引数の逆順に行う +template +class FeatureSet : + public FeatureSetBase< + FeatureSet> { + private: + using Head = FirstFeatureType; + using Tail = FeatureSet; + + public: + // 評価関数ファイルに埋め込むハッシュ値 + static constexpr std::uint32_t kHashValue = + Head::kHashValue ^ (Tail::kHashValue << 1) ^ (Tail::kHashValue >> 31); + // 特徴量の次元数 + static constexpr IndexType kDimensions = + Head::kDimensions + Tail::kDimensions; + // 特徴量のうち、同時に値が1となるインデックスの数の最大値 + static constexpr IndexType kMaxActiveDimensions = + Head::kMaxActiveDimensions + Tail::kMaxActiveDimensions; + // 差分計算の代わりに全計算を行うタイミングのリスト + using SortedTriggerSet = typename InsertToSet::Result; + static constexpr auto kRefreshTriggers = SortedTriggerSet::kValues; + + // 特徴量名を取得する + static std::string GetName() { + return std::string(Head::kName) + "+" + Tail::GetName(); + } + + private: + // 特徴量のうち、値が1であるインデックスのリストを取得する + template + static void CollectActiveIndices( + const Position& pos, const TriggerEvent trigger, const Color perspective, + IndexListType* const active) { + Tail::CollectActiveIndices(pos, trigger, perspective, active); + if (Head::kRefreshTrigger == trigger) { + const auto start = active->size(); + Head::AppendActiveIndices(pos, perspective, active); + for (auto i = start; i < active->size(); ++i) { + (*active)[i] += Tail::kDimensions; + } + } + } + + // 特徴量のうち、一手前から値が変化したインデックスのリストを取得する + template + static void CollectChangedIndices( + const Position& pos, const TriggerEvent trigger, const Color perspective, + IndexListType* const removed, IndexListType* const added) { + Tail::CollectChangedIndices(pos, trigger, perspective, removed, added); + if (Head::kRefreshTrigger == trigger) { + const auto start_removed = removed->size(); + const auto start_added = added->size(); + Head::AppendChangedIndices(pos, perspective, removed, added); + for (auto i = start_removed; i < removed->size(); ++i) { + (*removed)[i] += Tail::kDimensions; + } + for (auto i = start_added; i < added->size(); ++i) { + (*added)[i] += Tail::kDimensions; + } + } + } + + // 基底クラスと、自身を再帰的に利用するクラステンプレートをfriendにする + friend class FeatureSetBase; + template + friend class FeatureSet; +}; + +// 特徴量セットを表すクラステンプレート +// テンプレート引数が1つの場合の特殊化 +template +class FeatureSet : public FeatureSetBase> { + public: + // 評価関数ファイルに埋め込むハッシュ値 + static constexpr std::uint32_t kHashValue = FeatureType::kHashValue; + // 特徴量の次元数 + static constexpr IndexType kDimensions = FeatureType::kDimensions; + // 特徴量のうち、同時に値が1となるインデックスの数の最大値 + static constexpr IndexType kMaxActiveDimensions = + FeatureType::kMaxActiveDimensions; + // 差分計算の代わりに全計算を行うタイミングのリスト + using SortedTriggerSet = + CompileTimeList; + static constexpr auto kRefreshTriggers = SortedTriggerSet::kValues; + + // 特徴量名を取得する + static std::string GetName() { + return FeatureType::kName; + } + + private: + // 特徴量のうち、値が1であるインデックスのリストを取得する + static void CollectActiveIndices( + const Position& pos, const TriggerEvent trigger, const Color perspective, + IndexList* const active) { + if (FeatureType::kRefreshTrigger == trigger) { + FeatureType::AppendActiveIndices(pos, perspective, active); + } + } + + // 特徴量のうち、一手前から値が変化したインデックスのリストを取得する + static void CollectChangedIndices( + const Position& pos, const TriggerEvent trigger, const Color perspective, + IndexList* const removed, IndexList* const added) { + if (FeatureType::kRefreshTrigger == trigger) { + FeatureType::AppendChangedIndices(pos, perspective, removed, added); + } + } + + // 基底クラスと、自身を再帰的に利用するクラステンプレートをfriendにする + friend class FeatureSetBase; + template + friend class FeatureSet; +}; + +} // namespace Features + +} // namespace NNUE + +} // namespace Eval + +#endif // defined(EVAL_NNUE) + +#endif diff --git a/src/eval/nnue/features/features_common.h b/src/eval/nnue/features/features_common.h new file mode 100644 index 00000000..15ccb8a7 --- /dev/null +++ b/src/eval/nnue/features/features_common.h @@ -0,0 +1,47 @@ +// NNUE評価関数の入力特徴量の共通ヘッダ + +#ifndef _NNUE_FEATURES_COMMON_H_ +#define _NNUE_FEATURES_COMMON_H_ + +#if defined(EVAL_NNUE) + +#include "../../../evaluate.h" +#include "../nnue_common.h" + +namespace Eval { + +namespace NNUE { + +namespace Features { + +// インデックスリストの型 +class IndexList; + +// 特徴量セットを表すクラステンプレート +template +class FeatureSet; + +// 差分計算の代わりに全計算を行うタイミングの種類 +enum class TriggerEvent { + kNone, // 可能な場合は常に差分計算する + kFriendKingMoved, // 自玉が移動した場合に全計算する + kEnemyKingMoved, // 敵玉が移動した場合に全計算する + kAnyKingMoved, // どちらかの玉が移動した場合に全計算する + kAnyPieceMoved, // 常に全計算する +}; + +// 手番側or相手側 +enum class Side { + kFriend, // 手番側 + kEnemy, // 相手側 +}; + +} // namespace Features + +} // namespace NNUE + +} // namespace Eval + +#endif // defined(EVAL_NNUE) + +#endif diff --git a/src/eval/nnue/features/half_kp.cpp b/src/eval/nnue/features/half_kp.cpp new file mode 100644 index 00000000..5cd95637 --- /dev/null +++ b/src/eval/nnue/features/half_kp.cpp @@ -0,0 +1,84 @@ +// NNUE評価関数の入力特徴量HalfKPの定義 + +#if defined(EVAL_NNUE) + +#include "half_kp.h" +#include "index_list.h" + +namespace Eval { + +namespace NNUE { + +namespace Features { + +// 玉の位置とBonaPieceから特徴量のインデックスを求める +template +inline IndexType HalfKP::MakeIndex(Square sq_k, BonaPiece p) { + return static_cast(fe_end) * static_cast(sq_k) + p; +} + +// 駒の情報を取得する +template +inline void HalfKP::GetPieces( + const Position& pos, Color perspective, + BonaPiece** pieces, Square* sq_target_k) { + *pieces = (perspective == BLACK) ? + pos.eval_list()->piece_list_fb() : + pos.eval_list()->piece_list_fw(); + const PieceNumber target = (AssociatedKing == Side::kFriend) ? + static_cast(PIECE_NUMBER_KING + perspective) : + static_cast(PIECE_NUMBER_KING + ~perspective); + *sq_target_k = static_cast(((*pieces)[target] - f_king) % SQUARE_NB); +} + +// 特徴量のうち、値が1であるインデックスのリストを取得する +template +void HalfKP::AppendActiveIndices( + const Position& pos, Color perspective, IndexList* active) { + // コンパイラの警告を回避するため、配列サイズが小さい場合は何もしない + if (RawFeatures::kMaxActiveDimensions < kMaxActiveDimensions) return; + + BonaPiece* pieces; + Square sq_target_k; + GetPieces(pos, perspective, &pieces, &sq_target_k); + for (PieceNumber i = PIECE_NUMBER_ZERO; i < PIECE_NUMBER_KING; ++i) { + if (pieces[i] != Eval::BONA_PIECE_ZERO) { + active->push_back(MakeIndex(sq_target_k, pieces[i])); + } + } +} + +// 特徴量のうち、一手前から値が変化したインデックスのリストを取得する +template +void HalfKP::AppendChangedIndices( + const Position& pos, Color perspective, + IndexList* removed, IndexList* added) { + BonaPiece* pieces; + Square sq_target_k; + GetPieces(pos, perspective, &pieces, &sq_target_k); + const auto& dp = pos.state()->dirtyPiece; + for (int i = 0; i < dp.dirty_num; ++i) { + if (dp.pieceNo[i] >= PIECE_NUMBER_KING) continue; + const auto old_p = static_cast( + dp.changed_piece[i].old_piece.from[perspective]); + if (old_p != Eval::BONA_PIECE_ZERO) { + removed->push_back(MakeIndex(sq_target_k, old_p)); + } + const auto new_p = static_cast( + dp.changed_piece[i].new_piece.from[perspective]); + if (new_p != Eval::BONA_PIECE_ZERO) { + added->push_back(MakeIndex(sq_target_k, new_p)); + } + } +} + +template class HalfKP; +template class HalfKP; + +} // namespace Features + +} // namespace NNUE + +} // namespace Eval + +#endif // defined(EVAL_NNUE) diff --git a/src/eval/nnue/features/half_kp.h b/src/eval/nnue/features/half_kp.h new file mode 100644 index 00000000..556127d3 --- /dev/null +++ b/src/eval/nnue/features/half_kp.h @@ -0,0 +1,62 @@ +// NNUE評価関数の入力特徴量HalfKPの定義 + +#ifndef _NNUE_FEATURES_HALF_KP_H_ +#define _NNUE_FEATURES_HALF_KP_H_ + +#if defined(EVAL_NNUE) + +#include "../../../evaluate.h" +#include "features_common.h" + +namespace Eval { + +namespace NNUE { + +namespace Features { + +// 特徴量HalfKP:自玉または敵玉の位置と、玉以外の駒の位置の組み合わせ +template +class HalfKP { + public: + // 特徴量名 + static constexpr const char* kName = + (AssociatedKing == Side::kFriend) ? "HalfKP(Friend)" : "HalfKP(Enemy)"; + // 評価関数ファイルに埋め込むハッシュ値 + static constexpr std::uint32_t kHashValue = + 0x5D69D5B9u ^ (AssociatedKing == Side::kFriend); + // 特徴量の次元数 + static constexpr IndexType kDimensions = + static_cast(SQUARE_NB) * static_cast(fe_end); + // 特徴量のうち、同時に値が1となるインデックスの数の最大値 + static constexpr IndexType kMaxActiveDimensions = PIECE_NUMBER_KING; + // 差分計算の代わりに全計算を行うタイミング + static constexpr TriggerEvent kRefreshTrigger = + (AssociatedKing == Side::kFriend) ? + TriggerEvent::kFriendKingMoved : TriggerEvent::kEnemyKingMoved; + + // 特徴量のうち、値が1であるインデックスのリストを取得する + static void AppendActiveIndices(const Position& pos, Color perspective, + IndexList* active); + + // 特徴量のうち、一手前から値が変化したインデックスのリストを取得する + static void AppendChangedIndices(const Position& pos, Color perspective, + IndexList* removed, IndexList* added); + + // 玉の位置とBonaPieceから特徴量のインデックスを求める + static IndexType MakeIndex(Square sq_k, BonaPiece p); + + private: + // 駒の情報を取得する + static void GetPieces(const Position& pos, Color perspective, + BonaPiece** pieces, Square* sq_target_k); +}; + +} // namespace Features + +} // namespace NNUE + +} // namespace Eval + +#endif // defined(EVAL_NNUE) + +#endif diff --git a/src/eval/nnue/features/half_relative_kp.cpp b/src/eval/nnue/features/half_relative_kp.cpp new file mode 100644 index 00000000..d62beea0 --- /dev/null +++ b/src/eval/nnue/features/half_relative_kp.cpp @@ -0,0 +1,97 @@ +// NNUE評価関数の入力特徴量HalfRelativeKPの定義 + +#if defined(EVAL_NNUE) + +#include "half_relative_kp.h" +#include "index_list.h" + +namespace Eval { + +namespace NNUE { + +namespace Features { + +// 玉の位置とBonaPieceから特徴量のインデックスを求める +template +inline IndexType HalfRelativeKP::MakeIndex( + Square sq_k, BonaPiece p) { + constexpr IndexType W = kBoardWidth; + constexpr IndexType H = kBoardHeight; + const IndexType piece_index = (p - fe_hand_end) / SQUARE_NB; + const Square sq_p = static_cast((p - fe_hand_end) % SQUARE_NB); + const IndexType relative_file = file_of(sq_p) - file_of(sq_k) + (W / 2); + const IndexType relative_rank = rank_of(sq_p) - rank_of(sq_k) + (H / 2); + return H * W * piece_index + H * relative_file + relative_rank; +} + +// 駒の情報を取得する +template +inline void HalfRelativeKP::GetPieces( + const Position& pos, Color perspective, + BonaPiece** pieces, Square* sq_target_k) { + *pieces = (perspective == BLACK) ? + pos.eval_list()->piece_list_fb() : + pos.eval_list()->piece_list_fw(); + const PieceNumber target = (AssociatedKing == Side::kFriend) ? + static_cast(PIECE_NUMBER_KING + perspective) : + static_cast(PIECE_NUMBER_KING + ~perspective); + *sq_target_k = static_cast(((*pieces)[target] - f_king) % SQUARE_NB); +} + +// 特徴量のうち、値が1であるインデックスのリストを取得する +template +void HalfRelativeKP::AppendActiveIndices( + const Position& pos, Color perspective, IndexList* active) { + // コンパイラの警告を回避するため、配列サイズが小さい場合は何もしない + if (RawFeatures::kMaxActiveDimensions < kMaxActiveDimensions) return; + + BonaPiece* pieces; + Square sq_target_k; + GetPieces(pos, perspective, &pieces, &sq_target_k); + for (PieceNumber i = PIECE_NUMBER_ZERO; i < PIECE_NUMBER_KING; ++i) { + if (pieces[i] >= fe_hand_end) { + if (pieces[i] != Eval::BONA_PIECE_ZERO) { + active->push_back(MakeIndex(sq_target_k, pieces[i])); + } + } + } +} + +// 特徴量のうち、一手前から値が変化したインデックスのリストを取得する +template +void HalfRelativeKP::AppendChangedIndices( + const Position& pos, Color perspective, + IndexList* removed, IndexList* added) { + BonaPiece* pieces; + Square sq_target_k; + GetPieces(pos, perspective, &pieces, &sq_target_k); + const auto& dp = pos.state()->dirtyPiece; + for (int i = 0; i < dp.dirty_num; ++i) { + if (dp.pieceNo[i] >= PIECE_NUMBER_KING) continue; + const auto old_p = static_cast( + dp.changed_piece[i].old_piece.from[perspective]); + if (old_p >= fe_hand_end) { + if (old_p != Eval::BONA_PIECE_ZERO) { + removed->push_back(MakeIndex(sq_target_k, old_p)); + } + } + const auto new_p = static_cast( + dp.changed_piece[i].new_piece.from[perspective]); + if (new_p >= fe_hand_end) { + if (new_p != Eval::BONA_PIECE_ZERO) { + added->push_back(MakeIndex(sq_target_k, new_p)); + } + } + } +} + +template class HalfRelativeKP; +template class HalfRelativeKP; + +} // namespace Features + +} // namespace NNUE + +} // namespace Eval + +#endif // defined(EVAL_NNUE) diff --git a/src/eval/nnue/features/half_relative_kp.h b/src/eval/nnue/features/half_relative_kp.h new file mode 100644 index 00000000..99e10c57 --- /dev/null +++ b/src/eval/nnue/features/half_relative_kp.h @@ -0,0 +1,68 @@ +// NNUE評価関数の入力特徴量HalfRelativeKPの定義 + +#ifndef _NNUE_FEATURES_HALF_RELATIVE_KP_H_ +#define _NNUE_FEATURES_HALF_RELATIVE_KP_H_ + +#if defined(EVAL_NNUE) + +#include "../../../evaluate.h" +#include "features_common.h" + +namespace Eval { + +namespace NNUE { + +namespace Features { + +// 特徴量HalfRelativeKP:自玉または敵玉を基準とした、玉以外の各駒の相対位置 +template +class HalfRelativeKP { + public: + // 特徴量名 + static constexpr const char* kName = (AssociatedKing == Side::kFriend) ? + "HalfRelativeKP(Friend)" : "HalfRelativeKP(Enemy)"; + // 評価関数ファイルに埋め込むハッシュ値 + static constexpr std::uint32_t kHashValue = + 0xF9180919u ^ (AssociatedKing == Side::kFriend); + // 玉を除いた駒種 + static constexpr IndexType kNumPieceKinds = (fe_end - fe_hand_end) / SQUARE_NB; + // 玉を中央に置いた仮想的な盤の幅 + static constexpr IndexType kBoardWidth = FILE_NB * 2 - 1; + // 玉を中央に置いた仮想的な盤の高さ + static constexpr IndexType kBoardHeight = RANK_NB * 2 - 1; + // 特徴量の次元数 + static constexpr IndexType kDimensions = + kNumPieceKinds * kBoardHeight * kBoardWidth; + // 特徴量のうち、同時に値が1となるインデックスの数の最大値 + static constexpr IndexType kMaxActiveDimensions = PIECE_NUMBER_KING; + // 差分計算の代わりに全計算を行うタイミング + static constexpr TriggerEvent kRefreshTrigger = + (AssociatedKing == Side::kFriend) ? + TriggerEvent::kFriendKingMoved : TriggerEvent::kEnemyKingMoved; + + // 特徴量のうち、値が1であるインデックスのリストを取得する + static void AppendActiveIndices(const Position& pos, Color perspective, + IndexList* active); + + // 特徴量のうち、一手前から値が変化したインデックスのリストを取得する + static void AppendChangedIndices(const Position& pos, Color perspective, + IndexList* removed, IndexList* added); + + // 玉の位置とBonaPieceから特徴量のインデックスを求める + static IndexType MakeIndex(Square sq_k, BonaPiece p); + + private: + // 駒の情報を取得する + static void GetPieces(const Position& pos, Color perspective, + BonaPiece** pieces, Square* sq_target_k); +}; + +} // namespace Features + +} // namespace NNUE + +} // namespace Eval + +#endif // defined(EVAL_NNUE) + +#endif diff --git a/src/eval/nnue/features/index_list.h b/src/eval/nnue/features/index_list.h new file mode 100644 index 00000000..a5a71011 --- /dev/null +++ b/src/eval/nnue/features/index_list.h @@ -0,0 +1,55 @@ +// 入力特徴量のインデックスリストの定義 + +#ifndef _NNUE_FEATURES_INDEX_LIST_H_ +#define _NNUE_FEATURES_INDEX_LIST_H_ + +#if defined(EVAL_NNUE) + +#include "../../../position.h" +#include "../nnue_architecture.h" + +namespace Eval { + +namespace NNUE { + +namespace Features { + +// 特徴量のインデックスリストに使うクラステンプレート +template +class ValueList { + public: + std::size_t size() const { return size_; } + void resize(std::size_t size) { size_ = size; } + void push_back(const T& value) { values_[size_++] = value; } + T& operator[](std::size_t index) { return values_[index]; } + T* begin() { return values_; } + T* end() { return values_ + size_; } + const T& operator[](std::size_t index) const { return values_[index]; } + const T* begin() const { return values_; } + const T* end() const { return values_ + size_; } + void swap(ValueList& other) { + const std::size_t max_size = std::max(size_, other.size_); + for (std::size_t i = 0; i < max_size; ++i) { + std::swap(values_[i], other.values_[i]); + } + std::swap(size_, other.size_); + } + private: + T values_[MaxSize]; + std::size_t size_ = 0; +}; + +// 特徴量のインデックスリストの型 +class IndexList + : public ValueList { +}; + +} // namespace Features + +} // namespace NNUE + +} // namespace Eval + +#endif // defined(EVAL_NNUE) + +#endif diff --git a/src/eval/nnue/features/k.cpp b/src/eval/nnue/features/k.cpp new file mode 100644 index 00000000..03f66ff5 --- /dev/null +++ b/src/eval/nnue/features/k.cpp @@ -0,0 +1,49 @@ +// NNUE評価関数の入力特徴量Kの定義 + +#if defined(EVAL_NNUE) + +#include "k.h" +#include "index_list.h" + +namespace Eval { + +namespace NNUE { + +namespace Features { + +// 特徴量のうち、値が1であるインデックスのリストを取得する +void K::AppendActiveIndices( + const Position& pos, Color perspective, IndexList* active) { + // コンパイラの警告を回避するため、配列サイズが小さい場合は何もしない + if (RawFeatures::kMaxActiveDimensions < kMaxActiveDimensions) return; + + const BonaPiece* pieces = (perspective == BLACK) ? + pos.eval_list()->piece_list_fb() : + pos.eval_list()->piece_list_fw(); + assert(pieces[PIECE_NUMBER_BKING] != BONA_PIECE_ZERO); + assert(pieces[PIECE_NUMBER_WKING] != BONA_PIECE_ZERO); + for (PieceNumber i = PIECE_NUMBER_KING; i < PIECE_NUMBER_NB; ++i) { + active->push_back(pieces[i] - fe_end); + } +} + +// 特徴量のうち、一手前から値が変化したインデックスのリストを取得する +void K::AppendChangedIndices( + const Position& pos, Color perspective, + IndexList* removed, IndexList* added) { + const auto& dp = pos.state()->dirtyPiece; + if (dp.pieceNo[0] >= PIECE_NUMBER_KING) { + removed->push_back( + dp.changed_piece[0].old_piece.from[perspective] - fe_end); + added->push_back( + dp.changed_piece[0].new_piece.from[perspective] - fe_end); + } +} + +} // namespace Features + +} // namespace NNUE + +} // namespace Eval + +#endif // defined(EVAL_NNUE) diff --git a/src/eval/nnue/features/k.h b/src/eval/nnue/features/k.h new file mode 100644 index 00000000..1a01c471 --- /dev/null +++ b/src/eval/nnue/features/k.h @@ -0,0 +1,48 @@ +// NNUE評価関数の入力特徴量Kの定義 + +#ifndef _NNUE_FEATURES_K_H_ +#define _NNUE_FEATURES_K_H_ + +#if defined(EVAL_NNUE) + +#include "../../../evaluate.h" +#include "features_common.h" + +namespace Eval { + +namespace NNUE { + +namespace Features { + +// 特徴量K:玉の位置 +class K { + public: + // 特徴量名 + static constexpr const char* kName = "K"; + // 評価関数ファイルに埋め込むハッシュ値 + static constexpr std::uint32_t kHashValue = 0xD3CEE169u; + // 特徴量の次元数 + static constexpr IndexType kDimensions = SQUARE_NB * 2; + // 特徴量のうち、同時に値が1となるインデックスの数の最大値 + static constexpr IndexType kMaxActiveDimensions = 2; + // 差分計算の代わりに全計算を行うタイミング + static constexpr TriggerEvent kRefreshTrigger = TriggerEvent::kNone; + + // 特徴量のうち、値が1であるインデックスのリストを取得する + static void AppendActiveIndices(const Position& pos, Color perspective, + IndexList* active); + + // 特徴量のうち、一手前から値が変化したインデックスのリストを取得する + static void AppendChangedIndices(const Position& pos, Color perspective, + IndexList* removed, IndexList* added); +}; + +} // namespace Features + +} // namespace NNUE + +} // namespace Eval + +#endif // defined(EVAL_NNUE) + +#endif diff --git a/src/eval/nnue/features/p.cpp b/src/eval/nnue/features/p.cpp new file mode 100644 index 00000000..56bca0a4 --- /dev/null +++ b/src/eval/nnue/features/p.cpp @@ -0,0 +1,52 @@ +// NNUE評価関数の入力特徴量Pの定義 + +#if defined(EVAL_NNUE) + +#include "p.h" +#include "index_list.h" + +namespace Eval { + +namespace NNUE { + +namespace Features { + +// 特徴量のうち、値が1であるインデックスのリストを取得する +void P::AppendActiveIndices( + const Position& pos, Color perspective, IndexList* active) { + // コンパイラの警告を回避するため、配列サイズが小さい場合は何もしない + if (RawFeatures::kMaxActiveDimensions < kMaxActiveDimensions) return; + + const BonaPiece* pieces = (perspective == BLACK) ? + pos.eval_list()->piece_list_fb() : + pos.eval_list()->piece_list_fw(); + for (PieceNumber i = PIECE_NUMBER_ZERO; i < PIECE_NUMBER_KING; ++i) { + if (pieces[i] != Eval::BONA_PIECE_ZERO) { + active->push_back(pieces[i]); + } + } +} + +// 特徴量のうち、一手前から値が変化したインデックスのリストを取得する +void P::AppendChangedIndices( + const Position& pos, Color perspective, + IndexList* removed, IndexList* added) { + const auto& dp = pos.state()->dirtyPiece; + for (int i = 0; i < dp.dirty_num; ++i) { + if (dp.pieceNo[i] >= PIECE_NUMBER_KING) continue; + if (dp.changed_piece[i].old_piece.from[perspective] != Eval::BONA_PIECE_ZERO) { + removed->push_back(dp.changed_piece[i].old_piece.from[perspective]); + } + if (dp.changed_piece[i].new_piece.from[perspective] != Eval::BONA_PIECE_ZERO) { + added->push_back(dp.changed_piece[i].new_piece.from[perspective]); + } + } +} + +} // namespace Features + +} // namespace NNUE + +} // namespace Eval + +#endif // defined(EVAL_NNUE) diff --git a/src/eval/nnue/features/p.h b/src/eval/nnue/features/p.h new file mode 100644 index 00000000..77ea882d --- /dev/null +++ b/src/eval/nnue/features/p.h @@ -0,0 +1,48 @@ +// NNUE評価関数の入力特徴量Pの定義 + +#ifndef _NNUE_FEATURES_P_H_ +#define _NNUE_FEATURES_P_H_ + +#if defined(EVAL_NNUE) + +#include "../../../evaluate.h" +#include "features_common.h" + +namespace Eval { + +namespace NNUE { + +namespace Features { + +// 特徴量P:玉以外の駒のBonaPiece +class P { + public: + // 特徴量名 + static constexpr const char* kName = "P"; + // 評価関数ファイルに埋め込むハッシュ値 + static constexpr std::uint32_t kHashValue = 0x764CFB4Bu; + // 特徴量の次元数 + static constexpr IndexType kDimensions = fe_end; + // 特徴量のうち、同時に値が1となるインデックスの数の最大値 + static constexpr IndexType kMaxActiveDimensions = PIECE_NUMBER_KING; + // 差分計算の代わりに全計算を行うタイミング + static constexpr TriggerEvent kRefreshTrigger = TriggerEvent::kNone; + + // 特徴量のうち、値が1であるインデックスのリストを取得する + static void AppendActiveIndices(const Position& pos, Color perspective, + IndexList* active); + + // 特徴量のうち、一手前から値が変化したインデックスのリストを取得する + static void AppendChangedIndices(const Position& pos, Color perspective, + IndexList* removed, IndexList* added); +}; + +} // namespace Features + +} // namespace NNUE + +} // namespace Eval + +#endif // defined(EVAL_NNUE) + +#endif diff --git a/src/eval/nnue/layers/affine_transform.h b/src/eval/nnue/layers/affine_transform.h new file mode 100644 index 00000000..d8101ba4 --- /dev/null +++ b/src/eval/nnue/layers/affine_transform.h @@ -0,0 +1,178 @@ +// NNUE評価関数の層AffineTransformの定義 + +#ifndef _NNUE_LAYERS_AFFINE_TRANSFORM_H_ +#define _NNUE_LAYERS_AFFINE_TRANSFORM_H_ + +#if defined(EVAL_NNUE) + +#include "../nnue_common.h" + +namespace Eval { + +namespace NNUE { + +namespace Layers { + +// アフィン変換層 +template +class AffineTransform { + public: + // 入出力の型 + using InputType = typename PreviousLayer::OutputType; + using OutputType = std::int32_t; + static_assert(std::is_same::value, ""); + + // 入出力の次元数 + static constexpr IndexType kInputDimensions = + PreviousLayer::kOutputDimensions; + static constexpr IndexType kOutputDimensions = OutputDimensions; + static constexpr IndexType kPaddedInputDimensions = + CeilToMultiple(kInputDimensions, kMaxSimdWidth); + + // この層で使用する順伝播用バッファのサイズ + static constexpr std::size_t kSelfBufferSize = + CeilToMultiple(kOutputDimensions * sizeof(OutputType), kCacheLineSize); + + // 入力層からこの層までで使用する順伝播用バッファのサイズ + static constexpr std::size_t kBufferSize = + PreviousLayer::kBufferSize + kSelfBufferSize; + + // 評価関数ファイルに埋め込むハッシュ値 + static constexpr std::uint32_t GetHashValue() { + std::uint32_t hash_value = 0xCC03DAE4u; + hash_value += kOutputDimensions; + hash_value ^= PreviousLayer::GetHashValue() >> 1; + hash_value ^= PreviousLayer::GetHashValue() << 31; + return hash_value; + } + + // 入力層からこの層までの構造を表す文字列 + static std::string GetStructureString() { + return "AffineTransform[" + + std::to_string(kOutputDimensions) + "<-" + + std::to_string(kInputDimensions) + "](" + + PreviousLayer::GetStructureString() + ")"; + } + + // パラメータを読み込む + bool ReadParameters(std::istream& stream) { + if (!previous_layer_.ReadParameters(stream)) return false; + stream.read(reinterpret_cast(biases_), + kOutputDimensions * sizeof(BiasType)); + stream.read(reinterpret_cast(weights_), + kOutputDimensions * kPaddedInputDimensions * + sizeof(WeightType)); + return !stream.fail(); + } + + // パラメータを書き込む + bool WriteParameters(std::ostream& stream) const { + if (!previous_layer_.WriteParameters(stream)) return false; + stream.write(reinterpret_cast(biases_), + kOutputDimensions * sizeof(BiasType)); + stream.write(reinterpret_cast(weights_), + kOutputDimensions * kPaddedInputDimensions * + sizeof(WeightType)); + return !stream.fail(); + } + + // 順伝播 + const OutputType* Propagate( + const TransformedFeatureType* transformed_features, char* buffer) const { + const auto input = previous_layer_.Propagate( + transformed_features, buffer + kSelfBufferSize); + const auto output = reinterpret_cast(buffer); +#if defined(USE_AVX2) + constexpr IndexType kNumChunks = kPaddedInputDimensions / kSimdWidth; + const __m256i kOnes = _mm256_set1_epi16(1); + const auto input_vector = reinterpret_cast(input); +#elif defined(USE_SSE41) + constexpr IndexType kNumChunks = kPaddedInputDimensions / kSimdWidth; + const __m128i kOnes = _mm_set1_epi16(1); + const auto input_vector = reinterpret_cast(input); +#elif defined(IS_ARM) + constexpr IndexType kNumChunks = kPaddedInputDimensions / kSimdWidth; + const auto input_vector = reinterpret_cast(input); +#endif + for (IndexType i = 0; i < kOutputDimensions; ++i) { + const IndexType offset = i * kPaddedInputDimensions; +#if defined(USE_AVX2) + __m256i sum = _mm256_set_epi32(0, 0, 0, 0, 0, 0, 0, biases_[i]); + const auto row = reinterpret_cast(&weights_[offset]); + for (IndexType j = 0; j < kNumChunks; ++j) { + __m256i product = _mm256_maddubs_epi16( +#if defined(__MINGW32__) || defined(__MINGW64__) + // HACK: Use _mm256_loadu_si256() instead of _mm256_load_si256. Because the binary + // compiled with g++ in MSYS2 crashes here because the output memory is not aligned + // even though alignas is specified. + _mm256_loadu_si256 +#else + _mm256_load_si256 +#endif + (&input_vector[j]), _mm256_load_si256(&row[j])); + product = _mm256_madd_epi16(product, kOnes); + sum = _mm256_add_epi32(sum, product); + } + sum = _mm256_hadd_epi32(sum, sum); + sum = _mm256_hadd_epi32(sum, sum); + const __m128i lo = _mm256_extracti128_si256(sum, 0); + const __m128i hi = _mm256_extracti128_si256(sum, 1); + output[i] = _mm_cvtsi128_si32(lo) + _mm_cvtsi128_si32(hi); +#elif defined(USE_SSE41) + __m128i sum = _mm_cvtsi32_si128(biases_[i]); + const auto row = reinterpret_cast(&weights_[offset]); + for (IndexType j = 0; j < kNumChunks; ++j) { + __m128i product = _mm_maddubs_epi16( + _mm_load_si128(&input_vector[j]), _mm_load_si128(&row[j])); + product = _mm_madd_epi16(product, kOnes); + sum = _mm_add_epi32(sum, product); + } + sum = _mm_hadd_epi32(sum, sum); + sum = _mm_hadd_epi32(sum, sum); + output[i] = _mm_cvtsi128_si32(sum); +#elif defined(IS_ARM) + int32x4_t sum = {biases_[i]}; + const auto row = reinterpret_cast(&weights_[offset]); + for (IndexType j = 0; j < kNumChunks; ++j) { + int16x8_t product = vmull_s8(input_vector[j * 2], row[j * 2]); + product = vmlal_s8(product, input_vector[j * 2 + 1], row[j * 2 + 1]); + sum = vpadalq_s16(sum, product); + } + output[i] = sum[0] + sum[1] + sum[2] + sum[3]; +#else + OutputType sum = biases_[i]; + for (IndexType j = 0; j < kInputDimensions; ++j) { + sum += weights_[offset + j] * input[j]; + } + output[i] = sum; +#endif + } + return output; + } + + private: + // パラメータの型 + using BiasType = OutputType; + using WeightType = std::int8_t; + + // 学習用クラスをfriendにする + friend class Trainer; + + // この層の直前の層 + PreviousLayer previous_layer_; + + // パラメータ + alignas(kCacheLineSize) BiasType biases_[kOutputDimensions]; + alignas(kCacheLineSize) + WeightType weights_[kOutputDimensions * kPaddedInputDimensions]; +}; + +} // namespace Layers + +} // namespace NNUE + +} // namespace Eval + +#endif // defined(EVAL_NNUE) + +#endif diff --git a/src/eval/nnue/layers/clipped_relu.h b/src/eval/nnue/layers/clipped_relu.h new file mode 100644 index 00000000..5877fc32 --- /dev/null +++ b/src/eval/nnue/layers/clipped_relu.h @@ -0,0 +1,168 @@ +// NNUE評価関数の層ClippedReLUの定義 + +#ifndef _NNUE_LAYERS_CLIPPED_RELU_H_ +#define _NNUE_LAYERS_CLIPPED_RELU_H_ + +#if defined(EVAL_NNUE) + +#include "../nnue_common.h" + +namespace Eval { + +namespace NNUE { + +namespace Layers { + +// Clipped ReLU +template +class ClippedReLU { + public: + // 入出力の型 + using InputType = typename PreviousLayer::OutputType; + using OutputType = std::uint8_t; + static_assert(std::is_same::value, ""); + + // 入出力の次元数 + static constexpr IndexType kInputDimensions = + PreviousLayer::kOutputDimensions; + static constexpr IndexType kOutputDimensions = kInputDimensions; + + // この層で使用する順伝播用バッファのサイズ + static constexpr std::size_t kSelfBufferSize = + CeilToMultiple(kOutputDimensions * sizeof(OutputType), kCacheLineSize); + + // 入力層からこの層までで使用する順伝播用バッファのサイズ + static constexpr std::size_t kBufferSize = + PreviousLayer::kBufferSize + kSelfBufferSize; + + // 評価関数ファイルに埋め込むハッシュ値 + static constexpr std::uint32_t GetHashValue() { + std::uint32_t hash_value = 0x538D24C7u; + hash_value += PreviousLayer::GetHashValue(); + return hash_value; + } + + // 入力層からこの層までの構造を表す文字列 + static std::string GetStructureString() { + return "ClippedReLU[" + + std::to_string(kOutputDimensions) + "](" + + PreviousLayer::GetStructureString() + ")"; + } + + // パラメータを読み込む + bool ReadParameters(std::istream& stream) { + return previous_layer_.ReadParameters(stream); + } + + // パラメータを書き込む + bool WriteParameters(std::ostream& stream) const { + return previous_layer_.WriteParameters(stream); + } + + // 順伝播 + const OutputType* Propagate( + const TransformedFeatureType* transformed_features, char* buffer) const { + const auto input = previous_layer_.Propagate( + transformed_features, buffer + kSelfBufferSize); + const auto output = reinterpret_cast(buffer); +#if defined(USE_AVX2) + constexpr IndexType kNumChunks = kInputDimensions / kSimdWidth; + const __m256i kZero = _mm256_setzero_si256(); + const __m256i kOffsets = _mm256_set_epi32(7, 3, 6, 2, 5, 1, 4, 0); + const auto in = reinterpret_cast(input); + const auto out = reinterpret_cast<__m256i*>(output); + for (IndexType i = 0; i < kNumChunks; ++i) { + const __m256i words0 = _mm256_srai_epi16(_mm256_packs_epi32( +#if defined(__MINGW32__) || defined(__MINGW64__) + // HACK: Use _mm256_loadu_si256() instead of _mm256_load_si256. Because the binary + // compiled with g++ in MSYS2 crashes here because the output memory is not aligned + // even though alignas is specified. + _mm256_loadu_si256 +#else + _mm256_load_si256 +#endif + (&in[i * 4 + 0]), +#if defined(__MINGW32__) || defined(__MINGW64__) + _mm256_loadu_si256 +#else + _mm256_load_si256 +#endif + (&in[i * 4 + 1])), kWeightScaleBits); + const __m256i words1 = _mm256_srai_epi16(_mm256_packs_epi32( +#if defined(__MINGW32__) || defined(__MINGW64__) + _mm256_loadu_si256 +#else + _mm256_load_si256 +#endif + (&in[i * 4 + 2]), +#if defined(__MINGW32__) || defined(__MINGW64__) + _mm256_loadu_si256 +#else + _mm256_load_si256 +#endif + (&in[i * 4 + 3])), kWeightScaleBits); +#if defined(__MINGW32__) || defined(__MINGW64__) + _mm256_storeu_si256 +#else + _mm256_store_si256 +#endif + (&out[i], _mm256_permutevar8x32_epi32(_mm256_max_epi8( + _mm256_packs_epi16(words0, words1), kZero), kOffsets)); + } + constexpr IndexType kStart = kNumChunks * kSimdWidth; +#elif defined(USE_SSE41) + constexpr IndexType kNumChunks = kInputDimensions / kSimdWidth; + const __m128i kZero = _mm_setzero_si128(); + const auto in = reinterpret_cast(input); + const auto out = reinterpret_cast<__m128i*>(output); + for (IndexType i = 0; i < kNumChunks; ++i) { + const __m128i words0 = _mm_srai_epi16(_mm_packs_epi32( + _mm_load_si128(&in[i * 4 + 0]), + _mm_load_si128(&in[i * 4 + 1])), kWeightScaleBits); + const __m128i words1 = _mm_srai_epi16(_mm_packs_epi32( + _mm_load_si128(&in[i * 4 + 2]), + _mm_load_si128(&in[i * 4 + 3])), kWeightScaleBits); + _mm_store_si128(&out[i], _mm_max_epi8( + _mm_packs_epi16(words0, words1), kZero)); + } + constexpr IndexType kStart = kNumChunks * kSimdWidth; +#elif defined(IS_ARM) + constexpr IndexType kNumChunks = kInputDimensions / (kSimdWidth / 2); + const int8x8_t kZero = {0}; + const auto in = reinterpret_cast(input); + const auto out = reinterpret_cast(output); + for (IndexType i = 0; i < kNumChunks; ++i) { + int16x8_t shifted; + const auto pack = reinterpret_cast(&shifted); + pack[0] = vqshrn_n_s32(in[i * 2 + 0], kWeightScaleBits); + pack[1] = vqshrn_n_s32(in[i * 2 + 1], kWeightScaleBits); + out[i] = vmax_s8(vqmovn_s16(shifted), kZero); + } + constexpr IndexType kStart = kNumChunks * (kSimdWidth / 2); +#else + constexpr IndexType kStart = 0; +#endif + for (IndexType i = kStart; i < kInputDimensions; ++i) { + output[i] = static_cast( + std::max(0, std::min(127, input[i] >> kWeightScaleBits))); + } + return output; + } + + private: + // 学習用クラスをfriendにする + friend class Trainer; + + // この層の直前の層 + PreviousLayer previous_layer_; +}; + +} // namespace Layers + +} // namespace NNUE + +} // namespace Eval + +#endif // defined(EVAL_NNUE) + +#endif diff --git a/src/eval/nnue/layers/input_slice.h b/src/eval/nnue/layers/input_slice.h new file mode 100644 index 00000000..c9c6a7c9 --- /dev/null +++ b/src/eval/nnue/layers/input_slice.h @@ -0,0 +1,74 @@ +// NNUE評価関数の層InputSliceの定義 + +#ifndef _NNUE_LAYERS_INPUT_SLICE_H_ +#define _NNUE_LAYERS_INPUT_SLICE_H_ + +#if defined(EVAL_NNUE) + +#include "../nnue_common.h" + +namespace Eval { + +namespace NNUE { + +namespace Layers { + +// 入力層 +template +class InputSlice { + public: + // アライメントを維持する必要がある + static_assert(Offset % kMaxSimdWidth == 0, ""); + + // 出力の型 + using OutputType = TransformedFeatureType; + + // 出力の次元数 + static constexpr IndexType kOutputDimensions = OutputDimensions; + + // 入力層からこの層までで使用する順伝播用バッファのサイズ + static constexpr std::size_t kBufferSize = 0; + + // 評価関数ファイルに埋め込むハッシュ値 + static constexpr std::uint32_t GetHashValue() { + std::uint32_t hash_value = 0xEC42E90Du; + hash_value ^= kOutputDimensions ^ (Offset << 10); + return hash_value; + } + + // 入力層からこの層までの構造を表す文字列 + static std::string GetStructureString() { + return "InputSlice[" + std::to_string(kOutputDimensions) + "(" + + std::to_string(Offset) + ":" + + std::to_string(Offset + kOutputDimensions) + ")]"; + } + + // パラメータを読み込む + bool ReadParameters(std::istream& /*stream*/) { + return true; + } + + // パラメータを書き込む + bool WriteParameters(std::ostream& /*stream*/) const { + return true; + } + + // 順伝播 + const OutputType* Propagate( + const TransformedFeatureType* transformed_features, + char* /*buffer*/) const { + return transformed_features + Offset; + } + + private: +}; + +} // namespace Layers + +} // namespace NNUE + +} // namespace Eval + +#endif // defined(EVAL_NNUE) + +#endif diff --git a/src/eval/nnue/layers/sum.h b/src/eval/nnue/layers/sum.h new file mode 100644 index 00000000..3fe000cf --- /dev/null +++ b/src/eval/nnue/layers/sum.h @@ -0,0 +1,163 @@ +// NNUE評価関数の層Sumの定義 + +#ifndef _NNUE_LAYERS_SUM_H_ +#define _NNUE_LAYERS_SUM_H_ + +#if defined(EVAL_NNUE) + +#include "../nnue_common.h" + +namespace Eval { + +namespace NNUE { + +namespace Layers { + +// 複数の層の出力の和を取る層 +template +class Sum : public Sum { + private: + using Head = FirstPreviousLayer; + using Tail = Sum; + + public: + // 入出力の型 + using InputType = typename Head::OutputType; + using OutputType = InputType; + static_assert(std::is_same::value, ""); + + // 入出力の次元数 + static constexpr IndexType kInputDimensions = Head::kOutputDimensions; + static constexpr IndexType kOutputDimensions = kInputDimensions; + static_assert(kInputDimensions == Tail::kInputDimensions , ""); + + // この層で使用する順伝播用バッファのサイズ + static constexpr std::size_t kSelfBufferSize = + CeilToMultiple(kOutputDimensions * sizeof(OutputType), kCacheLineSize); + + // 入力層からこの層までで使用する順伝播用バッファのサイズ + static constexpr std::size_t kBufferSize = + std::max(Head::kBufferSize + kSelfBufferSize, Tail::kBufferSize); + + // 評価関数ファイルに埋め込むハッシュ値 + static constexpr std::uint32_t GetHashValue() { + std::uint32_t hash_value = 0xBCE400B4u; + hash_value ^= Head::GetHashValue() >> 1; + hash_value ^= Head::GetHashValue() << 31; + hash_value ^= Tail::GetHashValue() >> 2; + hash_value ^= Tail::GetHashValue() << 30; + return hash_value; + } + + // 入力層からこの層までの構造を表す文字列 + static std::string GetStructureString() { + return "Sum[" + + std::to_string(kOutputDimensions) + "](" + GetSummandsString() + ")"; + } + + // パラメータを読み込む + bool ReadParameters(std::istream& stream) { + if (!Tail::ReadParameters(stream)) return false; + return previous_layer_.ReadParameters(stream); + } + + // パラメータを書き込む + bool WriteParameters(std::ostream& stream) const { + if (!Tail::WriteParameters(stream)) return false; + return previous_layer_.WriteParameters(stream); + } + + // 順伝播 + const OutputType* Propagate( + const TransformedFeatureType* transformed_features, char* buffer) const { + Tail::Propagate(transformed_features, buffer); + const auto head_output = previous_layer_.Propagate( + transformed_features, buffer + kSelfBufferSize); + const auto output = reinterpret_cast(buffer); + for (IndexType i = 0; i < kOutputDimensions; ++i) { + output[i] += head_output[i]; + } + return output; + } + + protected: + // 和を取る対象となる層のリストを表す文字列 + static std::string GetSummandsString() { + return Head::GetStructureString() + "," + Tail::GetSummandsString(); + } + + // 学習用クラスをfriendにする + friend class Trainer; + + // この層の直前の層 + FirstPreviousLayer previous_layer_; +}; + +// 複数の層の出力の和を取る層(テンプレート引数が1つの場合) +template +class Sum { + public: + // 入出力の型 + using InputType = typename PreviousLayer::OutputType; + using OutputType = InputType; + + // 入出力の次元数 + static constexpr IndexType kInputDimensions = + PreviousLayer::kOutputDimensions; + static constexpr IndexType kOutputDimensions = kInputDimensions; + + // 入力層からこの層までで使用する順伝播用バッファのサイズ + static constexpr std::size_t kBufferSize = PreviousLayer::kBufferSize; + + // 評価関数ファイルに埋め込むハッシュ値 + static constexpr std::uint32_t GetHashValue() { + std::uint32_t hash_value = 0xBCE400B4u; + hash_value ^= PreviousLayer::GetHashValue() >> 1; + hash_value ^= PreviousLayer::GetHashValue() << 31; + return hash_value; + } + + // 入力層からこの層までの構造を表す文字列 + static std::string GetStructureString() { + return "Sum[" + + std::to_string(kOutputDimensions) + "](" + GetSummandsString() + ")"; + } + + // パラメータを読み込む + bool ReadParameters(std::istream& stream) { + return previous_layer_.ReadParameters(stream); + } + + // パラメータを書き込む + bool WriteParameters(std::ostream& stream) const { + return previous_layer_.WriteParameters(stream); + } + + // 順伝播 + const OutputType* Propagate( + const TransformedFeatureType* transformed_features, char* buffer) const { + return previous_layer_.Propagate(transformed_features, buffer); + } + + protected: + // 和を取る対象となる層のリストを表す文字列 + static std::string GetSummandsString() { + return PreviousLayer::GetStructureString(); + } + + // 学習用クラスをfriendにする + friend class Trainer; + + // この層の直前の層 + PreviousLayer previous_layer_; +}; + +} // namespace Layers + +} // namespace NNUE + +} // namespace Eval + +#endif // defined(EVAL_NNUE) + +#endif diff --git a/src/eval/nnue/nnue_accumulator.h b/src/eval/nnue/nnue_accumulator.h new file mode 100644 index 00000000..4241edb3 --- /dev/null +++ b/src/eval/nnue/nnue_accumulator.h @@ -0,0 +1,30 @@ +// NNUE評価関数の差分計算用のクラス + +#ifndef _NNUE_ACCUMULATOR_H_ +#define _NNUE_ACCUMULATOR_H_ + +#if defined(EVAL_NNUE) + +#include "nnue_architecture.h" + +namespace Eval { + +namespace NNUE { + +// 入力特徴量をアフィン変換した結果を保持するクラス +// 最終的な出力である評価値も一緒に持たせておく +struct alignas(32) Accumulator { + std::int16_t + accumulation[2][kRefreshTriggers.size()][kTransformedFeatureDimensions]; + Value score = VALUE_ZERO; + bool computed_accumulation = false; + bool computed_score = false; +}; + +} // namespace NNUE + +} // namespace Eval + +#endif // defined(EVAL_NNUE) + +#endif diff --git a/src/eval/nnue/nnue_architecture.h b/src/eval/nnue/nnue_architecture.h new file mode 100644 index 00000000..7479ac0a --- /dev/null +++ b/src/eval/nnue/nnue_architecture.h @@ -0,0 +1,32 @@ +// NNUE評価関数で用いる入力特徴量とネットワーク構造 + +#ifndef _NNUE_ARCHITECTURE_H_ +#define _NNUE_ARCHITECTURE_H_ + +#if defined(EVAL_NNUE) + +// 入力特徴量とネットワーク構造が定義されたヘッダをincludeする +//#include "architectures/k-p_256x2-32-32.h" +//#include "architectures/k-p-cr_256x2-32-32.h" +//#include "architectures/k-p-cr-ep_256x2-32-32.h" +#include "architectures/halfkp_256x2-32-32.h" +//#include "architectures/halfkp-cr-ep_256x2-32-32.h" + +namespace Eval { + +namespace NNUE { + +static_assert(kTransformedFeatureDimensions % kMaxSimdWidth == 0, ""); +static_assert(Network::kOutputDimensions == 1, ""); +static_assert(std::is_same::value, ""); + +// 差分計算の代わりに全計算を行うタイミングのリスト +constexpr auto kRefreshTriggers = RawFeatures::kRefreshTriggers; + +} // namespace NNUE + +} // namespace Eval + +#endif // defined(EVAL_NNUE) + +#endif diff --git a/src/eval/nnue/nnue_common.h b/src/eval/nnue/nnue_common.h new file mode 100644 index 00000000..8ef8fee4 --- /dev/null +++ b/src/eval/nnue/nnue_common.h @@ -0,0 +1,60 @@ +// NNUE評価関数で用いる定数など + +#ifndef _NNUE_COMMON_H_ +#define _NNUE_COMMON_H_ + +#if defined(EVAL_NNUE) + +#if defined(USE_AVX2) +#include +#elif defined(USE_SSE2) +#include +#endif + +namespace Eval { + +namespace NNUE { + +// 評価関数ファイルのバージョンを表す定数 +constexpr std::uint32_t kVersion = 0x7AF32F16u; + +// 評価値の計算で利用する定数 +constexpr int FV_SCALE = 16; +constexpr int kWeightScaleBits = 6; + +// キャッシュラインのサイズ(バイト単位) +constexpr std::size_t kCacheLineSize = 64; + +// SIMD幅(バイト単位) +#if defined(USE_AVX2) +constexpr std::size_t kSimdWidth = 32; +#elif defined(USE_SSE2) +constexpr std::size_t kSimdWidth = 16; +#elif defined(IS_ARM) +constexpr std::size_t kSimdWidth = 16; +#endif +constexpr std::size_t kMaxSimdWidth = 32; + +// 変換後の入力特徴量の型 +using TransformedFeatureType = std::uint8_t; + +// インデックスの型 +using IndexType = std::uint32_t; + +// 学習用クラステンプレートの前方宣言 +template +class Trainer; + +// n以上で最小のbaseの倍数を求める +template +constexpr IntType CeilToMultiple(IntType n, IntType base) { + return (n + base - 1) / base * base; +} + +} // namespace NNUE + +} // namespace Eval + +#endif // defined(EVAL_NNUE) + +#endif diff --git a/src/eval/nnue/nnue_feature_transformer.h b/src/eval/nnue/nnue_feature_transformer.h new file mode 100644 index 00000000..57d25310 --- /dev/null +++ b/src/eval/nnue/nnue_feature_transformer.h @@ -0,0 +1,347 @@ +// NNUE評価関数の入力特徴量の変換を行うクラス + +#ifndef _NNUE_FEATURE_TRANSFORMER_H_ +#define _NNUE_FEATURE_TRANSFORMER_H_ + +#if defined(EVAL_NNUE) + +#include "nnue_common.h" +#include "nnue_architecture.h" +#include "features/index_list.h" + +#include // std::memset() + +namespace Eval { + +namespace NNUE { + +// 入力特徴量変換器 +class FeatureTransformer { + private: + // 片側分の出力の次元数 + static constexpr IndexType kHalfDimensions = kTransformedFeatureDimensions; + + public: + // 出力の型 + using OutputType = TransformedFeatureType; + + // 入出力の次元数 + static constexpr IndexType kInputDimensions = RawFeatures::kDimensions; + static constexpr IndexType kOutputDimensions = kHalfDimensions * 2; + + // 順伝播用バッファのサイズ + static constexpr std::size_t kBufferSize = + kOutputDimensions * sizeof(OutputType); + + // 評価関数ファイルに埋め込むハッシュ値 + static constexpr std::uint32_t GetHashValue() { + return RawFeatures::kHashValue ^ kOutputDimensions; + } + + // 構造を表す文字列 + static std::string GetStructureString() { + return RawFeatures::GetName() + "[" + + std::to_string(kInputDimensions) + "->" + + std::to_string(kHalfDimensions) + "x2]"; + } + + // パラメータを読み込む + bool ReadParameters(std::istream& stream) { + stream.read(reinterpret_cast(biases_), + kHalfDimensions * sizeof(BiasType)); + stream.read(reinterpret_cast(weights_), + kHalfDimensions * kInputDimensions * sizeof(WeightType)); + return !stream.fail(); + } + + // パラメータを書き込む + bool WriteParameters(std::ostream& stream) const { + stream.write(reinterpret_cast(biases_), + kHalfDimensions * sizeof(BiasType)); + stream.write(reinterpret_cast(weights_), + kHalfDimensions * kInputDimensions * sizeof(WeightType)); + return !stream.fail(); + } + + // 可能なら差分計算を進める + bool UpdateAccumulatorIfPossible(const Position& pos) const { + const auto now = pos.state(); + if (now->accumulator.computed_accumulation) { + return true; + } + const auto prev = now->previous; + if (prev && prev->accumulator.computed_accumulation) { + UpdateAccumulator(pos); + return true; + } + return false; + } + + // 入力特徴量を変換する + void Transform(const Position& pos, OutputType* output, bool refresh) const { + if (refresh || !UpdateAccumulatorIfPossible(pos)) { + RefreshAccumulator(pos); + } + const auto& accumulation = pos.state()->accumulator.accumulation; +#if defined(USE_AVX2) + constexpr IndexType kNumChunks = kHalfDimensions / kSimdWidth; + constexpr int kControl = 0b11011000; + const __m256i kZero = _mm256_setzero_si256(); +#elif defined(USE_SSE41) + constexpr IndexType kNumChunks = kHalfDimensions / kSimdWidth; + const __m128i kZero = _mm_setzero_si128(); +#elif defined(IS_ARM) + constexpr IndexType kNumChunks = kHalfDimensions / (kSimdWidth / 2); + const int8x8_t kZero = {0}; +#endif + const Color perspectives[2] = {pos.side_to_move(), ~pos.side_to_move()}; + for (IndexType p = 0; p < 2; ++p) { + const IndexType offset = kHalfDimensions * p; +#if defined(USE_AVX2) + auto out = reinterpret_cast<__m256i*>(&output[offset]); + for (IndexType j = 0; j < kNumChunks; ++j) { + __m256i sum0 = +#if defined(__MINGW32__) || defined(__MINGW64__) + // HACK: Use _mm256_loadu_si256() instead of _mm256_load_si256. Because the binary + // compiled with g++ in MSYS2 crashes here because the output memory is not aligned + // even though alignas is specified. + _mm256_loadu_si256 +#else + _mm256_load_si256 +#endif + (&reinterpret_cast( + accumulation[perspectives[p]][0])[j * 2 + 0]); + __m256i sum1 = +#if defined(__MINGW32__) || defined(__MINGW64__) + _mm256_loadu_si256 +#else + _mm256_load_si256 +#endif + (&reinterpret_cast( + accumulation[perspectives[p]][0])[j * 2 + 1]); + for (IndexType i = 1; i < kRefreshTriggers.size(); ++i) { + sum0 = _mm256_add_epi16(sum0, reinterpret_cast( + accumulation[perspectives[p]][i])[j * 2 + 0]); + sum1 = _mm256_add_epi16(sum1, reinterpret_cast( + accumulation[perspectives[p]][i])[j * 2 + 1]); + } +#if defined(__MINGW32__) || defined(__MINGW64__) + _mm256_storeu_si256 +#else + _mm256_store_si256 +#endif + (&out[j], _mm256_permute4x64_epi64(_mm256_max_epi8( + _mm256_packs_epi16(sum0, sum1), kZero), kControl)); + } +#elif defined(USE_SSE41) + auto out = reinterpret_cast<__m128i*>(&output[offset]); + for (IndexType j = 0; j < kNumChunks; ++j) { + __m128i sum0 = _mm_load_si128(&reinterpret_cast( + accumulation[perspectives[p]][0])[j * 2 + 0]); + __m128i sum1 = _mm_load_si128(&reinterpret_cast( + accumulation[perspectives[p]][0])[j * 2 + 1]); + for (IndexType i = 1; i < kRefreshTriggers.size(); ++i) { + sum0 = _mm_add_epi16(sum0, reinterpret_cast( + accumulation[perspectives[p]][i])[j * 2 + 0]); + sum1 = _mm_add_epi16(sum1, reinterpret_cast( + accumulation[perspectives[p]][i])[j * 2 + 1]); + } + _mm_store_si128(&out[j], _mm_max_epi8( + _mm_packs_epi16(sum0, sum1), kZero)); + } +#elif defined(IS_ARM) + const auto out = reinterpret_cast(&output[offset]); + for (IndexType j = 0; j < kNumChunks; ++j) { + int16x8_t sum = reinterpret_cast( + accumulation[perspectives[p]][0])[j]; + for (IndexType i = 1; i < kRefreshTriggers.size(); ++i) { + sum = vaddq_s16(sum, reinterpret_cast( + accumulation[perspectives[p]][i])[j]); + } + out[j] = vmax_s8(vqmovn_s16(sum), kZero); + } +#else + for (IndexType j = 0; j < kHalfDimensions; ++j) { + BiasType sum = accumulation[static_cast(perspectives[p])][0][j]; + for (IndexType i = 1; i < kRefreshTriggers.size(); ++i) { + sum += accumulation[static_cast(perspectives[p])][i][j]; + } + output[offset + j] = static_cast( + std::max(0, std::min(127, sum))); + } +#endif + } + } + + private: + // 差分計算を用いずに累積値を計算する + void RefreshAccumulator(const Position& pos) const { + auto& accumulator = pos.state()->accumulator; + for (IndexType i = 0; i < kRefreshTriggers.size(); ++i) { + Features::IndexList active_indices[2]; + RawFeatures::AppendActiveIndices(pos, kRefreshTriggers[i], + active_indices); + for (const auto perspective : Colors) { + if (i == 0) { + std::memcpy(accumulator.accumulation[perspective][i], biases_, + kHalfDimensions * sizeof(BiasType)); + } else { + std::memset(accumulator.accumulation[perspective][i], 0, + kHalfDimensions * sizeof(BiasType)); + } + for (const auto index : active_indices[perspective]) { + const IndexType offset = kHalfDimensions * index; +#if defined(USE_AVX2) + auto accumulation = reinterpret_cast<__m256i*>( + &accumulator.accumulation[perspective][i][0]); + auto column = reinterpret_cast(&weights_[offset]); + constexpr IndexType kNumChunks = kHalfDimensions / (kSimdWidth / 2); + for (IndexType j = 0; j < kNumChunks; ++j) { +#if defined(__MINGW32__) || defined(__MINGW64__) + _mm256_storeu_si256(&accumulation[j], _mm256_add_epi16(_mm256_loadu_si256(&accumulation[j]), column[j])); +#else + accumulation[j] = _mm256_add_epi16(accumulation[j], column[j]); +#endif + } +#elif defined(USE_SSE2) + auto accumulation = reinterpret_cast<__m128i*>( + &accumulator.accumulation[perspective][i][0]); + auto column = reinterpret_cast(&weights_[offset]); + constexpr IndexType kNumChunks = kHalfDimensions / (kSimdWidth / 2); + for (IndexType j = 0; j < kNumChunks; ++j) { + accumulation[j] = _mm_add_epi16(accumulation[j], column[j]); + } +#elif defined(IS_ARM) + auto accumulation = reinterpret_cast( + &accumulator.accumulation[perspective][i][0]); + auto column = reinterpret_cast(&weights_[offset]); + constexpr IndexType kNumChunks = kHalfDimensions / (kSimdWidth / 2); + for (IndexType j = 0; j < kNumChunks; ++j) { + accumulation[j] = vaddq_s16(accumulation[j], column[j]); + } +#else + for (IndexType j = 0; j < kHalfDimensions; ++j) { + accumulator.accumulation[perspective][i][j] += weights_[offset + j]; + } +#endif + } + } + } + + accumulator.computed_accumulation = true; + accumulator.computed_score = false; + } + + // 差分計算を用いて累積値を計算する + void UpdateAccumulator(const Position& pos) const { + const auto prev_accumulator = pos.state()->previous->accumulator; + auto& accumulator = pos.state()->accumulator; + for (IndexType i = 0; i < kRefreshTriggers.size(); ++i) { + Features::IndexList removed_indices[2], added_indices[2]; + bool reset[2]; + RawFeatures::AppendChangedIndices(pos, kRefreshTriggers[i], + removed_indices, added_indices, reset); + for (const auto perspective : Colors) { +#if defined(USE_AVX2) + constexpr IndexType kNumChunks = kHalfDimensions / (kSimdWidth / 2); + auto accumulation = reinterpret_cast<__m256i*>( + &accumulator.accumulation[perspective][i][0]); +#elif defined(USE_SSE2) + constexpr IndexType kNumChunks = kHalfDimensions / (kSimdWidth / 2); + auto accumulation = reinterpret_cast<__m128i*>( + &accumulator.accumulation[perspective][i][0]); +#elif defined(IS_ARM) + constexpr IndexType kNumChunks = kHalfDimensions / (kSimdWidth / 2); + auto accumulation = reinterpret_cast( + &accumulator.accumulation[perspective][i][0]); +#endif + if (reset[perspective]) { + if (i == 0) { + std::memcpy(accumulator.accumulation[perspective][i], biases_, + kHalfDimensions * sizeof(BiasType)); + } else { + std::memset(accumulator.accumulation[perspective][i], 0, + kHalfDimensions * sizeof(BiasType)); + } + } else { // 1から0に変化した特徴量に関する差分計算 + std::memcpy(accumulator.accumulation[perspective][i], + prev_accumulator.accumulation[perspective][i], + kHalfDimensions * sizeof(BiasType)); + for (const auto index : removed_indices[perspective]) { + const IndexType offset = kHalfDimensions * index; +#if defined(USE_AVX2) + auto column = reinterpret_cast(&weights_[offset]); + for (IndexType j = 0; j < kNumChunks; ++j) { + accumulation[j] = _mm256_sub_epi16(accumulation[j], column[j]); + } +#elif defined(USE_SSE2) + auto column = reinterpret_cast(&weights_[offset]); + for (IndexType j = 0; j < kNumChunks; ++j) { + accumulation[j] = _mm_sub_epi16(accumulation[j], column[j]); + } +#elif defined(IS_ARM) + auto column = reinterpret_cast(&weights_[offset]); + for (IndexType j = 0; j < kNumChunks; ++j) { + accumulation[j] = vsubq_s16(accumulation[j], column[j]); + } +#else + for (IndexType j = 0; j < kHalfDimensions; ++j) { + accumulator.accumulation[perspective][i][j] -= + weights_[offset + j]; + } +#endif + } + } + { // 0から1に変化した特徴量に関する差分計算 + for (const auto index : added_indices[perspective]) { + const IndexType offset = kHalfDimensions * index; +#if defined(USE_AVX2) + auto column = reinterpret_cast(&weights_[offset]); + for (IndexType j = 0; j < kNumChunks; ++j) { + accumulation[j] = _mm256_add_epi16(accumulation[j], column[j]); + } +#elif defined(USE_SSE2) + auto column = reinterpret_cast(&weights_[offset]); + for (IndexType j = 0; j < kNumChunks; ++j) { + accumulation[j] = _mm_add_epi16(accumulation[j], column[j]); + } +#elif defined(IS_ARM) + auto column = reinterpret_cast(&weights_[offset]); + for (IndexType j = 0; j < kNumChunks; ++j) { + accumulation[j] = vaddq_s16(accumulation[j], column[j]); + } +#else + for (IndexType j = 0; j < kHalfDimensions; ++j) { + accumulator.accumulation[perspective][i][j] += + weights_[offset + j]; + } +#endif + } + } + } + } + + accumulator.computed_accumulation = true; + accumulator.computed_score = false; + } + + // パラメータの型 + using BiasType = std::int16_t; + using WeightType = std::int16_t; + + // 学習用クラスをfriendにする + friend class Trainer; + + // パラメータ + alignas(kCacheLineSize) BiasType biases_[kHalfDimensions]; + alignas(kCacheLineSize) + WeightType weights_[kHalfDimensions * kInputDimensions]; +}; + +} // namespace NNUE + +} // namespace Eval + +#endif // defined(EVAL_NNUE) + +#endif diff --git a/src/eval/nnue/nnue_test_command.cpp b/src/eval/nnue/nnue_test_command.cpp new file mode 100644 index 00000000..28e44273 --- /dev/null +++ b/src/eval/nnue/nnue_test_command.cpp @@ -0,0 +1,201 @@ +// NNUE評価関数に関するUSI拡張コマンド + +#if defined(ENABLE_TEST_CMD) && defined(EVAL_NNUE) + +#include "../../thread.h" +#include "../../uci.h" +#include "evaluate_nnue.h" +#include "nnue_test_command.h" + +#include +#include + +#define ASSERT(X) { if (!(X)) { std::cout << "\nError : ASSERT(" << #X << "), " << __FILE__ << "(" << __LINE__ << "): " << __func__ << std::endl; \ + std::this_thread::sleep_for(std::chrono::microseconds(3000)); *(int*)1 =0;} } + +namespace Eval { + +namespace NNUE { + +namespace { + +// 主に差分計算に関するRawFeaturesのテスト +void TestFeatures(Position& pos) { + const std::uint64_t num_games = 1000; + StateInfo si; + pos.set(StartFEN, false, &si, Threads.main()); + const int MAX_PLY = 256; // 256手までテスト + + StateInfo state[MAX_PLY]; // StateInfoを最大手数分だけ + int ply; // 初期局面からの手数 + + PRNG prng(20171128); + + std::uint64_t num_moves = 0; + std::vector num_updates(kRefreshTriggers.size() + 1); + std::vector num_resets(kRefreshTriggers.size()); + constexpr IndexType kUnknown = -1; + std::vector trigger_map(RawFeatures::kDimensions, kUnknown); + auto make_index_sets = [&](const Position& pos) { + std::vector>> index_sets( + kRefreshTriggers.size(), std::vector>(2)); + for (IndexType i = 0; i < kRefreshTriggers.size(); ++i) { + Features::IndexList active_indices[2]; + RawFeatures::AppendActiveIndices(pos, kRefreshTriggers[i], + active_indices); + for (const auto perspective : Colors) { + for (const auto index : active_indices[perspective]) { + ASSERT(index < RawFeatures::kDimensions); + ASSERT(index_sets[i][perspective].count(index) == 0); + ASSERT(trigger_map[index] == kUnknown || trigger_map[index] == i); + index_sets[i][perspective].insert(index); + trigger_map[index] = i; + } + } + } + return index_sets; + }; + auto update_index_sets = [&](const Position& pos, auto* index_sets) { + for (IndexType i = 0; i < kRefreshTriggers.size(); ++i) { + Features::IndexList removed_indices[2], added_indices[2]; + bool reset[2]; + RawFeatures::AppendChangedIndices(pos, kRefreshTriggers[i], + removed_indices, added_indices, reset); + for (const auto perspective : Colors) { + if (reset[perspective]) { + (*index_sets)[i][perspective].clear(); + ++num_resets[i]; + } else { + for (const auto index : removed_indices[perspective]) { + ASSERT(index < RawFeatures::kDimensions); + ASSERT((*index_sets)[i][perspective].count(index) == 1); + ASSERT(trigger_map[index] == kUnknown || trigger_map[index] == i); + (*index_sets)[i][perspective].erase(index); + ++num_updates.back(); + ++num_updates[i]; + trigger_map[index] = i; + } + } + for (const auto index : added_indices[perspective]) { + ASSERT(index < RawFeatures::kDimensions); + ASSERT((*index_sets)[i][perspective].count(index) == 0); + ASSERT(trigger_map[index] == kUnknown || trigger_map[index] == i); + (*index_sets)[i][perspective].insert(index); + ++num_updates.back(); + ++num_updates[i]; + trigger_map[index] = i; + } + } + } + }; + + std::cout << "feature set: " << RawFeatures::GetName() + << "[" << RawFeatures::kDimensions << "]" << std::endl; + std::cout << "start testing with random games"; + + for (std::uint64_t i = 0; i < num_games; ++i) { + auto index_sets = make_index_sets(pos); + for (ply = 0; ply < MAX_PLY; ++ply) { + MoveList mg(pos); // 全合法手の生成 + + // 合法な指し手がなかった == 詰み + if (mg.size() == 0) + break; + + // 生成された指し手のなかからランダムに選び、その指し手で局面を進める。 + Move m = mg.begin()[prng.rand(mg.size())]; + pos.do_move(m, state[ply]); + + ++num_moves; + update_index_sets(pos, &index_sets); + ASSERT(index_sets == make_index_sets(pos)); + } + + pos.set(StartFEN, false, &si, Threads.main()); + + // 100回に1回ごとに'.'を出力(進んでいることがわかるように) + if ((i % 100) == 0) + std::cout << "." << std::flush; + } + std::cout << "passed." << std::endl; + std::cout << num_games << " games, " << num_moves << " moves, " + << num_updates.back() << " updates, " + << (1.0 * num_updates.back() / num_moves) + << " updates per move" << std::endl; + std::size_t num_observed_indices = 0; + for (IndexType i = 0; i < kRefreshTriggers.size(); ++i) { + const auto count = std::count(trigger_map.begin(), trigger_map.end(), i); + num_observed_indices += count; + std::cout << "TriggerEvent(" << static_cast(kRefreshTriggers[i]) + << "): " << count << " features (" + << (100.0 * count / RawFeatures::kDimensions) << "%), " + << num_updates[i] << " updates (" + << (1.0 * num_updates[i] / num_moves) << " per move), " + << num_resets[i] << " resets (" + << (100.0 * num_resets[i] / num_moves) << "%)" + << std::endl; + } + std::cout << "observed " << num_observed_indices << " (" + << (100.0 * num_observed_indices / RawFeatures::kDimensions) + << "% of " << RawFeatures::kDimensions + << ") features" << std::endl; +} + +// 評価関数の構造を表す文字列を出力する +void PrintInfo(std::istream& stream) { + std::cout << "network architecture: " << GetArchitectureString() << std::endl; + + while (true) { + std::string file_name; + stream >> file_name; + if (file_name.empty()) break; + + std::uint32_t hash_value; + std::string architecture; + const bool success = [&]() { + std::ifstream file_stream(file_name, std::ios::binary); + if (!file_stream) return false; + if (!ReadHeader(file_stream, &hash_value, &architecture)) return false; + return true; + }(); + + std::cout << file_name << ": "; + if (success) { + if (hash_value == kHashValue) { + std::cout << "matches with this binary"; + if (architecture != GetArchitectureString()) { + std::cout << ", but architecture string differs: " << architecture; + } + std::cout << std::endl; + } else { + std::cout << architecture << std::endl; + } + } else { + std::cout << "failed to read header" << std::endl; + } + } +} + +} // namespace + +// NNUE評価関数に関するUSI拡張コマンド +void TestCommand(Position& pos, std::istream& stream) { + std::string sub_command; + stream >> sub_command; + + if (sub_command == "test_features") { + TestFeatures(pos); + } else if (sub_command == "info") { + PrintInfo(stream); + } else { + std::cout << "usage:" << std::endl; + std::cout << " test nnue test_features" << std::endl; + std::cout << " test nnue info [path/to/" << kFileName << "...]" << std::endl; + } +} + +} // namespace NNUE + +} // namespace Eval + +#endif // defined(ENABLE_TEST_CMD) && defined(EVAL_NNUE) diff --git a/src/eval/nnue/nnue_test_command.h b/src/eval/nnue/nnue_test_command.h new file mode 100644 index 00000000..10f57f6c --- /dev/null +++ b/src/eval/nnue/nnue_test_command.h @@ -0,0 +1,21 @@ +// NNUE評価関数に関するUSI拡張コマンドのインターフェイス + +#ifndef _NNUE_TEST_COMMAND_H_ +#define _NNUE_TEST_COMMAND_H_ + +#if defined(ENABLE_TEST_CMD) && defined(EVAL_NNUE) + +namespace Eval { + +namespace NNUE { + +// NNUE評価関数に関するUSI拡張コマンド +void TestCommand(Position& pos, std::istream& stream); + +} // namespace NNUE + +} // namespace Eval + +#endif // defined(ENABLE_TEST_CMD) && defined(EVAL_NNUE) + +#endif diff --git a/src/eval/nnue/trainer/features/factorizer.h b/src/eval/nnue/trainer/features/factorizer.h new file mode 100644 index 00000000..3bc59260 --- /dev/null +++ b/src/eval/nnue/trainer/features/factorizer.h @@ -0,0 +1,110 @@ +// NNUE評価関数の特徴量変換クラステンプレート + +#ifndef _NNUE_TRAINER_FEATURES_FACTORIZER_H_ +#define _NNUE_TRAINER_FEATURES_FACTORIZER_H_ + +#if defined(EVAL_NNUE) + +#include "../../nnue_common.h" +#include "../trainer.h" + +namespace Eval { + +namespace NNUE { + +namespace Features { + +// 入力特徴量を学習用特徴量に変換するクラステンプレート +// デフォルトでは学習用特徴量は元の入力特徴量と同じとし、必要に応じて特殊化する +template +class Factorizer { + public: + // 学習用特徴量の次元数を取得する + static constexpr IndexType GetDimensions() { + return FeatureType::kDimensions; + } + + // 学習用特徴量のインデックスと学習率のスケールを取得する + static void AppendTrainingFeatures( + IndexType base_index, std::vector* training_features) { + assert(base_index < FeatureType::kDimensions); + training_features->emplace_back(base_index); + } +}; + +// 学習用特徴量の情報 +struct FeatureProperties { + bool active; + IndexType dimensions; +}; + +// 元の入力特徴量を学習用特徴量に追加する +template +IndexType AppendBaseFeature( + FeatureProperties properties, IndexType base_index, + std::vector* training_features) { + assert(properties.dimensions == FeatureType::kDimensions); + assert(base_index < FeatureType::kDimensions); + training_features->emplace_back(base_index); + return properties.dimensions; +} + +// 学習率のスケールが0でなければ他の種類の学習用特徴量を引き継ぐ +template +IndexType InheritFeaturesIfRequired( + IndexType index_offset, FeatureProperties properties, IndexType base_index, + std::vector* training_features) { + if (!properties.active) { + return 0; + } + assert(properties.dimensions == Factorizer::GetDimensions()); + assert(base_index < FeatureType::kDimensions); + const auto start = training_features->size(); + Factorizer::AppendTrainingFeatures( + base_index, training_features); + for (auto i = start; i < training_features->size(); ++i) { + auto& feature = (*training_features)[i]; + assert(feature.GetIndex() < Factorizer::GetDimensions()); + feature.ShiftIndex(index_offset); + } + return properties.dimensions; +} + +// 学習用特徴量を追加せず、必要に応じてインデックスの差分を返す +// 対応する特徴量がない場合にInheritFeaturesIfRequired()の代わりに呼ぶ +IndexType SkipFeatures(FeatureProperties properties) { + if (!properties.active) { + return 0; + } + return properties.dimensions; +} + +// 学習用特徴量の次元数を取得する +template +constexpr IndexType GetActiveDimensions( + const FeatureProperties (&properties)[N]) { + static_assert(N > 0, ""); + IndexType dimensions = properties[0].dimensions; + for (std::size_t i = 1; i < N; ++i) { + if (properties[i].active) { + dimensions += properties[i].dimensions; + } + } + return dimensions; +} + +// 配列の要素数を取得する +template +constexpr std::size_t GetArrayLength(const T (&/*array*/)[N]) { + return N; +} + +} // namespace Features + +} // namespace NNUE + +} // namespace Eval + +#endif // defined(EVAL_NNUE) + +#endif diff --git a/src/eval/nnue/trainer/features/factorizer_feature_set.h b/src/eval/nnue/trainer/features/factorizer_feature_set.h new file mode 100644 index 00000000..111678e4 --- /dev/null +++ b/src/eval/nnue/trainer/features/factorizer_feature_set.h @@ -0,0 +1,104 @@ +// NNUE評価関数の特徴量変換クラステンプレートのFeatureSet用特殊化 + +#ifndef _NNUE_TRAINER_FEATURES_FACTORIZER_FEATURE_SET_H_ +#define _NNUE_TRAINER_FEATURES_FACTORIZER_FEATURE_SET_H_ + +#if defined(EVAL_NNUE) + +#include "../../features/feature_set.h" +#include "factorizer.h" + +namespace Eval { + +namespace NNUE { + +namespace Features { + +// 入力特徴量を学習用特徴量に変換するクラステンプレート +// FeatureSet用特殊化 +template +class Factorizer> { + private: + using Head = Factorizer>; + using Tail = Factorizer>; + + public: + // 元の入力特徴量の次元数 + static constexpr IndexType kBaseDimensions = + FeatureSet::kDimensions; + + // 学習用特徴量の次元数を取得する + static constexpr IndexType GetDimensions() { + return Head::GetDimensions() + Tail::GetDimensions(); + } + + // 学習用特徴量のインデックスと学習率のスケールを取得する + static void AppendTrainingFeatures( + IndexType base_index, std::vector* training_features, + IndexType base_dimensions = kBaseDimensions) { + assert(base_index < kBaseDimensions); + constexpr auto boundary = FeatureSet::kDimensions; + if (base_index < boundary) { + Tail::AppendTrainingFeatures( + base_index, training_features, base_dimensions); + } else { + const auto start = training_features->size(); + Head::AppendTrainingFeatures( + base_index - boundary, training_features, base_dimensions); + for (auto i = start; i < training_features->size(); ++i) { + auto& feature = (*training_features)[i]; + const auto index = feature.GetIndex(); + assert(index < Head::GetDimensions() || + (index >= base_dimensions && + index < base_dimensions + + Head::GetDimensions() - Head::kBaseDimensions)); + if (index < Head::kBaseDimensions) { + feature.ShiftIndex(Tail::kBaseDimensions); + } else { + feature.ShiftIndex(Tail::GetDimensions() - Tail::kBaseDimensions); + } + } + } + } +}; + +// 入力特徴量を学習用特徴量に変換するクラステンプレート +// FeatureSetのテンプレート引数が1つの場合の特殊化 +template +class Factorizer> { +public: + // 元の入力特徴量の次元数 + static constexpr IndexType kBaseDimensions = FeatureType::kDimensions; + + // 学習用特徴量の次元数を取得する + static constexpr IndexType GetDimensions() { + return Factorizer::GetDimensions(); + } + + // 学習用特徴量のインデックスと学習率のスケールを取得する + static void AppendTrainingFeatures( + IndexType base_index, std::vector* training_features, + IndexType base_dimensions = kBaseDimensions) { + assert(base_index < kBaseDimensions); + const auto start = training_features->size(); + Factorizer::AppendTrainingFeatures( + base_index, training_features); + for (auto i = start; i < training_features->size(); ++i) { + auto& feature = (*training_features)[i]; + assert(feature.GetIndex() < Factorizer::GetDimensions()); + if (feature.GetIndex() >= kBaseDimensions) { + feature.ShiftIndex(base_dimensions - kBaseDimensions); + } + } + } +}; + +} // namespace Features + +} // namespace NNUE + +} // namespace Eval + +#endif // defined(EVAL_NNUE) + +#endif diff --git a/src/eval/nnue/trainer/features/factorizer_half_kp.h b/src/eval/nnue/trainer/features/factorizer_half_kp.h new file mode 100644 index 00000000..28c11074 --- /dev/null +++ b/src/eval/nnue/trainer/features/factorizer_half_kp.h @@ -0,0 +1,103 @@ +// NNUE評価関数の特徴量変換クラステンプレートのHalfKP用特殊化 + +#ifndef _NNUE_TRAINER_FEATURES_FACTORIZER_HALF_KP_H_ +#define _NNUE_TRAINER_FEATURES_FACTORIZER_HALF_KP_H_ + +#if defined(EVAL_NNUE) + +#include "../../features/half_kp.h" +#include "../../features/p.h" +#include "../../features/half_relative_kp.h" +#include "factorizer.h" + +namespace Eval { + +namespace NNUE { + +namespace Features { + +// 入力特徴量を学習用特徴量に変換するクラステンプレート +// HalfKP用特殊化 +template +class Factorizer> { + private: + using FeatureType = HalfKP; + + // 特徴量のうち、同時に値が1となるインデックスの数の最大値 + static constexpr IndexType kMaxActiveDimensions = + FeatureType::kMaxActiveDimensions; + + // 学習用特徴量の種類 + enum TrainingFeatureType { + kFeaturesHalfKP, + kFeaturesHalfK, + kFeaturesP, + kFeaturesHalfRelativeKP, + kNumTrainingFeatureTypes, + }; + + // 学習用特徴量の情報 + static constexpr FeatureProperties kProperties[] = { + // kFeaturesHalfKP + {true, FeatureType::kDimensions}, + // kFeaturesHalfK + {true, SQUARE_NB}, + // kFeaturesP + {true, Factorizer

::GetDimensions()}, + // kFeaturesHalfRelativeKP + {true, Factorizer>::GetDimensions()}, + }; + static_assert(GetArrayLength(kProperties) == kNumTrainingFeatureTypes, ""); + + public: + // 学習用特徴量の次元数を取得する + static constexpr IndexType GetDimensions() { + return GetActiveDimensions(kProperties); + } + + // 学習用特徴量のインデックスと学習率のスケールを取得する + static void AppendTrainingFeatures( + IndexType base_index, std::vector* training_features) { + // kFeaturesHalfKP + IndexType index_offset = AppendBaseFeature( + kProperties[kFeaturesHalfKP], base_index, training_features); + + const auto sq_k = static_cast(base_index / fe_end); + const auto p = static_cast(base_index % fe_end); + // kFeaturesHalfK + { + const auto& properties = kProperties[kFeaturesHalfK]; + if (properties.active) { + training_features->emplace_back(index_offset + sq_k); + index_offset += properties.dimensions; + } + } + // kFeaturesP + index_offset += InheritFeaturesIfRequired

( + index_offset, kProperties[kFeaturesP], p, training_features); + // kFeaturesHalfRelativeKP + if (p >= fe_hand_end) { + index_offset += InheritFeaturesIfRequired>( + index_offset, kProperties[kFeaturesHalfRelativeKP], + HalfRelativeKP::MakeIndex(sq_k, p), + training_features); + } else { + index_offset += SkipFeatures(kProperties[kFeaturesHalfRelativeKP]); + } + + assert(index_offset == GetDimensions()); + } +}; + +template +constexpr FeatureProperties Factorizer>::kProperties[]; + +} // namespace Features + +} // namespace NNUE + +} // namespace Eval + +#endif // defined(EVAL_NNUE) + +#endif diff --git a/src/eval/nnue/trainer/trainer.h b/src/eval/nnue/trainer/trainer.h new file mode 100644 index 00000000..630f1a3d --- /dev/null +++ b/src/eval/nnue/trainer/trainer.h @@ -0,0 +1,125 @@ +// NNUE評価関数の学習用クラステンプレートの共通ヘッダ + +#ifndef _NNUE_TRAINER_H_ +#define _NNUE_TRAINER_H_ + +#if defined(EVAL_LEARN) && defined(EVAL_NNUE) + +#include "../nnue_common.h" +#include "../features/index_list.h" + +#include +#if defined(USE_BLAS) +static_assert(std::is_same::value, ""); +#include +#endif + +namespace Eval { + +namespace NNUE { + +// 評価値と勝率の関係式で用いるPonanza定数 +constexpr double kPonanzaConstant = 600.0; + +// 学習用特徴量のインデックス1つを表すクラス +class TrainingFeature { + using StorageType = std::uint32_t; + static_assert(std::is_unsigned::value, ""); + + public: + static constexpr std::uint32_t kIndexBits = 24; + static_assert(kIndexBits < std::numeric_limits::digits, ""); + static constexpr std::uint32_t kCountBits = + std::numeric_limits::digits - kIndexBits; + + explicit TrainingFeature(IndexType index) : + index_and_count_((index << kCountBits) | 1) { + assert(index < (1 << kIndexBits)); + } + TrainingFeature& operator+=(const TrainingFeature& other) { + assert(other.GetIndex() == GetIndex()); + assert(other.GetCount() + GetCount() < (1 << kCountBits)); + index_and_count_ += other.GetCount(); + return *this; + } + IndexType GetIndex() const { + return static_cast(index_and_count_ >> kCountBits); + } + void ShiftIndex(IndexType offset) { + assert(GetIndex() + offset < (1 << kIndexBits)); + index_and_count_ += offset << kCountBits; + } + IndexType GetCount() const { + return static_cast(index_and_count_ & ((1 << kCountBits) - 1)); + } + bool operator<(const TrainingFeature& other) const { + return index_and_count_ < other.index_and_count_; + } + + private: + StorageType index_and_count_; +}; + +// 学習データ1サンプルを表す構造体 +struct Example { + std::vector training_features[2]; + Learner::PackedSfenValue psv; + int sign; + double weight; +}; + +// ハイパーパラメータの設定などに使用するメッセージ +struct Message { + Message(const std::string& name, const std::string& value = "") : + name(name), value(value), num_peekers(0), num_receivers(0) {} + const std::string name; + const std::string value; + std::uint32_t num_peekers; + std::uint32_t num_receivers; +}; + +// メッセージを受理するかどうかを判定する +bool ReceiveMessage(const std::string& name, Message* message) { + const auto subscript = "[" + std::to_string(message->num_peekers) + "]"; + if (message->name.substr(0, name.size() + 1) == name + "[") { + ++message->num_peekers; + } + if (message->name == name || message->name == name + subscript) { + ++message->num_receivers; + return true; + } + return false; +} + +// 文字列を分割する +std::vector Split(const std::string& input, char delimiter) { + std::istringstream stream(input); + std::string field; + std::vector fields; + while (std::getline(stream, field, delimiter)) { + fields.push_back(field); + } + return fields; +} + +// 浮動小数点数を整数に丸める +template +IntType Round(double value) { + return static_cast(std::floor(value + 0.5)); +} + +// アライメント付きmake_shared +template +std::shared_ptr MakeAlignedSharedPtr(ArgumentTypes&&... arguments) { + const auto ptr = new(aligned_malloc(sizeof(T), alignof(T))) + T(std::forward(arguments)...); + return std::shared_ptr(ptr, AlignedDeleter()); +} + +} // namespace NNUE + +} // namespace Eval + +#endif // defined(EVAL_LEARN) && defined(EVAL_NNUE) + +#endif diff --git a/src/eval/nnue/trainer/trainer_affine_transform.h b/src/eval/nnue/trainer/trainer_affine_transform.h new file mode 100644 index 00000000..34c4816b --- /dev/null +++ b/src/eval/nnue/trainer/trainer_affine_transform.h @@ -0,0 +1,301 @@ +// NNUE評価関数の学習クラステンプレートのAffineTransform用特殊化 + +#ifndef _NNUE_TRAINER_AFFINE_TRANSFORM_H_ +#define _NNUE_TRAINER_AFFINE_TRANSFORM_H_ + +#if defined(EVAL_LEARN) && defined(EVAL_NNUE) + +#include "../../../learn/learn.h" +#include "../layers/affine_transform.h" +#include "trainer.h" + +#include + +namespace Eval { + +namespace NNUE { + +// 学習:アフィン変換層 +template +class Trainer> { + private: + // 学習対象の層の型 + using LayerType = Layers::AffineTransform; + + public: + // ファクトリ関数 + static std::shared_ptr Create( + LayerType* target_layer, FeatureTransformer* feature_transformer) { + return std::shared_ptr( + new Trainer(target_layer, feature_transformer)); + } + + // ハイパーパラメータなどのオプションを設定する + void SendMessage(Message* message) { + previous_layer_trainer_->SendMessage(message); + if (ReceiveMessage("momentum", message)) { + momentum_ = static_cast(std::stod(message->value)); + } + if (ReceiveMessage("learning_rate_scale", message)) { + learning_rate_scale_ = + static_cast(std::stod(message->value)); + } + if (ReceiveMessage("reset", message)) { + DequantizeParameters(); + } + if (ReceiveMessage("quantize_parameters", message)) { + QuantizeParameters(); + } + } + + // パラメータを乱数で初期化する + template + void Initialize(RNG& rng) { + previous_layer_trainer_->Initialize(rng); + if (kIsOutputLayer) { + // 出力層は0で初期化する + std::fill(std::begin(biases_), std::end(biases_), + static_cast(0.0)); + std::fill(std::begin(weights_), std::end(weights_), + static_cast(0.0)); + } else { + // 入力の分布が各ユニット平均0.5、等分散であることを仮定し、 + // 出力の分布が各ユニット平均0.5、入力と同じ等分散になるように初期化する + const double kSigma = 1.0 / std::sqrt(kInputDimensions); + auto distribution = std::normal_distribution(0.0, kSigma); + for (IndexType i = 0; i < kOutputDimensions; ++i) { + double sum = 0.0; + for (IndexType j = 0; j < kInputDimensions; ++j) { + const auto weight = static_cast(distribution(rng)); + weights_[kInputDimensions * i + j] = weight; + sum += weight; + } + biases_[i] = static_cast(0.5 - 0.5 * sum); + } + } + QuantizeParameters(); + } + + // 順伝播 + const LearnFloatType* Propagate(const std::vector& batch) { + if (output_.size() < kOutputDimensions * batch.size()) { + output_.resize(kOutputDimensions * batch.size()); + gradients_.resize(kInputDimensions * batch.size()); + } + batch_size_ = static_cast(batch.size()); + batch_input_ = previous_layer_trainer_->Propagate(batch); +#if defined(USE_BLAS) + for (IndexType b = 0; b < batch_size_; ++b) { + const IndexType batch_offset = kOutputDimensions * b; + cblas_scopy(kOutputDimensions, biases_, 1, &output_[batch_offset], 1); + } + cblas_sgemm(CblasColMajor, CblasTrans, CblasNoTrans, + kOutputDimensions, batch_size_, kInputDimensions, 1.0, + weights_, kInputDimensions, + batch_input_, kInputDimensions, + 1.0, &output_[0], kOutputDimensions); +#else + for (IndexType b = 0; b < batch_size_; ++b) { + const IndexType input_batch_offset = kInputDimensions * b; + const IndexType output_batch_offset = kOutputDimensions * b; + for (IndexType i = 0; i < kOutputDimensions; ++i) { + double sum = biases_[i]; + for (IndexType j = 0; j < kInputDimensions; ++j) { + const IndexType index = kInputDimensions * i + j; + sum += weights_[index] * batch_input_[input_batch_offset + j]; + } + output_[output_batch_offset + i] = static_cast(sum); + } + } +#endif + return output_.data(); + } + + // 逆伝播 + void Backpropagate(const LearnFloatType* gradients, + LearnFloatType learning_rate) { + const LearnFloatType local_learning_rate = + learning_rate * learning_rate_scale_; +#if defined(USE_BLAS) + // backpropagate + cblas_sgemm(CblasColMajor, CblasNoTrans, CblasNoTrans, + kInputDimensions, batch_size_, kOutputDimensions, 1.0, + weights_, kInputDimensions, + gradients, kOutputDimensions, + 0.0, &gradients_[0], kInputDimensions); + // update + cblas_sscal(kOutputDimensions, momentum_, biases_diff_, 1); + for (IndexType b = 0; b < batch_size_; ++b) { + const IndexType batch_offset = kOutputDimensions * b; + cblas_saxpy(kOutputDimensions, 1.0, + &gradients[batch_offset], 1, biases_diff_, 1); + } + cblas_saxpy(kOutputDimensions, -local_learning_rate, + biases_diff_, 1, biases_, 1); + cblas_sgemm(CblasRowMajor, CblasTrans, CblasNoTrans, + kOutputDimensions, kInputDimensions, batch_size_, 1.0, + gradients, kOutputDimensions, + batch_input_, kInputDimensions, + momentum_, weights_diff_, kInputDimensions); + cblas_saxpy(kOutputDimensions * kInputDimensions, -local_learning_rate, + weights_diff_, 1, weights_, 1); +#else + // backpropagate + for (IndexType b = 0; b < batch_size_; ++b) { + const IndexType input_batch_offset = kInputDimensions * b; + const IndexType output_batch_offset = kOutputDimensions * b; + for (IndexType j = 0; j < kInputDimensions; ++j) { + double sum = 0.0; + for (IndexType i = 0; i < kOutputDimensions; ++i) { + const IndexType index = kInputDimensions * i + j; + sum += weights_[index] * gradients[output_batch_offset + i]; + } + gradients_[input_batch_offset + j] = static_cast(sum); + } + } + // update + for (IndexType i = 0; i < kOutputDimensions; ++i) { + biases_diff_[i] *= momentum_; + } + for (IndexType i = 0; i < kOutputDimensions * kInputDimensions; ++i) { + weights_diff_[i] *= momentum_; + } + for (IndexType b = 0; b < batch_size_; ++b) { + const IndexType input_batch_offset = kInputDimensions * b; + const IndexType output_batch_offset = kOutputDimensions * b; + for (IndexType i = 0; i < kOutputDimensions; ++i) { + biases_diff_[i] += gradients[output_batch_offset + i]; + } + for (IndexType i = 0; i < kOutputDimensions; ++i) { + for (IndexType j = 0; j < kInputDimensions; ++j) { + const IndexType index = kInputDimensions * i + j; + weights_diff_[index] += gradients[output_batch_offset + i] * + batch_input_[input_batch_offset + j]; + } + } + } + for (IndexType i = 0; i < kOutputDimensions; ++i) { + biases_[i] -= local_learning_rate * biases_diff_[i]; + } + for (IndexType i = 0; i < kOutputDimensions * kInputDimensions; ++i) { + weights_[i] -= local_learning_rate * weights_diff_[i]; + } +#endif + previous_layer_trainer_->Backpropagate(gradients_.data(), learning_rate); + } + + private: + // コンストラクタ + Trainer(LayerType* target_layer, FeatureTransformer* feature_transformer) : + batch_size_(0), + batch_input_(nullptr), + previous_layer_trainer_(Trainer::Create( + &target_layer->previous_layer_, feature_transformer)), + target_layer_(target_layer), + biases_(), + weights_(), + biases_diff_(), + weights_diff_(), + momentum_(0.0), + learning_rate_scale_(1.0) { + DequantizeParameters(); + } + + // 重みの飽和とパラメータの整数化 + void QuantizeParameters() { + for (IndexType i = 0; i < kOutputDimensions * kInputDimensions; ++i) { + weights_[i] = std::max(-kMaxWeightMagnitude, + std::min(+kMaxWeightMagnitude, weights_[i])); + } + for (IndexType i = 0; i < kOutputDimensions; ++i) { + target_layer_->biases_[i] = + Round(biases_[i] * kBiasScale); + } + for (IndexType i = 0; i < kOutputDimensions; ++i) { + const auto offset = kInputDimensions * i; + const auto padded_offset = LayerType::kPaddedInputDimensions * i; + for (IndexType j = 0; j < kInputDimensions; ++j) { + target_layer_->weights_[padded_offset + j] = + Round( + weights_[offset + j] * kWeightScale); + } + } + } + + // 整数化されたパラメータの読み込み + void DequantizeParameters() { + for (IndexType i = 0; i < kOutputDimensions; ++i) { + biases_[i] = static_cast( + target_layer_->biases_[i] / kBiasScale); + } + for (IndexType i = 0; i < kOutputDimensions; ++i) { + const auto offset = kInputDimensions * i; + const auto padded_offset = LayerType::kPaddedInputDimensions * i; + for (IndexType j = 0; j < kInputDimensions; ++j) { + weights_[offset + j] = static_cast( + target_layer_->weights_[padded_offset + j] / kWeightScale); + } + } + std::fill(std::begin(biases_diff_), std::end(biases_diff_), + static_cast(0.0)); + std::fill(std::begin(weights_diff_), std::end(weights_diff_), + static_cast(0.0)); + } + + // 入出力の次元数 + static constexpr IndexType kInputDimensions = LayerType::kInputDimensions; + static constexpr IndexType kOutputDimensions = LayerType::kOutputDimensions; + + // 出力の次元数が1なら出力層 + static constexpr bool kIsOutputLayer = kOutputDimensions == 1; + + // パラメータの整数化で用いる係数 + static constexpr LearnFloatType kActivationScale = + std::numeric_limits::max(); + static constexpr LearnFloatType kBiasScale = kIsOutputLayer ? + (kPonanzaConstant * FV_SCALE) : + ((1 << kWeightScaleBits) * kActivationScale); + static constexpr LearnFloatType kWeightScale = kBiasScale / kActivationScale; + + // パラメータの整数化でオーバーフローさせないために用いる重みの絶対値の上限 + static constexpr LearnFloatType kMaxWeightMagnitude = + std::numeric_limits::max() / kWeightScale; + + // ミニバッチのサンプル数 + IndexType batch_size_; + + // ミニバッチの入力 + const LearnFloatType* batch_input_; + + // 直前の層のTrainer + const std::shared_ptr> previous_layer_trainer_; + + // 学習対象の層 + LayerType* const target_layer_; + + // パラメータ + LearnFloatType biases_[kOutputDimensions]; + LearnFloatType weights_[kOutputDimensions * kInputDimensions]; + + // パラメータの更新で用いるバッファ + LearnFloatType biases_diff_[kOutputDimensions]; + LearnFloatType weights_diff_[kOutputDimensions * kInputDimensions]; + + // 順伝播用バッファ + std::vector output_; + + // 逆伝播用バッファ + std::vector gradients_; + + // ハイパーパラメータ + LearnFloatType momentum_; + LearnFloatType learning_rate_scale_; +}; + +} // namespace NNUE + +} // namespace Eval + +#endif // defined(EVAL_LEARN) && defined(EVAL_NNUE) + +#endif diff --git a/src/eval/nnue/trainer/trainer_clipped_relu.h b/src/eval/nnue/trainer/trainer_clipped_relu.h new file mode 100644 index 00000000..bd894769 --- /dev/null +++ b/src/eval/nnue/trainer/trainer_clipped_relu.h @@ -0,0 +1,142 @@ +// NNUE評価関数の学習クラステンプレートのClippedReLU用特殊化 + +#ifndef _NNUE_TRAINER_CLIPPED_RELU_H_ +#define _NNUE_TRAINER_CLIPPED_RELU_H_ + +#if defined(EVAL_LEARN) && defined(EVAL_NNUE) + +#include "../../../learn/learn.h" +#include "../layers/clipped_relu.h" +#include "trainer.h" + +namespace Eval { + +namespace NNUE { + +// 学習:アフィン変換層 +template +class Trainer> { + private: + // 学習対象の層の型 + using LayerType = Layers::ClippedReLU; + + public: + // ファクトリ関数 + static std::shared_ptr Create( + LayerType* target_layer, FeatureTransformer* feature_transformer) { + return std::shared_ptr( + new Trainer(target_layer, feature_transformer)); + } + + // ハイパーパラメータなどのオプションを設定する + void SendMessage(Message* message) { + previous_layer_trainer_->SendMessage(message); + if (ReceiveMessage("check_health", message)) { + CheckHealth(); + } + } + + // パラメータを乱数で初期化する + template + void Initialize(RNG& rng) { + previous_layer_trainer_->Initialize(rng); + } + + // 順伝播 + const LearnFloatType* Propagate(const std::vector& batch) { + if (output_.size() < kOutputDimensions * batch.size()) { + output_.resize(kOutputDimensions * batch.size()); + gradients_.resize(kInputDimensions * batch.size()); + } + const auto input = previous_layer_trainer_->Propagate(batch); + batch_size_ = static_cast(batch.size()); + for (IndexType b = 0; b < batch_size_; ++b) { + const IndexType batch_offset = kOutputDimensions * b; + for (IndexType i = 0; i < kOutputDimensions; ++i) { + const IndexType index = batch_offset + i; + output_[index] = std::max(+kZero, std::min(+kOne, input[index])); + min_activations_[i] = std::min(min_activations_[i], output_[index]); + max_activations_[i] = std::max(max_activations_[i], output_[index]); + } + } + return output_.data(); + } + + // 逆伝播 + void Backpropagate(const LearnFloatType* gradients, + LearnFloatType learning_rate) { + for (IndexType b = 0; b < batch_size_; ++b) { + const IndexType batch_offset = kOutputDimensions * b; + for (IndexType i = 0; i < kOutputDimensions; ++i) { + const IndexType index = batch_offset + i; + gradients_[index] = gradients[index] * + (output_[index] > kZero) * (output_[index] < kOne); + } + } + previous_layer_trainer_->Backpropagate(gradients_.data(), learning_rate); + } + + private: + // コンストラクタ + Trainer(LayerType* target_layer, FeatureTransformer* feature_transformer) : + batch_size_(0), + previous_layer_trainer_(Trainer::Create( + &target_layer->previous_layer_, feature_transformer)), + target_layer_(target_layer) { + std::fill(std::begin(min_activations_), std::end(min_activations_), + std::numeric_limits::max()); + std::fill(std::begin(max_activations_), std::end(max_activations_), + std::numeric_limits::lowest()); + } + + // 学習に問題が生じていないかチェックする + void CheckHealth() { + const auto largest_min_activation = *std::max_element( + std::begin(min_activations_), std::end(min_activations_)); + const auto smallest_max_activation = *std::min_element( + std::begin(max_activations_), std::end(max_activations_)); + std::cout << "INFO: largest min activation = " << largest_min_activation + << ", smallest max activation = " << smallest_max_activation + << std::endl; + + std::fill(std::begin(min_activations_), std::end(min_activations_), + std::numeric_limits::max()); + std::fill(std::begin(max_activations_), std::end(max_activations_), + std::numeric_limits::lowest()); + } + + // 入出力の次元数 + static constexpr IndexType kInputDimensions = LayerType::kOutputDimensions; + static constexpr IndexType kOutputDimensions = LayerType::kOutputDimensions; + + // LearnFloatTypeの定数 + static constexpr LearnFloatType kZero = static_cast(0.0); + static constexpr LearnFloatType kOne = static_cast(1.0); + + // ミニバッチのサンプル数 + IndexType batch_size_; + + // 直前の層のTrainer + const std::shared_ptr> previous_layer_trainer_; + + // 学習対象の層 + LayerType* const target_layer_; + + // 順伝播用バッファ + std::vector output_; + + // 逆伝播用バッファ + std::vector gradients_; + + // ヘルスチェック用統計値 + LearnFloatType min_activations_[kOutputDimensions]; + LearnFloatType max_activations_[kOutputDimensions]; +}; + +} // namespace NNUE + +} // namespace Eval + +#endif // defined(EVAL_LEARN) && defined(EVAL_NNUE) + +#endif diff --git a/src/eval/nnue/trainer/trainer_feature_transformer.h b/src/eval/nnue/trainer/trainer_feature_transformer.h new file mode 100644 index 00000000..742da440 --- /dev/null +++ b/src/eval/nnue/trainer/trainer_feature_transformer.h @@ -0,0 +1,377 @@ +// NNUE評価関数の学習クラステンプレートのFeatureTransformer用特殊化 + +#ifndef _NNUE_TRAINER_FEATURE_TRANSFORMER_H_ +#define _NNUE_TRAINER_FEATURE_TRANSFORMER_H_ + +#if defined(EVAL_LEARN) && defined(EVAL_NNUE) + +#include "../../../learn/learn.h" +#include "../nnue_feature_transformer.h" +#include "trainer.h" +#include "features/factorizer_feature_set.h" + +#include +#include +#include +#include +#include + +#if defined(_OPENMP) +#include +#endif + +namespace Eval { + +namespace NNUE { + +// 学習:入力特徴量変換器 +template <> +class Trainer { + private: + // 学習対象の層の型 + using LayerType = FeatureTransformer; + + public: + template + friend struct AlignedDeleter; + template + friend std::shared_ptr MakeAlignedSharedPtr(ArgumentTypes&&... arguments); + + // ファクトリ関数 + static std::shared_ptr Create(LayerType* target_layer) { + return MakeAlignedSharedPtr(target_layer); + } + + // ハイパーパラメータなどのオプションを設定する + void SendMessage(Message* message) { + if (ReceiveMessage("momentum", message)) { + momentum_ = static_cast(std::stod(message->value)); + } + if (ReceiveMessage("learning_rate_scale", message)) { + learning_rate_scale_ = + static_cast(std::stod(message->value)); + } + if (ReceiveMessage("reset", message)) { + DequantizeParameters(); + } + if (ReceiveMessage("quantize_parameters", message)) { + QuantizeParameters(); + } + if (ReceiveMessage("clear_unobserved_feature_weights", message)) { + ClearUnobservedFeatureWeights(); + } + if (ReceiveMessage("check_health", message)) { + CheckHealth(); + } + } + + // パラメータを乱数で初期化する + template + void Initialize(RNG& rng) { + std::fill(std::begin(weights_), std::end(weights_), +kZero); + const double kSigma = 0.1 / std::sqrt(RawFeatures::kMaxActiveDimensions); + auto distribution = std::normal_distribution(0.0, kSigma); + for (IndexType i = 0; i < kHalfDimensions * RawFeatures::kDimensions; ++i) { + const auto weight = static_cast(distribution(rng)); + weights_[i] = weight; + } + for (IndexType i = 0; i < kHalfDimensions; ++i) { + biases_[i] = static_cast(0.5); + } + QuantizeParameters(); + } + + // 順伝播 + const LearnFloatType* Propagate(const std::vector& batch) { + if (output_.size() < kOutputDimensions * batch.size()) { + output_.resize(kOutputDimensions * batch.size()); + gradients_.resize(kOutputDimensions * batch.size()); + } + batch_ = &batch; + // affine transform +#pragma omp parallel for + for (IndexType b = 0; b < batch.size(); ++b) { + const IndexType batch_offset = kOutputDimensions * b; + for (IndexType c = 0; c < 2; ++c) { + const IndexType output_offset = batch_offset + kHalfDimensions * c; +#if defined(USE_BLAS) + cblas_scopy(kHalfDimensions, biases_, 1, &output_[output_offset], 1); + for (const auto& feature : batch[b].training_features[c]) { + const IndexType weights_offset = kHalfDimensions * feature.GetIndex(); + cblas_saxpy(kHalfDimensions, (float)feature.GetCount(), + &weights_[weights_offset], 1, &output_[output_offset], 1); + } +#else + for (IndexType i = 0; i < kHalfDimensions; ++i) { + output_[output_offset + i] = biases_[i]; + } + for (const auto& feature : batch[b].training_features[c]) { + const IndexType weights_offset = kHalfDimensions * feature.GetIndex(); + for (IndexType i = 0; i < kHalfDimensions; ++i) { + output_[output_offset + i] += + feature.GetCount() * weights_[weights_offset + i]; + } + } +#endif + } + } + // clipped ReLU + for (IndexType b = 0; b < batch.size(); ++b) { + const IndexType batch_offset = kOutputDimensions * b; + for (IndexType i = 0; i < kOutputDimensions; ++i) { + const IndexType index = batch_offset + i; + min_pre_activation_ = std::min(min_pre_activation_, output_[index]); + max_pre_activation_ = std::max(max_pre_activation_, output_[index]); + output_[index] = std::max(+kZero, std::min(+kOne, output_[index])); + const IndexType t = i % kHalfDimensions; + min_activations_[t] = std::min(min_activations_[t], output_[index]); + max_activations_[t] = std::max(max_activations_[t], output_[index]); + } + } + return output_.data(); + } + + // 逆伝播 + void Backpropagate(const LearnFloatType* gradients, + LearnFloatType learning_rate) { + const LearnFloatType local_learning_rate = + learning_rate * learning_rate_scale_; + for (IndexType b = 0; b < batch_->size(); ++b) { + const IndexType batch_offset = kOutputDimensions * b; + for (IndexType i = 0; i < kOutputDimensions; ++i) { + const IndexType index = batch_offset + i; + gradients_[index] = gradients[index] * + ((output_[index] > kZero) * (output_[index] < kOne)); + } + } + // 重み行列は入力に出現した特徴量に対応する列のみを更新するため、 + // momentumを使用せず、学習率を補正してスケールを合わせる + const LearnFloatType effective_learning_rate = + static_cast(local_learning_rate / (1.0 - momentum_)); +#if defined(USE_BLAS) + cblas_sscal(kHalfDimensions, momentum_, biases_diff_, 1); + for (IndexType b = 0; b < batch_->size(); ++b) { + const IndexType batch_offset = kOutputDimensions * b; + for (IndexType c = 0; c < 2; ++c) { + const IndexType output_offset = batch_offset + kHalfDimensions * c; + cblas_saxpy(kHalfDimensions, 1.0, + &gradients_[output_offset], 1, biases_diff_, 1); + } + } + cblas_saxpy(kHalfDimensions, -local_learning_rate, + biases_diff_, 1, biases_, 1); +#pragma omp parallel + { +#if defined(_OPENMP) + const IndexType num_threads = omp_get_num_threads(); + const IndexType thread_index = omp_get_thread_num(); +#endif + for (IndexType b = 0; b < batch_->size(); ++b) { + const IndexType batch_offset = kOutputDimensions * b; + for (IndexType c = 0; c < 2; ++c) { + const IndexType output_offset = batch_offset + kHalfDimensions * c; + for (const auto& feature : (*batch_)[b].training_features[c]) { +#if defined(_OPENMP) + if (feature.GetIndex() % num_threads != thread_index) continue; +#endif + const IndexType weights_offset = + kHalfDimensions * feature.GetIndex(); + const auto scale = static_cast( + effective_learning_rate / feature.GetCount()); + cblas_saxpy(kHalfDimensions, -scale, + &gradients_[output_offset], 1, + &weights_[weights_offset], 1); + } + } + } + } +#else + for (IndexType i = 0; i < kHalfDimensions; ++i) { + biases_diff_[i] *= momentum_; + } + for (IndexType b = 0; b < batch_->size(); ++b) { + const IndexType batch_offset = kOutputDimensions * b; + for (IndexType c = 0; c < 2; ++c) { + const IndexType output_offset = batch_offset + kHalfDimensions * c; + for (IndexType i = 0; i < kHalfDimensions; ++i) { + biases_diff_[i] += gradients_[output_offset + i]; + } + } + } + for (IndexType i = 0; i < kHalfDimensions; ++i) { + biases_[i] -= local_learning_rate * biases_diff_[i]; + } + for (IndexType b = 0; b < batch_->size(); ++b) { + const IndexType batch_offset = kOutputDimensions * b; + for (IndexType c = 0; c < 2; ++c) { + const IndexType output_offset = batch_offset + kHalfDimensions * c; + for (const auto& feature : (*batch_)[b].training_features[c]) { + const IndexType weights_offset = kHalfDimensions * feature.GetIndex(); + const auto scale = static_cast( + effective_learning_rate / feature.GetCount()); + for (IndexType i = 0; i < kHalfDimensions; ++i) { + weights_[weights_offset + i] -= + scale * gradients_[output_offset + i]; + } + } + } + } +#endif + for (IndexType b = 0; b < batch_->size(); ++b) { + for (IndexType c = 0; c < 2; ++c) { + for (const auto& feature : (*batch_)[b].training_features[c]) { + observed_features.set(feature.GetIndex()); + } + } + } + } + + private: + // コンストラクタ + Trainer(LayerType* target_layer) : + batch_(nullptr), + target_layer_(target_layer), + biases_(), + weights_(), + biases_diff_(), + momentum_(0.0), + learning_rate_scale_(1.0) { + min_pre_activation_ = std::numeric_limits::max(); + max_pre_activation_ = std::numeric_limits::lowest(); + std::fill(std::begin(min_activations_), std::end(min_activations_), + std::numeric_limits::max()); + std::fill(std::begin(max_activations_), std::end(max_activations_), + std::numeric_limits::lowest()); + DequantizeParameters(); + } + + // 重みの飽和とパラメータの整数化 + void QuantizeParameters() { + for (IndexType i = 0; i < kHalfDimensions; ++i) { + target_layer_->biases_[i] = + Round(biases_[i] * kBiasScale); + } + std::vector training_features; +#pragma omp parallel for private(training_features) + for (IndexType j = 0; j < RawFeatures::kDimensions; ++j) { + training_features.clear(); + Features::Factorizer::AppendTrainingFeatures( + j, &training_features); + for (IndexType i = 0; i < kHalfDimensions; ++i) { + double sum = 0.0; + for (const auto& feature : training_features) { + sum += weights_[kHalfDimensions * feature.GetIndex() + i]; + } + target_layer_->weights_[kHalfDimensions * j + i] = + Round(sum * kWeightScale); + } + } + } + + // 整数化されたパラメータの読み込み + void DequantizeParameters() { + for (IndexType i = 0; i < kHalfDimensions; ++i) { + biases_[i] = static_cast( + target_layer_->biases_[i] / kBiasScale); + } + std::fill(std::begin(weights_), std::end(weights_), +kZero); + for (IndexType i = 0; i < kHalfDimensions * RawFeatures::kDimensions; ++i) { + weights_[i] = static_cast( + target_layer_->weights_[i] / kWeightScale); + } + std::fill(std::begin(biases_diff_), std::end(biases_diff_), +kZero); + } + + // 学習データに出現していない特徴量に対応する重みを0にする + void ClearUnobservedFeatureWeights() { + for (IndexType i = 0; i < kInputDimensions; ++i) { + if (!observed_features.test(i)) { + std::fill(std::begin(weights_) + kHalfDimensions * i, + std::begin(weights_) + kHalfDimensions * (i + 1), +kZero); + } + } + QuantizeParameters(); + } + + // 学習に問題が生じていないかチェックする + void CheckHealth() { + std::cout << "INFO: observed " << observed_features.count() + << " (out of " << kInputDimensions << ") features" << std::endl; + + constexpr LearnFloatType kPreActivationLimit = + std::numeric_limits::max() / + kWeightScale; + std::cout << "INFO: (min, max) of pre-activations = " + << min_pre_activation_ << ", " + << max_pre_activation_ << " (limit = " + << kPreActivationLimit << ")" << std::endl; + + const auto largest_min_activation = *std::max_element( + std::begin(min_activations_), std::end(min_activations_)); + const auto smallest_max_activation = *std::min_element( + std::begin(max_activations_), std::end(max_activations_)); + std::cout << "INFO: largest min activation = " << largest_min_activation + << ", smallest max activation = " << smallest_max_activation + << std::endl; + + std::fill(std::begin(min_activations_), std::end(min_activations_), + std::numeric_limits::max()); + std::fill(std::begin(max_activations_), std::end(max_activations_), + std::numeric_limits::lowest()); + } + + // 入出力の次元数 + static constexpr IndexType kInputDimensions = + Features::Factorizer::GetDimensions(); + static constexpr IndexType kOutputDimensions = LayerType::kOutputDimensions; + static constexpr IndexType kHalfDimensions = LayerType::kHalfDimensions; + + // パラメータの整数化で用いる係数 + static constexpr LearnFloatType kActivationScale = + std::numeric_limits::max(); + static constexpr LearnFloatType kBiasScale = kActivationScale; + static constexpr LearnFloatType kWeightScale = kActivationScale; + + // LearnFloatTypeの定数 + static constexpr LearnFloatType kZero = static_cast(0.0); + static constexpr LearnFloatType kOne = static_cast(1.0); + + // ミニバッチ + const std::vector* batch_; + + // 学習対象の層 + LayerType* const target_layer_; + + // パラメータ + alignas(kCacheLineSize) LearnFloatType biases_[kHalfDimensions]; + alignas(kCacheLineSize) + LearnFloatType weights_[kHalfDimensions * kInputDimensions]; + + // パラメータの更新で用いるバッファ + LearnFloatType biases_diff_[kHalfDimensions]; + std::vector gradients_; + + // 順伝播用バッファ + std::vector output_; + + // 学習データに出現した特徴量 + std::bitset observed_features; + + // ハイパーパラメータ + LearnFloatType momentum_; + LearnFloatType learning_rate_scale_; + + // ヘルスチェック用統計値 + LearnFloatType min_pre_activation_; + LearnFloatType max_pre_activation_; + LearnFloatType min_activations_[kHalfDimensions]; + LearnFloatType max_activations_[kHalfDimensions]; +}; + +} // namespace NNUE + +} // namespace Eval + +#endif // defined(EVAL_LEARN) && defined(EVAL_NNUE) + +#endif diff --git a/src/eval/nnue/trainer/trainer_input_slice.h b/src/eval/nnue/trainer/trainer_input_slice.h new file mode 100644 index 00000000..0660e987 --- /dev/null +++ b/src/eval/nnue/trainer/trainer_input_slice.h @@ -0,0 +1,251 @@ +// NNUE評価関数の学習クラステンプレートのInputSlice用特殊化 + +#ifndef _NNUE_TRAINER_INPUT_SLICE_H_ +#define _NNUE_TRAINER_INPUT_SLICE_H_ + +#if defined(EVAL_LEARN) && defined(EVAL_NNUE) + +#include "../../../learn/learn.h" +#include "../layers/input_slice.h" +#include "trainer.h" + +namespace Eval { + +namespace NNUE { + +// 学習:入力層 +class SharedInputTrainer { + public: + // ファクトリ関数 + static std::shared_ptr Create( + FeatureTransformer* feature_transformer) { + static std::shared_ptr instance; + if (!instance) { + instance.reset(new SharedInputTrainer(feature_transformer)); + } + ++instance->num_referrers_; + return instance; + } + + // ハイパーパラメータなどのオプションを設定する + void SendMessage(Message* message) { + if (num_calls_ == 0) { + current_operation_ = Operation::kSendMessage; + feature_transformer_trainer_->SendMessage(message); + } + assert(current_operation_ == Operation::kSendMessage); + if (++num_calls_ == num_referrers_) { + num_calls_ = 0; + current_operation_ = Operation::kNone; + } + } + + // パラメータを乱数で初期化する + template + void Initialize(RNG& rng) { + if (num_calls_ == 0) { + current_operation_ = Operation::kInitialize; + feature_transformer_trainer_->Initialize(rng); + } + assert(current_operation_ == Operation::kInitialize); + if (++num_calls_ == num_referrers_) { + num_calls_ = 0; + current_operation_ = Operation::kNone; + } + } + + // 順伝播 + const LearnFloatType* Propagate(const std::vector& batch) { + if (gradients_.size() < kInputDimensions * batch.size()) { + gradients_.resize(kInputDimensions * batch.size()); + } + batch_size_ = static_cast(batch.size()); + if (num_calls_ == 0) { + current_operation_ = Operation::kPropagate; + output_ = feature_transformer_trainer_->Propagate(batch); + } + assert(current_operation_ == Operation::kPropagate); + if (++num_calls_ == num_referrers_) { + num_calls_ = 0; + current_operation_ = Operation::kNone; + } + return output_; + } + + // 逆伝播 + void Backpropagate(const LearnFloatType* gradients, + LearnFloatType learning_rate) { + if (num_referrers_ == 1) { + feature_transformer_trainer_->Backpropagate(gradients, learning_rate); + return; + } + if (num_calls_ == 0) { + current_operation_ = Operation::kBackPropagate; + for (IndexType b = 0; b < batch_size_; ++b) { + const IndexType batch_offset = kInputDimensions * b; + for (IndexType i = 0; i < kInputDimensions; ++i) { + gradients_[batch_offset + i] = static_cast(0.0); + } + } + } + assert(current_operation_ == Operation::kBackPropagate); + for (IndexType b = 0; b < batch_size_; ++b) { + const IndexType batch_offset = kInputDimensions * b; + for (IndexType i = 0; i < kInputDimensions; ++i) { + gradients_[batch_offset + i] += gradients[batch_offset + i]; + } + } + if (++num_calls_ == num_referrers_) { + feature_transformer_trainer_->Backpropagate( + gradients_.data(), learning_rate); + num_calls_ = 0; + current_operation_ = Operation::kNone; + } + } + + private: + // コンストラクタ + SharedInputTrainer(FeatureTransformer* feature_transformer) : + batch_size_(0), + num_referrers_(0), + num_calls_(0), + current_operation_(Operation::kNone), + feature_transformer_trainer_(Trainer::Create( + feature_transformer)), + output_(nullptr) { + } + + // 入出力の次元数 + static constexpr IndexType kInputDimensions = + FeatureTransformer::kOutputDimensions; + + // 処理の種類 + enum class Operation { + kNone, + kSendMessage, + kInitialize, + kPropagate, + kBackPropagate, + }; + + // ミニバッチのサンプル数 + IndexType batch_size_; + + // この層を入力として共有する層の数 + std::uint32_t num_referrers_; + + // 現在の処理が呼び出された回数 + std::uint32_t num_calls_; + + // 現在の処理の種類 + Operation current_operation_; + + // 入力特徴量変換器のTrainer + const std::shared_ptr> + feature_transformer_trainer_; + + // 順伝播用に共有する出力のポインタ + const LearnFloatType* output_; + + // 逆伝播用バッファ + std::vector gradients_; +}; + +// 学習:入力層 +template +class Trainer> { + private: + // 学習対象の層の型 + using LayerType = Layers::InputSlice; + + public: + // ファクトリ関数 + static std::shared_ptr Create( + LayerType* /*target_layer*/, FeatureTransformer* feature_transformer) { + return std::shared_ptr(new Trainer(feature_transformer)); + } + + // ハイパーパラメータなどのオプションを設定する + void SendMessage(Message* message) { + shared_input_trainer_->SendMessage(message); + } + + // パラメータを乱数で初期化する + template + void Initialize(RNG& rng) { + shared_input_trainer_->Initialize(rng); + } + + // 順伝播 + const LearnFloatType* Propagate(const std::vector& batch) { + if (output_.size() < kOutputDimensions * batch.size()) { + output_.resize(kOutputDimensions * batch.size()); + gradients_.resize(kInputDimensions * batch.size()); + } + batch_size_ = static_cast(batch.size()); + const auto input = shared_input_trainer_->Propagate(batch); + for (IndexType b = 0; b < batch_size_; ++b) { + const IndexType input_offset = kInputDimensions * b; + const IndexType output_offset = kOutputDimensions * b; +#if defined(USE_BLAS) + cblas_scopy(kOutputDimensions, &input[input_offset + Offset], 1, + &output_[output_offset], 1); +#else + for (IndexType i = 0; i < kOutputDimensions; ++i) { + output_[output_offset + i] = input[input_offset + Offset + i]; + } +#endif + } + return output_.data(); + } + + // 逆伝播 + void Backpropagate(const LearnFloatType* gradients, + LearnFloatType learning_rate) { + for (IndexType b = 0; b < batch_size_; ++b) { + const IndexType input_offset = kInputDimensions * b; + const IndexType output_offset = kOutputDimensions * b; + for (IndexType i = 0; i < kInputDimensions; ++i) { + if (i < Offset || i >= Offset + kOutputDimensions) { + gradients_[input_offset + i] = static_cast(0.0); + } else { + gradients_[input_offset + i] = gradients[output_offset + i - Offset]; + } + } + } + shared_input_trainer_->Backpropagate(gradients_.data(), learning_rate); + } + + private: + // コンストラクタ + Trainer(FeatureTransformer* feature_transformer) : + batch_size_(0), + shared_input_trainer_(SharedInputTrainer::Create(feature_transformer)) { + } + + // 入出力の次元数 + static constexpr IndexType kInputDimensions = + FeatureTransformer::kOutputDimensions; + static constexpr IndexType kOutputDimensions = OutputDimensions; + static_assert(Offset + kOutputDimensions <= kInputDimensions, ""); + + // ミニバッチのサンプル数 + IndexType batch_size_; + + // 共有入力層のTrainer + const std::shared_ptr shared_input_trainer_; + + // 順伝播用バッファ + std::vector output_; + + // 逆伝播用バッファ + std::vector gradients_; +}; + +} // namespace NNUE + +} // namespace Eval + +#endif // defined(EVAL_LEARN) && defined(EVAL_NNUE) + +#endif diff --git a/src/eval/nnue/trainer/trainer_sum.h b/src/eval/nnue/trainer/trainer_sum.h new file mode 100644 index 00000000..76f6073f --- /dev/null +++ b/src/eval/nnue/trainer/trainer_sum.h @@ -0,0 +1,190 @@ +// NNUE評価関数の学習クラステンプレートのSum用特殊化 + +#ifndef _NNUE_TRAINER_SUM_H_ +#define _NNUE_TRAINER_SUM_H_ + +#if defined(EVAL_LEARN) && defined(EVAL_NNUE) + +#include "../../../learn/learn.h" +#include "../layers/sum.h" +#include "trainer.h" + +namespace Eval { + +namespace NNUE { + +// 学習:複数の層の出力の和を取る層 +template +class Trainer> : + Trainer> { + private: + // 学習対象の層の型 + using LayerType = Layers::Sum; + using Tail = Trainer>; + + public: + // ファクトリ関数 + static std::shared_ptr Create( + LayerType* target_layer, FeatureTransformer* feature_transformer) { + return std::shared_ptr( + new Trainer(target_layer, feature_transformer)); + } + + // ハイパーパラメータなどのオプションを設定する + void SendMessage(Message* message) { + // 他のメンバ関数の結果は処理の順番に依存しないため、 + // 実装をシンプルにすることを目的としてTailを先に処理するが、 + // SendMessageは添字の対応を分かりやすくするためにHeadを先に処理する + previous_layer_trainer_->SendMessage(message); + Tail::SendMessage(message); + } + + // パラメータを乱数で初期化する + template + void Initialize(RNG& rng) { + Tail::Initialize(rng); + previous_layer_trainer_->Initialize(rng); + } + + // 順伝播 + /*const*/ LearnFloatType* Propagate(const std::vector& batch) { + batch_size_ = static_cast(batch.size()); + auto output = Tail::Propagate(batch); + const auto head_output = previous_layer_trainer_->Propagate(batch); +#if defined(USE_BLAS) + cblas_saxpy(kOutputDimensions * batch_size_, 1.0, + head_output, 1, output, 1); +#else + for (IndexType b = 0; b < batch_size_; ++b) { + const IndexType batch_offset = kOutputDimensions * b; + for (IndexType i = 0; i < kOutputDimensions; ++i) { + output[batch_offset + i] += head_output[batch_offset + i]; + } + } +#endif + return output; + } + + // 逆伝播 + void Backpropagate(const LearnFloatType* gradients, + LearnFloatType learning_rate) { + Tail::Backpropagate(gradients, learning_rate); + previous_layer_trainer_->Backpropagate(gradients, learning_rate); + } + + private: + // コンストラクタ + Trainer(LayerType* target_layer, FeatureTransformer* feature_transformer) : + Tail(target_layer, feature_transformer), + batch_size_(0), + previous_layer_trainer_(Trainer::Create( + &target_layer->previous_layer_, feature_transformer)), + target_layer_(target_layer) { + } + + // 入出力の次元数 + static constexpr IndexType kOutputDimensions = LayerType::kOutputDimensions; + + // サブクラスをfriendにする + template + friend class Trainer; + + // ミニバッチのサンプル数 + IndexType batch_size_; + + // 直前の層のTrainer + const std::shared_ptr> previous_layer_trainer_; + + // 学習対象の層 + LayerType* const target_layer_; +}; + + +// 学習:複数の層の出力の和を取る層(テンプレート引数が1つの場合) +template +class Trainer> { + private: + // 学習対象の層の型 + using LayerType = Layers::Sum; + + public: + // ファクトリ関数 + static std::shared_ptr Create( + LayerType* target_layer, FeatureTransformer* feature_transformer) { + return std::shared_ptr( + new Trainer(target_layer, feature_transformer)); + } + + // ハイパーパラメータなどのオプションを設定する + void SendMessage(Message* message) { + previous_layer_trainer_->SendMessage(message); + } + + // パラメータを乱数で初期化する + template + void Initialize(RNG& rng) { + previous_layer_trainer_->Initialize(rng); + } + + // 順伝播 + /*const*/ LearnFloatType* Propagate(const std::vector& batch) { + if (output_.size() < kOutputDimensions * batch.size()) { + output_.resize(kOutputDimensions * batch.size()); + } + batch_size_ = static_cast(batch.size()); + const auto output = previous_layer_trainer_->Propagate(batch); +#if defined(USE_BLAS) + cblas_scopy(kOutputDimensions * batch_size_, output, 1, &output_[0], 1); +#else + for (IndexType b = 0; b < batch_size_; ++b) { + const IndexType batch_offset = kOutputDimensions * b; + for (IndexType i = 0; i < kOutputDimensions; ++i) { + output_[batch_offset + i] = output[batch_offset + i]; + } + } +#endif + return output_.data(); + } + + // 逆伝播 + void Backpropagate(const LearnFloatType* gradients, + LearnFloatType learning_rate) { + previous_layer_trainer_->Backpropagate(gradients, learning_rate); + } + + private: + // コンストラクタ + Trainer(LayerType* target_layer, FeatureTransformer* feature_transformer) : + batch_size_(0), + previous_layer_trainer_(Trainer::Create( + &target_layer->previous_layer_, feature_transformer)), + target_layer_(target_layer) { + } + + // 入出力の次元数 + static constexpr IndexType kOutputDimensions = LayerType::kOutputDimensions; + + // サブクラスをfriendにする + template + friend class Trainer; + + // ミニバッチのサンプル数 + IndexType batch_size_; + + // 直前の層のTrainer + const std::shared_ptr> previous_layer_trainer_; + + // 学習対象の層 + LayerType* const target_layer_; + + // 順伝播用バッファ + std::vector output_; +}; + +} // namespace NNUE + +} // namespace Eval + +#endif // defined(EVAL_LEARN) && defined(EVAL_NNUE) + +#endif diff --git a/src/evaluate.cpp b/src/evaluate.cpp index 8b4a27bc..921cb808 100644 --- a/src/evaluate.cpp +++ b/src/evaluate.cpp @@ -22,6 +22,7 @@ #include #include // For std::memset #include +#include #include #include "bitboard.h" @@ -29,6 +30,7 @@ #include "material.h" #include "pawns.h" #include "thread.h" +#include "eval/nnue/evaluate_nnue.h" namespace Trace { @@ -877,9 +879,11 @@ namespace { /// evaluate() is the evaluator for the outer world. It returns a static /// evaluation of the position from the point of view of the side to move. +#if !defined(EVAL_NNUE) Value Eval::evaluate(const Position& pos) { return Evaluation(pos).value(); } +#endif // defined(EVAL_NNUE) /// trace() is like evaluate(), but instead of returning a value, it returns @@ -924,3 +928,138 @@ std::string Eval::trace(const Position& pos) { return ss.str(); } + +#if defined(EVAL_NNUE) || defined(EVAL_LEARN) +namespace Eval { +ExtBonaPiece kpp_board_index[PIECE_NB] = { + { BONA_PIECE_ZERO, BONA_PIECE_ZERO }, + { f_pawn, e_pawn }, + { f_knight, e_knight }, + { f_bishop, e_bishop }, + { f_rook, e_rook }, + { f_queen, e_queen }, + { f_king, e_king }, + { BONA_PIECE_ZERO, BONA_PIECE_ZERO }, + + // 肩猩ꍇBfeւB + { BONA_PIECE_ZERO, BONA_PIECE_ZERO }, + { e_pawn, f_pawn }, + { e_knight, f_knight }, + { e_bishop, f_bishop }, + { e_rook, f_rook }, + { e_queen, f_queen }, + { e_king, f_king }, + { BONA_PIECE_ZERO, BONA_PIECE_ZERO }, // ̐͂Ȃ +}; + +// ŕێĂpieceListFw[]BonaPieceł邩B +// : fobOpBxB +bool EvalList::is_valid(const Position& pos) +{ + std::set piece_numbers; + for (Square sq = SQ_A1; sq != SQUARE_NB; ++sq) { + auto piece_number = piece_no_of_board(sq); + if (piece_number == PIECE_NUMBER_NB) { + continue; + } + assert(!piece_numbers.count(piece_number)); + piece_numbers.insert(piece_number); + } + + for (int i = 0; i < length(); ++i) + { + BonaPiece fw = pieceListFw[i]; + // fw{ɑ݂邩PositionNX̂قɒׂɍsB + + if (fw == Eval::BONA_PIECE_ZERO) { + continue; + } + + // ͈͊O + if (!(0 <= fw && fw < fe_end)) + return false; + + // Տ̋Ȃ̂ł̋{ɑ݂邩ׂɂB + for (Piece pc = NO_PIECE; pc < PIECE_NB; ++pc) + { + auto pt = type_of(pc); + if (pt == NO_PIECE || pt == 7) // ݂Ȃ + continue; + + // pcBonaPiece̊Jnԍ + auto s = BonaPiece(kpp_board_index[pc].fw); + if (s <= fw && fw < s + SQUARE_NB) + { + // ‚̂ł̋sq̒n_ɂ邩𒲂ׂB + Square sq = (Square)(fw - s); + Piece pc2 = pos.piece_on(sq); + + if (pc2 != pc) + return false; + + goto Found; + } + } + // ݂̂Ȃł.. + return false; + Found:; + } + + // Validate piece_no_list_board + for (auto sq = SQUARE_ZERO; sq < SQUARE_NB; ++sq) { + Piece expected_piece = pos.piece_on(sq); + PieceNumber piece_number = piece_no_list_board[sq]; + if (piece_number == PIECE_NUMBER_NB) { + assert(expected_piece == NO_PIECE); + if (expected_piece != NO_PIECE) { + return false; + } + continue; + } + + BonaPiece bona_piece_white = pieceListFw[piece_number]; + Piece actual_piece; + for (actual_piece = NO_PIECE; actual_piece < PIECE_NB; ++actual_piece) { + if (kpp_board_index[actual_piece].fw == BONA_PIECE_ZERO) { + continue; + } + + if (kpp_board_index[actual_piece].fw <= bona_piece_white + && bona_piece_white < kpp_board_index[actual_piece].fw + SQUARE_NB) { + break; + } + } + + assert(actual_piece != PIECE_NB); + if (actual_piece == PIECE_NB) { + return false; + } + + assert(actual_piece == expected_piece); + if (actual_piece != expected_piece) { + return false; + } + + Square actual_square = static_cast( + bona_piece_white - kpp_board_index[actual_piece].fw); + assert(sq == actual_square); + if (sq != actual_square) { + return false; + } + } + + return true; +} +} +#endif // defined(EVAL_NNUE) || defined(EVAL_LEARN) + +#if !defined(EVAL_NNUE) +namespace Eval { +void evaluate_with_no_return(const Position& pos) {} +void update_weights(uint64_t epoch, const std::array & freeze) {} +void init_grad(double eta1, uint64_t eta_epoch, double eta2, uint64_t eta2_epoch, double eta3) {} +void add_grad(Position& pos, Color rootColor, double delt_grad, const std::array & freeze) {} +void save_eval(std::string suffix) {} +double get_eta() { return 0.0; } +} +#endif // defined(EVAL_NNUE) diff --git a/src/evaluate.h b/src/evaluate.h index 7c8a2a6f..a9e6a563 100644 --- a/src/evaluate.h +++ b/src/evaluate.h @@ -32,6 +32,191 @@ namespace Eval { std::string trace(const Position& pos); Value evaluate(const Position& pos); + +void evaluate_with_no_return(const Position& pos); + +Value compute_eval(const Position& pos); + +#if defined(EVAL_NNUE) || defined(EVAL_LEARN) +// ]֐t@CǂݍށB +// ́A"is_ready"R}h̉1xĂяoB2xĂяoƂ͑z肵ĂȂB +// (AEvalDir(]֐tH_)ύXɂȂƁAisreadyēxĂǂ݂ȂB) +void load_eval(); + +static uint64_t calc_check_sum() { return 0; } + +static void print_softname(uint64_t check_sum) {} + +// --- ]֐Ŏg萔 KPP(ʂƔC2)Pɑenum + +// (]֐̎̂Ƃɂ́ABonaPiece͎Rɒ`̂łł͒`ȂB) + + +// BonanzaKKP/KPPƌƂP(Piece)\^B +// KPP߂ƂɁA39̒n_̂̕悤ɁA~ɑ΂ĈӂȔԍKvƂȂB +enum BonaPiece : int32_t +{ + // f = friend()̈ӖBe = enemy()̈Ӗ + + // ̎̒l + BONA_PIECE_NOT_INIT = -1, + + // ȋB̂ƂȂǂ́AsvȋɈړB + BONA_PIECE_ZERO = 0, + + fe_hand_end = BONA_PIECE_ZERO + 1, + + // Bonanzâ悤ɔՏ̂肦Ȃ̕⍁̔ԍl߂ȂB + // R1) wK̂ƂɑPP1iڂɍƂāAtϊɂĐ\̂B + // R2) c^BitboardSquare̕ϊɍB + + // --- Տ̋ + f_pawn = fe_hand_end, + e_pawn = f_pawn + SQUARE_NB, + f_knight = e_pawn + SQUARE_NB, + e_knight = f_knight + SQUARE_NB, + f_bishop = e_knight + SQUARE_NB, + e_bishop = f_bishop + SQUARE_NB, + f_rook = e_bishop + SQUARE_NB, + e_rook = f_rook + SQUARE_NB, + f_queen = e_rook + SQUARE_NB, + e_queen = f_queen + SQUARE_NB, + fe_end = e_queen + SQUARE_NB, + f_king = fe_end, + e_king = f_king + SQUARE_NB, + fe_end2 = e_king + SQUARE_NB, // ʂ܂߂̔ԍB +}; + +#define ENABLE_INCR_OPERATORS_ON(T) \ +inline T& operator++(T& d) { return d = T(int(d) + 1); } \ +inline T& operator--(T& d) { return d = T(int(d) - 1); } + +ENABLE_INCR_OPERATORS_ON(BonaPiece) + +#undef ENABLE_INCR_OPERATORS_ON + +// BonaPiece肩猩Ƃ(39̕肩猩ƌ71̕)̔ԍƂ +// yAɂ̂ExtBonaPiece^ƌĂԂƂɂB +union ExtBonaPiece +{ + struct { + BonaPiece fw; // from white + BonaPiece fb; // from black + }; + BonaPiece from[2]; + + ExtBonaPiece() {} + ExtBonaPiece(BonaPiece fw_, BonaPiece fb_) : fw(fw_), fb(fb_) {} +}; + +// ̎wɂĂǂǂɈړ̂̏B +// ExtBonaPiece\łƂB +struct ChangedBonaPiece +{ + ExtBonaPiece old_piece; + ExtBonaPiece new_piece; +}; + +// KPPe[u̔Տ̋pcɑΉBonaPiece߂邽߂̔zB +// ) +// BonaPiece fb = kpp_board_index[pc].fb + sq; // 肩猩sqɂpcɑΉBonaPiece +// BonaPiece fw = kpp_board_index[pc].fw + sq; // 肩猩sqɂpcɑΉBonaPiece +extern ExtBonaPiece kpp_board_index[PIECE_NB]; + +// ]֐ŗpXgBǂ̋(PieceNumber)ǂɂ̂(BonaPiece)ێĂ\ +struct EvalList +{ + // ]֐(FV38^)ŗpԍ̃Xg + BonaPiece* piece_list_fw() const { return const_cast(pieceListFw); } + BonaPiece* piece_list_fb() const { return const_cast(pieceListFb); } + + // w肳ꂽpiece_nőExtBonaPiece^ɕϊĕԂB + ExtBonaPiece bona_piece(PieceNumber piece_no) const + { + ExtBonaPiece bp; + bp.fw = pieceListFw[piece_no]; + bp.fb = pieceListFb[piece_no]; + return bp; + } + + // Տsq̏piece_nopc̋zu + void put_piece(PieceNumber piece_no, Square sq, Piece pc) { + set_piece_on_board(piece_no, BonaPiece(kpp_board_index[pc].fw + sq), BonaPiece(kpp_board_index[pc].fb + Inv(sq)), sq); + } + + // Տ̂鏡sqɑΉPieceNumberԂB + PieceNumber piece_no_of_board(Square sq) const { return piece_no_list_board[sq]; } + + // pieceListB + // ɑΉ鎞̂߂ɁAgp̋̒lBONA_PIECE_ZEROɂĂB + // ʏ̕]֐̕]֐ƂėpłB + // piece_no_list̂ق̓fobO悤PIECE_NUMBER_NBŏB + void clear() + { + + for (auto& p : pieceListFw) + p = BONA_PIECE_ZERO; + + for (auto& p : pieceListFb) + p = BONA_PIECE_ZERO; + + for (auto& v : piece_no_list_board) + v = PIECE_NUMBER_NB; + } + + // ŕێĂpieceListFw[]BonaPieceł邩B + // : fobOpBxB + bool is_valid(const Position& pos); + + // Տsqɂpiece_nőBonaPiecefb,fwł邱Ƃݒ肷B + inline void set_piece_on_board(PieceNumber piece_no, BonaPiece fw, BonaPiece fb, Square sq) + { + assert(is_ok(piece_no)); + pieceListFw[piece_no] = fw; + pieceListFb[piece_no] = fb; + piece_no_list_board[sq] = piece_no; + } + + // XgBԍ(PieceNumber)‚̋ǂɂ̂(BonaPiece)BFV38ȂǂŗpB + + // Xg̒ + // 38Œ +public: + int length() const { return PIECE_NUMBER_KING; } + + // VPGATHERDDgsA4̔{łȂ΂ȂȂB + // ܂AKPPT^]֐Ȃǂ́A39,40Ԗڂ̗vf[ł邱ƂOƂ + // ANZXĂӏ̂Œӂ邱ƁB + static const int MAX_LENGTH = 32; + + // Տ̋ɑ΂āA̋ԍ(PieceNumber)ێĂz + // ʂSQUARE_NBɈړĂƂp+1܂ŕێĂA + // SQUARE_NB̋ʂړȂ̂ŁA̒lgƂ͂Ȃ͂B + PieceNumber piece_no_list_board[SQUARE_NB_PLUS1]; +private: + + BonaPiece pieceListFw[MAX_LENGTH]; + BonaPiece pieceListFb[MAX_LENGTH]; +}; + +// ]l̍vZ̊Ǘp +// ŐǖʂړԍǗ邽߂̍\ +// ́Aő2B +struct DirtyPiece +{ + // ̋ԍ̋牽ɕς̂ + Eval::ChangedBonaPiece changed_piece[2]; + + // dirtyɂȂԍ + PieceNumber pieceNo[2]; + + // dirtyɂȂB + // null move0ƂƂ肤B + // ƎƂōő2B + int dirty_num; + +}; +#endif // defined(EVAL_NNUE) || defined(EVAL_LEARN) } #endif // #ifndef EVALUATE_H_INCLUDED diff --git a/src/extra/sfen_packer.cpp b/src/extra/sfen_packer.cpp new file mode 100644 index 00000000..d56e808b --- /dev/null +++ b/src/extra/sfen_packer.cpp @@ -0,0 +1,448 @@ +#if defined (EVAL_LEARN) + +#include "../misc.h" +#include "../position.h" + +#include +#include +#include // std::memset() + +using namespace std; + +// ----------------------------------- +// 局面の圧縮・解凍 +// ----------------------------------- + +// ビットストリームを扱うクラス +// 局面の符号化を行なうときに、これがあると便利 +struct BitStream +{ + // データを格納するメモリを事前にセットする。 + // そのメモリは0クリアされているものとする。 + void set_data(uint8_t* data_) { data = data_; reset(); } + + // set_data()で渡されたポインタの取得。 + uint8_t* get_data() const { return data; } + + // カーソルの取得。 + int get_cursor() const { return bit_cursor; } + + // カーソルのリセット + void reset() { bit_cursor = 0; } + + // ストリームに1bit書き出す。 + // bは非0なら1を書き出す。0なら0を書き出す。 + void write_one_bit(int b) + { + if (b) + data[bit_cursor / 8] |= 1 << (bit_cursor & 7); + + ++bit_cursor; + } + + // ストリームから1ビット取り出す。 + int read_one_bit() + { + int b = (data[bit_cursor / 8] >> (bit_cursor & 7)) & 1; + ++bit_cursor; + + return b; + } + + // nビットのデータを書き出す + // データはdの下位から順に書き出されるものとする。 + void write_n_bit(int d, int n) + { + for (int i = 0; i < n; ++i) + write_one_bit(d & (1 << i)); + } + + // nビットのデータを読み込む + // write_n_bit()の逆変換。 + int read_n_bit(int n) + { + int result = 0; + for (int i = 0; i < n; ++i) + result |= read_one_bit() ? (1 << i) : 0; + + return result; + } + +private: + // 次に読み書きすべきbit位置。 + int bit_cursor; + + // データの実体 + uint8_t* data; +}; + + +// ハフマン符号化 +// ※  なのはminiの符号化から、変換が楽になるように単純化。 +// +// 盤上の1升(NO_PIECE以外) = 2~6bit ( + 成りフラグ1bit+ 先後1bit ) +// 手駒の1枚 = 1~5bit ( + 成りフラグ1bit+ 先後1bit ) +// +// 空 xxxxx0 + 0 (none) +// 歩 xxxx01 + 2 xxxx0 + 2 +// 香 xx0011 + 2 xx001 + 2 +// 桂 xx1011 + 2 xx101 + 2 +// 銀 xx0111 + 2 xx011 + 2 +// 金 x01111 + 1 x0111 + 1 // 金は成りフラグはない。 +// 角 011111 + 2 01111 + 2 +// 飛 111111 + 2 11111 + 2 +// +// すべての駒が盤上にあるとして、 +// 空 81 - 40駒 = 41升 = 41bit +// 歩 4bit*18駒 = 72bit +// 香 6bit* 4駒 = 24bit +// 桂 6bit* 4駒 = 24bit +// 銀 6bit* 4駒 = 24bit +// 金 6bit* 4駒 = 24bit +// 角 8bit* 2駒 = 16bit +// 飛 8bit* 2駒 = 16bit +// ------- +// 241bit + 1bit(手番) + 7bit×2(王の位置先後) = 256bit +// +// 盤上の駒が手駒に移動すると盤上の駒が空になるので盤上のその升は1bitで表現でき、 +// 手駒は、盤上の駒より1bit少なく表現できるので結局、全体のbit数に変化はない。 +// ゆえに、この表現において、どんな局面でもこのbit数で表現できる。 +// 手駒に成りフラグは不要だが、これも含めておくと盤上の駒のbit数-1になるので +// 全体のbit数が固定化できるのでこれも含めておくことにする。 + +// Huffman Encoding +// +// Empty xxxxxxx0 +// Pawn xxxxx001 + 1 bit (Side to move) +// Knight xxxxx011 + 1 bit (Side to move) +// Bishop xxxxx101 + 1 bit (Side to move) +// Rook xxxxx111 + 1 bit (Side to move) + +struct HuffmanedPiece +{ + int code; // どうコード化されるか + int bits; // 何bit専有するのか +}; + +HuffmanedPiece huffman_table[] = +{ + {0b0000,1}, // NO_PIECE + {0b0001,4}, // PAWN + {0b0011,4}, // KNIGHT + {0b0101,4}, // BISHOP + {0b0111,4}, // ROOK + {0b1001,4}, // QUEEN +}; + +// sfenを圧縮/解凍するためのクラス +// sfenはハフマン符号化をすることで256bit(32bytes)にpackできる。 +// このことはなのはminiにより証明された。上のハフマン符号化である。 +// +// 内部フォーマット = 手番1bit+王の位置7bit*2 + 盤上の駒(ハフマン符号化) + 手駒(ハフマン符号化) +// Side to move (White = 0, Black = 1) (1bit) +// White King Position (6 bits) +// Black King Position (6 bits) +// Huffman Encoding of the board +// Castling availability (1 bit x 4) +// En passant square (1 or 1 + 6 bits) +// Rule 50 (6 bits) +// Game play (8 bits) +// +// TODO(someone): Rename SFEN to FEN. +// +struct SfenPacker +{ + // sfenをpackしてdata[32]に格納する。 + void pack(const Position& pos) + { +// cout << pos; + + memset(data, 0, 32 /* 256bit */); + stream.set_data(data); + + // 手番 + // Side to move. + stream.write_one_bit((int)(pos.side_to_move())); + + // 先手玉、後手玉の位置、それぞれ7bit + // White king and black king, 6 bits for each. + for(auto c : Colors) + stream.write_n_bit(pos.king_square(c), 6); + + // Write the pieces on the board other than the kings. + for (Rank r = RANK_8; r >= RANK_1; --r) + { + for (File f = FILE_A; f <= FILE_H; ++f) + { + Piece pc = pos.piece_on(make_square(f, r)); + if (type_of(pc) == KING) + continue; + write_board_piece_to_stream(pc); + } + } + + // TODO(someone): Support chess960. + stream.write_one_bit(pos.can_castle(WHITE_OO)); + stream.write_one_bit(pos.can_castle(WHITE_OOO)); + stream.write_one_bit(pos.can_castle(BLACK_OO)); + stream.write_one_bit(pos.can_castle(BLACK_OOO)); + + if (pos.ep_square() == SQ_NONE) { + stream.write_one_bit(0); + } + else { + stream.write_one_bit(1); + stream.write_n_bit(static_cast(pos.ep_square()), 6); + } + + stream.write_n_bit(pos.state()->rule50, 6); + + stream.write_n_bit(1 + (pos.game_ply() - (pos.side_to_move() == BLACK)) / 2, 8); + + assert(stream.get_cursor() <= 256); + } + + // pack()でpackされたsfen(256bit = 32bytes) + // もしくはunpack()でdecodeするsfen + uint8_t *data; // uint8_t[32]; + +//private: + // Position::set_from_packed_sfen(uint8_t data[32])でこれらの関数を使いたいので筋は悪いがpublicにしておく。 + + BitStream stream; + + // 盤面の駒をstreamに出力する。 + void write_board_piece_to_stream(Piece pc) + { + // 駒種 + PieceType pr = type_of(pc); + auto c = huffman_table[pr]; + stream.write_n_bit(c.code, c.bits); + + if (pc == NO_PIECE) + return; + + // 先後フラグ + stream.write_one_bit(color_of(pc)); + } + + // 盤面の駒を1枚streamから読み込む + Piece read_board_piece_from_stream() + { + PieceType pr = NO_PIECE_TYPE; + int code = 0, bits = 0; + while (true) + { + code |= stream.read_one_bit() << bits; + ++bits; + + assert(bits <= 6); + + for (pr = NO_PIECE_TYPE; pr < KING; ++pr) + if (huffman_table[pr].code == code + && huffman_table[pr].bits == bits) + goto Found; + } + Found:; + if (pr == NO_PIECE_TYPE) + return NO_PIECE; + + // 先後フラグ + Color c = (Color)stream.read_one_bit(); + + return make_piece(c, pr); + } +}; + + +// ----------------------------------- +// Positionクラスに追加 +// ----------------------------------- + +// 高速化のために直接unpackする関数を追加。かなりしんどい。 +// packer::unpack()とPosition::set()とを合体させて書く。 +// 渡された局面に問題があって、エラーのときは非0を返す。 +int Position::set_from_packed_sfen(const PackedSfen& sfen , StateInfo * si, Thread* th, bool mirror) +{ + SfenPacker packer; + auto& stream = packer.stream; + stream.set_data((uint8_t*)&sfen); + + std::memset(this, 0, sizeof(Position)); + std::memset(si, 0, sizeof(StateInfo)); + std::fill_n(&pieceList[0][0], sizeof(pieceList) / sizeof(Square), SQ_NONE); + st = si; + + // Active color + sideToMove = (Color)stream.read_one_bit(); + + // evalListのclear。上でmemsetでゼロクリアしたときにクリアされているが…。 + evalList.clear(); + + // PieceListを更新する上で、どの駒がどこにあるかを設定しなければならないが、 + // それぞれの駒をどこまで使ったかのカウンター + PieceNumber next_piece_number = PIECE_NUMBER_ZERO; + + pieceList[W_KING][0] = SQUARE_NB; + pieceList[B_KING][0] = SQUARE_NB; + + // まず玉の位置 + if (mirror) + { + for (auto c : Colors) + board[Mir((Square)stream.read_n_bit(6))] = make_piece(c, KING); + } + else + { + for (auto c : Colors) + board[stream.read_n_bit(6)] = make_piece(c, KING); + } + + // Piece placement + for (Rank r = RANK_8; r >= RANK_1; --r) + { + for (File f = FILE_A; f <= FILE_H; ++f) + { + auto sq = make_square(f, r); + if (mirror) { + sq = Mir(sq); + } + + // すでに玉がいるようだ + Piece pc; + if (type_of(board[sq]) != KING) + { + assert(board[sq] == NO_PIECE); + pc = packer.read_board_piece_from_stream(); + } + else + { + pc = board[sq]; + board[sq] = NO_PIECE; // いっかい取り除いておかないとput_piece()でASSERTに引っかかる。 + } + + // 駒がない場合もあるのでその場合はスキップする。 + if (pc == NO_PIECE) + continue; + + put_piece(Piece(pc), sq); + + // evalListの更新 + PieceNumber piece_no = + (pc == B_KING) ? PIECE_NUMBER_BKING : // 先手玉 + (pc == W_KING) ? PIECE_NUMBER_WKING : // 後手玉 + next_piece_number++; // それ以外 + + evalList.put_piece(piece_no, sq, pc); // sqの升にpcの駒を配置する + + //cout << sq << ' ' << board[sq] << ' ' << stream.get_cursor() << endl; + + if (stream.get_cursor() > 256) + return 1; + //assert(stream.get_cursor() <= 256); + + } + } + + // Castling availability. + // TODO(someone): Support chess960. + st->castlingRights = 0; + if (stream.read_one_bit()) { + Square rsq; + for (rsq = relative_square(WHITE, SQ_H1); piece_on(rsq) != W_ROOK; --rsq) {} + set_castling_right(WHITE, rsq); + } + if (stream.read_one_bit()) { + Square rsq; + for (rsq = relative_square(WHITE, SQ_A1); piece_on(rsq) != W_ROOK; ++rsq) {} + set_castling_right(WHITE, rsq); + } + if (stream.read_one_bit()) { + Square rsq; + for (rsq = relative_square(BLACK, SQ_H1); piece_on(rsq) != B_ROOK; --rsq) {} + set_castling_right(BLACK, rsq); + } + if (stream.read_one_bit()) { + Square rsq; + for (rsq = relative_square(BLACK, SQ_A1); piece_on(rsq) != B_ROOK; ++rsq) {} + set_castling_right(BLACK, rsq); + } + + // En passant square. Ignore if no pawn capture is possible + if (stream.read_one_bit()) { + Square ep_square = static_cast(stream.read_n_bit(6)); + if (mirror) { + ep_square = Mir(ep_square); + } + st->epSquare = ep_square; + + if (!(attackers_to(st->epSquare) & pieces(sideToMove, PAWN)) + || !(pieces(~sideToMove, PAWN) & (st->epSquare + pawn_push(~sideToMove)))) + st->epSquare = SQ_NONE; + } + else { + st->epSquare = SQ_NONE; + } + + // Halfmove clock + st->rule50 = static_cast(stream.read_n_bit(6)); + + // Fullmove number + gamePly = static_cast(stream.read_n_bit(8)); + // Convert from fullmove starting from 1 to gamePly starting from 0, + // handle also common incorrect FEN with fullmove = 0. + gamePly = std::max(2 * (gamePly - 1), 0) + (sideToMove == BLACK); + + assert(stream.get_cursor() <= 256); + + chess960 = false; + thisThread = th; + set_state(st); + + //std::cout << *this << std::endl; + + assert(pos_is_ok()); +#if defined(EVAL_NNUE) + assert(evalList.is_valid(*this)); +#endif // defined(EVAL_NNUE) + + return 0; +} + +// 盤面と手駒、手番を与えて、そのsfenを返す。 +//std::string Position::sfen_from_rawdata(Piece board[81], Hand hands[2], Color turn, int gamePly_) +//{ +// // 内部的な構造体にコピーして、sfen()を呼べば、変換過程がそこにしか依存していないならば +// // これで正常に変換されるのでは…。 +// Position pos; +// +// memcpy(pos.board, board, sizeof(Piece) * 81); +// memcpy(pos.hand, hands, sizeof(Hand) * 2); +// pos.sideToMove = turn; +// pos.gamePly = gamePly_; +// +// return pos.sfen(); +// +// // ↑の実装、美しいが、いかんせん遅い。 +// // 棋譜を大量に読み込ませて学習させるときにここがボトルネックになるので直接unpackする関数を書く。 +//} + +// packされたsfenを得る。引数に指定したバッファに返す。 +void Position::sfen_pack(PackedSfen& sfen) +{ + SfenPacker sp; + sp.data = (uint8_t*)&sfen; + sp.pack(*this); +} + +//// packされたsfenを解凍する。sfen文字列が返る。 +//std::string Position::sfen_unpack(const PackedSfen& sfen) +//{ +// SfenPacker sp; +// sp.data = (uint8_t*)&sfen; +// return sp.unpack(); +//} + + +#endif // USE_SFEN_PACKER + diff --git a/src/learn/gensfen2019.cpp b/src/learn/gensfen2019.cpp new file mode 100644 index 00000000..01293b9c --- /dev/null +++ b/src/learn/gensfen2019.cpp @@ -0,0 +1 @@ +// just a place holder diff --git a/src/learn/half_float.h b/src/learn/half_float.h new file mode 100644 index 00000000..31dc5a29 --- /dev/null +++ b/src/learn/half_float.h @@ -0,0 +1,133 @@ +#ifndef __HALF_FLOAT_H__ +#define __HALF_FLOAT_H__ + +// Half Float Library by yaneurao +// (16-bit float) + +// 16bit型による浮動小数点演算 +// コンパイラの生成するfloat型のコードがIEEE 754の形式であると仮定して、それを利用する。 + +#include "../types.h" + +namespace HalfFloat +{ + // IEEE 754 float 32 format is : + // sign(1bit) + exponent(8bits) + fraction(23bits) = 32bits + // + // Our float16 format is : + // sign(1bit) + exponent(5bits) + fraction(10bits) = 16bits + union float32_converter + { + int32_t n; + float f; + }; + + + // 16-bit float + struct float16 + { + // --- constructors + + float16() {} + float16(int16_t n) { from_float((float)n); } + float16(int32_t n) { from_float((float)n); } + float16(float n) { from_float(n); } + float16(double n) { from_float((float)n); } + + // build from a float + void from_float(float f) { *this = to_float16(f); } + + // --- implicit converters + + operator int32_t() const { return (int32_t)to_float(*this); } + operator float() const { return to_float(*this); } + operator double() const { return double(to_float(*this)); } + + // --- operators + + float16 operator += (float16 rhs) { from_float(to_float(*this) + to_float(rhs)); return *this; } + float16 operator -= (float16 rhs) { from_float(to_float(*this) - to_float(rhs)); return *this; } + float16 operator *= (float16 rhs) { from_float(to_float(*this) * to_float(rhs)); return *this; } + float16 operator /= (float16 rhs) { from_float(to_float(*this) / to_float(rhs)); return *this; } + float16 operator + (float16 rhs) const { return float16(*this) += rhs; } + float16 operator - (float16 rhs) const { return float16(*this) -= rhs; } + float16 operator * (float16 rhs) const { return float16(*this) *= rhs; } + float16 operator / (float16 rhs) const { return float16(*this) /= rhs; } + float16 operator - () const { return float16(-to_float(*this)); } + bool operator == (float16 rhs) const { return this->v_ == rhs.v_; } + bool operator != (float16 rhs) const { return !(*this == rhs); } + + static void UnitTest() { unit_test(); } + + private: + + // --- entity + + uint16_t v_; + + // --- conversion between float and float16 + + static float16 to_float16(float f) + { + float32_converter c; + c.f = f; + u32 n = c.n; + + // The sign bit is MSB in common. + uint16_t sign_bit = (n >> 16) & 0x8000; + + // The exponent of IEEE 754's float 32 is biased +127 , so we change this bias into +15 and limited to 5-bit. + uint16_t exponent = (((n >> 23) - 127 + 15) & 0x1f) << 10; + + // The fraction is limited to 10-bit. + uint16_t fraction = (n >> (23-10)) & 0x3ff; + + float16 f_; + f_.v_ = sign_bit | exponent | fraction; + + return f_; + } + + static float to_float(float16 v) + { + u32 sign_bit = (v.v_ & 0x8000) << 16; + u32 exponent = ((((v.v_ >> 10) & 0x1f) - 15 + 127) & 0xff) << 23; + u32 fraction = (v.v_ & 0x3ff) << (23 - 10); + + float32_converter c; + c.n = sign_bit | exponent | fraction; + return c.f; + } + + // unit testになってないが、一応計算が出来ることは確かめた。コードはあとでなおす(かも)。 + static void unit_test() + { + float16 a, b, c, d; + a = 1; + std::cout << (float)a << std::endl; + b = -118.625; + std::cout << (float)b << std::endl; + c = 2.5; + std::cout << (float)c << std::endl; + d = a + c; + std::cout << (float)d << std::endl; + + c *= 1.5; + std::cout << (float)c << std::endl; + + b /= 3; + std::cout << (float)b << std::endl; + + float f1 = 1.5; + a += f1; + std::cout << (float)a << std::endl; + + a += f1 * (float)a; + std::cout << (float)a << std::endl; + } + + }; + +} + +#endif // __HALF_FLOAT_H__ diff --git a/src/learn/learn.h b/src/learn/learn.h new file mode 100644 index 00000000..58a017bd --- /dev/null +++ b/src/learn/learn.h @@ -0,0 +1,237 @@ +#ifndef _LEARN_H_ +#define _LEARN_H_ + +#if defined(EVAL_LEARN) + +#include + +// ===================== +// 学習時の設定 +// ===================== + +// 以下のいずれかを選択すれば、そのあとの細々したものは自動的に選択される。 +// いずれも選択しない場合は、そのあとの細々したものをひとつひとつ設定する必要がある。 + +// elmo方式での学習設定。これをデフォルト設定とする。 +// 標準の雑巾絞りにするためにはlearnコマンドで "lambda 1"を指定してやれば良い。 +#define LEARN_ELMO_METHOD + + +// ---------------------- +// 更新式 +// ---------------------- + +// AdaGrad。これが安定しているのでお勧め。 +// #define ADA_GRAD_UPDATE + +// 勾配の符号だけ見るSGD。省メモリで済むが精度は…。 +// #define SGD_UPDATE + +// ---------------------- +// 学習時の設定 +// ---------------------- + +// mini-batchサイズ。 +// この数だけの局面をまとめて勾配を計算する。 +// 小さくするとupdate_weights()の回数が増えるので収束が速くなる。勾配が不正確になる。 +// 大きくするとupdate_weights()の回数が減るので収束が遅くなる。勾配は正確に出るようになる。 +// 多くの場合において、この値を変更する必要はないと思う。 + +#define LEARN_MINI_BATCH_SIZE (1000 * 1000 * 1) + +// ファイルから1回に読み込む局面数。これだけ読み込んだあとshuffleする。 +// ある程度大きいほうが良いが、この数×40byte×3倍ぐらいのメモリを消費する。10M局面なら400MB*3程度消費する。 +// THREAD_BUFFER_SIZE(=10000)の倍数にすること。 + +#define LEARN_SFEN_READ_SIZE (1000 * 1000 * 10) + +// 学習時の評価関数の保存間隔。この局面数だけ学習させるごとに保存。 +// 当然ながら、保存間隔を長くしたほうが学習時間は短くなる。 +// フォルダ名は 0/ , 1/ , 2/ ...のように保存ごとにインクリメントされていく。 +// デフォルトでは10億局面に1回。 +#define LEARN_EVAL_SAVE_INTERVAL (1000000000ULL) + + +// ---------------------- +// 目的関数の選択 +// ---------------------- + +// 目的関数が勝率の差の二乗和 +// 詳しい説明は、learner.cppを見ること。 + +//#define LOSS_FUNCTION_IS_WINNING_PERCENTAGE + +// 目的関数が交差エントロピー +// 詳しい説明は、learner.cppを見ること。 +// いわゆる、普通の「雑巾絞り」 +//#define LOSS_FUNCTION_IS_CROSS_ENTOROPY + +// 目的関数が交差エントロピーだが、勝率の関数を通さない版 +// #define LOSS_FUNCTION_IS_CROSS_ENTOROPY_FOR_VALUE + +// elmo(WCSC27)の方式 +// #define LOSS_FUNCTION_IS_ELMO_METHOD + +// ※ 他、色々追加するかも。 + + +// ---------------------- +// 学習に関するデバッグ設定 +// ---------------------- + +// 学習時のrmseの出力をこの回数に1回に減らす。 +// rmseの計算は1スレッドで行なうためそこそこ時間をとられるので出力を減らすと効果がある。 +#define LEARN_RMSE_OUTPUT_INTERVAL 1 + + +// ---------------------- +// ゼロベクトルからの学習 +// ---------------------- + +// 評価関数パラメーターをゼロベクトルから学習を開始する。 +// ゼロ初期化して棋譜生成してゼロベクトルから学習させて、 +// 棋譜生成→学習を繰り返すとプロの棋譜に依らないパラメーターが得られる。(かも) +// (すごく時間かかる) + +//#define RESET_TO_ZERO_VECTOR + + +// ---------------------- +// 学習のときの浮動小数 +// ---------------------- + +// これをdoubleにしたほうが計算精度は上がるが、重み配列絡みのメモリが倍必要になる。 +// 現状、ここをfloatにした場合、評価関数ファイルに対して、重み配列はその4.5倍のサイズ。(KPPTで4.5GB程度) +// double型にしても収束の仕方にほとんど差異がなかったのでfloatに固定する。 + +// floatを使う場合 +typedef float LearnFloatType; + +// doubleを使う場合 +//typedef double LearnFloatType; + +// float16を使う場合 +//#include "half_float.h" +//typedef HalfFloat::float16 LearnFloatType; + +// ---------------------- +// 省メモリ化 +// ---------------------- + +// Weight配列(のうちのKPP)に三角配列を用いて省メモリ化する。 +// これを用いると、学習用の重み配列は評価関数ファイルの3倍程度で済むようになる。 + +#define USE_TRIANGLE_WEIGHT_ARRAY + +// ---------------------- +// 次元下げ +// ---------------------- + +// ミラー(左右対称性)、インバース(先後対称性)に関して次元下げを行なう。 +// デフォルトではすべてオン。 + +// KKに対してミラー、インバースを利用した次元下げを行なう。(効果のほどは不明) +// USE_KK_INVERSE_WRITEをオンにするときはUSE_KK_MIRROR_WRITEもオンでなければならない。 +#define USE_KK_MIRROR_WRITE +#define USE_KK_INVERSE_WRITE + +// KKPに対してミラー、インバースを利用した次元下げを行なう。(インバースのほうは効果のほどは不明) +// USE_KKP_INVERSE_WRITEをオンにするときは、USE_KKP_MIRROR_WRITEもオンになっていなければならない。 +#define USE_KKP_MIRROR_WRITE +#define USE_KKP_INVERSE_WRITE + +// KPPに対してミラーを利用した次元下げを行なう。(これをオフにすると教師局面が倍ぐらい必要になる) +// KPPにはインバースはない。(先手側のKしかないので) +#define USE_KPP_MIRROR_WRITE + +// KPPPに対してミラーを利用した次元下げを行なう。(これをオフにすると教師局面が倍ぐらい必要になる) +// KPPPにもインバースはない。(先手側のKしかないので) +#define USE_KPPP_MIRROR_WRITE + +// KKPP成分に対して学習時にKPPによる次元下げを行なう。 +// 学習、めっちゃ遅くなる。 +// 未デバッグなので使わないこと。 +//#define USE_KKPP_LOWER_DIM + + +// ====================== +// 教師局面生成時の設定 +// ====================== + +// ---------------------- +// 引き分けを書き出す +// ---------------------- + +// 引き分けに至ったとき、それを教師局面として書き出す +// これをするほうが良いかどうかは微妙。 +// #define LEARN_GENSFEN_USE_DRAW_RESULT + + +// ====================== +// configure +// ====================== + +// ---------------------- +// elmo(WCSC27)の方法での学習 +// ---------------------- + +#if defined( LEARN_ELMO_METHOD ) +#define LOSS_FUNCTION_IS_ELMO_METHOD +#define ADA_GRAD_UPDATE +#endif + + +// ---------------------- +// Learnerで用いるstructの定義 +// ---------------------- +#include "../position.h" + +namespace Learner +{ + // PackedSfenと評価値が一体化した構造体 + // オプションごとに書き出す内容が異なると教師棋譜を再利用するときに困るので + // とりあえず、以下のメンバーはオプションによらずすべて書き出しておく。 + struct PackedSfenValue + { + // 局面 + PackedSfen sfen; + + // Learner::search()から返ってきた評価値 + int16_t score; + + // PVの初手 + // 教師との指し手一致率を求めるときなどに用いる + uint16_t move; + + // 初期局面からの局面の手数。 + uint16_t gamePly; + + // この局面の手番側が、ゲームを最終的に勝っているなら1。負けているなら-1。 + // 引き分けに至った場合は、0。 + // 引き分けは、教師局面生成コマンドgensfenにおいて、 + // LEARN_GENSFEN_DRAW_RESULTが有効なときにだけ書き出す。 + int8_t game_result; + + // 教師局面を書き出したファイルを他の人とやりとりするときに + // この構造体サイズが不定だと困るため、paddingしてどの環境でも必ず40bytesになるようにしておく。 + uint8_t padding; + + // 32 + 2 + 2 + 2 + 1 + 1 = 40bytes + }; + + // 読み筋とそのときの評価値を返す型 + // Learner::search() , Learner::qsearch()で用いる。 + typedef std::pair > ValueAndPV; + + // いまのところ、やねうら王2018 Otafukuしか、このスタブを持っていないが + // EVAL_LEARNをdefineするなら、このスタブが必須。 + extern Learner::ValueAndPV search(Position& pos, int depth , size_t multiPV = 1 , uint64_t NodesLimit = 0); + extern Learner::ValueAndPV qsearch(Position& pos); + + double calc_grad(Value shallow, const PackedSfenValue& psv); + +} + +#endif + +#endif // ifndef _LEARN_H_ \ No newline at end of file diff --git a/src/learn/learner.cpp b/src/learn/learner.cpp new file mode 100644 index 00000000..526c027c --- /dev/null +++ b/src/learn/learner.cpp @@ -0,0 +1,2952 @@ +// 学習関係のルーチン +// +// 1) 棋譜の自動生成 +// → "gensfen"コマンド +// 2) 生成した棋譜からの評価関数パラメーターの学習 +// → "learn"コマンド +// → 教師局面のshuffleもこのコマンドの拡張として行なう。 +// 例) "learn shuffle" +// 3) 定跡の自動生成 +// → "makebook think"コマンド +// → extra/book/book.cppで実装 +// 4) 局後自動検討モード +// → GUIが補佐すべき問題なのでエンジンでは関与しないことにする。 +// etc.. + +#if defined(EVAL_LEARN) + +#include +#include + +#include "learn.h" +#include "multi_think.h" +#include "../uci.h" + +// 学習用のevaluate絡みのheader +#include "../eval/evaluate_common.h" + +// ---------------------- +// 設定内容に基づく定数文字列 +// ---------------------- + +// 更新式に応じた文字列。(デバッグ用に出力する。) +// 色々更新式を実装したがAdaGradが速度面、メモリ面においてベストという結論になった。 +#if defined(ADA_GRAD_UPDATE) +#define LEARN_UPDATE "AdaGrad" +#elif defined(SGD_UPDATE) +#define LEARN_UPDATE "SGD" +#endif + +#if defined(LOSS_FUNCTION_IS_WINNING_PERCENTAGE) +#define LOSS_FUNCTION "WINNING_PERCENTAGE" +#elif defined(LOSS_FUNCTION_IS_CROSS_ENTOROPY) +#define LOSS_FUNCTION "CROSS_ENTOROPY" +#elif defined(LOSS_FUNCTION_IS_CROSS_ENTOROPY_FOR_VALUE) +#define LOSS_FUNCTION "CROSS_ENTOROPY_FOR_VALUE" +#elif defined(LOSS_FUNCTION_IS_ELMO_METHOD) +#define LOSS_FUNCTION "ELMO_METHOD(WCSC27)" +#endif + +// ----------------------------------- +// 以下、実装部。 +// ----------------------------------- + +#include +#include +#include +#include +#include +#include // std::exp(),std::pow(),std::log() +#include // memcpy() + +#if defined (_OPENMP) +#include +#endif + +#if defined(_MSC_VER) +// C++のfilesystemは、C++17以降か、MSVCでないと使えないようだ。 +// windows.hを使うようにしたが、msys2のg++だとうまくフォルダ内のファイルが取得できない。 +// 仕方ないのでdirent.hを用いる。 +#include +#elif defined(__GNUC__) +#include +#endif + +#include "../misc.h" +#include "../thread.h" +#include "../position.h" +//#include "../extra/book/book.h" +#include "../tt.h" +#include "multi_think.h" + +#if defined(EVAL_NNUE) +#include "../eval/nnue/evaluate_nnue_learner.h" +#include +#endif + +using namespace std; + +//// これは探索部で定義されているものとする。 +//extern Book::BookMoveSelector book; + +// atomicに対する足し算、引き算の定義 +// Apery/learner.hppにあるatomicAdd()に合わせてある。 +template +T operator += (std::atomic& x, const T rhs) +{ + T old = x.load(std::memory_order_consume); + // このタイミングで他スレッドから値が書き換えられることは許容する。 + // 値が破壊されなければ良しという考え。 + T desired = old + rhs; + while (!x.compare_exchange_weak(old, desired, std::memory_order_release, std::memory_order_consume)) + desired = old + rhs; + return desired; +} +template +T operator -= (std::atomic& x, const T rhs) { return x += -rhs; } + +namespace Learner +{ + +// 局面の配列 : PSVector は packed sfen vector の略。 +typedef std::vector PSVector; + +// ----------------------------------- +// 局面のファイルへの書き出し +// ----------------------------------- + +// Sfenを書き出して行くためのヘルパクラス +struct SfenWriter +{ + // 書き出すファイル名と生成するスレッドの数 + SfenWriter(string filename, int thread_num) + { + sfen_buffers_pool.reserve((size_t)thread_num * 10); + sfen_buffers.resize(thread_num); + + // 追加学習するとき、評価関数の学習後も生成される教師の質はあまり変わらず、教師局面数を稼ぎたいので + // 古い教師も使うのが好ましいのでこういう仕様にしてある。 + fs.open(filename, ios::out | ios::binary | ios::app); + filename_ = filename; + + finished = false; + } + + ~SfenWriter() + { + finished = true; + file_worker_thread.join(); + fs.close(); + + // file_worker_threadがすべて書き出したあとなのでbufferはすべて空のはずなのだが.. + for (auto p : sfen_buffers) { assert(p == nullptr); } + assert(sfen_buffers_pool.empty()); + } + + // 各スレッドについて、この局面数ごとにファイルにflushする。 + const size_t SFEN_WRITE_SIZE = 5000; + + // 局面と評価値をペアにして1つ書き出す(packされたsfen形式で) + void write(size_t thread_id, const PackedSfenValue& psv) + { + // スレッドごとにbufferを持っていて、そこに追加する。 + // bufferが溢れたら、ファイルに書き出す。 + + // このバッファはスレッドごとに用意されている。 + auto& buf = sfen_buffers[thread_id]; + + // 初回とスレッドバッファを書き出した直後はbufがないので確保する。 + if (!buf) + { + buf = new PSVector(); + buf->reserve(SFEN_WRITE_SIZE); + } + + // スレッドごとに用意されており、一つのスレッドが同時にこのwrite()関数を呼び出さないので + // この時点では排他する必要はない。 + buf->push_back(psv); + + if (buf->size() >= SFEN_WRITE_SIZE) + { + // sfen_buffers_poolに積んでおけばあとはworkerがよきに計らってくれる。 + + // sfen_buffers_poolの内容を変更するときはmutexのlockが必要。 + std::unique_lock lk(mutex); + sfen_buffers_pool.push_back(buf); + + buf = nullptr; + // buf == nullptrにしておけば次回にこの関数が呼び出されたときにバッファは確保される。 + } + } + + // 自分のスレッド用のバッファに残っている分をファイルに書き出すためのバッファに移動させる。 + void finalize(size_t thread_id) + { + std::unique_lock lk(mutex); + + auto& buf = sfen_buffers[thread_id]; + + // buf==nullptrであるケースもあるのでそのチェックが必要。 + if (buf && buf->size() != 0) + sfen_buffers_pool.push_back(buf); + + buf = nullptr; + } + + // write_workerスレッドを開始する。 + void start_file_write_worker() + { + file_worker_thread = std::thread([&] { this->file_write_worker(); }); + } + + // ファイルに書き出すの専用スレッド + void file_write_worker() + { + auto output_status = [&]() + { + // 現在時刻も出力 + sync_cout << endl << sfen_write_count << " sfens , at " << now_string() << sync_endl; + + // flush()はこのタイミングで十分。 + fs.flush(); + }; + + while (!finished || sfen_buffers_pool.size()) + { + vector buffers; + { + std::unique_lock lk(mutex); + + // まるごとコピー + buffers = sfen_buffers_pool; + sfen_buffers_pool.clear(); + } + + // 何も取得しなかったならsleep() + if (!buffers.size()) + sleep(100); + else + { + for (auto ptr : buffers) + { + fs.write((const char*)&((*ptr)[0]), sizeof(PackedSfenValue) * ptr->size()); + + sfen_write_count += ptr->size(); + +#if 1 + // 処理した件数をここに加算していき、save_everyを超えたら、ファイル名を変更し、このカウンターをリセットする。 + save_every_counter += ptr->size(); + if (save_every_counter >= save_every) + { + save_every_counter = 0; + // ファイル名を変更。 + + fs.close(); + + // ファイルにつける連番 + int n = (int)(sfen_write_count / save_every); + // ファイル名を変更して再度openする。上書き考慮してios::appをつけておく。(運用によっては、ないほうがいいかも..) + string filename = filename_ + "_" + std::to_string(n); + fs.open(filename, ios::out | ios::binary | ios::app); + cout << endl << "output sfen file = " << filename << endl; + } +#endif + + // 棋譜を書き出すごとに'.'を出力。 + std::cout << "."; + + // 40回ごとに処理した局面数を出力 + // 最後、各スレッドの教師局面の余りを書き出すので中途半端な数が表示されるが、まあいいか…。 + // スレッドを論理コアの最大数まで酷使するとコンソールが詰まるのでもう少し間隔甘くてもいいと思う。 + if ((++time_stamp_count % 40) == 0) + output_status(); + + // このメモリは不要なのでこのタイミングで開放しておく。 + delete ptr; + } + } + } + + // 終了前にもう一度、タイムスタンプを出力。 + output_status(); + } + + // この単位でファイル名を変更する。 + uint64_t save_every = UINT64_MAX; + +private: + + fstream fs; + + // コンストラクタで渡されたファイル名 + std::string filename_; + + // 処理した件数をここに加算していき、save_everyを超えたら、ファイル名を変更し、このカウンターをリセットする。 + uint64_t save_every_counter = 0; + + // ファイルに書き込む用のthread + std::thread file_worker_thread; + // すべてのスレッドが終了したかのフラグ + atomic finished; + + // タイムスタンプの出力用のカウンター + uint64_t time_stamp_count = 0; + + // ファイルに書き出す前のバッファ + // sfen_buffersは各スレッドに対するバッファ + // sfen_buffers_poolは書き出しのためのバッファ。 + // 前者のバッファに局面をSFEN_WRITE_SIZEだけ積んだら、後者に積み替える。 + std::vector sfen_buffers; + std::vector sfen_buffers_pool; + + // sfen_buffers_poolにアクセスするときに必要なmutex + std::mutex mutex; + + // 書きだした局面の数 + uint64_t sfen_write_count = 0; +}; + +// ----------------------------------- +// 棋譜を生成するworker(スレッドごと) +// ----------------------------------- + +// 複数スレッドでsfenを生成するためのクラス +struct MultiThinkGenSfen : public MultiThink +{ + MultiThinkGenSfen(int search_depth_, int search_depth2_, SfenWriter& sw_) + : search_depth(search_depth_), search_depth2(search_depth2_), sw(sw_) + { + hash.resize(GENSFEN_HASH_SIZE); + + // PCを並列化してgensfenするときに同じ乱数seedを引いていないか確認用の出力。 + std::cout << prng << std::endl; + } + + virtual void thread_worker(size_t thread_id); + void start_file_write_worker() { sw.start_file_write_worker(); } + + // search_depth = 通常探索の探索深さ + int search_depth; + int search_depth2; + + // 生成する局面の評価値の上限 + int eval_limit; + + // ランダムムーブを行なう最小ply + int random_move_minply; + // ランダムムーブを行なう最大ply + int random_move_maxply; + // 1局のなかでランダムムーブを行なう回数 + int random_move_count; + // Aperyのようにランダムムーブのときに1/Nの確率で玉を動かす。 + // また玉を動かしたときは1/Nの確率で相手番で1回ランダムムーブする。 + // AperyはN=2。ここ0を指定するとこの機能を無効化する。 + int random_move_like_apery; + + // ランダムムーブの代わりにmulti pvを使うとき用。 + // random_multi_pvは、MultiPVのときの候補手の数。 + // 候補手の指し手を採択するとき、1位の指し手の評価値とN位の指し手の評価値との差が + // random_multi_pv_diffの範囲でなければならない。 + // random_multi_pv_depthはMultiPVのときの探索深さ。 + int random_multi_pv; + int random_multi_pv_diff; + int random_multi_pv_depth; + + // 書き出す局面のply(初期局面からの手数)の最小、最大。 + int write_minply; + int write_maxply; + + // sfenの書き出し器 + SfenWriter& sw; + + // 同一局面の書き出しを制限するためのhash + // hash_indexを求めるためのmaskに使うので、2**Nでなければならない。 + static const uint64_t GENSFEN_HASH_SIZE = 64 * 1024 * 1024; + + vector hash; // 64MB*sizeof(HASH_KEY) = 512MB +}; + +// thread_id = 0..Threads.size()-1 +void MultiThinkGenSfen::thread_worker(size_t thread_id) +{ + // とりあえず、書き出す手数の最大のところで引き分け扱いになるものとする。 + const int MAX_PLY2 = write_maxply; + + // StateInfoを最大手数分 + SearchのPVでleafにまで進めるbuffer + std::vector> states(MAX_PLY2 + MAX_PLY /* == search_depth + α */); + StateInfo si; + + // 今回の指し手。この指し手で局面を進める。 + Move m = MOVE_NONE; + + // 終了フラグ + bool quit = false; + + // 規定回数回になるまで繰り返し + while (!quit) + { + // Positionに対して従属スレッドの設定が必要。 + // 並列化するときは、Threads (これが実体が vectorなので、 + // Threads[0]...Threads[thread_num-1]までに対して同じようにすれば良い。 + auto th = Threads[thread_id]; + + auto& pos = th->rootPos; + pos.set(StartFEN, false, &si, th); + + // Test cod for Packed SFEN. + //{ + // PackedSfen packed_sfen; + // pos.sfen_pack(packed_sfen); + // std::cout << pos << std::endl; + // pos.set_from_packed_sfen(packed_sfen, &si, th); + // std::string actual = pos.fen(); + // assert(actual == StartFEN); + //} + + // 探索部で定義されているBookMoveSelectorのメンバを参照する。 + //auto& book = ::book; + + // 1局分の局面を保存しておき、終局のときに勝敗を含めて書き出す。 + // 書き出す関数は、この下にあるflush_psv()である。 + PSVector a_psv; + a_psv.reserve(MAX_PLY2 + MAX_PLY); + + // a_psvに積まれている局面をファイルに書き出す。 + // lastTurnIsWin : a_psvに積まれている最終局面の次の局面での勝敗 + // 勝ちのときは1。負けのときは-1。引き分けのときは0を渡す。 + // 返し値 : もう規定局面数に達したので終了する場合にtrue。 + auto flush_psv = [&](int8_t lastTurnIsWin) + { + int8_t isWin = lastTurnIsWin; + + // 終局の局面(の一つ前)から初手に向けて、各局面に関して、対局の勝敗の情報を付与しておく。 + // a_psvに保存されている局面は(手番的に)連続しているものとする。 + for (auto it = a_psv.rbegin(); it != a_psv.rend(); ++it) + { + // isWin == 0(引き分け)なら -1を掛けても 0(引き分け)のまま + isWin = - isWin; + it->game_result = isWin; + + // 局面を書き出そうと思ったら規定回数に達していた。 + // get_next_loop_count()内でカウンターを加算するので + // 局面を出力したときにこれを呼び出さないとカウンターが狂う。 + auto loop_count = get_next_loop_count(); + if (loop_count == UINT64_MAX) + { + // 終了フラグを立てておく。 + quit = true; + return; + } + + // 局面を一つ書き出す。 + sw.write(thread_id, *it); + +#if 0 + pos.set_from_packed_sfen(it->sfen); + cout << pos << "Win : " << it->isWin << " , " << it->score << endl; +#endif + } + }; + + // ply手目でランダムムーブをするかどうかのフラグ + vector random_move_flag; + { + // ランダムムーブを入れるならrandom_move_maxply手目までに絶対にrandom_move_count回入れる。 + // そこそこばらけて欲しい。 + // どれくらいがベストなのかはよくわからない。色々条件を変えて実験中。 + + // a[0] = 0 , a[1] = 1, ... みたいな配列を作って、これを + // Fisher-Yates shuffleして先頭のN個を取り出せば良い。 + // 実際には、N個欲しいだけなので先頭N個分だけFisher-Yatesでshuffleすれば良い。 + + vector a; + a.reserve((size_t)random_move_maxply); + + // random_move_minply , random_move_maxplyは1 originで指定されるが、 + // ここでは0 originで扱っているので注意。 + for (int i = std::max(random_move_minply - 1 , 0) ; i < random_move_maxply; ++i) + a.push_back(i); + + // Apery方式のランダムムーブの場合、insert()がrandom_move_count回呼び出される可能性があるので + // それを考慮したサイズだけ確保しておく。 + random_move_flag.resize((size_t)random_move_maxply + random_move_count); + + // a[]のsize()を超える回数のランダムムーブは適用できないので制限する。 + for (int i = 0 ; i < std::min(random_move_count, (int)a.size()) ; ++i) + { + swap(a[i], a[prng.rand((uint64_t)a.size() - i) + i]); + random_move_flag[a[i]] = true; + } + } + + // random moveを行なった回数をカウントしておくカウンター + // random_move_minply == -1のときに、連続してランダムムーブを行なうので、このときに用いる。 + int random_move_c = 0; + + // ply : 初期局面からの手数 + for (int ply = 0; ; ++ply) + { + //cout << pos << endl; + + // 今回の探索depth + // gotoで飛ぶので先に宣言しておく。 + int depth = search_depth + (int)prng.rand(search_depth2 - search_depth + 1); + + // 長手数に達したのか + if (ply >= MAX_PLY2) + { +#if defined (LEARN_GENSFEN_USE_DRAW_RESULT) + // 勝敗 = 引き分けとして書き出す。 + // こうしたほうが自分が入玉したときに、相手の入玉を許しにくい(かも) + flush_psv(0); +#endif + break; + } + + if (pos.is_draw(ply)) { + // Do not write if draw. + break; + } + + // 全駒されて詰んでいたりしないか? + if (MoveList(pos).size() == 0) + { + // (この局面の一つ前の局面までは書き出す) + // Write the positions other than this position if checkmated. + flush_psv(-1); + break; + } + + //// 定跡 + //if ((m = book.probe(pos)) != MOVE_NONE) + //{ + // // 定跡にhitした。 + // // その指し手はmに格納された。 + + // // 定跡の局面は学習には用いない。 + // a_psv.clear(); + + // if (random_move_minply != -1) + // // 定跡の局面であっても、一定確率でランダムムーブは行なう。 + // goto RANDOM_MOVE; + // else + // // random_move_minplyとして-1が指定されているときは定跡を抜けるところまでは定跡に従って指す。 + // // 巨大定跡を用いて、ConsiderBookMoveCount trueとして定跡を抜けた局面を無数に用意しておき、 + // // そこから5回ランダムムーブを行なう、などの用途に用いる。 + // goto DO_MOVE; + //} + + { + // search_depth~search_depth2 手読みの評価値とPV(最善応手列) + // 探索窓を狭めておいても問題ないはず。 + + auto pv_value1 = search(pos, depth); + + auto value1 = pv_value1.first; + auto& pv1 = pv_value1.second; + + // 評価値の絶対値がこの値以上の局面については + // その局面を学習に使うのはあまり意味がないのでこの試合を終了する。 + // これをもって勝敗がついたという扱いをする。 + + // 1手詰め、宣言勝ちならば、ここでmate_in(2)が返るのでeval_limitの上限値と同じ値になり、 + // このif式は必ず真になる。resignについても同様。 + + if (abs(value1) >= eval_limit) + { +// sync_cout << pos << "eval limit = " << eval_limit << " over , move = " << pv1[0] << sync_endl; + + // この局面でvalue1 >= eval_limitならば、(この局面の手番側の)勝ちである。 + flush_psv((value1 >= eval_limit) ? 1 : -1); + break; + } + + // おかしな指し手の検証 + if (pv1.size() > 0 + && (pv1[0] == MOVE_NONE || pv1[0] == MOVE_NULL) + ) + { + // MOVE_WINは、この手前で宣言勝ちの局面であるかチェックしているので + // ここで宣言勝ちの指し手が返ってくることはないはず。 + // また、MOVE_RESIGNのときvalue1は1手詰めのスコアであり、eval_limitの最小値(-31998)のはずなのだが…。 + cout << "Error! : " << pos.fen() << m << value1 << endl; + break; + } + + // 各千日手に応じた処理。 + + if (pos.is_draw(0)) { +#if defined (LEARN_GENSFEN_USE_DRAW_RESULT) + // 引き分けを書き出すとき + flush_psv(is_win); +#endif + break; + } + + // PVの指し手でleaf nodeまで進めて、そのleaf nodeでevaluate()を呼び出した値を用いる。 + auto evaluate_leaf = [&](Position& pos , vector& pv) + { + auto rootColor = pos.side_to_move(); + + int ply2 = ply; + for (auto m : pv) + { + // デバッグ用の検証として、途中に非合法手が存在しないことを確認する。 + // NULL_MOVEはこないものとする。 + + // 十分にテストしたのでコメントアウトで良い。 +#if 1 + // 非合法手はやってこないはずなのだが。 + // 宣言勝ちとmated()でないことは上でテストしているので + // 読み筋としてMOVE_WINとMOVE_RESIGNが来ないことは保証されている。(はずだが…) + if (!pos.pseudo_legal(m) || !pos.legal(m)) + { + cout << "Error! : " << pos.fen() << m << endl; + } +#endif + pos.do_move(m, states[ply2++]); + + // 毎ノードevaluate()を呼び出さないと、evaluate()の差分計算が出来ないので注意! + // depthが8以上だとこの差分計算はしないほうが速いと思われる。 +#if defined(EVAL_NNUE) + if (depth < 8) + Eval::evaluate_with_no_return(pos); +#endif // defined(EVAL_NNUE) + } + + // leafに到達 + // cout << pos; + + auto v = Eval::evaluate(pos); + // evaluate()は手番側の評価値を返すので、 + // root_colorと違う手番なら、vを反転させて返さないといけない。 + if (rootColor != pos.side_to_move()) + v = -v; + + // 巻き戻す。 + // C++x14にもなって、いまだreverseで回すforeachすらないのか…。 + // for (auto it : boost::adaptors::reverse(pv)) + + for (auto it = pv.rbegin(); it != pv.rend(); ++it) + pos.undo_move(*it); + + return v; + }; + +#if 0 + dbg_hit_on(pv_value1.first == leaf_value); + // gensfen depth 3 eval_limit 32000 + // Total 217749 Hits 203579 hit rate (%) 93.490 + // gensfen depth 6 eval_limit 32000 + // Total 78407 Hits 69190 hit rate (%) 88.245 + // gensfen depth 6 eval_limit 3000 + // Total 53879 Hits 43713 hit rate (%) 81.132 + + // 置換表の指し手で枝刈りされるなどの問題。 + // これ、教師としては少し気持ち悪いが…。 +#endif + + // depth 0の場合、pvが得られていないのでdepth 2で探索しなおす。 + if (search_depth <= 0) + { + pv_value1 = search(pos, 2); + pv1 = pv_value1.second; + } + + // 初期局面周辺はは類似局面ばかりなので + // 学習に用いると過学習になりかねないから書き出さない。 + // → 比較実験すべき + if (ply < write_minply - 1) + { + a_psv.clear(); + goto SKIP_SAVE; + } + + // 同一局面を書き出したところか? + // これ、複数のPCで並列して生成していると同じ局面が含まれることがあるので + // 読み込みのときにも同様の処理をしたほうが良い。 + { + auto key = pos.key(); + auto hash_index = (size_t)(key & (GENSFEN_HASH_SIZE - 1)); + auto key2 = hash[hash_index]; + if (key == key2) + { + // スキップするときはこれ以前に関する + // 勝敗の情報がおかしくなるので保存している局面をクリアする。 + // どのみち、hashが合致した時点でそこ以前の局面も合致している可能性が高いから + // 書き出す価値がない。 + a_psv.clear(); + goto SKIP_SAVE; + } + hash[hash_index] = key; // 今回のkeyに入れ替えておく。 + } + + // 局面の一時保存。 + { + a_psv.emplace_back(PackedSfenValue()); + auto &psv = a_psv.back(); + + // packを要求されているならpackされたsfenとそのときの評価値を書き出す。 + // 最終的な書き出しは、勝敗がついてから。 + pos.sfen_pack(psv.sfen); + + //{ + // std::string before_fen = pos.fen(); + // pos.set_from_packed_sfen(psv.sfen, &si, th); + // std::string after_fen = pos.fen(); + // assert(before_fen == after_fen); + //} + + // PV lineのleaf nodeでのroot colorから見たevaluate()の値を取得。 + // search()の返し値をそのまま使うのとこうするのとの善悪は良くわからない。 + psv.score = evaluate_leaf(pos, pv1); + psv.gamePly = ply; + + // PVの初手を取り出す。これはdepth 0でない限りは存在するはず。 + assert(pv_value1.second.size() >= 1); + Move pv_move1 = pv_value1.second[0]; + psv.move = pv_move1; + } + + SKIP_SAVE:; + + // 何故かPVが得られなかった(置換表などにhitして詰んでいた?)ので次の対局に行く。 + // かなりのレアケースなので無視して良いと思う。 + if (pv1.size() == 0) + break; + + // search_depth手読みの指し手で局面を進める。 + m = pv1[0]; + } + + RANDOM_MOVE:; + + // 合法手のなかからランダムに1手選ぶフェーズ + if ( + // 1. random_move_minplyからrandom_move_maxplyの間でrandom_move_count回のランダムムーブを行なうモード + (random_move_minply != -1 && ply < (int)random_move_flag.size() && random_move_flag[ply]) || + // 2. 定跡を抜けたあとにまとめてrandom_move_count回のランダムムーブを行なうモード + (random_move_minply == -1 && random_move_c < random_move_count)) + { + ++random_move_c; + + // mateではないので合法手が1手はあるはず…。 + if (random_multi_pv == 0) + { + // 普通のランダムムーブ + + MoveList list(pos); + + // ここをApery方式にするのとの善悪はよくわからない。 + if (random_move_like_apery == 0 + || prng.rand(random_move_like_apery) != 0 + ) + { + // 普通に合法手から1手選択 + m = list.at((size_t)prng.rand((uint64_t)list.size())); + } + else { + // 玉が動かせるなら玉を動かす + Move moves[8]; // 8近傍 + Move* p = &moves[0]; + for (auto& m : list) + if (type_of(pos.moved_piece(m)) == KING) + *(p++) = m; + size_t n = p - &moves[0]; + if (n != 0) + { + // 玉を動かす指し手 + m = moves[prng.rand(n)]; + + // Apery方式ではこのとき1/2の確率で相手もランダムムーブ + if (prng.rand(2) == 0) + { + // random_move_flag[ply]の次のところに"1"を追加するのがシンプルなhackか。 + random_move_flag.insert(random_move_flag.begin() + ply + 1, 1, true); + } + } + else + // 普通に合法手から1手選択 + m = list.at((size_t)prng.rand((uint64_t)list.size())); + } + + // 玉の2手指しのコードを入れていたが、合法手から1手選べばそれに相当するはずで + // コードが複雑化するだけだから不要だと判断した。 + } + else { + // ロジックが複雑になるので、すまんがここで再度MultiPVで探索する。 + Learner::search(pos, random_multi_pv_depth, random_multi_pv); + // rootMovesの上位N手のなかから一つ選択 + + auto& rm = pos.this_thread()->rootMoves; + + uint64_t s = min((uint64_t)rm.size(), (uint64_t)random_multi_pv); + for (uint64_t i = 1; i < s; ++i) + { + // rm[0]の評価値との差がrandom_multi_pv_diffの範囲でなければならない。 + // rm[x].scoreは、降順に並んでいると仮定できる。 + if (rm[0].score > rm[i].score + random_multi_pv_diff) + { + s = i; + break; + } + } + + m = rm[prng.rand(s)].pv[0]; + + // まだ1局面も書き出していないのに終局してたので書き出し処理は端折って次の対局に。 + if (!is_ok(m)) + break; + } + + // ゲームの勝敗から指し手を評価しようとするとき、 + // 今回のrandom moveがあるので、ここ以前には及ばないようにする。 + a_psv.clear(); // 保存していた局面のクリア + } + + DO_MOVE:; + pos.do_move(m, states[ply]); + + // 差分計算を行なうために毎node evaluate()を呼び出しておく。 + Eval::evaluate_with_no_return(pos); + + } // for (int ply = 0; ; ++ply) + + } // while(!quit) + + sw.finalize(thread_id); +} + +// ----------------------------------- +// 棋譜を生成するコマンド(master thread) +// ----------------------------------- + +// 棋譜を生成するコマンド +void gen_sfen(Position&, istringstream& is) +{ + // スレッド数(これは、USIのsetoptionで与えられる) + uint32_t thread_num = (uint32_t)Options["Threads"]; + + // 生成棋譜の個数 default = 80億局面(Ponanza仕様) + uint64_t loop_max = 8000000000UL; + + // 評価値がこの値になったら生成を打ち切る。 + int eval_limit = 3000; + + // 探索深さ + int search_depth = 3; + int search_depth2 = INT_MIN; + + // ランダムムーブを行なう最小plyと最大plyと回数 + int random_move_minply = 1; + int random_move_maxply = 24; + int random_move_count = 5; + // ランダムムーブをAperyのように玉を主に動かす機能 + // これを例えば3にすると1/3の確率で玉を動かす。 + int random_move_like_apery = 0; + // ランダムムーブの代わりにmultipvで探索してそのなかからランダムに選ぶときはrandom_multi_pv = 1以上の数にする。 + int random_multi_pv = 0; + int random_multi_pv_diff = 32000; + int random_multi_pv_depth = INT_MIN; + + // 書き出す局面のply(初期局面からの手数)の最小、最大。 + int write_minply = 16; + int write_maxply = 400; + + // 書き出すファイル名 + string output_file_name = "generated_kifu.bin"; + + string token; + + // eval hashにhitすると初期局面付近の評価値として、hash衝突して大きな値を書き込まれてしまうと + // eval_limitが小さく設定されているときに初期局面で毎回eval_limitを超えてしまい局面の生成が進まなくなる。 + // そのため、eval hashは無効化する必要がある。 + // あとeval hashのhash衝突したときに、変な値の評価値が使われ、それを教師に使うのが気分が悪いというのもある。 + bool use_eval_hash = false; + + // この単位でファイルに保存する。 + // ファイル名は file_1.bin , file_2.binのように連番がつく。 + uint64_t save_every = UINT64_MAX; + + // ファイル名の末尾にランダムな数値を付与する。 + bool random_file_name = false; + + while (true) + { + token = ""; + is >> token; + if (token == "") + break; + + if (token == "depth") + is >> search_depth; + else if (token == "depth2") + is >> search_depth2; + else if (token == "loop") + is >> loop_max; + else if (token == "output_file_name") + is >> output_file_name; + else if (token == "eval_limit") + { + is >> eval_limit; + // 最大値を1手詰みのスコアに制限する。(そうしないとループを終了しない可能性があるので) + eval_limit = std::min(eval_limit, (int)mate_in(2)); + } + else if (token == "random_move_minply") + is >> random_move_minply; + else if (token == "random_move_maxply") + is >> random_move_maxply; + else if (token == "random_move_count") + is >> random_move_count; + else if (token == "random_move_like_apery") + is >> random_move_like_apery; + else if (token == "random_multi_pv") + is >> random_multi_pv; + else if (token == "random_multi_pv_diff") + is >> random_multi_pv_diff; + else if (token == "random_multi_pv_depth") + is >> random_multi_pv_depth; + else if (token == "write_minply") + is >> write_minply; + else if (token == "write_maxply") + is >> write_maxply; + else if (token == "use_eval_hash") + is >> use_eval_hash; + else if (token == "save_every") + is >> save_every; + else if (token == "random_file_name") + is >> random_file_name; + else + cout << "Error! : Illegal token " << token << endl; + } + +#if defined(USE_GLOBAL_OPTIONS) + // あとで復元するために保存しておく。 + auto oldGlobalOptions = GlobalOptions; + GlobalOptions.use_eval_hash = use_eval_hash; +#endif + + // search depth2が設定されていないなら、search depthと同じにしておく。 + if (search_depth2 == INT_MIN) + search_depth2 = search_depth; + if (random_multi_pv_depth == INT_MIN) + random_multi_pv_depth = search_depth; + + if (random_file_name) + { + // output_file_nameにこの時点でランダムな数値を付与してしまう。 + std::random_device seed_gen; + PRNG r(seed_gen()); + // 念のため乱数振り直しておく。 + for(int i=0;i<10;++i) + r.rand(1); + auto to_hex = [](uint64_t u){ + std::stringstream ss; + ss << std::hex << u; + return ss.str(); + }; + // 64bitの数値で偶然かぶると嫌なので念のため64bitの数値2つくっつけておく。 + output_file_name = output_file_name + "_" + to_hex(r.rand()) + to_hex(r.rand()); + } + + std::cout << "gensfen : " << endl + << " search_depth = " << search_depth << " to " << search_depth2 << endl + << " loop_max = " << loop_max << endl + << " eval_limit = " << eval_limit << endl + << " thread_num (set by USI setoption) = " << thread_num << endl + << " book_moves (set by USI setoption) = " << Options["BookMoves"] << endl + << " random_move_minply = " << random_move_minply << endl + << " random_move_maxply = " << random_move_maxply << endl + << " random_move_count = " << random_move_count << endl + << " random_move_like_apery = " << random_move_like_apery << endl + << " random_multi_pv = " << random_multi_pv << endl + << " random_multi_pv_diff = " << random_multi_pv_diff << endl + << " random_multi_pv_depth = " << random_multi_pv_depth << endl + << " write_minply = " << write_minply << endl + << " write_maxply = " << write_maxply << endl + << " output_file_name = " << output_file_name << endl + << " use_eval_hash = " << use_eval_hash << endl + << " save_every = " << save_every << endl + << " random_file_name = " << random_file_name << endl; + + // Options["Threads"]の数だけスレッドを作って実行。 + { + SfenWriter sw(output_file_name, thread_num); + sw.save_every = save_every; + + MultiThinkGenSfen multi_think(search_depth, search_depth2, sw); + multi_think.set_loop_max(loop_max); + multi_think.eval_limit = eval_limit; + multi_think.random_move_minply = random_move_minply; + multi_think.random_move_maxply = random_move_maxply; + multi_think.random_move_count = random_move_count; + multi_think.random_move_like_apery = random_move_like_apery; + multi_think.random_multi_pv = random_multi_pv; + multi_think.random_multi_pv_diff = random_multi_pv_diff; + multi_think.random_multi_pv_depth = random_multi_pv_depth; + multi_think.write_minply = write_minply; + multi_think.write_maxply = write_maxply; + multi_think.start_file_write_worker(); + multi_think.go_think(); + + // SfenWriterのデストラクタでjoinするので、joinが終わってから終了したというメッセージを + // 表示させるべきなのでここをブロックで囲む。 + } + + std::cout << "gensfen finished." << endl; + +#if defined(USE_GLOBAL_OPTIONS) + // GlobalOptionsの復元。 + GlobalOptions = oldGlobalOptions; +#endif + +} + +// ----------------------------------- +// 生成した棋譜から学習させるコマンド(learn) +// ----------------------------------- + +// 普通のシグモイド関数 +double sigmoid(double x) +{ + return 1.0 / (1.0 + std::exp(-x)); +} + +// 評価値を勝率[0,1]に変換する関数 +double winning_percentage(double value) +{ + // この600.0という定数は、ponanza定数。(ponanzaがそうしているらしいという意味で) + // ゲームの進行度に合わせたものにしたほうがいいかも知れないけども、その効果のほどは不明。 + return sigmoid(value / 600.0); +} + +// 普通のシグモイド関数の導関数。 +double dsigmoid(double x) +{ + // シグモイド関数 + // f(x) = 1/(1+exp(-x)) + // に対して1階微分は、 + // f'(x) = df/dx = f(x)・{ 1 - f(x) } + // となる。 + + return sigmoid(x) * (1.0 - sigmoid(x)); +} + +// 目的関数が勝率の差の二乗和のとき +#if defined (LOSS_FUNCTION_IS_WINNING_PERCENTAGE) +// 勾配を計算する関数 +double calc_grad(Value deep, Value shallow, PackedSfenValue& psv) +{ + // 勝率の差の2乗が目的関数それを最小化する。 + // 目的関数 J = 1/2m Σ ( win_rate(shallow) - win_rate(deep) ) ^2 + // ただし、σはシグモイド関数で、評価値を勝率の差に変換するもの。 + // mはサンプルの件数。shallowは浅い探索(qsearch())のときの評価値。deepは深い探索のときの評価値。 + // また、Wを特徴ベクトル(評価関数のパラメーター)、Xi,Yiを教師とすると + // shallow = W*Xi // *はアダマール積で、Wの転置・X の意味 + // f(Xi) = win_rate(W*Xi) + // σ(i番目のdeep) = Yi とおくと、 + // J = m/2 Σ ( f(Xi) - Yi )^2 + // とよくある式になる。 + // Wはベクトルで、j番目の要素をWjと書くとすると、連鎖律から + // ∂J/∂Wj = ∂J/∂f ・ ∂f/∂W ・ ∂W/∂Wj + // = 1/m Σ ( f(Xi) - y ) ・ f'(Xi) ・ 1 + + // 1/mはあとで掛けるとして、勾配の値としてはΣの中身を配列に保持しておけば良い。 + // f'(Xi) = win_rate'(shallow) = sigmoid'(shallow/600) = dsigmoid(shallow / 600) / 600 + // この末尾の /600 は学習率で調整するから書かなくていいか.. + // また1/mという係数も、Adam , AdaGradのような勾配の自動調整機能を持つ更新式を用いるなら不要。 + // ゆえにメモリ上に保存しておく必要はない。 + + double p = winning_percentage(deep); + double q = winning_percentage(shallow); + return (q - p) * dsigmoid(double(shallow) / 600.0); +} +#endif + +#if defined (LOSS_FUNCTION_IS_CROSS_ENTOROPY) +double calc_grad(Value deep, Value shallow, const PackedSfenValue& psv) +{ + // 交差エントロピーを用いた目的関数 + + // 交差エントロピーの概念と性質については、 + // http://nnadl-ja.github.io/nnadl_site_ja/chap3.html#the_cross-entropy_cost_function + // http://postd.cc/visual-information-theory-3/ + // などを参考に。 + + // 目的関数の設計) + // pの分布をqの分布に近づけたい → pとqの確率分布間の交差エントロピーの最小化問題と考える。 + // J = H(p,q) = - Σ p(x) log(q(x)) = -p log q - (1-p) log(1-q) + // x + + // pは定数、qはWiの関数(q = σ(W・Xi) )としてWiに対する偏微分を求める。 + // ∂J/∂Wi = -p・q'/q - (1-p)(1-q)'/(1-q) + // = ... + // = q - p. + + double p = winning_percentage(deep); + double q = winning_percentage(shallow); + + return q - p; +} +#endif + +#if defined ( LOSS_FUNCTION_IS_CROSS_ENTOROPY_FOR_VALUE ) +double calc_grad(Value deep, Value shallow, const PackedSfenValue& psv) +{ + // 勝率の関数を通さない版 + // これ、EVAL_LIMITを低くしておかないと、終盤の形に対して評価値を一致させようとして + // evalがevalの範囲を超えかねない。 + return shallow - deep; +} +#endif + +#if defined ( LOSS_FUNCTION_IS_ELMO_METHOD ) + +// elmo(WCSC27)で使われている定数。要調整。 +// elmoのほうは式を内分していないので値が違う。 +// learnコマンドでこの値を設定できる。 +// 0.33は、elmo(WCSC27)で使われていた定数(0.5)相当 +double ELMO_LAMBDA = 0.33; +double ELMO_LAMBDA2 = 0.33; +double ELMO_LAMBDA_LIMIT = 32000; + +double calc_grad(Value deep, Value shallow , const PackedSfenValue& psv) +{ + // elmo(WCSC27)方式 + // 実際のゲームの勝敗で補正する。 + + const double eval_winrate = winning_percentage(shallow); + const double teacher_winrate = winning_percentage(deep); + + // 期待勝率を勝っていれば1、負けていれば 0、引き分けなら0.5として補正項として用いる。 + // game_result = 1,0,-1なので1足して2で割る。 + const double t = double(psv.game_result + 1) / 2; + + // 深い探索での評価値がELMO_LAMBDA_LIMITを超えているならELMO_LAMBDAではなくELMO_LAMBDA2を適用する。 + const double lambda = (abs(deep) >= ELMO_LAMBDA_LIMIT) ? ELMO_LAMBDA2 : ELMO_LAMBDA; + + // 実際の勝率を補正項として使っている。 + // これがelmo(WCSC27)のアイデアで、現代のオーパーツ。 + const double grad = (1 - lambda) * (eval_winrate - t) + lambda * (eval_winrate - teacher_winrate); + + return grad; +} + +// 学習時の交差エントロピーの計算 +// elmo式の勝敗項と勝率項との個別の交差エントロピーが引数であるcross_entropy_evalとcross_entropy_winに返る。 +void calc_cross_entropy(Value deep, Value shallow, const PackedSfenValue& psv, + double& cross_entropy_eval, double& cross_entropy_win, double& cross_entropy, + double& entropy_eval, double& entropy_win, double& entropy) +{ + const double p /* teacher_winrate */ = winning_percentage(deep); + const double q /* eval_winrate */ = winning_percentage(shallow); + const double t = double(psv.game_result + 1) / 2; + + constexpr double epsilon = 0.000001; + + // 深い探索での評価値がELMO_LAMBDA_LIMITを超えているならELMO_LAMBDAではなくELMO_LAMBDA2を適用する。 + const double lambda = (abs(deep) >= ELMO_LAMBDA_LIMIT) ? ELMO_LAMBDA2 : ELMO_LAMBDA; + + const double m = (1.0 - lambda) * t + lambda * p; + + cross_entropy_eval = + (-p * std::log(q + epsilon) - (1.0 - p) * std::log(1.0 - q + epsilon)); + cross_entropy_win = + (-t * std::log(q + epsilon) - (1.0 - t) * std::log(1.0 - q + epsilon)); + entropy_eval = + (-p * std::log(p + epsilon) - (1.0 - p) * std::log(1.0 - p + epsilon)); + entropy_win = + (-t * std::log(t + epsilon) - (1.0 - t) * std::log(1.0 - t + epsilon)); + + cross_entropy = + (-m * std::log(q + epsilon) - (1.0 - m) * std::log(1.0 - q + epsilon)); + entropy = + (-m * std::log(m + epsilon) - (1.0 - m) * std::log(1.0 - m + epsilon)); +} + +#endif + + +// 目的関数として他のバリエーションも色々用意するかも.. + +double calc_grad(Value shallow, const PackedSfenValue& psv) { + return calc_grad((Value)psv.score, shallow, psv); +} + +// Sfenの読み込み機 +struct SfenReader +{ + SfenReader(int thread_num) : prng((std::random_device())()) + { + packed_sfens.resize(thread_num); + total_read = 0; + total_done = 0; + last_done = 0; + next_update_weights = 0; + save_count = 0; + end_of_files = false; + no_shuffle = false; + stop_flag = false; + + hash.resize(READ_SFEN_HASH_SIZE); + } + + ~SfenReader() + { + if (file_worker_thread.joinable()) + file_worker_thread.join(); + + for (auto p : packed_sfens) + delete p; + for (auto p : packed_sfens_pool) + delete p; + } + + // mseなどの計算用に用いる局面数 + // mini-batch size = 1Mが標準的なので、その0.2%程度なら時間的には無視できるはず。 + // 指し手一致率の計算でdepth = 1でsearch()をするので、単純比較はできないが…。 + const uint64_t sfen_for_mse_size = 2000; + + // mseなどの計算用に局面を読み込んでおく。 + void read_for_mse() + { + auto th = Threads.main(); + Position& pos = th->rootPos; + for (uint64_t i = 0; i < sfen_for_mse_size; ++i) + { + PackedSfenValue ps; + if (!read_to_thread_buffer(0, ps)) + { + cout << "Error! read packed sfen , failed." << endl; + break; + } + sfen_for_mse.push_back(ps); + + // hash keyを求める。 + StateInfo si; + pos.set_from_packed_sfen(ps.sfen,&si,th); + sfen_for_mse_hash.insert(pos.key()); + } + } + + void read_validation_set(const string file_name, int eval_limit) + { + ifstream fs(file_name, ios::binary); + + while (fs) + { + PackedSfenValue p; + if (fs.read((char*)&p, sizeof(PackedSfenValue))) + { + if (eval_limit < abs(p.score)) + continue; +#if !defined (LEARN_GENSFEN_USE_DRAW_RESULT) + if (p.game_result == 0) + continue; +#endif + + sfen_for_mse.push_back(p); + } else { + break; + } + } + } + + // 各スレッドがバッファリングしている局面数 0.1M局面。40HTで4M局面 + const size_t THREAD_BUFFER_SIZE = 10 * 1000; + + // ファイル読み込み用のバッファ(これ大きくしたほうが局面がshuffleが大きくなるので局面がバラけていいと思うが + // あまり大きいとメモリ消費量も上がる。 + // SFEN_READ_SIZEはTHREAD_BUFFER_SIZEの倍数であるものとする。 + const size_t SFEN_READ_SIZE = LEARN_SFEN_READ_SIZE; + + // [ASYNC] スレッドが局面を一つ返す。なければfalseが返る。 + bool read_to_thread_buffer(size_t thread_id, PackedSfenValue& ps) + { + // スレッドバッファに局面が残っているなら、それを1つ取り出して返す。 + auto& thread_ps = packed_sfens[thread_id]; + + // バッファに残りがなかったらread bufferから充填するが、それすらなかったらもう終了。 + if ((thread_ps == nullptr || thread_ps->size() == 0) // バッファが空なら充填する。 + && !read_to_thread_buffer_impl(thread_id)) + return false; + + // read_to_thread_buffer_impl()がtrueを返したというこは、 + // スレッドバッファへの局面の充填が無事完了したということなので + // thread_ps->rbegin()は健在。 + + ps = *(thread_ps->rbegin()); + thread_ps->pop_back(); + + // バッファを使いきったのであれば自らdeleteを呼び出してこのバッファを開放する。 + if (thread_ps->size() == 0) + { + delete thread_ps; + thread_ps = nullptr; + } + + return true; + } + + // [ASYNC] スレッドバッファに局面をある程度読み込む。 + bool read_to_thread_buffer_impl(size_t thread_id) + { + while (true) + { + { + std::unique_lock lk(mutex); + // ファイルバッファから充填できたなら、それで良し。 + if (packed_sfens_pool.size() != 0) + { + // 充填可能なようなので充填して終了。 + + packed_sfens[thread_id] = packed_sfens_pool.front(); + packed_sfens_pool.pop_front(); + + total_read += THREAD_BUFFER_SIZE; + + return true; + } + } + + // もうすでに読み込むファイルは無くなっている。もうダメぽ。 + if (end_of_files) + return false; + + // file workerがpacked_sfens_poolに充填してくれるのを待っている。 + // mutexはlockしていないのでいずれ充填してくれるはずだ。 + sleep(1); + } + + } + + // 局面ファイルをバックグラウンドで読み込むスレッドを起動する。 + void start_file_read_worker() + { + file_worker_thread = std::thread([&] { this->file_read_worker(); }); + } + + // ファイルの読み込み専用スレッド用 + void file_read_worker() + { + auto open_next_file = [&]() + { + if (fs.is_open()) + fs.close(); + + // もう無い + if (filenames.size() == 0) + return false; + + // 次のファイル名ひとつ取得。 + string filename = *filenames.rbegin(); + filenames.pop_back(); + + fs.open(filename, ios::in | ios::binary); + cout << "open filename = " << filename << endl; + assert(fs); + + return true; + }; + + while (true) + { + // バッファが減ってくるのを待つ。 + // このsize()の読み取りはread onlyなのでlockしなくていいだろう。 + while (!stop_flag && packed_sfens_pool.size() >= SFEN_READ_SIZE / THREAD_BUFFER_SIZE) + sleep(100); + if (stop_flag) + return; + + PSVector sfens; + sfens.reserve(SFEN_READ_SIZE); + + // ファイルバッファにファイルから読み込む。 + while (sfens.size() < SFEN_READ_SIZE) + { + PackedSfenValue p; + if (fs.read((char*)&p, sizeof(PackedSfenValue))) + { + sfens.push_back(p); + } else + { + // 読み込み失敗 + if (!open_next_file()) + { + // 次のファイルもなかった。あぼーん。 + cout << "..end of files." << endl; + end_of_files = true; + return; + } + } + } + + // この読み込んだ局面データをshuffleする。 + // random shuffle by Fisher-Yates algorithm + + if (!no_shuffle) + { + auto size = sfens.size(); + for (size_t i = 0; i < size; ++i) + swap(sfens[i], sfens[(size_t)(prng.rand((uint64_t)size - i) + i)]); + } + + // これをTHREAD_BUFFER_SIZEごとの細切れにする。それがsize個あるはず。 + // SFEN_READ_SIZEはTHREAD_BUFFER_SIZEの倍数であるものとする。 + assert((SFEN_READ_SIZE % THREAD_BUFFER_SIZE)==0); + + auto size = size_t(SFEN_READ_SIZE / THREAD_BUFFER_SIZE); + std::vector ptrs; + ptrs.reserve(size); + + for (size_t i = 0; i < size; ++i) + { + // このポインターのdeleteは、受け側で行なう。 + PSVector* ptr = new PSVector(); + ptr->resize(THREAD_BUFFER_SIZE); + memcpy(&((*ptr)[0]), &sfens[i * THREAD_BUFFER_SIZE], sizeof(PackedSfenValue) * THREAD_BUFFER_SIZE); + + ptrs.push_back(ptr); + } + + // sfensの用意が出来たので、折を見てコピー + { + std::unique_lock lk(mutex); + + // ポインタをコピーするだけなのでこの時間は無視できるはず…。 + // packed_sfens_poolの内容を変更するのでmutexのlockが必要。 + + for (size_t i = 0; i < size; ++i) + packed_sfens_pool.push_back(ptrs[i]); + } + } + } + + // sfenファイル群 + vector filenames; + + // 読み込んだ局面数(ファイルからメモリ上のバッファへ) + atomic total_read; + + // 処理した局面数 + atomic total_done; + + // 前回までに処理した件数 + uint64_t last_done; + + // total_readがこの値を超えたらupdate_weights()してmseの計算をする。 + uint64_t next_update_weights; + + uint64_t save_count; + + // 局面読み込み時のシャッフルを行わない。 + bool no_shuffle; + + bool stop_flag; + + // rmseの計算用の局面であるかどうかを判定する。 + // (rmseの計算用の局面は学習のために使うべきではない。) + bool is_for_rmse(Key key) const + { + return sfen_for_mse_hash.count(key) != 0; + } + + // 同一局面の読み出しを制限するためのhash + // 6400万局面って多すぎるか?そうでもないか.. + // hash_indexを求めるためのmaskに使うので、2**Nでなければならない。 + static const uint64_t READ_SFEN_HASH_SIZE = 64 * 1024 * 1024; + vector hash; // 64MB*8 = 512MB + + // mse計算用のtest局面 + PSVector sfen_for_mse; + +protected: + + // fileをバックグラウンドで読み込みしているworker thread + std::thread file_worker_thread; + + // 局面の読み込み時にshuffleするための乱数 + PRNG prng; + + // ファイル群を読み込んでいき、最後まで到達したか。 + atomic end_of_files; + + + // sfenファイルのハンドル + std::fstream fs; + + // 各スレッド用のsfen + // (使いきったときにスレッドが自らdeleteを呼び出して開放すべし。) + std::vector packed_sfens; + + // packed_sfens_poolにアクセスするときのmutex + std::mutex mutex; + + // sfenのpool。fileから読み込むworker threadはここに補充する。 + // 各worker threadはここから自分のpacked_sfens[thread_id]に充填する。 + // ※ mutexをlockしてアクセスすること。 + std::list packed_sfens_pool; + + // mse計算用の局面を学習に用いないためにhash keyを保持しておく。 + std::unordered_set sfen_for_mse_hash; +}; + +// 複数スレッドでsfenを生成するためのクラス +struct LearnerThink: public MultiThink +{ + LearnerThink(SfenReader& sr_):sr(sr_),stop_flag(false), save_only_once(false) + { +#if defined ( LOSS_FUNCTION_IS_ELMO_METHOD ) + learn_sum_cross_entropy_eval = 0.0; + learn_sum_cross_entropy_win = 0.0; + learn_sum_cross_entropy = 0.0; + learn_sum_entropy_eval = 0.0; + learn_sum_entropy_win = 0.0; + learn_sum_entropy = 0.0; +#endif +#if defined(EVAL_NNUE) + newbob_scale = 1.0; + newbob_decay = 1.0; + newbob_num_trials = 2; + best_loss = std::numeric_limits::infinity(); + latest_loss_sum = 0.0; + latest_loss_count = 0; +#endif + } + + virtual void thread_worker(size_t thread_id); + + // 局面ファイルをバックグラウンドで読み込むスレッドを起動する。 + void start_file_read_worker() { sr.start_file_read_worker(); } + + // 評価関数パラメーターをファイルに保存 + bool save(bool is_final=false); + + // sfenの読み出し器 + SfenReader& sr; + + // 学習の反復回数のカウンター + uint64_t epoch = 0; + + // ミニバッチサイズのサイズ。必ずこのclassを使う側で設定すること。 + uint64_t mini_batch_size = 1000*1000; + + bool stop_flag; + + // 割引率 + double discount_rate; + + // 序盤を学習対象から外すオプション + int reduction_gameply; + + // kk/kkp/kpp/kpppを学習させないオプション + std::array freeze; + + // 教師局面の深い探索の評価値の絶対値がこの値を超えていたらその教師局面を捨てる。 + int eval_limit; + + // 評価関数の保存するときに都度フォルダを掘るかのフラグ。 + // trueだとフォルダを掘らない。 + bool save_only_once; + + // --- lossの計算 + +#if defined ( LOSS_FUNCTION_IS_ELMO_METHOD ) + // 学習用データのロスの計算用 + atomic learn_sum_cross_entropy_eval; + atomic learn_sum_cross_entropy_win; + atomic learn_sum_cross_entropy; + atomic learn_sum_entropy_eval; + atomic learn_sum_entropy_win; + atomic learn_sum_entropy; +#endif + +#if defined(EVAL_NNUE) + shared_timed_mutex nn_mutex; + double newbob_scale; + double newbob_decay; + int newbob_num_trials; + double best_loss; + double latest_loss_sum; + uint64_t latest_loss_count; + std::string best_nn_directory; +#endif + + uint64_t eval_save_interval; + uint64_t loss_output_interval; + uint64_t mirror_percentage; + + // ロスの計算。 + // done : 今回対象とした局面数 + void calc_loss(size_t thread_id , uint64_t done); + + // ↑のlossの計算をタスクとして定義してやり、それを実行する + TaskDispatcher task_dispatcher; +}; + +void LearnerThink::calc_loss(size_t thread_id, uint64_t done) +{ + // 置換表にhitされてもかなわんので、このタイミングで置換表の世代を新しくする。 + // 置換表を無効にしているなら関係ないのだが。 + TT.new_search(); + +#if defined(EVAL_NNUE) + std::cout << "PROGRESS: " << now_string() << ", "; + std::cout << sr.total_done << " sfens"; + std::cout << ", iteration " << epoch; + std::cout << ", eta = " << Eval::get_eta() << ", "; +#endif + +#if !defined(LOSS_FUNCTION_IS_ELMO_METHOD) + double sum_error = 0; + double sum_error2 = 0; + double sum_error3 = 0; +#endif + +#if defined ( LOSS_FUNCTION_IS_ELMO_METHOD ) + // 検証用データのロスの計算用 + atomic test_sum_cross_entropy_eval,test_sum_cross_entropy_win,test_sum_cross_entropy; + atomic test_sum_entropy_eval,test_sum_entropy_win,test_sum_entropy; + test_sum_cross_entropy_eval = 0; + test_sum_cross_entropy_win = 0; + test_sum_cross_entropy = 0; + test_sum_entropy_eval = 0; + test_sum_entropy_win = 0; + test_sum_entropy = 0; + + // 学習時のnorm + atomic sum_norm; + sum_norm = 0; +#endif + + // 深い探索のpvの初手と、search(1)のpvの初手の指し手が一致した回数。 + atomic move_accord_count; + move_accord_count = 0; + + // 平手の初期局面のeval()の値を表示させて、揺れを見る。 + auto th = Threads[thread_id]; + auto& pos = th->rootPos; + StateInfo si; + pos.set(StartFEN, false, &si, th); + std::cout << "hirate eval = " << Eval::evaluate(pos); + + //Eval::print_eval_stat(pos); + + // ここ、並列化したほうが良いのだがslaveの前の探索が終わってなかったりしてちょっと面倒。 + // taskを呼び出すための仕組みを作ったのでそれを用いる。 + + // こなすべきtaskの数。 + atomic task_count; + task_count = (int)sr.sfen_for_mse.size(); + task_dispatcher.task_reserve(task_count); + + // 局面の探索をするtaskを生成して各スレッドに振ってやる。 + for (const auto& ps : sr.sfen_for_mse) + { + // TaskDispatcherを用いて各スレッドに作業を振る。 + // そのためのタスクの定義。 + // ↑で使っているposをcaptureされるとたまらんのでcaptureしたい変数は一つずつ指定しておく。 + auto task = [&ps,&test_sum_cross_entropy_eval,&test_sum_cross_entropy_win,&test_sum_cross_entropy,&test_sum_entropy_eval,&test_sum_entropy_win,&test_sum_entropy, &sum_norm,&task_count ,&move_accord_count](size_t thread_id) + { + // これ、C++ではループごとに新たなpsのインスタンスをちゃんとcaptureするのだろうか.. → するようだ。 + auto th = Threads[thread_id]; + auto& pos = th->rootPos; + StateInfo si; + if (pos.set_from_packed_sfen(ps.sfen ,&si, th) != 0) + { + // 運悪くrmse計算用のsfenとして、不正なsfenを引いてしまっていた。 + cout << "Error! : illegal packed sfen " << pos.fen() << endl; + } + + // 浅い探索の評価値 + // evaluate()の値を用いても良いのだが、ロスを計算するときにlearn_cross_entropyと + // 値が比較しにくくて困るのでqsearch()を用いる。 + // EvalHashは事前に無効化してある。(そうしないと毎回同じ値が返ってしまう) + auto r = qsearch(pos); + + auto shallow_value = r.first; + { + const auto rootColor = pos.side_to_move(); + const auto pv = r.second; + std::vector> states(pv.size()); + for (size_t i = 0; i < pv.size(); ++i) + { + pos.do_move(pv[i], states[i]); + Eval::evaluate_with_no_return(pos); + } + shallow_value = (rootColor == pos.side_to_move()) ? Eval::evaluate(pos) : -Eval::evaluate(pos); + for (auto it = pv.rbegin(); it != pv.rend(); ++it) + pos.undo_move(*it); + } + + // 深い探索の評価値 + auto deep_value = (Value)ps.score; + + // 注) このコードは、learnコマンドでeval_limitを指定しているときのことを考慮してない。 + + // --- 誤差の計算 + +#if !defined(LOSS_FUNCTION_IS_ELMO_METHOD) + auto grad = calc_grad(deep_value, shallow_value, ps); + + // rmse的なもの + sum_error += grad*grad; + // 勾配の絶対値を足したもの + sum_error2 += abs(grad); + // 評価値の差の絶対値を足したもの + sum_error3 += abs(shallow_value - deep_value); +#endif + + // --- 交差エントロピーの計算 + + // とりあえずelmo methodの時だけ勝率項と勝敗項に関して + // 交差エントロピーを計算して表示させる。 + +#if defined ( LOSS_FUNCTION_IS_ELMO_METHOD ) + double test_cross_entropy_eval, test_cross_entropy_win, test_cross_entropy; + double test_entropy_eval, test_entropy_win, test_entropy; + calc_cross_entropy(deep_value, shallow_value, ps, test_cross_entropy_eval, test_cross_entropy_win, test_cross_entropy, test_entropy_eval, test_entropy_win, test_entropy); + // 交差エントロピーの合計は定義的にabs()をとる必要がない。 + test_sum_cross_entropy_eval += test_cross_entropy_eval; + test_sum_cross_entropy_win += test_cross_entropy_win; + test_sum_cross_entropy += test_cross_entropy; + test_sum_entropy_eval += test_entropy_eval; + test_sum_entropy_win += test_entropy_win; + test_sum_entropy += test_entropy; + sum_norm += (double)abs(shallow_value); +#endif + + // 教師の指し手と浅い探索のスコアが一致するかの判定 + { + auto r = search(pos,1); + if ((uint16_t)r.second[0] == ps.move) + move_accord_count.fetch_add(1, std::memory_order_relaxed); + } + + // こなしたのでタスク一つ減る + --task_count; + }; + + // 定義したタスクをslaveに投げる。 + task_dispatcher.push_task_async(task); + } + + // 自分自身もslaveとして参加する + task_dispatcher.on_idle(thread_id); + + // すべてのtaskの完了を待つ + while (task_count) + sleep(1); + +#if !defined(LOSS_FUNCTION_IS_ELMO_METHOD) + // rmse = root mean square error : 平均二乗誤差 + // mae = mean absolute error : 平均絶対誤差 + auto dsig_rmse = std::sqrt(sum_error / (sfen_for_mse.size() + epsilon)); + auto dsig_mae = sum_error2 / (sfen_for_mse.size() + epsilon); + auto eval_mae = sum_error3 / (sfen_for_mse.size() + epsilon); + cout << " , dsig rmse = " << dsig_rmse << " , dsig mae = " << dsig_mae + << " , eval mae = " << eval_mae; +#endif + +#if defined ( LOSS_FUNCTION_IS_ELMO_METHOD ) +#if defined(EVAL_NNUE) + latest_loss_sum += test_sum_cross_entropy - test_sum_entropy; + latest_loss_count += sr.sfen_for_mse.size(); +#endif + + // learn_cross_entropyは、機械学習の世界ではtrain cross entropyと呼ぶべきかも知れないが、 + // 頭文字を略するときに、lceと書いて、test cross entropy(tce)と区別出来たほうが嬉しいのでこうしてある。 + + if (sr.sfen_for_mse.size() && done) + { + cout + << " , test_cross_entropy_eval = " << test_sum_cross_entropy_eval / sr.sfen_for_mse.size() + << " , test_cross_entropy_win = " << test_sum_cross_entropy_win / sr.sfen_for_mse.size() + << " , test_entropy_eval = " << test_sum_entropy_eval / sr.sfen_for_mse.size() + << " , test_entropy_win = " << test_sum_entropy_win / sr.sfen_for_mse.size() + << " , test_cross_entropy = " << test_sum_cross_entropy / sr.sfen_for_mse.size() + << " , test_entropy = " << test_sum_entropy / sr.sfen_for_mse.size() + << " , norm = " << sum_norm + << " , move accuracy = " << (move_accord_count * 100.0 / sr.sfen_for_mse.size()) << "%"; + if (done != static_cast(-1)) + { + cout + << " , learn_cross_entropy_eval = " << learn_sum_cross_entropy_eval / done + << " , learn_cross_entropy_win = " << learn_sum_cross_entropy_win / done + << " , learn_entropy_eval = " << learn_sum_entropy_eval / done + << " , learn_entropy_win = " << learn_sum_entropy_win / done + << " , learn_cross_entropy = " << learn_sum_cross_entropy / done + << " , learn_entropy = " << learn_sum_entropy / done; + } + cout << endl; + } + else { + cout << "Error! : sr.sfen_for_mse.size() = " << sr.sfen_for_mse.size() << " , done = " << done << endl; + } + + // 次回のために0クリアしておく。 + learn_sum_cross_entropy_eval = 0.0; + learn_sum_cross_entropy_win = 0.0; + learn_sum_cross_entropy = 0.0; + learn_sum_entropy_eval = 0.0; + learn_sum_entropy_win = 0.0; + learn_sum_entropy = 0.0; +#else + << endl; +#endif +} + + +void LearnerThink::thread_worker(size_t thread_id) +{ +#if defined(_OPENMP) + omp_set_num_threads((int)Options["Threads"]); +#endif + + auto th = Threads[thread_id]; + auto& pos = th->rootPos; + + while (true) + { + // mseの表示(これはthread 0のみときどき行う) + // ファイルから読み込んだ直後とかでいいような…。 + +#if defined(EVAL_NNUE) + // 更新中に評価関数を使わないようにロックする。 + shared_lock read_lock(nn_mutex, defer_lock); + if (sr.next_update_weights <= sr.total_done || + (thread_id != 0 && !read_lock.try_lock())) +#else + if (sr.next_update_weights <= sr.total_done) +#endif + { + if (thread_id != 0) + { + // thread_id == 0以外は、待機。 + + if (stop_flag) + break; + + // rmseの計算などを並列化したいのでtask()が積まれていればそれを処理する。 + task_dispatcher.on_idle(thread_id); + continue; + } + else + { + // thread_id == 0だけが以下の更新処理を行なう。 + + // 初回はweight配列の更新は行わない。 + if (sr.next_update_weights == 0) + { + sr.next_update_weights += mini_batch_size; + continue; + } + +#if !defined(EVAL_NNUE) + // 現在時刻を出力。毎回出力する。 + std::cout << sr.total_done << " sfens , at " << now_string() << std::endl; + + // このタイミングで勾配をweight配列に反映。勾配の計算も1M局面ごとでmini-batch的にはちょうどいいのでは。 + Eval::update_weights(epoch , freeze); + + // デバッグ用にepochと現在のetaを表示してやる。 + std::cout << "epoch = " << epoch << " , eta = " << Eval::get_eta() << std::endl; +#else + { + // パラメータの更新 + + // 更新中に評価関数を使わないようにロックする。 + lock_guard write_lock(nn_mutex); + Eval::NNUE::UpdateParameters(epoch); + } +#endif + ++epoch; + + // 10億局面ごとに1回保存、ぐらいの感じで。 + + // ただし、update_weights(),calc_rmse()している間の時間経過は無視するものとする。 + if (++sr.save_count * mini_batch_size >= eval_save_interval) + { + sr.save_count = 0; + + // この間、gradientの計算が進むと値が大きくなりすぎて困る気がするので他のスレッドを停止させる。 + const bool converged = save(); + if (converged) + { + stop_flag = true; + sr.stop_flag = true; + break; + } + } + + // rmseを計算する。1万局面のサンプルに対して行う。 + // 40コアでやると100万局面ごとにupdate_weightsするとして、特定のスレッドが + // つきっきりになってしまうのあまりよくないような気も…。 + static uint64_t loss_output_count = 0; + if (++loss_output_count * mini_batch_size >= loss_output_interval) + { + loss_output_count = 0; + + // 今回処理した件数 + uint64_t done = sr.total_done - sr.last_done; + + // lossの計算 + calc_loss(thread_id , done); + +#if defined(EVAL_NNUE) + Eval::NNUE::CheckHealth(); +#endif + + // どこまで集計したかを記録しておく。 + sr.last_done = sr.total_done; + } + + // 次回、この一連の処理は、次回、mini_batch_sizeだけ処理したときに再度やって欲しい。 + sr.next_update_weights += mini_batch_size; + + // main thread以外は、このsr.next_update_weightsの更新を待っていたので + // この値が更新されると再度動き始める。 + } + } + + PackedSfenValue ps; + RetryRead:; + if (!sr.read_to_thread_buffer(thread_id, ps)) + { + // 自分のスレッド用の局面poolを使い尽くした。 + // 局面がもうほとんど残っていないということだから、 + // 他のスレッドもすべて終了させる。 + + stop_flag = true; + break; + } + + // 評価値が学習対象の値を超えている。 + // この局面情報を無視する。 + if (eval_limit < abs(ps.score)) + goto RetryRead; + +#if !defined (LEARN_GENSFEN_USE_DRAW_RESULT) + if (ps.game_result == 0) + goto RetryRead; +#endif + + // 序盤局面に関する読み飛ばし + if (ps.gamePly < prng.rand(reduction_gameply)) + goto RetryRead; + +#if 0 + auto sfen = pos.sfen_unpack(ps.data); + pos.set(sfen); +#endif + // ↑sfenを経由すると遅いので専用の関数を作った。 + StateInfo si; + const bool mirror = prng.rand(100) < mirror_percentage; + if (pos.set_from_packed_sfen(ps.sfen,&si,th,mirror) != 0) + { + // 変なsfenを掴かまされた。デバッグすべき! + // 不正なsfenなのでpos.sfen()で表示できるとは限らないが、しないよりマシ。 + cout << "Error! : illigal packed sfen = " << pos.fen() << endl; + goto RetryRead; + } +#if !defined(EVAL_NNUE) + { + auto key = pos.key(); + // rmseの計算用に使っている局面なら除外する。 + if (sr.is_for_rmse(key)) + goto RetryRead; + + // 直近で用いた局面も除外する。 + auto hash_index = size_t(key & (sr.READ_SFEN_HASH_SIZE - 1)); + auto key2 = sr.hash[hash_index]; + if (key == key2) + goto RetryRead; + sr.hash[hash_index] = key; // 今回のkeyに入れ替えておく。 + } +#endif + + // 全駒されて詰んでいる可能性がある。 + // また宣言勝ちの局面はPVの指し手でleafに行けないので学習から除外しておく。 + // (そのような教師局面自体を書き出すべきではないのだが古い生成ルーチンで書き出しているかも知れないので) + // Skip the position if there are no legal moves (=checkmated or stalemate). + if (MoveList(pos).size() == 0) + goto RetryRead; + + // 読み込めたので試しに表示してみる。 + // cout << pos << value << endl; + + // 浅い探索(qsearch)の評価値 + auto r = qsearch(pos); + auto pv = r.second; + + // 深い探索の評価値 + auto deep_value = (Value)ps.score; + + // mini batchのほうが勾配が出ていいような気がする。 + // このままleaf nodeに行って、勾配配列にだけ足しておき、あとでrmseの集計のときにAdaGradしてみる。 + + auto rootColor = pos.side_to_move(); + + // PVの初手が異なる場合は学習に用いないほうが良いのでは…。 + // 全然違うところを探索した結果だとそれがノイズに成りかねない。 + // 評価値の差が大きすぎるところも学習対象としないほうがいいかも…。 + +#if 0 + // これやると13%程度の局面が学習対象から外れてしまう。善悪は微妙。 + if (pv.size() >= 1 && (uint16_t)pv[0] != ps.move) + { +// dbg_hit_on(false); + continue; + } +#endif + +#if 0 + // 評価値の差が大きすぎるところも学習対象としないほうがいいかも…。 + // → 勝率の関数を通すのでまあいいか…。30%ぐらいの局面が学習対象から外れてしまうしな…。 + if (abs((int16_t)r.first - ps.score) >= Eval::PawnValue * 4) + { +// dbg_hit_on(false); + continue; + } + // dbg_hit_on(true); +#endif + + int ply = 0; + + // 現在の局面に対して勾配を加算するヘルパー関数。 + auto pos_add_grad = [&]() { + // shallow_valueとして、leafでのevaluateの値を用いる。 + // qsearch()の戻り値をshallow_valueとして用いると、 + // PVが途中で途切れている場合、勾配を計算するのにevaluate()を呼び出した局面と、 + // その勾配を与える局面とが異なることになるので、これはあまり好ましい性質ではないと思う。 + // 置換表をオフにはしているのだが、1手詰みなどはpv配列を更新していないので…。 + + Value shallow_value = (rootColor == pos.side_to_move()) ? Eval::evaluate(pos) : -Eval::evaluate(pos); + +#if defined ( LOSS_FUNCTION_IS_ELMO_METHOD ) + // 学習データに対するロスの計算 + double learn_cross_entropy_eval, learn_cross_entropy_win, learn_cross_entropy; + double learn_entropy_eval, learn_entropy_win, learn_entropy; + calc_cross_entropy(deep_value, shallow_value, ps, learn_cross_entropy_eval, learn_cross_entropy_win, learn_cross_entropy, learn_entropy_eval, learn_entropy_win, learn_entropy); + learn_sum_cross_entropy_eval += learn_cross_entropy_eval; + learn_sum_cross_entropy_win += learn_cross_entropy_win; + learn_sum_cross_entropy += learn_cross_entropy; + learn_sum_entropy_eval += learn_entropy_eval; + learn_sum_entropy_win += learn_entropy_win; + learn_sum_entropy += learn_entropy; +#endif + +#if !defined(EVAL_NNUE) + // 勾配 + double dj_dw = calc_grad(deep_value, shallow_value, ps); + + // 現在、leaf nodeで出現している特徴ベクトルに対する勾配(∂J/∂Wj)として、jd_dwを加算する。 + + // PV終端でなければ割引率みたいなものを適用。 + if (discount_rate != 0 && ply != (int)pv.size()) + dj_dw *= discount_rate; + + // leafに到達したのでこの局面に出現している特徴に勾配を加算しておく。 + // 勾配に基づくupdateはのちほど行なう。 + Eval::add_grad(pos, rootColor, dj_dw, freeze); +#else + const double example_weight = + (discount_rate != 0 && ply != (int)pv.size()) ? discount_rate : 1.0; + Eval::NNUE::AddExample(pos, rootColor, ps, example_weight); +#endif + + // 処理が終了したので処理した件数のカウンターをインクリメント + sr.total_done++; + }; + + StateInfo state[MAX_PLY]; // qsearchのPVがそんなに長くなることはありえない。 + bool illegal_move = false; + for (auto m : pv) + { + // 非合法手はやってこないはずなのだが。 + // An illegal move sometimes comes here... + if (!pos.pseudo_legal(m) || !pos.legal(m)) + { + //cout << pos << m << endl; + //assert(false); + illegal_move = true; + break; + } + + // 各PV上のnodeでも勾配を加算する場合の処理。 + // discount_rateが0のときはこの処理は行わない。 + if (discount_rate != 0) + pos_add_grad(); + + pos.do_move(m, state[ply++]); + + // leafでのevaluateの値を用いるので差分更新していく。 + Eval::evaluate_with_no_return(pos); + } + + if (illegal_move) { + sync_cout << "An illical move was detected... Excluded the position from the learning data..." << sync_endl; + continue; + } + + // PVの終端局面に達したので、ここで勾配を加算する。 + pos_add_grad(); + + // 局面を巻き戻す + for (auto it = pv.rbegin(); it != pv.rend(); ++it) + pos.undo_move(*it); + +#if 0 + // rootの局面にも勾配を加算する場合 + shallow_value = (rootColor == pos.side_to_move()) ? Eval::evaluate(pos) : -Eval::evaluate(pos); + dj_dw = calc_grad(deep_value, shallow_value, ps); + Eval::add_grad(pos, rootColor, dj_dw , without_kpp); +#endif + + } + +} + +// 評価関数ファイルの書き出し。 +bool LearnerThink::save(bool is_final) +{ + // 保存前にcheck sumを計算して出力しておく。(次に読み込んだときに合致するか調べるため) + std::cout << "Check Sum = " << std::hex << Eval::calc_check_sum() << std::dec << std::endl; + + // 保存ごとにファイル名の拡張子部分を"0","1","2",..のように変えていく。 + // (あとでそれぞれの評価関数パラメーターにおいて勝率を比較したいため) + + if (save_only_once) + { + // EVAL_SAVE_ONLY_ONCEが定義されているときは、 + // 1度だけの保存としたいのでサブフォルダを掘らない。 + Eval::save_eval(""); + } + else if (is_final) { + Eval::save_eval("final"); + return true; + } + else { + static int dir_number = 0; + const std::string dir_name = std::to_string(dir_number++); + Eval::save_eval(dir_name); +#if defined(EVAL_NNUE) + if (newbob_decay != 1.0 && latest_loss_count > 0) { + static int trials = newbob_num_trials; + const double latest_loss = latest_loss_sum / latest_loss_count; + latest_loss_sum = 0.0; + latest_loss_count = 0; + cout << "loss: " << latest_loss; + if (latest_loss < best_loss) { + cout << " < best (" << best_loss << "), accepted" << endl; + best_loss = latest_loss; + best_nn_directory = Path::Combine((std::string)Options["EvalSaveDir"], dir_name); + trials = newbob_num_trials; + } else { + cout << " >= best (" << best_loss << "), rejected" << endl; + if (best_nn_directory.empty()) { + cout << "WARNING: no improvement from initial model" << endl; + } else { + cout << "restoring parameters from " << best_nn_directory << endl; + Eval::NNUE::RestoreParameters(best_nn_directory); + } + if (--trials > 0 && !is_final) { + cout << "reducing learning rate scale from " << newbob_scale + << " to " << (newbob_scale * newbob_decay) + << " (" << trials << " more trials)" << endl; + newbob_scale *= newbob_decay; + Eval::NNUE::SetGlobalLearningRateScale(newbob_scale); + } + } + if (trials == 0) { + cout << "converged" << endl; + return true; + } + } +#endif + } + return false; +} + +// shuffle_files() , shuffle_files_quick()の下請けで、書き出し部分。 +// output_file_name : 書き出すファイル名 +// prng : 乱数 +// afs : それぞれの教師局面ファイルのfstream +// a_count : それぞれのファイルに内在する教師局面の数。 +void shuffle_write(const string& output_file_name , PRNG& prng , vector& afs , vector& a_count) +{ + uint64_t total_sfen_count = 0; + for (auto c : a_count) + total_sfen_count += c; + + // 書き出した局面数 + uint64_t write_sfen_count = 0; + + // 進捗をこの局面数ごとに画面に出力する。 + const uint64_t buffer_size = 10000000; + + auto print_status = [&]() + { + // 10M局面ごと、もしくは、すべての書き出しが終わったときに進捗を出力する + if (((write_sfen_count % buffer_size) == 0) || + (write_sfen_count == total_sfen_count)) + cout << write_sfen_count << " / " << total_sfen_count << endl; + }; + + + cout << endl << "write : " << output_file_name << endl; + + fstream fs(output_file_name, ios::out | ios::binary); + + // 教師局面の合計 + uint64_t sum = 0; + for (auto c : a_count) + sum += c; + + while (sum != 0) + { + auto r = prng.rand(sum); + + // fs[0]のファイルに格納されている局面 ... fs[1]のファイルに格納されている局面 ... + // のようにひと続きになっているものと考えて、rがどのファイルに格納されている局面を指しているかを確定させる。 + // ファイルの中身はシャッフルされているので、そのファイルから次の要素を1つ取ってくれば良い。 + // それぞれのファイルにはa_count[x]ずつ局面が残っているので、この処理は以下のように書ける。 + + uint64_t n = 0; + while (a_count[n] <= r) + r -= a_count[n++]; + + // これでnが確定した。忘れないうちに残り件数を減らしておく。 + + --a_count[n]; + --sum; + + PackedSfenValue psv; + // これ、パフォーマンスあんまりよくないまでまとめて読み書きしたほうが良いのだが…。 + if (afs[n].read((char*)&psv, sizeof(PackedSfenValue))) + { + fs.write((char*)&psv, sizeof(PackedSfenValue)); + ++write_sfen_count; + print_status(); + } + } + print_status(); + fs.close(); + cout << "done!" << endl; +} + +// 教師局面のシャッフル "learn shuffle"コマンドの下請け。 +// output_file_name : シャッフルされた教師局面が書き出される出力ファイル名 +void shuffle_files(const vector& filenames , const string& output_file_name , uint64_t buffer_size ) +{ + // 出力先のフォルダは + // tmp/ 一時書き出し用 + + // テンポラリファイルはbuffer_size局面ずつtmp/フォルダにいったん書き出す。 + // 例えば、buffer_size = 20Mならば 20M*40bytes = 800MBのバッファが必要。 + // メモリが少ないPCでは、ここを減らすと良いと思う。 + // ただし、あまりファイル数が増えるとOSの制限などから同時にopen出来なくなる。 + // Windowsだと1プロセス512という制約があったはずなので、ここでopen出来るのが500として、 + // 現在の設定で500ファイル×20M = 10G = 100億局面が限度。 + + PSVector buf; + buf.resize(buffer_size); + // ↑のバッファ、どこまで使ったかを示すマーカー + uint64_t buf_write_marker = 0; + + // 書き出すファイル名(連番なのでインクリメンタルカウンター) + uint64_t write_file_count = 0; + + // シャッフルするための乱数 + PRNG prng((std::random_device())()); + + // テンポラリファイルの名前を生成する + auto make_filename = [](uint64_t i) + { + return "tmp/" + to_string(i) + ".bin"; + }; + + // 書き出したtmp/フォルダのファイル、それぞれに格納されている教師局面の数 + vector a_count; + + auto write_buffer = [&](uint64_t size) + { + // buf[0]~buf[size-1]までをshuffle + for (uint64_t i = 0; i < size; ++i) + swap(buf[i], buf[(uint64_t)(prng.rand(size - i) + i)]); + + // ファイルに書き出す + fstream fs; + fs.open(make_filename(write_file_count++), ios::out | ios::binary); + fs.write((char*)&buf[0], size * sizeof(PackedSfenValue)); + fs.close(); + a_count.push_back(size); + + buf_write_marker = 0; + cout << "."; + }; + + Dependency::mkdir("tmp"); + + // 10M局面の細切れファイルとしてシャッフルして書き出す。 + for (auto filename : filenames) + { + fstream fs(filename, ios::in | ios::binary); + cout << endl << "open file = " << filename; + while (fs.read((char*)&buf[buf_write_marker], sizeof(PackedSfenValue))) + if (++buf_write_marker == buffer_size) + write_buffer(buffer_size); + + // sizeof(PackedSfenValue)単位で読み込んでいき、 + // 最後に残っている端数は無視する。(fs.readで失敗するのでwhileを抜ける) + // (最後に残っている端数は、教師生成時に途中で停止させたために出来た中途半端なデータだと思われる。) + + } + + if (buf_write_marker != 0) + write_buffer(buf_write_marker); + + // シャッフルされたファイルがwrite_file_count個だけ書き出された。 + // 2pass目として、これをすべて同時にオープンし、ランダムに1つずつ選択して1局面ずつ読み込めば + // これにてシャッフルされたことになる。 + + // シャツフルする元ファイル+tmpファイル+書き出すファイルで元ファイルの3倍のストレージ容量が必要になる。 + // 100億局面400GBなのでシャッフルするために1TBのSSDでは足りない。 + // tmpに書き出しが終わったこのタイミングで元ファイルを消す(あるいは手で削除してしまう)なら、 + // 元ファイルの2倍程度のストレージ容量で済む。 + // だから、元ファイルを消すためのオプションを用意すべきかも知れない。 + + // ファイルの同時openをしている。これがFOPEN_MAXなどを超える可能性は高い。 + // その場合、buffer_sizeを調整して、ファイルの数を減らすよりない。 + + vector afs; + for (uint64_t i = 0; i < write_file_count; ++i) + afs.emplace_back(fstream(make_filename(i),ios::in | ios::binary)); + + // 下請け関数に丸投げして終わり。 + shuffle_write(output_file_name, prng, afs, a_count); +} + +// 教師局面のシャッフル "learn shuffleq"コマンドの下請け。 +// こちらは1passで書き出す。 +// output_file_name : シャッフルされた教師局面が書き出される出力ファイル名 +void shuffle_files_quick(const vector& filenames, const string& output_file_name) +{ + // 読み込んだ局面数 + uint64_t read_sfen_count = 0; + + // シャッフルするための乱数 + PRNG prng((std::random_device())()); + + // ファイルの数 + size_t file_count = filenames.size(); + + // filenamesのファイルそれぞれに格納されている教師局面の数 + vector a_count(file_count); + + // それぞれのファイルの教師局面の数をカウントする。 + vector afs(file_count); + + for (size_t i = 0; i < file_count ; ++i) + { + auto filename = filenames[i]; + auto& fs = afs[i]; + + fs.open(filename, ios::in | ios::binary); + fs.seekg(0, fstream::end); + uint64_t eofPos = (uint64_t)fs.tellg(); + fs.clear(); // これをしないと次のseekに失敗することがある。 + fs.seekg(0, fstream::beg); + uint64_t begPos = (uint64_t)fs.tellg(); + uint64_t file_size = eofPos - begPos; + uint64_t sfen_count = file_size / sizeof(PackedSfenValue); + a_count[i] = sfen_count; + + // 各ファイルに格納されていたsfenの数を出力する。 + cout << filename << " = " << sfen_count << " sfens." << endl; + } + + // それぞれのファイルのファイルサイズがわかったので、 + // これらをすべて同時にオープンし(すでにオープンされている)、 + // ランダムに1つずつ選択して1局面ずつ読み込めば + // これにてシャッフルされたことになる。 + + // 下請け関数に丸投げして終わり。 + shuffle_write(output_file_name, prng, afs, a_count); +} + +// 教師局面のシャッフル "learn shufflem"コマンドの下請け。 +// メモリに丸読みして指定ファイル名で書き出す。 +void shuffle_files_on_memory(const vector& filenames,const string output_file_name) +{ + PSVector buf; + + for (auto filename : filenames) + { + std::cout << "read : " << filename << std::endl; + read_file_to_memory(filename, [&buf](uint64_t size) { + assert((size % sizeof(PackedSfenValue)) == 0); + // バッファを拡充して、前回の末尾以降に読み込む。 + uint64_t last = buf.size(); + buf.resize(last + size / sizeof(PackedSfenValue)); + return (void*)&buf[last]; + }); + } + + // buf[0]~buf[size-1]までをshuffle + PRNG prng((std::random_device())()); + uint64_t size = (uint64_t)buf.size(); + std::cout << "shuffle buf.size() = " << size << std::endl; + for (uint64_t i = 0; i < size; ++i) + swap(buf[i], buf[(uint64_t)(prng.rand(size - i) + i)]); + + std::cout << "write : " << output_file_name << endl; + + // 書き出すファイルが2GBを超えるとfstream::write一発では書き出せないのでwrapperを用いる。 + write_memory_to_file(output_file_name, (void*)&buf[0], (uint64_t)sizeof(PackedSfenValue)*(uint64_t)buf.size()); + + std::cout << "..shuffle_on_memory done." << std::endl; +} + +void convert_bin(const vector& filenames , const string& output_file_name) +{ + std::fstream fs; + auto th = Threads.main(); + auto &tpos = th->rootPos; + // plain形式の雑巾をやねうら王用のpackedsfenvalueに変換する + fs.open(output_file_name, ios::app | ios::binary); + + for (auto filename : filenames) { + std::cout << "convert " << filename << " ... "; + std::string line; + ifstream ifs; + ifs.open(filename); + PackedSfenValue p; + p.gamePly = 1; // apery形式では含まれない。一応初期化するべし + while (std::getline(ifs, line)) { + std::stringstream ss(line); + std::string token; + std::string value; + ss >> token; + if (token == "sfen") { + StateInfo si; + tpos.set(line.substr(5), false, &si, Threads.main()); + tpos.sfen_pack(p.sfen); + } + else if (token == "move") { + ss >> value; + p.move = UCI::to_move(tpos, value); + } + else if (token == "score") { + ss >> p.score; + } + else if (token == "ply") { + int temp; + ss >> temp; + p.gamePly = uint16_t(temp); // 此処のキャストいらない? + } + else if (token == "result") { + int temp; + ss >> temp; + p.game_result = int8_t(temp); // 此処のキャストいらない? + } + else if (token == "e") { + fs.write((char*)&p, sizeof(PackedSfenValue)); + // debug + /* + std::cout<& filenames , const string& output_file_name) +//{ +// Position tpos; +// std::ofstream ofs; +// ofs.open(output_file_name, ios::app); +// for (auto filename : filenames) { +// std::cout << "convert " << filename << " ... "; +// +// // ひたすらpackedsfenvalueをテキストに変換する +// std::fstream fs; +// fs.open(filename, ios::in | ios::binary); +// PackedSfenValue p; +// while (true) +// { +// if (fs.read((char*)&p, sizeof(PackedSfenValue))) { +// // plain textとして書き込む +// ofs << "sfen " << tpos.sfen_unpack(p.sfen) << std::endl; +// ofs << "move " << to_usi_string(Move(p.move)) << std::endl; +// ofs << "score " << p.score << std::endl; +// ofs << "ply " << int(p.gamePly) << std::endl; +// ofs << "result " << int(p.game_result) << std::endl; +// ofs << "e" << std::endl; +// } +// else { +// break; +// } +// } +// fs.close(); +// std::cout << "done" << std::endl; +// } +// ofs.close(); +// std::cout << "all done" << std::endl; +//} + +// 生成した棋譜からの学習 +void learn(Position&, istringstream& is) +{ + auto thread_num = (int)Options["Threads"]; + SfenReader sr(thread_num); + + LearnerThink learn_think(sr); + vector filenames; + + // mini_batch_size デフォルトで1M局面。これを大きくできる。 + auto mini_batch_size = LEARN_MINI_BATCH_SIZE; + + // ループ回数(この回数だけ棋譜ファイルを読み込む) + int loop = 1; + + // 棋譜ファイル格納フォルダ(ここから相対pathで棋譜ファイルを取得) + string base_dir; + + string target_dir; + + // 0であれば、デフォルト値になる。 + double eta1 = 0.0; + double eta2 = 0.0; + double eta3 = 0.0; + uint64_t eta1_epoch = 0; // defaultではeta2は適用されない + uint64_t eta2_epoch = 0; // defaultではeta3は適用されない + +#if defined(USE_GLOBAL_OPTIONS) + // あとで復元するために保存しておく。 + auto oldGlobalOptions = GlobalOptions; + // eval hashにhitするとrmseなどの計算ができなくなるのでオフにしておく。 + GlobalOptions.use_eval_hash = false; + // 置換表にhitするとそこで以前の評価値で枝刈りがされることがあるのでオフにしておく。 + GlobalOptions.use_hash_probe = false; +#endif + + // --- 教師局面をシャッフルするだけの機能 + + // 通常シャッフル + bool shuffle_normal = false; + uint64_t buffer_size = 20000000; + // それぞれのファイルがシャッフルされていると仮定しての高速シャッフル + bool shuffle_quick = false; + // メモリにファイルを丸読みしてシャッフルする機能。(要、ファイルサイズのメモリ) + bool shuffle_on_memory = false; + // packed sfenの変換。plainではsfen(string), 評価値(整数), 指し手(例:7g7f, string)、結果(負け-1、勝ち1、引き分け0)からなる + bool use_convert_plain = false; + // plain形式の教師をやねうら王のbinに変換する + bool use_convert_bin = false; + // それらのときに書き出すファイル名(デフォルトでは"shuffled_sfen.bin") + string output_file_name = "shuffled_sfen.bin"; + + // 教師局面の深い探索での評価値の絶対値が、この値を超えていたらその局面は捨てる。 + int eval_limit = 32000; + + // 評価関数ファイルの保存は終了間際の1回に限定するかのフラグ。 + bool save_only_once = false; + + // 教師局面を先読みしている分に関してシャッフルする。(1000万局面単位ぐらいのシャッフル) + // 事前にシャッフルされているファイルを渡すならオンにすれば良い。 + bool no_shuffle = false; + +#if defined (LOSS_FUNCTION_IS_ELMO_METHOD) + // elmo lambda + ELMO_LAMBDA = 0.33; + ELMO_LAMBDA2 = 0.33; + ELMO_LAMBDA_LIMIT = 32000; +#endif + + // 割引率。これを0以外にすると、PV終端以外でも勾配を加算する。(そのとき、この割引率を適用する) + double discount_rate = 0; + + // if (gamePly < rand(reduction_gameply)) continue; + // のようにして、序盤を学習対象から程よく除外するためのオプション + // 1にしてあるとrand(1)==0なので、何も除外されない。 + int reduction_gameply = 1; + + // KK/KKP/KPP/KPPPを学習させないオプション項目 + array freeze = {}; + +#if defined(EVAL_NNUE) + uint64_t nn_batch_size = 1000; + double newbob_decay = 1.0; + int newbob_num_trials = 2; + string nn_options; +#endif + + uint64_t eval_save_interval = LEARN_EVAL_SAVE_INTERVAL; + uint64_t loss_output_interval = 0; + uint64_t mirror_percentage = 0; + + string validation_set_file_name; + + // ファイル名が後ろにずらずらと書かれていると仮定している。 + while (true) + { + string option; + is >> option; + + if (option == "") + break; + + // mini-batchの局面数を指定 + if (option == "bat") + { + is >> mini_batch_size; + mini_batch_size *= 10000; // 単位は万 + } + + // 棋譜が格納されているフォルダを指定して、根こそぎ対象とする。 + else if (option == "targetdir") is >> target_dir; + + // ループ回数の指定 + else if (option == "loop") is >> loop; + + // 棋譜ファイル格納フォルダ(ここから相対pathで棋譜ファイルを取得) + else if (option == "basedir") is >> base_dir; + + // ミニバッチのサイズ + else if (option == "batchsize") is >> mini_batch_size; + + // 学習率 + else if (option == "eta") is >> eta1; + else if (option == "eta1") is >> eta1; // alias + else if (option == "eta2") is >> eta2; + else if (option == "eta3") is >> eta3; + else if (option == "eta1_epoch") is >> eta1_epoch; + else if (option == "eta2_epoch") is >> eta2_epoch; + + // 割引率 + else if (option == "discount_rate") is >> discount_rate; + + // KK/KKP/KPP/KPPPの学習なし。 + else if (option == "freeze_kk") is >> freeze[0]; + else if (option == "freeze_kkp") is >> freeze[1]; + else if (option == "freeze_kpp") is >> freeze[2]; + +#if defined(EVAL_KPPT) || defined(EVAL_KPP_KKPT) || defined(EVAL_KPP_KKPT_FV_VAR) || defined(EVAL_NABLA) + +#elif defined(EVAL_KPPPT) || defined(EVAL_KPPP_KKPT) || defined(EVAL_HELICES) + else if (option == "freeze_kppp") is >> freeze[3]; +#elif defined(EVAL_KKPP_KKPT) || defined(EVAL_KKPPT) + else if (option == "freeze_kkpp") is >> freeze[3]; +#endif + +#if defined (LOSS_FUNCTION_IS_ELMO_METHOD) + // LAMBDA + else if (option == "lambda") is >> ELMO_LAMBDA; + else if (option == "lambda2") is >> ELMO_LAMBDA2; + else if (option == "lambda_limit") is >> ELMO_LAMBDA_LIMIT; + +#endif + else if (option == "reduction_gameply") is >> reduction_gameply; + + // シャッフル関連 + else if (option == "shuffle") shuffle_normal = true; + else if (option == "buffer_size") is >> buffer_size; + else if (option == "shuffleq") shuffle_quick = true; + else if (option == "shufflem") shuffle_on_memory = true; + else if (option == "output_file_name") is >> output_file_name; + + else if (option == "eval_limit") is >> eval_limit; + else if (option == "save_only_once") save_only_once = true; + else if (option == "no_shuffle") no_shuffle = true; + +#if defined(EVAL_NNUE) + else if (option == "nn_batch_size") is >> nn_batch_size; + else if (option == "newbob_decay") is >> newbob_decay; + else if (option == "newbob_num_trials") is >> newbob_num_trials; + else if (option == "nn_options") is >> nn_options; +#endif + else if (option == "eval_save_interval") is >> eval_save_interval; + else if (option == "loss_output_interval") is >> loss_output_interval; + else if (option == "mirror_percentage") is >> mirror_percentage; + else if (option == "validation_set_file_name") is >> validation_set_file_name; + + // 雑巾のconvert関連 + else if (option == "convert_plain") use_convert_plain = true; + else if (option == "convert_bin") use_convert_bin = true; + // さもなくば、それはファイル名である。 + else + filenames.push_back(option); + } + if (loss_output_interval == 0) + loss_output_interval = LEARN_RMSE_OUTPUT_INTERVAL * mini_batch_size; + + cout << "learn command , "; + + // OpenMP無効なら警告を出すように。 +#if !defined(_OPENMP) + cout << "Warning! OpenMP disabled." << endl; +#endif + + // 学習棋譜ファイルの表示 + if (target_dir != "") + { + string kif_base_dir = Path::Combine(base_dir, target_dir); + + // このフォルダを根こそぎ取る。base_dir相対にしておく。 +#if defined(_MSC_VER) + // std::tr2を使用するとwaring C4996が出るので抑制。 + // ※ std::tr2は、std:c++14 の下では既定で非推奨の警告を出し、/std:c++17 では既定で削除された。 + #pragma warning(push) + #pragma warning(disable:4996) + + namespace sys = std::filesystem; + sys::path p(kif_base_dir); // 列挙の起点 + std::for_each(sys::directory_iterator(p), sys::directory_iterator(), + [&](const sys::path& p) { + if (sys::is_regular_file(p)) + filenames.push_back(Path::Combine(target_dir, p.filename().generic_string())); + }); + #pragma warning(pop) + +#elif defined(__GNUC__) + + auto ends_with = [](std::string const & value, std::string const & ending) + { + if (ending.size() > value.size()) return false; + return std::equal(ending.rbegin(), ending.rend(), value.rbegin()); + }; + + // 仕方ないのでdirent.hを用いて読み込む。 + DIR *dp; // ディレクトリへのポインタ + dirent* entry; // readdir() で返されるエントリーポイント + + dp = opendir(kif_base_dir.c_str()); + if (dp != NULL) + { + do { + entry = readdir(dp); + // ".bin"で終わるファイルのみを列挙 + // → 連番でファイル生成するときにこの制約ちょっと嫌だな…。 + if (entry != NULL && ends_with(entry->d_name, ".bin") ) + { + //cout << entry->d_name << endl; + filenames.push_back(Path::Combine(target_dir, entry->d_name)); + } + } while (entry != NULL); + closedir(dp); + } +#endif + } + + cout << "learn from "; + for (auto s : filenames) + cout << s << " , "; + cout << endl; + if (!validation_set_file_name.empty()) + { + cout << "validation set : " << validation_set_file_name << endl; + } + + cout << "base dir : " << base_dir << endl; + cout << "target dir : " << target_dir << endl; + + // シャッフルモード + if (shuffle_normal) + { + cout << "buffer_size : " << buffer_size << endl; + cout << "shuffle mode.." << endl; + shuffle_files(filenames,output_file_name , buffer_size); + return; + } + if (shuffle_quick) + { + cout << "quick shuffle mode.." << endl; + shuffle_files_quick(filenames, output_file_name); + return; + } + if (shuffle_on_memory) + { + cout << "shuffle on memory.." << endl; + shuffle_files_on_memory(filenames,output_file_name); + return; + } + //if (use_convert_plain) + //{ + // is_ready(true); + // cout << "convert_plain.." << endl; + // convert_plain(filenames,output_file_name); + // return; + // + //} + if (use_convert_bin) + { + is_ready(true); + cout << "convert_bin.." << endl; + convert_bin(filenames,output_file_name); + return; + + } + + cout << "loop : " << loop << endl; + cout << "eval_limit : " << eval_limit << endl; + cout << "save_only_once : " << (save_only_once ? "true" : "false") << endl; + cout << "no_shuffle : " << (no_shuffle ? "true" : "false") << endl; + + // ループ回数分だけファイル名を突っ込む。 + for (int i = 0; i < loop; ++i) + // sfen reader、逆順で読むからここでreverseしておく。すまんな。 + for (auto it = filenames.rbegin(); it != filenames.rend(); ++it) + sr.filenames.push_back(Path::Combine(base_dir, *it)); + +#if !defined(EVAL_NNUE) + cout << "Gradient Method : " << LEARN_UPDATE << endl; +#endif + cout << "Loss Function : " << LOSS_FUNCTION << endl; + cout << "mini-batch size : " << mini_batch_size << endl; +#if defined(EVAL_NNUE) + cout << "nn_batch_size : " << nn_batch_size << endl; + cout << "nn_options : " << nn_options << endl; +#endif + cout << "learning rate : " << eta1 << " , " << eta2 << " , " << eta3 << endl; + cout << "eta_epoch : " << eta1_epoch << " , " << eta2_epoch << endl; +#if defined(EVAL_NNUE) + if (newbob_decay != 1.0) { + cout << "scheduling : newbob with decay = " << newbob_decay + << ", " << newbob_num_trials << " trials" << endl; + } else { + cout << "scheduling : default" << endl; + } +#endif + cout << "discount rate : " << discount_rate << endl; + + // reduction_gameplyに0を設定されるとrand(0)が0除算になってしまうので1に補正。 + reduction_gameply = max(reduction_gameply, 1); + cout << "reduction_gameply : " << reduction_gameply << endl; + +#if defined (LOSS_FUNCTION_IS_ELMO_METHOD) + cout << "LAMBDA : " << ELMO_LAMBDA << endl; + cout << "LAMBDA2 : " << ELMO_LAMBDA2 << endl; + cout << "LAMBDA_LIMIT : " << ELMO_LAMBDA_LIMIT << endl; +#endif + cout << "mirror_percentage : " << mirror_percentage << endl; + cout << "eval_save_interval : " << eval_save_interval << " sfens" << endl; + cout << "loss_output_interval: " << loss_output_interval << " sfens" << endl; + +#if defined(EVAL_KPPT) || defined(EVAL_KPP_KKPT) || defined(EVAL_KPP_KKPT_FV_VAR) || defined(EVAL_NABLA) + cout << "freeze_kk/kkp/kpp : " << freeze[0] << " , " << freeze[1] << " , " << freeze[2] << endl; +#elif defined(EVAL_KPPPT) || defined(EVAL_KPPP_KKPT) || defined(EVAL_HELICES) + cout << "freeze_kk/kkp/kpp/kppp : " << freeze[0] << " , " << freeze[1] << " , " << freeze[2] << " , " << freeze[3] << endl; +#elif defined(EVAL_KKPP_KKPT) || defined(EVAL_KKPPT) + cout << "freeze_kk/kkp/kpp/kkpp : " << freeze[0] << " , " << freeze[1] << " , " << freeze[2] << " , " << freeze[3] << endl; +#endif + + // ----------------------------------- + // 各種初期化 + // ----------------------------------- + + cout << "init.." << endl; + + // 評価関数パラメーターの読み込み + is_ready(true); + +#if !defined(EVAL_NNUE) + cout << "init_grad.." << endl; + + // 評価関数パラメーターの勾配配列の初期化 + Eval::init_grad(eta1,eta1_epoch,eta2,eta2_epoch,eta3); +#else + cout << "init_training.." << endl; + Eval::NNUE::InitializeTraining(eta1,eta1_epoch,eta2,eta2_epoch,eta3); + Eval::NNUE::SetBatchSize(nn_batch_size); + Eval::NNUE::SetOptions(nn_options); + if (newbob_decay != 1.0 && !Options["SkipLoadingEval"]) { + learn_think.best_nn_directory = std::string(Options["EvalDir"]); + } +#endif + +#if 0 + // 平手の初期局面に対して1.0の勾配を与えてみるテスト。 + pos.set_hirate(); + cout << Eval::evaluate(pos) << endl; + //Eval::print_eval_stat(pos); + Eval::add_grad(pos, BLACK, 32.0 , false); + Eval::update_weights(1); + pos.state()->sum.p[2][0] = VALUE_NOT_EVALUATED; + cout << Eval::evaluate(pos) << endl; + //Eval::print_eval_stat(pos); +#endif + + cout << "init done." << endl; + + // その他、オプション設定を反映させる。 + learn_think.discount_rate = discount_rate; + learn_think.eval_limit = eval_limit; + learn_think.save_only_once = save_only_once; + learn_think.sr.no_shuffle = no_shuffle; + learn_think.freeze = freeze; + learn_think.reduction_gameply = reduction_gameply; +#if defined(EVAL_NNUE) + learn_think.newbob_scale = 1.0; + learn_think.newbob_decay = newbob_decay; + learn_think.newbob_num_trials = newbob_num_trials; +#endif + learn_think.eval_save_interval = eval_save_interval; + learn_think.loss_output_interval = loss_output_interval; + learn_think.mirror_percentage = mirror_percentage; + + // 局面ファイルをバックグラウンドで読み込むスレッドを起動 + // (これを開始しないとmseの計算が出来ない。) + learn_think.start_file_read_worker(); + + learn_think.mini_batch_size = mini_batch_size; + + if (validation_set_file_name.empty()) { + // mse計算用にデータ1万件ほど取得しておく。 + sr.read_for_mse(); + } else { + sr.read_validation_set(validation_set_file_name, eval_limit); + } + + // この時点で一度rmseを計算(0 sfenのタイミング) + // sr.calc_rmse(); +#if defined(EVAL_NNUE) + if (newbob_decay != 1.0) { + learn_think.calc_loss(0, -1); + learn_think.best_loss = learn_think.latest_loss_sum / learn_think.latest_loss_count; + learn_think.latest_loss_sum = 0.0; + learn_think.latest_loss_count = 0; + cout << "initial loss: " << learn_think.best_loss << endl; + } +#endif + + // ----------------------------------- + // 評価関数パラメーターの学習の開始 + // ----------------------------------- + + // 学習開始。 + learn_think.go_think(); + + // 最後に一度保存。 + learn_think.save(true); + +#if defined(USE_GLOBAL_OPTIONS) + // GlobalOptionsの復元。 + GlobalOptions = oldGlobalOptions; +#endif +} + + +} // namespace Learner + +#if defined(GENSFEN2019) +#include "gensfen2019.cpp" +#endif + + +#endif // EVAL_LEARN diff --git a/src/learn/learning_tools.cpp b/src/learn/learning_tools.cpp new file mode 100644 index 00000000..d3a7858f --- /dev/null +++ b/src/learn/learning_tools.cpp @@ -0,0 +1,256 @@ +#include "learning_tools.h" + +#if defined (EVAL_LEARN) + +#if defined(_OPENMP) +#include +#endif +#include "../misc.h" + +using namespace Eval; + +namespace EvalLearningTools +{ + + // --- static variables + + double Weight::eta; + double Weight::eta1; + double Weight::eta2; + double Weight::eta3; + uint64_t Weight::eta1_epoch; + uint64_t Weight::eta2_epoch; + + std::vector min_index_flag; + + // --- 個別のテーブルごとの初期化 + + void init_min_index_flag() + { + // mir_piece、inv_pieceの初期化が終わっていなければならない。 + assert(mir_piece(Eval::f_pawn) == Eval::e_pawn); + + // 次元下げ用フラグ配列の初期化 + // KPPPに関しては関与しない。 + + KK g_kk; + g_kk.set(SQUARE_NB, Eval::fe_end, 0); + KKP g_kkp; + g_kkp.set(SQUARE_NB, Eval::fe_end, g_kk.max_index()); + KPP g_kpp; + g_kpp.set(SQUARE_NB, Eval::fe_end, g_kkp.max_index()); + + uint64_t size = g_kpp.max_index(); + min_index_flag.resize(size); + +#pragma omp parallel + { +#if defined(_OPENMP) + // Windows環境下でCPUが2つあるときに、論理64コアまでしか使用されないのを防ぐために + // ここで明示的にCPUに割り当てる + int thread_index = omp_get_thread_num(); // 自分のthread numberを取得 + WinProcGroup::bindThisThread(thread_index); +#endif + +#pragma omp for schedule(dynamic,20000) + + for (int64_t index_ = 0; index_ < (int64_t)size; ++index_) + { + // OpenMPの制約からループ変数は符号型でないといけないらしいのだが、 + // さすがに使いにくい。 + uint64_t index = (uint64_t)index_; + + if (g_kk.is_ok(index)) + { + // indexからの変換と逆変換によって元のindexに戻ることを確認しておく。 + // 起動時に1回しか実行しない処理なのでassertで書いておく。 + assert(g_kk.fromIndex(index).toIndex() == index); + + KK a[KK_LOWER_COUNT]; + g_kk.fromIndex(index).toLowerDimensions(a); + + // 次元下げの1つ目の要素が元のindexと同一であることを確認しておく。 + assert(a[0].toIndex() == index); + + uint64_t min_index = UINT64_MAX; + for (auto& e : a) + min_index = std::min(min_index, e.toIndex()); + min_index_flag[index] = (min_index == index); + } + else if (g_kkp.is_ok(index)) + { + assert(g_kkp.fromIndex(index).toIndex() == index); + + KKP x = g_kkp.fromIndex(index); + KKP a[KKP_LOWER_COUNT]; + x.toLowerDimensions(a); + + assert(a[0].toIndex() == index); + + uint64_t min_index = UINT64_MAX; + for (auto& e : a) + min_index = std::min(min_index, e.toIndex()); + min_index_flag[index] = (min_index == index); + } + else if (g_kpp.is_ok(index)) + { + assert(g_kpp.fromIndex(index).toIndex() == index); + + KPP x = g_kpp.fromIndex(index); + KPP a[KPP_LOWER_COUNT]; + x.toLowerDimensions(a); + + assert(a[0].toIndex() == index); + + uint64_t min_index = UINT64_MAX; + for (auto& e : a) + min_index = std::min(min_index, e.toIndex()); + min_index_flag[index] = (min_index == index); + } + else + { + assert(false); + } + } + } + } + + void learning_tools_unit_test_kpp() + { + + // KPPの三角配列化にバグがないかテストする + // k-p0-p1のすべての組み合わせがきちんとKPPの扱う対象になっていかと、そのときの次元下げが + // 正しいかを判定する。 + + KK g_kk; + g_kk.set(SQUARE_NB, Eval::fe_end, 0); + KKP g_kkp; + g_kkp.set(SQUARE_NB, Eval::fe_end, g_kk.max_index()); + KPP g_kpp; + g_kpp.set(SQUARE_NB, Eval::fe_end, g_kkp.max_index()); + + std::vector f; + f.resize(g_kpp.max_index() - g_kpp.min_index()); + + for(auto k = SQUARE_ZERO ; k < SQUARE_NB ; ++k) + for(auto p0 = BonaPiece::BONA_PIECE_ZERO; p0 < fe_end ; ++p0) + for (auto p1 = BonaPiece::BONA_PIECE_ZERO; p1 < fe_end; ++p1) + { + KPP kpp_org = g_kpp.fromKPP(k,p0,p1); + KPP kpp0; + KPP kpp1 = g_kpp.fromKPP(Mir(k), mir_piece(p0), mir_piece(p1)); + KPP kpp_array[2]; + + auto index = kpp_org.toIndex(); + assert(g_kpp.is_ok(index)); + + kpp0 = g_kpp.fromIndex(index); + + //if (kpp0 != kpp_org) + // std::cout << "index = " << index << "," << kpp_org << "," << kpp0 << std::endl; + + kpp0.toLowerDimensions(kpp_array); + + assert(kpp_array[0] == kpp0); + assert(kpp0 == kpp_org); + assert(kpp_array[1] == kpp1); + + auto index2 = kpp1.toIndex(); + f[index - g_kpp.min_index()] = f[index2-g_kpp.min_index()] = true; + } + + // 抜けてるindexがなかったかの確認。 + for(size_t index = 0 ; index < f.size(); index++) + if (!f[index]) + { + std::cout << index << g_kpp.fromIndex(index + g_kpp.min_index()) << std::endl; + } + } + + void learning_tools_unit_test_kppp() + { + // KPPPの計算に抜けがないかをテストする + + KPPP g_kppp; + g_kppp.set(15, Eval::fe_end,0); + uint64_t min_index = g_kppp.min_index(); + uint64_t max_index = g_kppp.max_index(); + + // 最後の要素の確認。 + //KPPP x = KPPP::fromIndex(max_index-1); + //std::cout << x << std::endl; + + for (uint64_t index = min_index; index < max_index; ++index) + { + KPPP x = g_kppp.fromIndex(index); + //std::cout << x << std::endl; + +#if 0 + if ((index % 10000000) == 0) + std::cout << "index = " << index << std::endl; + + // index = 9360000000 + // done. + + if (x.toIndex() != index) + { + std::cout << "assertion failed , index = " << index << std::endl; + } +#endif + + assert(x.toIndex() == index); + +// ASSERT((&kppp_ksq_pcpcpc(x.king(), x.piece0(), x.piece1(), x.piece2()) - &kppp[0][0]) == (index - min_index)); + } + + } + + void learning_tools_unit_test_kkpp() + { + KKPP g_kkpp; + g_kkpp.set(SQUARE_NB, 10000 , 0); + uint64_t n = 0; + for (int k = 0; k + +#include "../eval/evaluate_mir_inv_tools.h" + +#if defined(SGD_UPDATE) || defined(USE_KPPP_MIRROR_WRITE) +#include "../misc.h" // PRNG , my_insertion_sort +#endif + +#include // std::sqrt() + +namespace EvalLearningTools +{ + // ------------------------------------------------- + // 初期化 + // ------------------------------------------------- + + // このEvalLearningTools名前空間にあるテーブル類を初期化する。 + // 学習の開始までに必ず一度呼び出すこと。 + // この関数のなかで、init_mir_inv_tables()も呼び出している。 + // (この関数を呼ぶときは、init_mir_inv_tables()を呼び出す必要はない。) + void init(); + + // ------------------------------------------------- + // flags + // ------------------------------------------------- + + // 次元下げしたときに、そのなかの一番小さなindexになることが + // わかっているindexに対してtrueとなっているフラグ配列。 + // この配列もinit()によって初期化される。 + // KPPPに関しては、関与しない。 + // ゆえに、この配列の有効なindexの範囲は、KK::min_index()~KPP::max_index()まで。 + extern std::vector min_index_flag; + + // ------------------------------------------------- + // 勾配等を格納している学習用の配列 + // ------------------------------------------------- + +#if defined(_MSC_VER) +#pragma pack(push,2) +#elif defined(__GNUC__) +#pragma pack(2) +#endif + struct Weight + { + // mini-batch 1回分の勾配の累積値 + LearnFloatType g = LearnFloatType(0); + + // ADA_GRAD_UPDATEのとき。LearnFloatType == floatとして、 + // 合計 4*2 + 4*2 + 1*2 = 18 bytes + // 1GBの評価関数パラメーターに対してその4.5倍のサイズのWeight配列が確保できれば良い。 + // ただし、構造体のアライメントが4バイト単位になっているとsizeof(Weight)==20なコードが生成されるので + // pragma pack(2)を指定しておく。 + + // SGD_UPDATE の場合、この構造体はさらに10バイト減って、8バイトで済む。 + + // AdaGradなどの学習率η(eta)。 + // updateFV()が呼び出されるまでにeta1,2,3,eta1_epoch,eta2_epochは設定されているものとする。 + // update_weights()のepochが、eta1_epochまでeta1から徐々にeta2に変化する。 + // eta2_epoch以降は、eta2から徐々にeta3に変化する。 + static double eta; + static double eta1; + static double eta2; + static double eta3; + static uint64_t eta1_epoch; + static uint64_t eta2_epoch; + + // etaの一括初期化。0が渡された場合、デフォルト値が設定される。 + static void init_eta(double eta1, double eta2, double eta3, uint64_t eta1_epoch, uint64_t eta2_epoch) + { + Weight::eta1 = (eta1 != 0) ? eta1 : 30.0; + Weight::eta2 = (eta2 != 0) ? eta2 : 30.0; + Weight::eta3 = (eta3 != 0) ? eta3 : 30.0; + Weight::eta1_epoch = (eta1_epoch != 0) ? eta1_epoch : 0; + Weight::eta2_epoch = (eta2_epoch != 0) ? eta2_epoch : 0; + } + + // epochに応じたetaを設定してやる。 + static void calc_eta(uint64_t epoch) + { + if (Weight::eta1_epoch == 0) // eta2適用除外 + Weight::eta = Weight::eta1; + else if (epoch < Weight::eta1_epoch) + // 按分する + Weight::eta = Weight::eta1 + (Weight::eta2 - Weight::eta1) * epoch / Weight::eta1_epoch; + else if (Weight::eta2_epoch == 0) // eta3適用除外 + Weight::eta = Weight::eta2; + else if (epoch < Weight::eta2_epoch) + Weight::eta = Weight::eta2 + (Weight::eta3 - Weight::eta2) * (epoch - Weight::eta1_epoch) / (Weight::eta2_epoch - Weight::eta1_epoch); + else + Weight::eta = Weight::eta3; + } + + template void updateFV(T& v) { updateFV(v, 1.0); } + +#if defined (ADA_GRAD_UPDATE) + + // floatで正確に計算できる最大値はINT16_MAX*256-1なのでそれより + // 小さい値をマーカーにしておく。 + const LearnFloatType V0_NOT_INIT = (INT16_MAX * 128); + + // vを内部的に保持しているもの。以前の実装ではメモリの節約のために固定小数で小数部だけを保持していたが + // 精度的に怪しいし、見通しが悪くなるので廃止した。 + LearnFloatType v0 = LearnFloatType(V0_NOT_INIT); + + // AdaGradのg2 + LearnFloatType g2 = LearnFloatType(0); + + // AdaGradでupdateする + // この関数を実行しているときにgの値やメンバーが書き変わらないことは + // 呼び出し側で保証されている。atomic演算である必要はない。 + // kはetaに掛かる係数。普通は1.0で良い。手番項に対してetaを下げたいときにここを1/8.0などとする。 + template + void updateFV(T& v,double k) + { + // AdaGradの更新式 + // 勾配ベクトルをg、更新したいベクトルをv、η(eta)は定数として、 + // g2 = g2 + g^2 + // v = v - ηg/sqrt(g2) + + constexpr double epsilon = 0.000001; + + if (g == LearnFloatType(0)) + return; + + g2 += g * g; + + // v0がV0_NOT_INITであるなら、値がKK/KKP/KPP配列の値で初期化されていないということだから、 + // この場合、vの値を引数で渡されたものから読み込む。 + double V = (v0 == V0_NOT_INIT) ? v : v0; + + V -= k * eta * (double)g / sqrt((double)g2 + epsilon); + + // Vの値を型の範囲に収まるように制限する。 + // ちなみに、windows.hがmin,maxマクロを定義してしまうのでそれを回避するために、 + // ここでは括弧で括ることで関数形式マクロとして扱われないようにしている。 + V = (std::min)((double)(std::numeric_limits::max)() , V); + V = (std::max)((double)(std::numeric_limits::min)() , V); + + v0 = (LearnFloatType)V; + v = (T)round(V); + + // この要素に関するmini-batchの1回分の更新が終わったのでgをクリア + // g[i] = 0; + // →次元下げの問題があるので、これは呼び出し側で行なうことにする。 + } + +#elif defined(SGD_UPDATE) + + // 勾配の符号だけ見るSGDでupdateする + // この関数を実行しているときにgの値やメンバーが書き変わらないことは + // 呼び出し側で保証されている。atomic演算である必要はない。 + template + void updateFV(T & v , double k) + { + if (g == 0) + return; + + // gの符号だけ見てupdateする。 + // g < 0 なら vを少し足す。 + // g > 0 なら vを少し引く。 + + // 整数しか足さないので小数部不要。 + + // 0~5ぐらいずつ動かすのがよさげ。 + // ガウス分布っぽいほうが良いので5bitの乱数を発生させて(それぞれのbitは1/2の確率で1である)、 + // それをpop_count()する。このとき、二項分布になっている。 + //int16_t diff = (int16_t)POPCNT32((u32)prng.rand(31)); + // → これ80スレッドでやったら、このAsyncPRNG::rand()がlockするのでslow downした。この実装良くない。 + int16_t diff = 1; + + double V = v; + if (g > 0.0) + V-= diff; + else + V+= diff; + + V = (std::min)((double)(std::numeric_limits::max)(), V); + V = (std::max)((double)(std::numeric_limits::min)(), V); + + v = (T)V; + } + +#endif + + // gradの設定 + template void set_grad(const T& g_) { g = g_; } + + // gradの加算 + template void add_grad(const T& g_) { g += g_; } + + LearnFloatType get_grad() const { return g; } + }; +#if defined(_MSC_VER) +#pragma pack(pop) +#elif defined(__GNUC__) +#pragma pack(0) +#endif + + // 手番つきのweight配列 + // 透過的に扱えるようにするために、Weightと同じメンバを持たせておいてやる。 + struct Weight2 + { + Weight w[2]; + + // 手番評価、etaを1/8に評価しておく。 + template void updateFV(std::array& v) { w[0].updateFV(v[0] , 1.0); w[1].updateFV(v[1],1.0/8.0); } + + template void set_grad(const std::array& g) { for (int i = 0; i<2; ++i) w[i].set_grad(g[i]); } + template void add_grad(const std::array& g) { for (int i = 0; i<2; ++i) w[i].add_grad(g[i]); } + + std::array get_grad() const { return std::array{w[0].get_grad(), w[1].get_grad()}; } + }; + + // ------------------------------------------------- + // Weight配列を直列化したときのindexを計算したりするヘルパー。 + // ------------------------------------------------- + + // KK,KKP,KPP,KKPPの基底クラス + // これらのクラスの使い方 + // + // 1. まずset()で初期化する。例) KK g_kk; g_kk.set(SQUARE_NB,fe_end,0); + // 2. 次にfromIndex(),fromKK()などでインスタンスを生成 + // 3. king(),piece0(),piece1()などのプロパティを用いてアクセス。 + // + // この説明だけではわかりにくいかも知れないが、学習部のinit_grad(),add_grad(),update_weights()などを見れば + // 必要性を含めて理解できると思う。 + // + // 注意 : この派生クラスでは次元下げのために上記のinv_piece/mir_pieceを間接的に参照することがあるので、 + // 最初にEvalLearningTools::init()かinit_mir_inv_tables()を呼び出して初期化すること。 + // + // 備考) 派生クラス側でoverrideすべきではない関数名には/*final*/と書いてある。 + // 派生クラス側でoverrideすべき関数は "= 0"をつけて、純粋仮想関数にしてある。 + // 派生クラス側でoverrideしてもしなくても良い関数はvirtualだけつけてある。 + // + struct SerializerBase + { + + // KK,KKP,KPP配列を直列化するときの通し番号の最小値、最大値+1。 + /*final*/ uint64_t min_index() const { return min_index_; } + /*final*/ uint64_t max_index() const { return min_index() + max_raw_index_; } + + // max_index() - min_index()の値。 + // 派生クラス側でmax_king_sq_,fe_end_などから、値を計算して返すようにする。 + virtual uint64_t size() const = 0; + + // 与えられたindexが、min_index()以上、max_index()未満にあるかを判定する。 + /*final*/ bool is_ok(uint64_t index) { return min_index() <= index && index < max_index(); } + + // 必ずこのset()を呼び出して使う。さもなくば、派生クラス側のfromKK()/fromIndex()などでインスタンスを構築して使う。 + virtual void set(int max_king_sq, uint64_t fe_end, uint64_t min_index) + { + max_king_sq_ = max_king_sq; + fe_end_ = fe_end; + min_index_ = min_index; + max_raw_index_ = size(); + } + + // 現在のメンバの値に基いて、直列化されたときのindexを取得する。 + /*final*/ uint64_t toIndex() const { + return min_index() + toRawIndex(); + } + + // 直列化するときのindexを返す。(min_index()の値は加算する前のもの) + virtual uint64_t toRawIndex() const = 0; + + protected: + // このクラスの返すmin_index()の値 + uint64_t min_index_; + + // このクラスの返すmax_index()の値 = min_index() + max_raw_index_ + // この変数は派生クラスのsize()で計算されたもの。 + uint64_t max_raw_index_; + + // サポートする玉の升の数(通常SQUARE_NB) + int max_king_sq_; + + // サポートするBonaPieceの最大値 + uint64_t fe_end_; + + }; + + struct KK : public SerializerBase + { + protected: + KK(Square king0, Square king1,bool inverse) : king0_(king0), king1_(king1) , inverse_sign(inverse) {} + public: + KK() {} + + virtual uint64_t size() const { return max_king_sq_ * max_king_sq_; } + + // index(通し番号)からKKのオブジェクトを生成するbuilder + KK fromIndex(uint64_t index) const { assert(index >= min_index()); return fromRawIndex(index - min_index()); } + + // raw_index(通し番号ではなく0から始まる番号)からKKのオブジェクトを生成するbuilder + KK fromRawIndex(uint64_t raw_index) const + { + int king1 = (int)(raw_index % SQUARE_NB); + raw_index /= SQUARE_NB; + int king0 = (int)(raw_index /* % SQUARE_NB */); + assert(king0 < SQUARE_NB); + return fromKK((Square)king0, (Square)king1 , false); + } + KK fromKK(Square king0, Square king1 , bool inverse) const + { + // kkという変数名はEval::kk配列などで使っているので別の名前にする必要がある。(以下、KKP,KPPクラスなどでも同様) + KK my_kk(king0, king1, inverse); + my_kk.set(max_king_sq_, fe_end_, min_index()); + return my_kk; + } + KK fromKK(Square king0, Square king1) const { return fromKK(king0, king1, false); } + + // fromIndex()を用いてこのオブジェクトを構築したときに、以下のアクセッサで情報が得られる。 + Square king0() const { return king0_; } + Square king1() const { return king1_; } + +// 次元下げの数 +#if defined(USE_KK_INVERSE_WRITE) + #define KK_LOWER_COUNT 4 +#elif defined(USE_KK_MIRROR_WRITE) + #define KK_LOWER_COUNT 2 +#else + #define KK_LOWER_COUNT 1 +#endif + +#if defined(USE_KK_INVERSE_WRITE) && !defined(USE_KK_MIRROR_WRITE) + // USE_KK_INVERSE_WRITEわ使うならUSE_KK_MIRROR_WRITEも定義して欲しい。 + static_assert(false, "define also USE_KK_MIRROR_WRITE!"); +#endif + + // 低次元の配列のindexを得る。 + // USE_KK_INVERSE_WRITEが有効なときは、それらをinverseしたものが[2],[3]に入る。 + // この次元下げに関して、gradの符号は反転させないといけないので注意すること。 + // is_inverse()で判定できるのでこれを利用すると良い。 + void toLowerDimensions(/*out*/KK kk_[KK_LOWER_COUNT]) const { + kk_[0] = fromKK(king0_, king1_,false); +#if defined(USE_KK_MIRROR_WRITE) + kk_[1] = fromKK(Mir(king0_),Mir(king1_),false); +#if defined(USE_KK_INVERSE_WRITE) + kk_[2] = fromKK(Inv(king1_), Inv(king0_),true); + kk_[3] = fromKK(Inv(Mir(king1_)) , Inv(Mir(king0_)),true); +#endif +#endif + } + + // このクラスのmin_index()の値を0として数えたときのindexを取得する。 + virtual uint64_t toRawIndex() const { + return (uint64_t)king0_ * (uint64_t)max_king_sq_ + (uint64_t)king1_; + } + + // toLowerDimensionsで次元下げしたものがinverseしたものであるかを返す。 + bool is_inverse() const { + return inverse_sign; + } + + // is_inverse() == trueのときに、gradの手番ではないほうの符号を反転させて返す。 + template + std::array apply_inverse_sign(const std::array& rhs) + { + return !is_inverse() ? rhs : std::array{-rhs[0], rhs[1]}; + } + + // 比較演算子 + bool operator==(const KK& rhs) { return king0() == rhs.king0() && king1() == rhs.king1(); } + bool operator!=(const KK& rhs) { return !(*this == rhs); } + + private: + Square king0_, king1_ ; + bool inverse_sign; + }; + + // デバッグ用出力。 + static std::ostream& operator<<(std::ostream& os, KK rhs) + { + os << "KK(" << rhs.king0() << "," << rhs.king1() << ")"; + return os; + } + + // KKと同じく。KKP用。 + struct KKP : public SerializerBase + { + protected: + KKP(Square king0, Square king1, Eval::BonaPiece p) : king0_(king0), king1_(king1), piece_(p), inverse_sign(false) {} + KKP(Square king0, Square king1, Eval::BonaPiece p, bool inverse) : king0_(king0), king1_(king1), piece_(p),inverse_sign(inverse) {} + public: + KKP() {} + + virtual uint64_t size() const { return (uint64_t)max_king_sq_*(uint64_t)max_king_sq_*(uint64_t)fe_end_; } + + // index(通し番号)からKKPのオブジェクトを生成するbuilder + KKP fromIndex(uint64_t index) const { assert(index >= min_index()); return fromRawIndex(index - min_index()); } + + // raw_index(通し番号ではなく0から始まる番号)からKKPのオブジェクトを生成するbuilder + KKP fromRawIndex(uint64_t raw_index) const + { + int piece = (int)(raw_index % Eval::fe_end); + raw_index /= Eval::fe_end; + int king1 = (int)(raw_index % SQUARE_NB); + raw_index /= SQUARE_NB; + int king0 = (int)(raw_index /* % SQUARE_NB */); + assert(king0 < SQUARE_NB); + return fromKKP((Square)king0, (Square)king1, (Eval::BonaPiece)piece,false); + } + + KKP fromKKP(Square king0, Square king1, Eval::BonaPiece p, bool inverse) const + { + KKP my_kkp(king0, king1, p, inverse); + my_kkp.set(max_king_sq_,fe_end_,min_index()); + return my_kkp; + } + KKP fromKKP(Square king0, Square king1, Eval::BonaPiece p) const { return fromKKP(king0, king1, p, false); } + + // fromIndex()を用いてこのオブジェクトを構築したときに、以下のアクセッサで情報が得られる。 + Square king0() const { return king0_; } + Square king1() const { return king1_; } + Eval::BonaPiece piece() const { return piece_; } + + // KKPの次元下げの数 +#if defined(USE_KKP_INVERSE_WRITE) + #define KKP_LOWER_COUNT 4 +#elif defined(USE_KKP_MIRROR_WRITE) + #define KKP_LOWER_COUNT 2 +#else + #define KKP_LOWER_COUNT 1 +#endif + +#if defined(USE_KKP_INVERSE_WRITE) && !defined(USE_KKP_MIRROR_WRITE) + // USE_KKP_INVERSE_WRITEわ使うならUSE_KKP_MIRROR_WRITEも定義して欲しい。 + static_assert(false, "define also USE_KKP_MIRROR_WRITE!"); +#endif + + // 低次元の配列のindexを得る。ミラーしたものがkkp_[1]に返る。 + // USE_KKP_INVERSE_WRITEが有効なときは、それらをinverseしたものが[2],[3]に入る。 + // この次元下げに関して、gradの符号は反転させないといけないので注意すること。 + // is_inverse()で判定できるのでこれを利用すると良い。 + void toLowerDimensions(/*out*/ KKP kkp_[KKP_LOWER_COUNT]) const { + kkp_[0] = fromKKP(king0_, king1_, piece_,false); +#if defined(USE_KKP_MIRROR_WRITE) + kkp_[1] = fromKKP(Mir(king0_), Mir(king1_), mir_piece(piece_),false); +#if defined(USE_KKP_INVERSE_WRITE) + kkp_[2] = fromKKP( Inv(king1_), Inv(king0_), inv_piece(piece_),true); + kkp_[3] = fromKKP( Inv(Mir(king1_)), Inv(Mir(king0_)) , inv_piece(mir_piece(piece_)),true); +#endif +#endif + } + + // このクラスのmin_index()の値を0として数えたときのindexを取得する。 + virtual uint64_t toRawIndex() const { + return ((uint64_t)king0_ * (uint64_t)max_king_sq_ + (uint64_t)king1_) * (uint64_t)fe_end_ + (uint64_t)piece_; + } + + // toLowerDimensionsで次元下げしたものがinverseしたものであるかを返す。 + bool is_inverse() const { + return inverse_sign; + } + + // is_inverse() == trueのときに、gradの手番ではないほうの符号を反転させて返す。 + template + std::array apply_inverse_sign(const std::array& rhs) + { + return !is_inverse() ? rhs : std::array{-rhs[0], rhs[1]}; + } + + // 比較演算子 + bool operator==(const KKP& rhs) { return king0() == rhs.king0() && king1() == rhs.king1() && piece() == rhs.piece(); } + bool operator!=(const KKP& rhs) { return !(*this == rhs); } + + private: + Square king0_, king1_; + Eval::BonaPiece piece_; + bool inverse_sign; + }; + + // デバッグ用出力。 + static std::ostream& operator<<(std::ostream& os, KKP rhs) + { + os << "KKP(" << rhs.king0() << "," << rhs.king1() << "," << rhs.piece() << ")"; + return os; + } + + + // KK,KKPと同様。KPP用 + struct KPP : public SerializerBase + { + protected: + KPP(Square king, Eval::BonaPiece p0, Eval::BonaPiece p1) : king_(king), piece0_(p0), piece1_(p1) {} + + public: + KPP() {} + + // KK,KKP,KPP配列を直列化するときの通し番号の、KPPの最小値、最大値。 +#if !defined(USE_TRIANGLE_WEIGHT_ARRAY) + virtual uint64_t size() const { return (uint64_t)max_king_sq_*(uint64_t)fe_end_*(uint64_t)fe_end_; } +#else + // kpp[SQUARE_NB][fe_end][fe_end]の[fe_end][fe_end]な正方配列の部分を三角配列化する。 + // kpp[SQUARE_NB][triangle_fe_end]とすると、この三角配列の1行目は要素1個、2行目は2個、…。 + // ゆえに、triangle_fe_end = 1 + 2 + .. + fe_end = fe_end * (fe_end + 1) / 2 + virtual uint64_t size() const { return (uint64_t)max_king_sq_*(uint64_t)triangle_fe_end; } +#endif + + virtual void set(int max_king_sq, uint64_t fe_end, uint64_t min_index) + { + // この値、size()で用いていて、SerializerBase::set()でsize()を使うので先に計算する。 + triangle_fe_end = (uint64_t)fe_end*((uint64_t)fe_end + 1) / 2; + + SerializerBase::set(max_king_sq, fe_end, min_index); + } + + // index(通し番号)からKPPのオブジェクトを生成するbuilder + KPP fromIndex(uint64_t index) const { assert(index >= min_index()); return fromRawIndex(index - min_index()); } + + // raw_index(通し番号ではなく0から始まる番号)からKPPのオブジェクトを生成するbuilder + KPP fromRawIndex(uint64_t raw_index) const + { + const uint64_t triangle_fe_end = (uint64_t)fe_end_*((uint64_t)fe_end_ + 1) / 2; + +#if !defined(USE_TRIANGLE_WEIGHT_ARRAY) + int piece1 = (int)(raw_index % fe_end_); + raw_index /= fe_end_; + int piece0 = (int)(raw_index % fe_end_); + raw_index /= fe_end_; +#else + uint64_t index2 = raw_index % triangle_fe_end; + + // ここにindex2からpiece0,piece1を求める式を書く。 + // これは index2 = i * (i+1) / 2 + j の逆関数となる。 + // j = 0 の場合、i^2 + i - 2 * index2 == 0なので + // 2次方程式の解の公式から i = (sqrt(8*index2+1) - 1) / 2である。 + // iを整数化したのちに、j = index2 - i * (i + 1) / 2としてjを求めれば良い。 + + // BonaPieceは32bit(16bitに収まらない可能性)を想定しているのでこの掛け算は64bitでないといけない。 + int piece1 = int(sqrt(8 * index2 + 1) - 1) / 2; + int piece0 = int(index2 - (uint64_t)piece1*((uint64_t)piece1 + 1) / 2); + + assert(piece1 < (int)fe_end_); + assert(piece0 < (int)fe_end_); + assert(piece0 > piece1); + + raw_index /= triangle_fe_end; +#endif + int king = (int)(raw_index /* % SQUARE_NB */); + assert(king < max_king_sq_); + return fromKPP((Square)king, (Eval::BonaPiece)piece0, (Eval::BonaPiece)piece1); + } + + KPP fromKPP(Square king, Eval::BonaPiece p0, Eval::BonaPiece p1) const + { + KPP my_kpp(king, p0, p1); + my_kpp.set(max_king_sq_,fe_end_,min_index()); + return my_kpp; + } + + // fromIndex()を用いてこのオブジェクトを構築したときに、以下のアクセッサで情報が得られる。 + Square king() const { return king_; } + Eval::BonaPiece piece0() const { return piece0_; } + Eval::BonaPiece piece1() const { return piece1_; } + + + // 次元下げの数 +#if defined(USE_KPP_MIRROR_WRITE) + #if !defined(USE_TRIANGLE_WEIGHT_ARRAY) + #define KPP_LOWER_COUNT 4 + #else + #define KPP_LOWER_COUNT 2 + #endif +#else + #if !defined(USE_TRIANGLE_WEIGHT_ARRAY) + #define KPP_LOWER_COUNT 2 + #else + #define KPP_LOWER_COUNT 1 + #endif +#endif + + // 低次元の配列のindexを得る。p1,p2を入れ替えたもの、ミラーしたものなどが返る。 + void toLowerDimensions(/*out*/ KPP kpp_[KPP_LOWER_COUNT]) const { + +#if defined(USE_TRIANGLE_WEIGHT_ARRAY) + // 三角配列を用いる場合は、piece0とpiece1を入れ替えたものは返らないので注意。 + kpp_[0] = fromKPP(king_, piece0_, piece1_); +#if defined(USE_KPP_MIRROR_WRITE) + kpp_[1] = fromKPP(Mir(king_), mir_piece(piece0_), mir_piece(piece1_)); +#endif + +#else + // 三角配列を用いない場合 + kpp_[0] = fromKPP(king_, piece0_, piece1_); + kpp_[1] = fromKPP(king_, piece1_, piece0_); +#if defined(USE_KPP_MIRROR_WRITE) + kpp_[2] = fromKPP(Mir(king_), mir_piece(piece0_), mir_piece(piece1_)); + kpp_[3] = fromKPP(Mir(king_), mir_piece(piece1_), mir_piece(piece0_)); +#endif +#endif + } + + // このクラスのmin_index()の値を0として数えたときのindexを取得する。 + virtual uint64_t toRawIndex() const { + +#if !defined(USE_TRIANGLE_WEIGHT_ARRAY) + + return ((uint64_t)king_ * (uint64_t)fe_end_ + (uint64_t)piece0_) * (uint64_t)fe_end_ + (uint64_t)piece1_; + +#else + // Bonanza6.0で使われているのに似せたマクロ + auto PcPcOnSq = [&](Square k, Eval::BonaPiece i, Eval::BonaPiece j) + { + + // この三角配列の(i,j)は、i行目のj列目の要素。 + // i行目0列目は、そこまでの要素の合計であるから、1 + 2 + ... + i = i * (i+1) / 2 + // i行目j列目は、これにjを足したもの。i * (i + 1) /2 + j + + // BonaPiece型は、32bitを想定しているので掛け算には気をつけないとオーバーフローする。 + return (uint64_t)k * triangle_fe_end + (uint64_t)(uint64_t(i)*(uint64_t(i)+1) / 2 + uint64_t(j)); + }; + + auto k = king_; + auto i = piece0_; + auto j = piece1_; + + return (i >= j) ? PcPcOnSq(k, i, j) : PcPcOnSq(k, j, i); +#endif + } + + // toLowerDimensionsで次元下げしたものがinverseしたものであるかを返す。 + // KK,KKPとinterfaceを合せるために用意してある。このKPPクラスでは、このメソッドは常にfalseを返す。 + bool is_inverse() const { + return false; + } + + // 比較演算子 + bool operator==(const KPP& rhs) { + return king() == rhs.king() && + ((piece0() == rhs.piece0() && piece1() == rhs.piece1()) +#if defined(USE_TRIANGLE_WEIGHT_ARRAY) + // 三角配列を用いるときはpiece0とpiece1の入れ替わりを許容する。 + || (piece0() == rhs.piece1() && piece1() == rhs.piece0()) +#endif + ); } + bool operator!=(const KPP& rhs) { return !(*this == rhs); } + + + private: + Square king_; + Eval::BonaPiece piece0_, piece1_; + + uint64_t triangle_fe_end; // = (uint64_t)fe_end_*((uint64_t)fe_end_ + 1) / 2; + }; + + // デバッグ用出力。 + static std::ostream& operator<<(std::ostream& os, KPP rhs) + { + os << "KPP(" << rhs.king() << "," << rhs.piece0() << "," << rhs.piece1() << ")"; + return os; + } + + // KPPPの4駒関係。ただし、手番ありでミラー等を考慮しないと学習に2TB以上のメモリが必要…。 + // 三角配列を使っても学習のために50GB×12バイト = 600GB必要。 + // ミラーしたもののみを格納するようにしてもの半分ぐらい必要。 + // ここでは、三角配列は必ず用いて、かつミラーしたものを格納するものとする。 + // + // また、このクラスのking()は、実際のkingのSquareとは限らず、単に、0~(king_sq-1)までの値が返る。 + // これは、ミラーを利用した圧縮を行なう場合など、利用側で適切な玉の位置に変換してやる必要がある。 + // + // あと、このクラスの返すpiece0,1,2に関して、 + // piece0() > piece1() > piece2() + // であり、コンストラクタでpiece0,1,2を渡すときも、この制約を守る必要がある。 + struct KPPP : public SerializerBase + { + protected: + KPPP(int king, Eval::BonaPiece p0, Eval::BonaPiece p1, Eval::BonaPiece p2) : + king_(king), piece0_(p0), piece1_(p1), piece2_(p2) + { + assert(piece0_ > piece1_ && piece1_ > piece2_); + /* sort_piece(); */ + } + + public: + KPPP() {} + + virtual uint64_t size() const { return (uint64_t)max_king_sq_*triangle_fe_end; } + + // fe_endとking_sqを設定する。 + // fe_end : このKPPPクラスの想定するfe_end + // king_sq : KPPPのときに扱う玉の升の数。 + // 3段×ミラーなら3段×5筋 = 15みたいな感じ。 + // 2段×ミラーなしなら2×9筋 = 18みたいな感じ。 + // これをこのKPPPクラスを使う側でset()を用いて最初に設定する。 + virtual void set(int max_king_sq, uint64_t fe_end,uint64_t min_index) { + // この値、size()で用いていて、SerializerBase::set()でsize()を使うので先に計算する。 + triangle_fe_end = fe_end * (fe_end - 1) * (fe_end - 2) / 6; + + SerializerBase::set(max_king_sq, fe_end, min_index); + } + + // 次元下げの数 + // とりあえず、ミラーの次元下げ非対応。ここでやることもないかと…。 +/* +#if defined(USE_KPPP_MIRROR_WRITE) +#define KPPP_LOWER_COUNT 2 +#else +#define KPPP_LOWER_COUNT 1 +#endif +*/ +#define KPPP_LOWER_COUNT 1 + + // 低次元の配列のindexを得る。 + // p0,p1,p2を入れ替えたものは返らないので注意。 + // またミラーしたものも、USE_KPPP_MIRROR_WRITEが有効なときしか返さない。 + void toLowerDimensions(/*out*/ KPPP kppp_[KPPP_LOWER_COUNT]) const + { + kppp_[0] = fromKPPP(king_, piece0_, piece1_,piece2_); +#if KPPP_LOWER_COUNT > 1 + // mir_pieceするとsortされてない状態になる。sortするコードが必要。 + Eval::BonaPiece p_list[3] = { mir_piece(piece2_), mir_piece(piece1_), mir_piece(piece0_) }; + my_insertion_sort(p_list, 0, 3); + kppp_[1] = fromKPPP((int)Mir((Square)king_), p_list[2] , p_list[1], p_list[0]); +#endif + } + + // index(通し番号)からKPPPのオブジェクトを生成するbuilder + KPPP fromIndex(uint64_t index) const { assert(index >= min_index()); return fromRawIndex(index - min_index()); } + + // raw_index(通し番号ではなく0から始まる番号)からKPPPのオブジェクトを生成するbuilder + KPPP fromRawIndex(uint64_t raw_index) const + { + uint64_t index2 = raw_index % triangle_fe_end; + + // ここにindex2からpiece0,piece1,piece2を求める式を書く。 + // これは index2 = i(i-1)(i-2)/6-1 + j(j+1)/2 + k の逆関数となる。 + // j = k = 0 の場合、3次方程式の解の公式から実根は、 i = ...である。(以下式) + // ただしindex2が0,1のときは実数解が複数ある。これを考慮しないといけない。計算精度が足りないことに対する対策必要。 + // iが求まったあとはiを整数化したのちに、最初の式に入れてKPPのとき同様にjを求めれば良い。 + + // この処理、数値計算としてわりと難しい。色々工夫が必要。 + + int piece0; + if (index2 <= 1) + { + // index2 == 0,1のときだけ実数解が複数ある。 + piece0 = (int)index2 + 2; + + } else { + + //double t = pow(sqrt((243 *index2 * index2 - 1) * 3) + 27 * index2, 1.0 / 3); + // → これだとindex2が大きくなるとsqrt()の中身、オーバーフローする。 + + // sqrt()の中身がオーバーフローするので、sqrtのなかで3.0を掛けずにsqrtの外側でsqrt(3.0)を掛ける。 + // sqrt()の中身がオーバーフローするので、index2が大きいときは近似式を用いる。 + + double t; + + if (index2 < 100000000) + t = pow(sqrt((243.0 *index2 * index2 - 1)) * sqrt(3.0) + 27 * index2, 1.0 / 3); + else + // index2が非常に大きいとき、sqrtの中身、近似的に √243 * index2とみなせるだろう。 + t = pow( index2 * sqrt(243 * 3.0) + 27 * index2, 1.0 / 3); + + // 丸めのときに計算誤差でわずかに足りないのを防ぐためデルタを加算する。 + // 大きすぎると1大きい数になってしまう時があるので調整が必要。 + + const double delta = 0.000000001; + + piece0 = int(t / pow(3.0, 2.0 / 3) + 1.0 / (pow(3.0, 1.0 / 3) * t) + delta) + 1; + // ううう。ほんまにこんなことせんとあかんのか?(´ω`) + } + + // piece2が求まったので、上式のi(i-1)(i-2)/6(=aとする)のiにpiece2を代入。また、k = 0を代入。 + // j(j+1)/2 = index2 - a + // これは、2次方程式の解の公式より.. + + uint64_t a = (uint64_t)piece0*((uint64_t)piece0 - 1)*((uint64_t)piece0 - 2) / 6; + int piece1 = int((1 + sqrt(8.0 * (index2 - a ) + 1)) / 2); + uint64_t b = (uint64_t)piece1 * (piece1 - 1) / 2; + int piece2 = int(index2 - a - b); + +#if 0 + if (!((piece0 > piece1 && piece1 > piece2))) + { + std::cout << index << " , " << index2 << "," << a << "," << sqrt(8.0 * (index2 - a) + 1); + } +#endif + + assert(piece0 > piece1 && piece1 > piece2); + + assert(piece2 < (int)fe_end_); + assert(piece1 < (int)fe_end_); + assert(piece0 < (int)fe_end_); + + raw_index /= triangle_fe_end; + + int king = (int)(raw_index /* % SQUARE_NB */); + assert(king < max_king_sq_); + + // king_sqとfe_endに関しては伝播させる。 + return fromKPPP((Square)king, (Eval::BonaPiece)piece0, (Eval::BonaPiece)piece1 , (Eval::BonaPiece)piece2); + } + + // k,p0,p1,p2を指定してKPPPのインスタンスをbuildする。 + // 内部的に保持しているset()で渡されたking_sqとfe_endは引き継ぐ。 + KPPP fromKPPP(int king, Eval::BonaPiece p0, Eval::BonaPiece p1, Eval::BonaPiece p2) const + { + KPPP kppp(king, p0, p1, p2); + kppp.set(max_king_sq_, fe_end_,min_index()); + return kppp; + } + + // このクラスのmin_index()の値を0として数えたときのindexを取得する。 + virtual uint64_t toRawIndex() const { + + // Bonanza 6.0で使われているのに似せたマクロ + // 前提条件) i > j > k であること。 + // i==j,j==kのケースはNG。 + auto PcPcPcOnSq = [this](int king, Eval::BonaPiece i, Eval::BonaPiece j , Eval::BonaPiece k) + { + // この三角配列の(i,j,k)は、i行目のj列目の要素。 + // i行目0列0番目は、そこまでの要素の合計であるから、0 + 0 + 1 + 3 + 6 + ... + (i)*(i-1)/2 = i*(i-1)*(i-2)/ 6 + // i行目j列0番目は、そこにjを加味したもの。 + j*(j-1) / 2 + // i行目j列k番目は、そこにkを足したもの。 + k + assert(i > j && j > k); + + // BonaPiece型は、32bitを想定しているので掛け算には気をつけないとオーバーフローする。 + return (uint64_t)king * triangle_fe_end + (uint64_t)( + uint64_t(i)*(uint64_t(i) - 1) * (uint64_t(i) - 2) / 6 + + uint64_t(j)*(uint64_t(j) - 1) / 2 + + uint64_t(k) + ); + }; + + return PcPcPcOnSq(king_, piece0_, piece1_, piece2_); + } + + // fromIndex()を用いてこのオブジェクトを構築したときに、以下のアクセッサで情報が得られる。 + int king() const { return king_; } + Eval::BonaPiece piece0() const { return piece0_; } + Eval::BonaPiece piece1() const { return piece1_; } + Eval::BonaPiece piece2() const { return piece2_; } + // toLowerDimensionsで次元下げしたものがinverseしたものであるかを返す。 + // KK,KKPとinterfaceを合せるために用意してある。このKPPPクラスでは、このメソッドは常にfalseを返す。 + bool is_inverse() const { + return false; + } + + // 3角配列化したときの要素の数を返す。kppp配列が、以下のような2次元配列だと想定している。 + // kppp[king_sq][triangle_fe_end]; + uint64_t get_triangle_fe_end() const { return triangle_fe_end; } + + // 比較演算子 + bool operator==(const KPPP& rhs) { + // piece0 > piece1 > piece2を前提とするので、入れ替わりの可能性はない。 + return king() == rhs.king() && piece0() == rhs.piece0() && piece1() == rhs.piece1() && piece2() == rhs.piece2(); + } + bool operator!=(const KPPP& rhs) { return !(*this == rhs); } + + private: + + int king_; + Eval::BonaPiece piece0_, piece1_,piece2_; + + // kppp[king_sq][fe_end][fe_end][fe_end]の[fe_end][fe_end][fe_end]な正方配列の部分を三角配列化する。 + // kppp[king_sq][triangle_fe_end]とすると、この三角配列の0行目から要素数は、0,0,1,3,…,n行目はn(n-1)/2個。 + // ゆえに、 + // triangle_fe_end = Σn(n-1)/2 , n=0..fe_end-1 + // = fe_end * (fe_end - 1) * (fe_end - 2) / 6 + uint64_t triangle_fe_end; // ((uint64_t)Eval::fe_end)*((uint64_t)Eval::fe_end - 1)*((uint64_t)Eval::fe_end - 2) / 6; + }; + + // デバッグ用出力。 + static std::ostream& operator<<(std::ostream& os, KPPP rhs) + { + os << "KPPP(" << rhs.king() << "," << rhs.piece0() << "," << rhs.piece1() << "," << rhs.piece2() << ")"; + return os; + } + + // KKPPによる4駒関係の学習用。 + // + // KPPPクラスと同じ設計。KPPPクラスで、pが一枚少ないものとして扱う。 + // 2つの玉の位置は0~king_sq-1までの値としてencodeされているものとする。 + // + // あと、このクラスの返すpiece0,1に関して、 + // piece0() > piece1() + // であり、コンストラクタでpiece0,1を渡すときも、この制約を守る必要がある。 + // + // この制約から、BonaPieceZeroをpiece0,piece1に同時に代入して渡すことは出来ない。 + // 駒落ちの学習に対応させるならevaluate()で工夫が必要。 + struct KKPP : SerializerBase + { + protected: + KKPP(int king, Eval::BonaPiece p0, Eval::BonaPiece p1) : + king_(king), piece0_(p0), piece1_(p1) + { + assert(piece0_ > piece1_); + /* sort_piece(); */ + } + + public: + KKPP() {} + + virtual uint64_t size() const { return (uint64_t)max_king_sq_*triangle_fe_end; } + + // fe_endとking_sqを設定する。 + // fe_end : このKPPPクラスの想定するfe_end + // king_sq : KPPPのときに扱う玉の升の数。 + // 9段×ミラーなら9段×5筋の2乗(先後の玉) = 45*45 = 2025 みたいな感じ。 + // これをこのKKPPクラスを使う側でset()を用いて最初に設定する。 + void set(int max_king_sq, uint64_t fe_end , uint64_t min_index) { + // この値、size()で用いていて、SerializerBase::set()でsize()を使うので先に計算する。 + triangle_fe_end = fe_end * (fe_end - 1) / 2; + + SerializerBase::set(max_king_sq, fe_end, min_index); + } + + // 次元下げの数 + // とりあえず、ミラーの次元下げ非対応。ここでやることもないかと…。(学習用のメモリがもったいないので) +#define KKPP_LOWER_COUNT 1 + + // 低次元の配列のindexを得る。 + // p0,p1,p2を入れ替えたものは返らないので注意。 + // またミラーしたものも、USE_KPPP_MIRROR_WRITEが有効なときしか返さない。 + void toLowerDimensions(/*out*/ KKPP kkpp_[KPPP_LOWER_COUNT]) const + { + kkpp_[0] = fromKKPP(king_, piece0_, piece1_); + + // ミラーする場合、mir_pieceするとsortされてない状態になる。sortするコードが必要。 + // あとking_に対するミラーを定義する必要も。 + } + + // index(通し番号)からKKPPのオブジェクトを生成するbuilder + KKPP fromIndex(uint64_t index) const { assert(index >= min_index()); return fromRawIndex(index - min_index()); } + + // raw_index(通し番号ではなく0から始まる番号)からKKPPのオブジェクトを生成するbuilder + KKPP fromRawIndex(uint64_t raw_index) const + { + uint64_t index2 = raw_index % triangle_fe_end; + + // ここにindex2からpiece0,piece1,piece2を求める式を書く。 + // これは index2 = i(i-1)/2 + j の逆関数となる。 + // j=0として、二次方程式の解の公式を用いる。 + // index2=0のときは重根だが小さいほうはi>jを満たさないので無視。 + + int piece0 = (int(sqrt(8 * index2 + 1)) + 1)/2; + int piece1 = int(index2 - piece0 * (piece0 - 1) /2 ); + + assert(piece0 > piece1); + + assert(piece1 < (int)fe_end_); + assert(piece0 < (int)fe_end_); + + raw_index /= triangle_fe_end; + + int king = (int)(raw_index /* % SQUARE_NB */); + assert(king < max_king_sq_); + + // king_sqとfe_endに関しては伝播させる。 + return fromKKPP(king, (Eval::BonaPiece)piece0, (Eval::BonaPiece)piece1); + } + + // k,p0,p1を指定してKKPPのインスタンスをbuildする。 + // 内部的に保持しているset()で渡されたking_sqとfe_endは引き継ぐ。 + KKPP fromKKPP(int king, Eval::BonaPiece p0, Eval::BonaPiece p1) const + { + KKPP kkpp(king, p0, p1); + kkpp.set(max_king_sq_, fe_end_,min_index()); + return kkpp; + } + + // このクラスのmin_index()の値を0として数えたときのindexを取得する。 + virtual uint64_t toRawIndex() const { + + // Bonanza 6.0で使われているのに似せたマクロ + // 前提条件) i > jであること。 + // i==j,j==kのケースはNG。 + auto PcPcOnSq = [this](int king, Eval::BonaPiece i, Eval::BonaPiece j) + { + assert(i > j); + + // BonaPiece型は、32bitを想定しているので掛け算には気をつけないとオーバーフローする。 + return (uint64_t)king * triangle_fe_end + (uint64_t)( + + uint64_t(i)*(uint64_t(i) - 1) / 2 + + uint64_t(j) + ); + }; + + return PcPcOnSq(king_, piece0_, piece1_); + } + + // fromIndex(),fromKKPP()を用いてこのオブジェクトを構築したときに、以下のアクセッサで情報が得られる。 + int king() const { return king_; } + Eval::BonaPiece piece0() const { return piece0_; } + Eval::BonaPiece piece1() const { return piece1_; } + + // toLowerDimensionsで次元下げしたものがinverseしたものであるかを返す。 + // KK,KKPとinterfaceを合せるために用意してある。このKKPPクラスでは、このメソッドは常にfalseを返す。 + bool is_inverse() const { + return false; + } + + // 3角配列化したときの要素の数を返す。kkpp配列が、以下のような2次元配列だと想定している。 + // kkpp[king_sq][triangle_fe_end]; + uint64_t get_triangle_fe_end() const { return triangle_fe_end; } + + // 比較演算子 + bool operator==(const KKPP& rhs) { + // piece0 > piece1を前提とするので、入れ替わりの可能性はない。 + return king() == rhs.king() && piece0() == rhs.piece0() && piece1() == rhs.piece1(); + } + bool operator!=(const KKPP& rhs) { return !(*this == rhs); } + + private: + + int king_; + Eval::BonaPiece piece0_, piece1_; + + // kppp[king_sq][fe_end][fe_end]の[fe_end][fe_end]な正方配列の部分を三角配列化する。 + uint64_t triangle_fe_end = 0; + + }; + + // デバッグ用出力。 + static std::ostream& operator<<(std::ostream& os, KKPP rhs) + { + os << "KKPP(" << rhs.king() << "," << rhs.piece0() << "," << rhs.piece1() << ")"; + return os; + } + + +} + +#endif // defined (EVAL_LEARN) +#endif diff --git a/src/learn/multi_think.cpp b/src/learn/multi_think.cpp new file mode 100644 index 00000000..2dcb5b46 --- /dev/null +++ b/src/learn/multi_think.cpp @@ -0,0 +1,123 @@ +#include "../types.h" + +#if defined(EVAL_LEARN) + +#include "multi_think.h" +#include "../tt.h" +#include "../uci.h" + +#include + +void MultiThink::go_think() +{ + // あとでOptionsの設定を復元するためにコピーで保持しておく。 + auto oldOptions = Options; + + // 定跡を用いる場合、on the flyで行なうとすごく時間がかかる&ファイルアクセスを行なう部分が + // thread safeではないので、メモリに丸読みされている状態であることをここで保証する。 + Options["BookOnTheFly"] = std::string("false"); + + // 評価関数の読み込み等 + // learnコマンドの場合、評価関数読み込み後に評価関数の値を補正している可能性があるので、 + // メモリの破損チェックは省略する。 + is_ready(true); + + // 派生クラスのinit()を呼び出す。 + init(); + + // ループ上限はset_loop_max()で設定されているものとする。 + loop_count = 0; + done_count = 0; + + // threadをOptions["Threads"]の数だけ生成して思考開始。 + std::vector threads; + auto thread_num = (size_t)Options["Threads"]; + + // worker threadの終了フラグの確保 + thread_finished.resize(thread_num); + + // worker threadの起動 + for (size_t i = 0; i < thread_num; ++i) + { + thread_finished[i] = 0; + threads.push_back(std::thread([i, this] + { + // プロセッサの全スレッドを使い切る。 + WinProcGroup::bindThisThread(i); + + // オーバーライドされている処理を実行 + this->thread_worker(i); + + // スレッドが終了したので終了フラグを立てる + this->thread_finished[i] = 1; + })); + } + + // すべてのthreadの終了待ちを + // for (auto& th : threads) + // th.join(); + // のように書くとスレッドがまだ仕事をしている状態でここに突入するので、 + // その間、callback_func()が呼び出せず、セーブできなくなる。 + // そこで終了フラグを自前でチェックする必要がある。 + + // すべてのスレッドが終了したかを判定する関数 + auto threads_done = [&]() + { + // ひとつでも終了していなければfalseを返す + for (auto& f : thread_finished) + if (!f) + return false; + return true; + }; + + // コールバック関数が設定されているならコールバックする。 + auto do_a_callback = [&]() + { + if (callback_func) + callback_func(); + }; + + + for (uint64_t i = 0 ; ; ) + { + // 全スレッドが終了していたら、ループを抜ける。 + if (threads_done()) + break; + + sleep(1000); + + // callback_secondsごとにcallback_func()が呼び出される。 + if (++i == callback_seconds) + { + do_a_callback(); + // ↑から戻ってきてからカウンターをリセットしているので、 + // do_a_callback()のなかでsave()などにどれだけ時間がかかろうと + // 次に呼び出すのは、そこから一定時間の経過を要する。 + i = 0; + } + } + + // 最後の保存。 + std::cout << std::endl << "finalize.."; + + // do_a_callback(); + // → 呼び出し元で保存するはずで、ここでは要らない気がする。 + + // 終了したフラグは立っているがスレッドの終了コードの実行中であるということはありうるので + // join()でその終了を待つ必要がある。 + for (auto& th : threads) + th.join(); + + // 全スレッドが終了しただけでfileの書き出しスレッドなどはまだ動いていて + // 作業自体は完了していない可能性があるのでスレッドがすべて終了したことだけ出力する。 + std::cout << "all threads are joined." << std::endl; + + // Optionsを書き換えたので復元。 + // 値を代入しないとハンドラが起動しないのでこうやって復元する。 + for (auto& s : oldOptions) + Options[s.first] = std::string(s.second); + +} + + +#endif // defined(EVAL_LEARN) diff --git a/src/learn/multi_think.h b/src/learn/multi_think.h new file mode 100644 index 00000000..ad6baa5e --- /dev/null +++ b/src/learn/multi_think.h @@ -0,0 +1,151 @@ +#ifndef _MULTI_THINK_ +#define _MULTI_THINK_ + +#if defined(EVAL_LEARN) + +#include + +#include "../misc.h" +#include "../learn/learn.h" +#include "../thread_win32_osx.h" + +#include + +// 棋譜からの学習や、自ら思考させて定跡を生成するときなど、 +// 複数スレッドが個別にSearch::think()を呼び出したいときに用いるヘルパクラス。 +// このクラスを派生させて用いる。 +struct MultiThink +{ + MultiThink() : prng(21120903) + { + loop_count = 0; + } + + // マスタースレッドからこの関数を呼び出すと、スレッドがそれぞれ思考して、 + // 思考終了条件を満たしたところで制御を返す。 + // 他にやってくれること。 + // ・各スレッドがLearner::search(),qsearch()を呼び出しても安全なように + //  置換表をスレッドごとに分離してくれる。(終了後、元に戻してくれる。) + // ・bookはon the flyモードだとthread safeではないので、このモードを一時的に + //  オフにしてくれる。 + // [要件] + // 1) thread_worker()のオーバーライド + // 2) set_loop_max()でループ回数の設定 + // 3) 定期的にcallbackされる関数を設定する(必要なら) + // callback_funcとcallback_interval + void go_think(); + + // 派生クラス側で初期化したいものがあればこれをoverrideしておけば、 + // go_think()で初期化が終わったタイミングで呼び出される。 + // 定跡の読み込みなどはそのタイミングで行うと良い。 + virtual void init() {} + + // go_think()したときにスレッドを生成して呼び出されるthread worker + // これをoverrideして用いる。 + virtual void thread_worker(size_t thread_id) = 0; + + // go_think()したときにcallback_seconds[秒]ごとにcallbackされる。 + std::function callback_func; + uint64_t callback_seconds = 600; + + // workerが処理する(Search::think()を呼び出す)回数を設定する。 + void set_loop_max(uint64_t loop_max_) { loop_max = loop_max_; } + + // set_loop_max()で設定した値を取得する。 + uint64_t get_loop_max() const { return loop_max; } + + // [ASYNC] ループカウンターの値を取り出して、取り出し後にループカウンターを加算する。 + // もしループカウンターがloop_maxに達していたらUINT64_MAXを返す。 + // 局面を生成する場合などは、局面を生成するタイミングでこの関数を呼び出すようにしないと、 + // 生成した局面数と、カウンターの値が一致しなくなってしまうので注意すること。 + uint64_t get_next_loop_count() { + std::unique_lock lk(loop_mutex); + if (loop_count >= loop_max) + return UINT64_MAX; + return loop_count++; + } + + // [ASYNC] 処理した個数を返す用。呼び出されるごとにインクリメントされたカウンターが返る。 + uint64_t get_done_count() { + std::unique_lock lk(loop_mutex); + return ++done_count; + } + + // worker threadがI/Oにアクセスするときのmutex + std::mutex io_mutex; + +protected: + // 乱数発生器本体 + AsyncPRNG prng; + +private: + // workerが処理する(Search::think()を呼び出す)回数 + std::atomic loop_max; + // workerが処理した(Search::think()を呼び出した)回数 + std::atomic loop_count; + // 処理した回数を返す用。 + std::atomic done_count; + + // ↑の変数を変更するときのmutex + std::mutex loop_mutex; + + // スレッドの終了フラグ。 + // vectorにすると複数スレッドから書き換えようとしたときに正しく反映されないことがある…はず。 + typedef uint8_t Flag; + std::vector thread_finished; + +}; + +// idle時間にtaskを処理する仕組み。 +// masterは好きなときにpush_task_async()でtaskを渡す。 +// slaveは暇なときにon_idle()を実行すると、taskを一つ取り出してqueueがなくなるまで実行を続ける。 +// MultiThinkのthread workerをmaster-slave方式で書きたいときに用いると便利。 +struct TaskDispatcher +{ + typedef std::function Task; + + // slaveはidle中にこの関数を呼び出す。 + void on_idle(size_t thread_id) + { + Task task; + while ((task = get_task_async()) != nullptr) + task(thread_id); + + sleep(1); + } + + // [ASYNC] taskを一つ積む。 + void push_task_async(Task task) + { + std::unique_lock lk(task_mutex); + tasks.push_back(task); + } + + // task用の配列の要素をsize分だけ事前に確保する。 + void task_reserve(size_t size) + { + tasks.reserve(size); + } + +protected: + // taskの集合 + std::vector tasks; + + // [ASYNC] taskを一つ取り出す。on_idle()から呼び出される。 + Task get_task_async() + { + std::unique_lock lk(task_mutex); + if (tasks.size() == 0) + return nullptr; + Task task = *tasks.rbegin(); + tasks.pop_back(); + return task; + } + + // tasksにアクセスするとき用のmutex + std::mutex task_mutex; +}; + +#endif // defined(EVAL_LEARN) && defined(YANEURAOU_2018_OTAFUKU_ENGINE) + +#endif diff --git a/src/misc.cpp b/src/misc.cpp index c6254784..25435ac5 100644 --- a/src/misc.cpp +++ b/src/misc.cpp @@ -42,6 +42,7 @@ typedef bool(*fun3_t)(HANDLE, CONST GROUP_AFFINITY*, PGROUP_AFFINITY); #endif #include +#include #include #include #include @@ -522,3 +523,164 @@ void bindThisThread(size_t idx) { #endif } // namespace WinProcGroup + +// 現在時刻を文字列化したもを返す。(評価関数の学習時などに用いる) +std::string now_string() +{ + // std::ctime(), localtime()を使うと、MSVCでセキュアでないという警告が出る。 + // C++標準的にはそんなことないはずなのだが…。 + +#if defined(_MSC_VER) + // C4996 : 'ctime' : This function or variable may be unsafe.Consider using ctime_s instead. +#pragma warning(disable : 4996) +#endif + + auto now = std::chrono::system_clock::now(); + auto tp = std::chrono::system_clock::to_time_t(now); + auto result = string(std::ctime(&tp)); + + // 末尾に改行コードが含まれているならこれを除去する + while (*result.rbegin() == '\n' || (*result.rbegin() == '\r')) + result.pop_back(); + return result; +} + +void sleep(int ms) +{ + std::this_thread::sleep_for(std::chrono::milliseconds(ms)); +} + +void* aligned_malloc(size_t size, size_t align) +{ + void* p = _mm_malloc(size, align); + if (p == nullptr) + { + std::cout << "info string can't allocate memory. sise = " << size << std::endl; + exit(1); + } + return p; +} + +int read_file_to_memory(std::string filename, std::function callback_func) +{ + fstream fs(filename, ios::in | ios::binary); + if (fs.fail()) + return 1; + + fs.seekg(0, fstream::end); + uint64_t eofPos = (uint64_t)fs.tellg(); + fs.clear(); // これをしないと次のseekに失敗することがある。 + fs.seekg(0, fstream::beg); + uint64_t begPos = (uint64_t)fs.tellg(); + uint64_t file_size = eofPos - begPos; + //std::cout << "filename = " << filename << " , file_size = " << file_size << endl; + + // ファイルサイズがわかったのでcallback_funcを呼び出してこの分のバッファを確保してもらい、 + // そのポインターをもらう。 + void* ptr = callback_func(file_size); + + // バッファが確保できなかった場合や、想定していたファイルサイズと異なった場合は、 + // nullptrを返すことになっている。このとき、読み込みを中断し、エラーリターンする。 + if (ptr == nullptr) + return 2; + + // 細切れに読み込む + + const uint64_t block_size = 1024 * 1024 * 1024; // 1回のreadで読み込む要素の数(1GB) + for (uint64_t pos = 0; pos < file_size; pos += block_size) + { + // 今回読み込むサイズ + uint64_t read_size = (pos + block_size < file_size) ? block_size : (file_size - pos); + fs.read((char*)ptr + pos, read_size); + + // ファイルの途中で読み込みエラーに至った。 + if (fs.fail()) + return 2; + + //cout << "."; + } + fs.close(); + + return 0; +} + +int write_memory_to_file(std::string filename, void* ptr, uint64_t size) +{ + fstream fs(filename, ios::out | ios::binary); + if (fs.fail()) + return 1; + + const uint64_t block_size = 1024 * 1024 * 1024; // 1回のwriteで書き出す要素の数(1GB) + for (uint64_t pos = 0; pos < size; pos += block_size) + { + // 今回書き出すメモリサイズ + uint64_t write_size = (pos + block_size < size) ? block_size : (size - pos); + fs.write((char*)ptr + pos, write_size); + //cout << "."; + } + fs.close(); + return 0; +} + +// ---------------------------- +// mkdir wrapper +// ---------------------------- + +// カレントフォルダ相対で指定する。成功すれば0、失敗すれば非0が返る。 +// フォルダを作成する。日本語は使っていないものとする。 +// どうもmsys2環境下のgccだと_wmkdir()だとフォルダの作成に失敗する。原因不明。 +// 仕方ないので_mkdir()を用いる。 + +#if defined(_WIN32) +// Windows用 + +#if defined(_MSC_VER) +#include // mkdirするのにwstringが欲しいのでこれが必要 +#include // wstring_convertにこれが必要。 + +namespace Dependency { + int mkdir(std::string dir_name) + { + std::wstring_convert, wchar_t> cv; + return _wmkdir(cv.from_bytes(dir_name).c_str()); + // ::CreateDirectory(cv.from_bytes(dir_name).c_str(),NULL); + } +} + +#elif defined(__GNUC__) + +#include +namespace Dependency { + int mkdir(std::string dir_name) + { + return _mkdir(dir_name.c_str()); + } +} + +#endif +#elif defined(__linux__) + +// linux環境において、この_LINUXというシンボルはmakefileにて定義されるものとする。 + +// Linux用のmkdir実装。 +#include "sys/stat.h" + +namespace Dependency { + int mkdir(std::string dir_name) + { + return ::mkdir(dir_name.c_str(), 0777); + } +} +#else + +// Linux環境かどうかを判定するためにはmakefileを分けないといけなくなってくるな.. +// linuxでフォルダ掘る機能は、とりあえずナシでいいや..。評価関数ファイルの保存にしか使ってないし…。 + +namespace Dependency { + int mkdir(std::string dir_name) + { + return 0; + } +} + +#endif diff --git a/src/misc.h b/src/misc.h index 373f1b77..bd866842 100644 --- a/src/misc.h +++ b/src/misc.h @@ -21,13 +21,20 @@ #ifndef MISC_H_INCLUDED #define MISC_H_INCLUDED +#include #include #include +#include +#include #include #include #include +#ifndef _MSC_VER +#include +#endif #include "types.h" +#include "thread_win32_osx.h" const std::string engine_info(bool to_uci = false); const std::string compiler_info(); @@ -108,8 +115,21 @@ public: /// Output values only have 1/8th of their bits set on average. template T sparse_rand() { return T(rand64() & rand64() & rand64()); } + + // 0からn-1までの乱数を返す。(一様分布ではないが現実的にはこれで十分) + uint64_t rand(uint64_t n) { return rand() % n; } + + // 内部で使用している乱数seedを返す。 + uint64_t get_seed() const { return s; } }; +// 乱数のseedを表示する。(デバッグ用) +inline std::ostream& operator<<(std::ostream& os, PRNG& prng) +{ + os << "PRNG::seed = " << std::hex << prng.get_seed() << std::dec; + return os; +} + inline uint64_t mul_hi64(uint64_t a, uint64_t b) { #if defined(__GNUC__) && defined(IS_64BIT) __extension__ typedef unsigned __int128 uint128; @@ -122,7 +142,6 @@ inline uint64_t mul_hi64(uint64_t a, uint64_t b) { uint64_t c3 = aL * bH + (uint32_t)c2; return aH * bH + (c2 >> 32) + (c3 >> 32); #endif -} /// Under Windows it is not possible for a process to run on more than one /// logical processor group. This usually means to be limited to use max 64 @@ -134,4 +153,155 @@ namespace WinProcGroup { void bindThisThread(size_t idx); } +// 指定されたミリ秒だけsleepする。 +extern void sleep(int ms); + +// 現在時刻を文字列化したもを返す。(評価関数の学習時などにログ出力のために用いる) +std::string now_string(); + +// 途中での終了処理のためのwrapper +static void my_exit() +{ + sleep(3000); // エラーメッセージが出力される前に終了するのはまずいのでwaitを入れておく。 + exit(EXIT_FAILURE); +} + +// msys2、Windows Subsystem for Linuxなどのgcc/clangでコンパイルした場合、 +// C++のstd::ifstreamで::read()は、一発で2GB以上のファイルの読み書きが出来ないのでそのためのwrapperである。 +// +// read_file_to_memory()の引数のcallback_funcは、ファイルがオープン出来た時点でそのファイルサイズを引数として +// callbackされるので、バッファを確保して、その先頭ポインタを返す関数を渡すと、そこに読み込んでくれる。 +// これらの関数は、ファイルが見つからないときなどエラーの際には非0を返す。 +// +// また、callbackされた関数のなかでバッファが確保できなかった場合や、想定していたファイルサイズと異なった場合は、 +// nullptrを返せば良い。このとき、read_file_to_memory()は、読み込みを中断し、エラーリターンする。 + +int read_file_to_memory(std::string filename, std::function callback_func); +int write_memory_to_file(std::string filename, void* ptr, uint64_t size); + +// -------------------- +// PRNGのasync版 +// -------------------- + +// PRNGのasync版 +struct AsyncPRNG +{ + AsyncPRNG(uint64_t seed) : prng(seed) { assert(seed); } + // [ASYNC] 乱数を一つ取り出す。 + template T rand() { + std::unique_lock lk(mutex); + return prng.rand(); + } + + // [ASYNC] 0からn-1までの乱数を返す。(一様分布ではないが現実的にはこれで十分) + uint64_t rand(uint64_t n) { + std::unique_lock lk(mutex); + return prng.rand(n); + } + + // 内部で使用している乱数seedを返す。 + uint64_t get_seed() const { return prng.get_seed(); } + +protected: + std::mutex mutex; + PRNG prng; +}; + +// 乱数のseedを表示する。(デバッグ用) +inline std::ostream& operator<<(std::ostream& os, AsyncPRNG& prng) +{ + os << "AsyncPRNG::seed = " << std::hex << prng.get_seed() << std::dec; + return os; +} + +// -------------------- +// Math +// -------------------- + +// 進行度の計算や学習で用いる数学的な関数 +namespace Math { + // シグモイド関数 + // = 1.0 / (1.0 + std::exp(-x)) + double sigmoid(double x); + + // シグモイド関数の微分 + // = sigmoid(x) * (1.0 - sigmoid(x)) + double dsigmoid(double x); + + // vを[lo,hi]の間に収まるようにクリップする。 + // ※ Stockfishではこの関数、bitboard.hに書いてある。 + template constexpr const T& clamp(const T& v, const T& lo, const T& hi) { + return v < lo ? lo : v > hi ? hi : v; + } + +} + +// -------------------- +// Path +// -------------------- + +// C#にあるPathクラス的なもの。ファイル名の操作。 +// C#のメソッド名に合わせておく。 +struct Path +{ + // path名とファイル名を結合して、それを返す。 + // folder名のほうは空文字列でないときに、末尾に'/'か'\\'がなければそれを付与する。 + static std::string Combine(const std::string& folder, const std::string& filename) + { + if (folder.length() >= 1 && *folder.rbegin() != '/' && *folder.rbegin() != '\\') + return folder + "/" + filename; + + return folder + filename; + } + + // full path表現から、(フォルダ名を除いた)ファイル名の部分を取得する。 + static std::string GetFileName(const std::string& path) + { + // "\"か"/"か、どちらを使ってあるかはわからない。 + auto path_index1 = path.find_last_of("\\") + 1; + auto path_index2 = path.find_last_of("/") + 1; + auto path_index = std::max(path_index1, path_index2); + + return path.substr(path_index); + } +}; + +extern void* aligned_malloc(size_t size, size_t align); +static void aligned_free(void* ptr) { _mm_free(ptr); } + +// alignasを指定しているのにnewのときに無視される&STLのコンテナがメモリ確保するときに無視するので、 +// そのために用いるカスタムアロケーター。 +template +class AlignedAllocator { +public: + using value_type = T; + + AlignedAllocator() {} + AlignedAllocator(const AlignedAllocator&) {} + AlignedAllocator(AlignedAllocator&&) {} + + template AlignedAllocator(const AlignedAllocator&) {} + + T* allocate(std::size_t n) { return (T*)aligned_malloc(n * sizeof(T), alignof(T)); } + void deallocate(T* p, std::size_t n) { aligned_free(p); } +}; + +// -------------------- +// Dependency Wrapper +// -------------------- + +namespace Dependency +{ + // Linux環境ではgetline()したときにテキストファイルが'\r\n'だと + // '\r'が末尾に残るのでこの'\r'を除去するためにwrapperを書く。 + // そのため、fstreamに対してgetline()を呼び出すときは、 + // std::getline()ではなく単にgetline()と書いて、この関数を使うべき。 + extern bool getline(std::ifstream& fs, std::string& s); + + // フォルダを作成する。 + // カレントフォルダ相対で指定する。dir_nameに日本語は使っていないものとする。 + // 成功すれば0、失敗すれば非0が返る。 + extern int mkdir(std::string dir_name); +} + #endif // #ifndef MISC_H_INCLUDED diff --git a/src/movegen.h b/src/movegen.h index c2e7c3f1..838541f1 100644 --- a/src/movegen.h +++ b/src/movegen.h @@ -68,6 +68,9 @@ struct MoveList { return std::find(begin(), end(), move) != end(); } + // iԖڂ̗vfԂ + const ExtMove at(size_t i) const { assert(0 <= i && i < size()); return begin()[i]; } + private: ExtMove moveList[MAX_MOVES], *last; }; diff --git a/src/position.cpp b/src/position.cpp index c9db6224..a3e63f99 100644 --- a/src/position.cpp +++ b/src/position.cpp @@ -209,6 +209,15 @@ Position& Position::set(const string& fenStr, bool isChess960, StateInfo* si, Th std::fill_n(&pieceList[0][0], sizeof(pieceList) / sizeof(Square), SQ_NONE); st = si; +#if defined(EVAL_NNUE) + // evalListclearBmemsetŃ[NAƂɃNAĂ邪cB + evalList.clear(); + + // PieceListXVŁAǂ̋ǂɂ邩ݒ肵Ȃ΂ȂȂA + // ꂼ̋ǂ܂Ŏg̃JE^[ + PieceNumber next_piece_number = PIECE_NUMBER_ZERO; +#endif // defined(EVAL_NNUE) + ss >> std::noskipws; // 1. Piece placement @@ -222,7 +231,17 @@ Position& Position::set(const string& fenStr, bool isChess960, StateInfo* si, Th else if ((idx = PieceToChar.find(token)) != string::npos) { - put_piece(Piece(idx), sq); + auto pc = Piece(idx); + put_piece(pc, sq); + +#if defined(EVAL_NNUE) + PieceNumber piece_no = + (idx == W_KING) ? PIECE_NUMBER_WKING : // + (idx == B_KING) ? PIECE_NUMBER_BKING : // + next_piece_number++; // ȊO + evalList.put_piece(piece_no, sq, pc); // sq̏pc̋zu +#endif // defined(EVAL_NNUE) + ++sq; } } @@ -285,6 +304,9 @@ Position& Position::set(const string& fenStr, bool isChess960, StateInfo* si, Th set_state(st); assert(pos_is_ok()); +#if defined(EVAL_NNUE) + assert(evalList.is_valid(*this)); +#endif // defined(EVAL_NNUE) return *this; } @@ -706,6 +728,11 @@ void Position::do_move(Move m, StateInfo& newSt, bool givesCheck) { ++st->rule50; ++st->pliesFromNull; +#if defined(EVAL_NNUE) + st->accumulator.computed_accumulation = false; + st->accumulator.computed_score = false; +#endif // defined(EVAL_NNUE) + Color us = sideToMove; Color them = ~us; Square from = from_sq(m); @@ -713,10 +740,20 @@ void Position::do_move(Move m, StateInfo& newSt, bool givesCheck) { Piece pc = piece_on(from); Piece captured = type_of(m) == ENPASSANT ? make_piece(them, PAWN) : piece_on(to); +#if defined(EVAL_NNUE) + PieceNumber piece_no0 = PIECE_NUMBER_NB; + PieceNumber piece_no1 = PIECE_NUMBER_NB; +#endif // defined(EVAL_NNUE) + assert(color_of(pc) == us); assert(captured == NO_PIECE || color_of(captured) == (type_of(m) != CASTLING ? them : us)); assert(type_of(captured) != KING); +#if defined(EVAL_NNUE) + auto& dp = st->dirtyPiece; + dp.dirty_num = 1; +#endif // defined(EVAL_NNUE) + if (type_of(m) == CASTLING) { assert(pc == make_piece(us, KING)); @@ -746,13 +783,32 @@ void Position::do_move(Move m, StateInfo& newSt, bool givesCheck) { assert(relative_rank(us, to) == RANK_6); assert(piece_on(to) == NO_PIECE); assert(piece_on(capsq) == make_piece(them, PAWN)); + +#if defined(EVAL_NNUE) + piece_no1 = piece_no_of(capsq); +#endif // defined(EVAL_NNUE) + + //board[capsq] = NO_PIECE; // Not done by remove_piece() +#if defined(EVAL_NNUE) + evalList.piece_no_list_board[capsq] = PIECE_NUMBER_NB; +#endif // defined(EVAL_NNUE) + } + else { +#if defined(EVAL_NNUE) + piece_no1 = piece_no_of(capsq); +#endif // defined(EVAL_NNUE) } st->pawnKey ^= Zobrist::psq[captured][capsq]; } - else + else { st->nonPawnMaterial[them] -= PieceValue[MG][captured]; +#if defined(EVAL_NNUE) + piece_no1 = piece_no_of(capsq); +#endif // defined(EVAL_NNUE) + } + // Update board and piece lists remove_piece(capsq); @@ -766,6 +822,21 @@ void Position::do_move(Move m, StateInfo& newSt, bool givesCheck) { // Reset rule 50 counter st->rule50 = 0; + +#if defined(EVAL_NNUE) + dp.dirty_num = 2; // 2 + + dp.pieceNo[1] = piece_no1; + dp.changed_piece[1].old_piece = evalList.bona_piece(piece_no1); + // Do not use Eval::EvalList::put_piece() because the piece is removed + // from the game, and the corresponding elements of the piece lists + // needs to be Eval::BONA_PIECE_ZERO. + evalList.set_piece_on_board(piece_no1, Eval::BONA_PIECE_ZERO, Eval::BONA_PIECE_ZERO, capsq); + // Set PIECE_NUMBER_NB to piece_no_of_board[capsq] directly because it + // will not be overritten to pc if the move type is enpassant. + evalList.piece_no_list_board[capsq] = PIECE_NUMBER_NB; + dp.changed_piece[1].new_piece = evalList.bona_piece(piece_no1); +#endif // defined(EVAL_NNUE) } // Update hash key @@ -787,8 +858,21 @@ void Position::do_move(Move m, StateInfo& newSt, bool givesCheck) { } // Move the piece. The tricky Chess960 castling is handled earlier - if (type_of(m) != CASTLING) - move_piece(from, to); + if (type_of(m) != CASTLING) { +#if defined(EVAL_NNUE) + piece_no0 = piece_no_of(from); +#endif // defined(EVAL_NNUE) + + move_piece(from, to); + +#if defined(EVAL_NNUE) + dp.pieceNo[0] = piece_no0; + dp.changed_piece[0].old_piece = evalList.bona_piece(piece_no0); + evalList.piece_no_list_board[from] = PIECE_NUMBER_NB; + evalList.put_piece(piece_no0, to, pc); + dp.changed_piece[0].new_piece = evalList.bona_piece(piece_no0); +#endif // defined(EVAL_NNUE) + } // If the moving piece is a pawn do some special extra work if (type_of(pc) == PAWN) @@ -811,6 +895,15 @@ void Position::do_move(Move m, StateInfo& newSt, bool givesCheck) { remove_piece(to); put_piece(promotion, to); +#if defined(EVAL_NNUE) + piece_no0 = piece_no_of(to); + //dp.pieceNo[0] = piece_no0; + //dp.changed_piece[0].old_piece = evalList.bona_piece(piece_no0); + assert(evalList.piece_no_list_board[from] == PIECE_NUMBER_NB); + evalList.put_piece(piece_no0, to, promotion); + dp.changed_piece[0].new_piece = evalList.bona_piece(piece_no0); +#endif // defined(EVAL_NNUE) + // Update hash keys k ^= Zobrist::psq[pc][to] ^ Zobrist::psq[promotion][to]; st->pawnKey ^= Zobrist::psq[pc][to]; @@ -861,7 +954,12 @@ void Position::do_move(Move m, StateInfo& newSt, bool givesCheck) { } } + //std::cout << *this << std::endl; + assert(pos_is_ok()); +#if defined(EVAL_NNUE) + assert(evalList.is_valid(*this)); +#endif // defined(EVAL_NNUE) } @@ -891,6 +989,11 @@ void Position::undo_move(Move m) { remove_piece(to); pc = make_piece(us, PAWN); put_piece(pc, to); + +#if defined(EVAL_NNUE) + PieceNumber piece_no0 = st->dirtyPiece.pieceNo[0]; + evalList.put_piece(piece_no0, to, pc); +#endif // defined(EVAL_NNUE) } if (type_of(m) == CASTLING) @@ -900,8 +1003,15 @@ void Position::undo_move(Move m) { } else { + move_piece(to, from); // Put the piece back at the source square +#if defined(EVAL_NNUE) + PieceNumber piece_no0 = st->dirtyPiece.pieceNo[0]; + evalList.put_piece(piece_no0, from, pc); + evalList.piece_no_list_board[to] = PIECE_NUMBER_NB; +#endif // defined(EVAL_NNUE) + if (st->capturedPiece) { Square capsq = to; @@ -918,6 +1028,13 @@ void Position::undo_move(Move m) { } put_piece(st->capturedPiece, capsq); // Restore the captured piece + +#if defined(EVAL_NNUE) + PieceNumber piece_no1 = st->dirtyPiece.pieceNo[1]; + assert(evalList.bona_piece(piece_no1).fw == Eval::BONA_PIECE_ZERO); + assert(evalList.bona_piece(piece_no1).fb == Eval::BONA_PIECE_ZERO); + evalList.put_piece(piece_no1, capsq, st->capturedPiece); +#endif // defined(EVAL_NNUE) } } @@ -926,6 +1043,9 @@ void Position::undo_move(Move m) { --gamePly; assert(pos_is_ok()); +#if defined(EVAL_NNUE) + assert(evalList.is_valid(*this)); +#endif // defined(EVAL_NNUE) } @@ -933,18 +1053,60 @@ void Position::undo_move(Move m) { /// is a bit tricky in Chess960 where from/to squares can overlap. template void Position::do_castling(Color us, Square from, Square& to, Square& rfrom, Square& rto) { +#if defined(EVAL_NNUE) + auto& dp = st->dirtyPiece; + // vẐ߂ɈړStateInfoɋL^ĂB + dp.dirty_num = 2; // 2 + + PieceNumber piece_no0; + PieceNumber piece_no1; + + if (Do) { + piece_no0 = piece_no_of(from); + piece_no1 = piece_no_of(to); + } +#endif // defined(EVAL_NNUE) bool kingSide = to > from; rfrom = to; // Castling is encoded as "king captures friendly rook" rto = relative_square(us, kingSide ? SQ_F1 : SQ_D1); to = relative_square(us, kingSide ? SQ_G1 : SQ_C1); +#if defined(EVAL_NNUE) + if (!Do) { + piece_no0 = piece_no_of(to); + piece_no1 = piece_no_of(rto); + } +#endif // defined(EVAL_NNUE) + // Remove both pieces first since squares could overlap in Chess960 remove_piece(Do ? from : to); remove_piece(Do ? rfrom : rto); board[Do ? from : to] = board[Do ? rfrom : rto] = NO_PIECE; // Since remove_piece doesn't do this for us put_piece(make_piece(us, KING), Do ? to : from); put_piece(make_piece(us, ROOK), Do ? rto : rfrom); + +#if defined(EVAL_NNUE) + if (Do) { + dp.pieceNo[0] = piece_no0; + dp.changed_piece[0].old_piece = evalList.bona_piece(piece_no0); + evalList.piece_no_list_board[from] = PIECE_NUMBER_NB; + evalList.put_piece(piece_no0, to, make_piece(us, KING)); + dp.changed_piece[0].new_piece = evalList.bona_piece(piece_no0); + + dp.pieceNo[1] = piece_no1; + dp.changed_piece[1].old_piece = evalList.bona_piece(piece_no1); + evalList.piece_no_list_board[rfrom] = PIECE_NUMBER_NB; + evalList.put_piece(piece_no1, rto, make_piece(us, ROOK)); + dp.changed_piece[1].new_piece = evalList.bona_piece(piece_no1); + } + else { + evalList.piece_no_list_board[to] = PIECE_NUMBER_NB; + evalList.put_piece(piece_no0, from, make_piece(us, KING)); + evalList.piece_no_list_board[rto] = PIECE_NUMBER_NB; + evalList.put_piece(piece_no1, rfrom, make_piece(us, ROOK)); + } +#endif // defined(EVAL_NNUE) } @@ -969,6 +1131,10 @@ void Position::do_null_move(StateInfo& newSt) { st->key ^= Zobrist::side; prefetch(TT.first_entry(st->key)); +#if defined(EVAL_NNUE) + st->accumulator.computed_score = false; +#endif + ++st->rule50; st->pliesFromNull = 0; @@ -1297,3 +1463,13 @@ bool Position::pos_is_ok() const { return true; } + +#if defined(EVAL_NNUE) +PieceNumber Position::piece_no_of(Square sq) const +{ + assert(piece_on(sq) != NO_PIECE); + PieceNumber n = evalList.piece_no_of_board(sq); + assert(is_ok(n)); + return n; +} +#endif // defined(EVAL_NNUE) diff --git a/src/position.h b/src/position.h index 8f8c8f7a..ec698bec 100644 --- a/src/position.h +++ b/src/position.h @@ -23,12 +23,17 @@ #include #include +#include #include // For std::unique_ptr #include #include "bitboard.h" +#include "evaluate.h" +#include "misc.h" #include "types.h" +#include "eval/nnue/nnue_accumulator.h" + /// StateInfo struct stores information needed to restore a Position object to /// its previous state when we retract a move. Whenever a move is made on the @@ -54,6 +59,13 @@ struct StateInfo { Bitboard pinners[COLOR_NB]; Bitboard checkSquares[PIECE_TYPE_NB]; int repetition; + +#if defined(EVAL_NNUE) + Eval::NNUE::Accumulator accumulator; + + // ]l̍vZ̊Ǘp + Eval::DirtyPiece dirtyPiece; +#endif // defined(EVAL_NNUE) }; /// A list to keep track of the position states along the setup moves (from the @@ -69,6 +81,9 @@ typedef std::unique_ptr> StateListPtr; /// traversing the search tree. class Thread; +// packꂽsfen +struct PackedSfen { uint8_t data[32]; }; + class Position { public: static void init(); @@ -162,6 +177,37 @@ public: bool pos_is_ok() const; void flip(); +#if defined(EVAL_NNUE) || defined(EVAL_LEARN) + // --- StateInfo + + // ݂̋ǖʂɑΉStateInfoԂB + // Ƃ΁Astate()->capturedPieceł΁AOǖʂŕߊlꂽi[ĂB + StateInfo* state() const { return st; } + + // ]֐Ŏg߂́Aǂ̋ԍ̋ǂɂ邩Ȃǂ̏B + const Eval::EvalList* eval_list() const { return &evalList; } +#endif // defined(EVAL_NNUE) || defined(EVAL_LEARN) + +#if defined(EVAL_LEARN) + // -- sfenwp + + // packꂽsfen𓾂BɎw肵obt@ɕԂB + // gamePlypackɊ܂߂ȂB + void sfen_pack(PackedSfen& sfen); + + // sfenoRƒx̂ŒpackꂽsfenZbg֐B + // pos.set(sfen_unpack(data),si,th); ƓB + // nꂽǖʂɖ肪āAG[̂Ƃ͔0ԂB + // PackedSfengamePly͊܂܂Ȃ̂ŕłȂBݒ肵̂łΈŎw肷邱ƁB + int set_from_packed_sfen(const PackedSfen& sfen, StateInfo* si, Thread* th, bool mirror = false); + + // ՖʂƎAԂ^āAsfenԂB + //static std::string sfen_from_rawdata(Piece board[81], Hand hands[2], Color turn, int gamePly); + + // c̋ʂ̈ʒuԂB + Square king_square(Color c) const { return pieceList[make_piece(c, KING)][0]; } +#endif // EVAL_LEARN + private: // Initialization helpers (used while setting up a position) void set_castling_right(Color c, Square rfrom); @@ -175,6 +221,11 @@ private: template void do_castling(Color us, Square from, Square& to, Square& rfrom, Square& rto); +#if defined(EVAL_NNUE) + // Տsq̏ɂPieceNumberԂB + PieceNumber piece_no_of(Square sq) const; +#endif // defined(EVAL_NNUE) + // Data members Piece board[SQUARE_NB]; Bitboard byTypeBB[PIECE_TYPE_NB]; @@ -191,6 +242,11 @@ private: Thread* thisThread; StateInfo* st; bool chess960; + +#if defined(EVAL_NNUE) || defined(EVAL_LEARN) + // ]֐ŗp̃Xg + Eval::EvalList evalList; +#endif // defined(EVAL_NNUE) || defined(EVAL_LEARN) }; namespace PSQT { diff --git a/src/search.cpp b/src/search.cpp index 563d1aab..6f3fc4ab 100644 --- a/src/search.cpp +++ b/src/search.cpp @@ -965,7 +965,7 @@ moves_loop: // When in check, search starts from here ss->moveCount = ++moveCount; - if (rootNode && thisThread == Threads.main() && Time.elapsed() > 3000) + if (rootNode && thisThread == Threads.main() && Time.elapsed() > 3000 && !Limits.silent) sync_cout << "info depth " << depth << " currmove " << UCI::move(move, pos.is_chess960()) << " currmovenumber " << moveCount + thisThread->pvIdx << sync_endl; @@ -1531,7 +1531,13 @@ moves_loop: // When in check, search starts from here prefetch(TT.first_entry(pos.key_after(move))); // Check for legality just before making the move - if (!pos.legal(move)) + if ( +#if defined(EVAL_LEARN) + // HACK: pos.piece_on(from_sq(m)) sometimes will be NO_PIECE during machine learning. + !pos.pseudo_legal(move) || +#endif // EVAL_LEARN + !pos.legal(move) + ) { moveCount--; continue; @@ -1927,3 +1933,315 @@ void Tablebases::rank_root_moves(Position& pos, Search::RootMoves& rootMoves) { m.tbRank = 0; } } + +// --- wKɗpAdepthŒTȂǂ̊֐Oɑ΂ČJ + +#if defined (EVAL_LEARN) + +namespace Learner +{ + // wKpɁA1‚̃Xbhsearch,qsearch()Ăяo悤ȃX^upӂB + // ܂ɂĎv΁AAperŷ悤SearcherăXbhƂɒu\Ȃǂpӂق + // ǂmȂB + + // wK̂߂̏B + // Learner::search(),Learner::qsearch()ĂяoB + void init_for_search(Position& pos, Stack* ss) + { + + // RootNodess->ply == 0̏B + // [NÂŁAss->ply == 0ƂȂ̂ővcB + + std::memset(ss - 7, 0, 10 * sizeof(Stack)); + + // Search::LimitsɊւ + // ̃o[ϐglobalȂ̂ő̃Xbhɉeyڂ̂ŋC‚邱ƁB + { + auto& limits = Search::Limits; + + // T"go infinite"R}hɂB(time managementƍ邽) + limits.infinite = true; + + // PV\ƎזȂ̂ŏĂB + limits.silent = true; + + // pƊeXbhnodesώẐƔrĂ܂B䂦ɎgpȂB + limits.nodes = 0; + + // depthALearner::search()̈Ƃēnꂽ̂ŏB + limits.depth = 0; + + // t߂̎萔ň̒lԂ̂h߂ɑ傫ȒlɂĂB + //limits.max_game_ply = 1 << 16; + + // ʃ[ĂȂƈɂȂȂɂB + //limits.enteringKingRule = EnteringKingRule::EKR_27_POINT; + } + + // DrawValue̐ݒ + { + // XbhƂɗpӂĂȂ̂ + // ̃Xbhŏ㏑ꂩ˂ȂBdȂB + // ǂȂȂA0ɂׂƎvB + //drawValueTable[REPETITION_DRAW][BLACK] = VALUE_ZERO; + //drawValueTable[REPETITION_DRAW][WHITE] = VALUE_ZERO; + } + + // this_threadɊւāB + { + auto th = pos.this_thread(); + + th->completedDepth = 0; + th->selDepth = 0; + th->rootDepth = 0; + + // Tm[h̃[ + th->nodes = 0; + + // historyނSNAB̏͏Ԃ邵AT̐x͂ނ뉺̂őP͂悭킩ȂB + // th->clear(); + + int ct = int(Options["Contempt"]) * PawnValueEg / 100; // From centipawns + Color us = pos.side_to_move(); + + // In analysis mode, adjust contempt in accordance with user preference + if (Limits.infinite || Options["UCI_AnalyseMode"]) + ct = Options["Analysis Contempt"] == "Off" ? 0 + : Options["Analysis Contempt"] == "Both" ? ct + : Options["Analysis Contempt"] == "White" && us == BLACK ? -ct + : Options["Analysis Contempt"] == "Black" && us == WHITE ? -ct + : ct; + + // Evaluation score is from the white point of view + th->contempt = (us == WHITE ? make_score(ct, ct / 2) + : -make_score(ct, ct / 2)); + + for (int i = 7; i > 0; i--) + (ss - i)->continuationHistory = &th->continuationHistory[0][0][NO_PIECE][0]; // Use as a sentinel + + // rootMoves̐ݒ + auto& rootMoves = th->rootMoves; + + rootMoves.clear(); + for (auto m : MoveList(pos)) + rootMoves.push_back(Search::RootMove(m)); + + assert(!rootMoves.empty()); + + //#if defined(USE_GLOBAL_OPTIONS) + // TXbhƂ̒u\̐ǗĂ͂Ȃ̂ŁA + // VK̒Tł邩ÃXbhɑ΂u\̐𑝂₷B + //TT.new_search(th->thread_id()); + + // new_searchĂяo1O̒TʂgȂđƂƂ͂̂ł́cB + // ł͂炸ɁAĂяo1ǂƂTT.new_search(th->thread_id())ׂł́cB + + // @̏Iǐ}Ɏ̂̂ŁAtɂ͒u\͑SXʂŎg悤ɂB + //#endif + } + } + + // ǂ݋؂ƕ]l̃yABLearner::search(),Learner::qsearch()ԂB + typedef std::pair > ValueAndPV; + + // Î~TB + // + // O) pos.set_this_thread(Threads[thread_id])ŒTXbhݒ肳Ă邱ƁB + // @܂AThreads.stopƒT𒆒fĂ܂̂ŁÂƂPV͐ȂB + // @search()߂ƁAThreads.stop == trueȂA̒TʂpĂ͂ȂȂB + // @ƁAĂяoÓAThreads.stop == falsȅԂŌĂяoȂƁAT𒆒fĕԂĂ܂̂ŒӁB + // + // l܂Ăꍇ́APVzMOVE_RESIGNԂB + // + // alpha,betawł悤ɂĂAꂪ̑ŒTƂ̌ʂ + // u\ɏނ̂ŁȂɑ΂Ď}肪o悤Ȓl܂ĊwK̂Ƃ + // êŁA͈̔͂wł悤ɂ̂߂邱ƂɂB + ValueAndPV qsearch(Position& pos) + { + Stack stack[MAX_PLY + 10], * ss = stack + 7; + Move pv[MAX_PLY + 1]; + + init_for_search(pos, ss); + ss->pv = pv; // Ƃ肠_~[łǂobt@ȂƂȂB + + if (pos.is_draw(0)) { + // Return draw value if draw. + return { VALUE_DRAW, {} }; + } + + // l܂Ă̂ + if (MoveList(pos).size() == 0) + { + // Return the mated value if checkmated. + return { mated_in(/*ss->ply*/ 0 + 1), {} }; + } + + auto bestValue = ::qsearch(pos, ss, -VALUE_INFINITE, VALUE_INFINITE, 0); + + // ꂽPVԂB + std::vector pvs; + for (Move* p = &ss->pv[0]; is_ok(*p); ++p) + pvs.push_back(*p); + + return ValueAndPV(bestValue, pvs); + } + + // ʏTB[depth(Ŏw)B + // 3ǂݎ̃XRA~ȂA + // auto v = search(pos,3); + // ̂悤ɂׂB + // v.firstɕ]lAv.secondPVB + // multi pvL̂Ƃ́Apos.this_thread()->rootMoves[N].pvɂPV(ǂ݋)̔z񂪓B + // multi pv̎w͂̊֐̈multiPVōsȂB(Options["MultiPV"]̒l͖) + // + // rootł̐錾͂Ȃ̂(ʓ|Ȃ̂)Ał͍sȂB + // Ăяoŏ邱ƁB + // + // O) pos.set_this_thread(Threads[thread_id])ŒTXbhݒ肳Ă邱ƁB + // @܂AThreads.stopƒT𒆒fĂ܂̂ŁÂƂPV͐ȂB + // @search()߂ƁAThreads.stop == trueȂA̒TʂpĂ͂ȂȂB + // @ƁAĂяoÓAThreads.stop == falsȅԂŌĂяoȂƁAT𒆒fĕԂĂ܂̂ŒӁB + + ValueAndPV search(Position& pos, int depth_, size_t multiPV /* = 1 */, uint64_t nodesLimit /* = 0 */) + { + std::vector pvs; + + Depth depth = depth_; + if (depth < 0) + return std::pair>(Eval::evaluate(pos), std::vector()); + + if (depth == 0) + return qsearch(pos); + + Stack stack[MAX_PLY + 10], * ss = stack + 7; + Move pv[MAX_PLY + 1]; + + init_for_search(pos, ss); + + ss->pv = pv; // Ƃ肠_~[łǂobt@ȂƂȂB + + // this_threadɊ֘Aϐ̏ + auto th = pos.this_thread(); + auto& rootDepth = th->rootDepth; + auto& pvIdx = th->pvIdx; + auto& pvLast = th->pvLast; + auto& rootMoves = th->rootMoves; + auto& completedDepth = th->completedDepth; + auto& selDepth = th->selDepth; + + // bestmoveƂĂ̋ǖʂ̏N‚T@\ + //size_t multiPV = Options["MultiPV"]; + + // ̋ǖʂł̎w̐Ă͂Ȃ + multiPV = std::min(multiPV, rootMoves.size()); + + // m[hMultiPV̒l|ĂȂƁAdepthŒAMultiPVɂƂ1‚̌ɓnodevlƂɂȂȂB + nodesLimit *= multiPV; + + Value alpha = -VALUE_INFINITE; + Value beta = VALUE_INFINITE; + Value delta = -VALUE_INFINITE; + Value bestValue = -VALUE_INFINITE; + + while ((rootDepth += 1) <= depth + // node𒴂ꍇ̃[v𔲂 + // Tm[h́Å֐̈œnĂB + && !(nodesLimit /*node*/ && th->nodes.load(std::memory_order_relaxed) >= nodesLimit) + ) + { + for (RootMove& rm : rootMoves) + rm.previousScore = rm.score; + + size_t pvFirst = 0; + pvLast = 0; + + // MultiPV loop. We perform a full root search for each PV line + for (pvIdx = 0; pvIdx < multiPV && !Threads.stop; ++pvIdx) + { + if (pvIdx == pvLast) + { + pvFirst = pvLast; + for (pvLast++; pvLast < rootMoves.size(); pvLast++) + if (rootMoves[pvLast].tbRank != rootMoves[pvFirst].tbRank) + break; + } + + // ꂼdepthPV lineɑ΂USI infoŏo͂selDepth + selDepth = 0; + + // depth 5ȏɂĂaspiration searchɐ؂ւB + if (rootDepth >= 5 * 1) + { + delta = Value(20); + + Value p = rootMoves[pvIdx].previousScore; + + alpha = std::max(p - delta, -VALUE_INFINITE); + beta = std::min(p + delta, VALUE_INFINITE); + } + + // aspiration search + int failedHighCnt = 0; + while (true) + { + Depth adjustedDepth = std::max(1, rootDepth - failedHighCnt * 1); + bestValue = ::search(pos, ss, alpha, beta, adjustedDepth, false); + + stable_sort(rootMoves.begin() + pvIdx, rootMoves.end()); + //my_stable_sort(pos.this_thread()->thread_id(),&rootMoves[0] + pvIdx, rootMoves.size() - pvIdx); + + // fail low/highɑ΂aspiration windowLB + // AŎw肳ĂlɂȂĂAfail low/highƂbreakB + if (bestValue <= alpha) + { + beta = (alpha + beta) / 2; + alpha = std::max(bestValue - delta, -VALUE_INFINITE); + + failedHighCnt = 0; + //if (mainThread) + // mainThread->stopOnPonderhit = false; + + } + else if (bestValue >= beta) + { + beta = std::min(bestValue + delta, VALUE_INFINITE); + ++failedHighCnt; + } + else + break; + + delta += delta / 4 + 5; + assert(-VALUE_INFINITE <= alpha && beta <= VALUE_INFINITE); + + // \`FbN + //assert(th->nodes.load(std::memory_order_relaxed) <= 1000000 ); + } + + stable_sort(rootMoves.begin(), rootMoves.begin() + pvIdx + 1); + //my_stable_sort(pos.this_thread()->thread_id() , &rootMoves[0] , pvIdx + 1); + + } // multi PV + + completedDepth = rootDepth; + } + + // PVArNULL_MOVẺ”\邩mȂ̂Ŕr邽߂is_ok()ʂB + // @PVȂ̂NULL_MOVE͂ȂƂɂȂĂ͂A + // MOVE_WIN˂܂Ă邱Ƃ͂ȂB(܂̂Ƃ) + for (Move move : rootMoves[0].pv) + { + if (!is_ok(move)) + break; + pvs.push_back(move); + } + + //sync_cout << rootDepth << sync_endl; + + // multiPVlāArootMoves[0]scorebestValueƂĕԂB + bestValue = rootMoves[0].score; + + return ValueAndPV(bestValue, pvs); + } + +} +#endif diff --git a/src/search.h b/src/search.h index 1653ce92..0d22b0ff 100644 --- a/src/search.h +++ b/src/search.h @@ -88,6 +88,7 @@ struct LimitsType { time[WHITE] = time[BLACK] = inc[WHITE] = inc[BLACK] = npmsec = movetime = TimePoint(0); movestogo = depth = mate = perft = infinite = 0; nodes = 0; + silent = false; } bool use_time_management() const { @@ -98,6 +99,9 @@ struct LimitsType { TimePoint time[COLOR_NB], inc[COLOR_NB], npmsec, movetime, startTime; int movestogo, depth, mate, perft, infinite; int64_t nodes; + // ʂɏo͂ȂTCg[h(vZXł̘Aȑΐ̂Ƃp) + // ̂ƂPVo͂ȂB + bool silent; }; extern LimitsType Limits; diff --git a/src/tt.cpp b/src/tt.cpp index d0a5d4e0..28a331f5 100644 --- a/src/tt.cpp +++ b/src/tt.cpp @@ -115,6 +115,9 @@ void TranspositionTable::clear() { /// TTEntry t2 if its replace value is greater than that of t2. TTEntry* TranspositionTable::probe(const Key key, bool& found) const { +#if defined(DISABLE_TT) + return found = false, first_entry(0); +#else TTEntry* const tte = first_entry(key); const uint16_t key16 = (uint16_t)key; // Use the low 16 bits as key inside the cluster @@ -139,6 +142,7 @@ TTEntry* TranspositionTable::probe(const Key key, bool& found) const { replace = &tte[i]; return found = false, replace; +#endif } diff --git a/src/types.h b/src/types.h index 969d4e65..2512fc29 100644 --- a/src/types.h +++ b/src/types.h @@ -131,6 +131,8 @@ enum Color { WHITE, BLACK, COLOR_NB = 2 }; +constexpr Color Colors[2] = { WHITE, BLACK }; + enum CastlingRights { NO_CASTLING, WHITE_OO, @@ -187,7 +189,10 @@ enum Value : int { QueenValueMg = 2538, QueenValueEg = 2682, Tempo = 28, - MidgameLimit = 15258, EndgameLimit = 3915 + MidgameLimit = 15258, EndgameLimit = 3915, + + // ]֐̕Ԃl̍ől(2**14炢Ɏ܂Ăė~Ƃ낾..) + VALUE_MAX_EVAL = 27000, }; enum PieceType { @@ -232,7 +237,8 @@ enum Square : int { SQ_A8, SQ_B8, SQ_C8, SQ_D8, SQ_E8, SQ_F8, SQ_G8, SQ_H8, SQ_NONE, - SQUARE_NB = 64 + SQUARE_ZERO = 0, SQUARE_NB = 64, + SQUARE_NB_PLUS1 = SQUARE_NB + 1, // ʂȂꍇASQUARE_NBɈړ̂ƂĈ߁AzSQUARE_NB+1ŊmۂȂƂȂƂ̂ł̒萔pB }; enum Direction : int { @@ -455,6 +461,44 @@ constexpr bool is_ok(Move m) { return from_sq(m) != to_sq(m); // Catch MOVE_NULL and MOVE_NONE } +// Ֆʂ180񂵂Ƃ̏ڂԂ +constexpr Square Inv(Square sq) { return (Square)((SQUARE_NB - 1) - sq); } + +// Ֆʂ~[Ƃ̏ڂԂ +constexpr Square Mir(Square sq) { return make_square(File(7 - (int)file_of(sq)), rank_of(sq)); } + +#if defined(EVAL_NNUE) || defined(EVAL_LEARN) +// -------------------- +//  +// -------------------- + +// PositionNXŗpAXg(ǂ̋ǂɂ̂)ǗƂ̔ԍB +enum PieceNumber : uint8_t +{ + PIECE_NUMBER_PAWN = 0, + PIECE_NUMBER_KNIGHT = 16, + PIECE_NUMBER_BISHOP = 20, + PIECE_NUMBER_ROOK = 24, + PIECE_NUMBER_QUEEN = 28, + PIECE_NUMBER_KING = 30, + PIECE_NUMBER_WKING = 30, + PIECE_NUMBER_BKING = 31, // A̋ʂ̔ԍKvȏꍇ͂p + PIECE_NUMBER_ZERO = 0, + PIECE_NUMBER_NB = 32, +}; + +inline PieceNumber& operator++(PieceNumber& d) { return d = PieceNumber(int8_t(d) + 1); } +inline PieceNumber operator++(PieceNumber& d, int) { + PieceNumber x = d; + d = PieceNumber(int8_t(d) + 1); + return x; +} +inline PieceNumber& operator--(PieceNumber& d) { return d = PieceNumber(int8_t(d) - 1); } + +// PieceNumber̐̌BassertpB +constexpr bool is_ok(PieceNumber pn) { return pn < PIECE_NUMBER_NB; } +#endif // defined(EVAL_NNUE) || defined(EVAL_LEARN) + #endif // #ifndef TYPES_H_INCLUDED #include "tune.h" // Global visibility to tuning setup diff --git a/src/uci.cpp b/src/uci.cpp index 11d5adc6..b7ece34b 100644 --- a/src/uci.cpp +++ b/src/uci.cpp @@ -33,16 +33,55 @@ #include "uci.h" #include "syzygy/tbprobe.h" +#if defined(EVAL_NNUE) && defined(ENABLE_TEST_CMD) +#include "eval/nnue/nnue_test_command.h" +#endif + using namespace std; extern vector setup_bench(const Position&, istream&); +// FEN string of the initial position, normal chess +const char* StartFEN = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"; + +// R}h +#if defined (EVAL_LEARN) +namespace Learner +{ + // tǖʂ̎ + void gen_sfen(Position& pos, istringstream& is); + + // ̊wK + void learn(Position& pos, istringstream& is); + +#if defined(GENSFEN2019) + // J̋tǖʂ̎R}h + void gen_sfen2019(Position& pos, istringstream& is); +#endif + + // ǂ݋؂ƕ]l̃yABLearner::search(),Learner::qsearch()ԂB + typedef std::pair > ValueAndPV; + + ValueAndPV qsearch(Position& pos); + ValueAndPV search(Position& pos, int depth_, size_t multiPV = 1, uint64_t nodesLimit = 0); + +} +#endif + +#if defined(EVAL_NNUE) && defined(ENABLE_TEST_CMD) +void test_cmd(Position& pos, istringstream& is) +{ + // T邩mȂ̂ŏĂB + is_ready(); + + std::string param; + is >> param; + + if (param == "nnue") Eval::NNUE::TestCommand(pos, is); +} +#endif + namespace { - - // FEN string of the initial position, normal chess - const char* StartFEN = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"; - - // position() is called when engine receives the "position" UCI command. // The function sets up the position described in the given FEN string ("fen") // or the starting position ("startpos") and then makes the moves given in the @@ -182,8 +221,115 @@ namespace { << "\nNodes/second : " << 1000 * nodes / elapsed << endl; } + // check sumvZƂAۑĂĂƂŎȍ~Ã`FbNsȂB + uint64_t eval_sum; } // namespace +// is_ready_cmd()OĂяo悤ɂĂB(benchR}hȂǂĂяo) +// ǖʂ͏Ȃ̂ŒӁB +void is_ready(bool skipCorruptCheck) +{ +#if defined(EVAL_NNUE) + // "isready"󂯎ƁA"readyok"Ԃ܂5bƂɉs𑗂悤ɏCB(keep aliveIȏ) + // USI2.0̎dlB + // -"isready"̂Ƃtime outԂ́A30bxƂB𒴂āA]֐̏Ahashe[ůmۂꍇA + // vlGWIɉ炩̃bZ[W(s)𑗂ׂłB + // -ShogiGUIł͂łɂȂĂ̂ŁAMyShogiɒǐB + // -܂A˂牤̃GẂA"isready"󂯎ƁA"readyok"Ԃ܂5bƂɉs𑗂悤ɏCB + + auto ended = false; + auto th = std::thread([&ended] { + int count = 0; + while (!ended) + { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + if (++count >= 50 /* 5b */) + { + count = 0; + sync_cout << sync_endl; // s𑗐MB + } + } + }); + + // ]֐̓ǂݍ݂ȂǎԂ̂ł낤͂̃^C~OōsȂB + // NɎԂ̂鏈Ă܂Ə^CAEgāAvlGWƂĂ̔F^CAĂ܂B + if (!UCI::load_eval_finished) + { + // ]֐̓ǂݍ + Eval::load_eval(); + + // `FbNŤvZƕۑ(̌̃j̃`FbN̂) + eval_sum = Eval::calc_check_sum(); + + // \tg̕\ + Eval::print_softname(eval_sum); + + UCI::load_eval_finished = true; + + } + else + { + // j󂳂ĂȂ𒲂ׂ邽߂Ƀ`FbNT𖈉񒲂ׂB + // ԂȂC邪.. 0.1b炢̂ƂȂ̂ŗǂƂB + if (!skipCorruptCheck && eval_sum != Eval::calc_check_sum()) + sync_cout << "Error! : EVAL memory is corrupted" << sync_endl; + } + + // isreadyɑ΂ĂreadyokԂ܂Ŏ̃R}hȂƂ͖񑩂Ă̂ + // ̃^C~OŊeϐ̏ĂB + + TT.resize(Options["Hash"]); + Search::clear(); + Time.availableNodes = 0; + + Threads.stop = false; + + // keep alive𑗐M邽߂ɐXbhIAҋ@B + ended = true; + th.join(); +#endif // defined(EVAL_NNUE) + + sync_cout << "readyok" << sync_endl; +} + + +// -------------------- +// eXgpqsearch(),search()𒼐ڌĂ +// -------------------- + +#if defined(EVAL_LEARN) +void qsearch_cmd(Position& pos) +{ + cout << "qsearch : "; + auto pv = Learner::qsearch(pos); + cout << "Value = " << pv.first << " , " << UCI::value(pv.first) << " , PV = "; + for (auto m : pv.second) + cout << UCI::move(m, false) << " "; + cout << endl; +} + +void search_cmd(Position& pos, istringstream& is) +{ + string token; + int depth = 1; + int multi_pv = (int)Options["MultiPV"]; + while (is >> token) + { + if (token == "depth") + is >> depth; + if (token == "multipv") + is >> multi_pv; + } + + cout << "search depth = " << depth << " , multi_pv = " << multi_pv << " : "; + auto pv = Learner::search(pos, depth, multi_pv); + cout << "Value = " << pv.first << " , " << UCI::value(pv.first) << " , PV = "; + for (auto m : pv.second) + cout << UCI::move(m, false) << " "; + cout << endl; +} + +#endif /// UCI::loop() waits for a command from stdin, parses it and calls the appropriate /// function. Also intercepts EOF from stdin to ensure gracefully exiting if the @@ -231,7 +377,7 @@ void UCI::loop(int argc, char* argv[]) { else if (token == "go") go(pos, is, states); else if (token == "position") position(pos, is, states); else if (token == "ucinewgame") Search::clear(); - else if (token == "isready") sync_cout << "readyok" << sync_endl; + else if (token == "isready") is_ready(); // Additional custom non-UCI commands, mainly for debugging. // Do not use these commands during a search! @@ -240,6 +386,28 @@ void UCI::loop(int argc, char* argv[]) { else if (token == "d") sync_cout << pos << sync_endl; else if (token == "eval") sync_cout << Eval::trace(pos) << sync_endl; else if (token == "compiler") sync_cout << compiler_info() << sync_endl; +#if defined (EVAL_LEARN) + else if (token == "gensfen") Learner::gen_sfen(pos, is); + else if (token == "learn") Learner::learn(pos, is); + +#if defined (GENSFEN2019) + // J̋tǖʐR}h + else if (token == "gensfen2019") Learner::gen_sfen2019(pos, is); +#endif + // eXgpqsearch(),search()𒼐ڌĂԃR}h + else if (token == "qsearch") qsearch_cmd(pos); + else if (token == "search") search_cmd(pos, is); + +#endif + +#if defined(EVAL_NNUE) + else if (token == "eval_nnue") sync_cout << "eval_nnue = " << Eval::compute_eval(pos) << sync_endl; +#endif + +#if defined(EVAL_NNUE) && defined(ENABLE_TEST_CMD) + // eXgR}h + else if (token == "test") test_cmd(pos, is); +#endif else sync_cout << "Unknown command: " << cmd << sync_endl; diff --git a/src/uci.h b/src/uci.h index b845889b..71e07787 100644 --- a/src/uci.h +++ b/src/uci.h @@ -75,8 +75,18 @@ std::string move(Move m, bool chess960); std::string pv(const Position& pos, Depth depth, Value alpha, Value beta); Move to_move(const Position& pos, std::string& str); +// ]֐ǂݍ񂾂̃tOBevaldir̕ύXɂƂȂfalseɂB +extern bool load_eval_finished; // = false; } // namespace UCI extern UCI::OptionsMap Options; +// USI"isready"R}hĂяoꂽƂ̏B̂Ƃɕ]֐̓ǂݍ݂ȂǂsȂB +// benchmarkR}h̃nhȂǂ"isready"ĂȂƂɕ]֐ǂݍ܂ƂɗpB +// skipCorruptCheck == truêƂ͕]֐2xڂ̓ǂݍ݂̂Ƃcheck sumɂ郁j`FbNȗB +// @̊֐́AStockfishɂ͂ȂȂƕsւȂ̂ŒljĂB +void is_ready(bool skipCorruptCheck = false); + +extern const char* StartFEN; + #endif // #ifndef UCI_H_INCLUDED diff --git a/src/ucioption.cpp b/src/ucioption.cpp index 7037ea57..69bc8ca8 100644 --- a/src/ucioption.cpp +++ b/src/ucioption.cpp @@ -42,6 +42,7 @@ void on_hash_size(const Option& o) { TT.resize(size_t(o)); } void on_logger(const Option& o) { start_logger(o); } void on_threads(const Option& o) { Threads.set(size_t(o)); } void on_tb_path(const Option& o) { Tablebases::init(o); } +void on_eval_dir(const Option& o) { load_eval_finished = false; } /// Our case insensitive less() function as required by UCI protocol @@ -79,6 +80,23 @@ void init(OptionsMap& o) { o["SyzygyProbeDepth"] << Option(1, 1, 100); o["Syzygy50MoveRule"] << Option(true); o["SyzygyProbeLimit"] << Option(7, 0, 7); + // ]֐tH_BύXƂA]֐isready^C~OœǂݒKvB + o["EvalDir"] << Option("eval", on_eval_dir); + // isready^C~Oŕ]֐ǂݍ܂ƁAV]֐̕ϊ̂߂ + // test evalconvertR}h@̂ɁA̐V]֐Ȃ߂ + // ̃R}h̎sOɈُIĂ܂B + // ł̉BIvVisready̕]֐̓ǂݍ݂}āA + // test evalconvertR}h@B + o["SkipLoadingEval"] << Option(false); + // Ղ̎wڂ܂ŗp邩 + o["BookMoves"] << Option(16, 0, 10000); + +#if defined(EVAL_LEARN) + // ]֐̊wKsȂƂ́A]֐̕ۑ̃tH_ύXłB + // ftHgłevalsaveB̃tH_͎OɗpӂĂ̂ƂB + // ̃tH_zɃtH_"0/","1/",ĉ悤ɎIɌ@Aɕ]֐t@CۑB + o["EvalSaveDir"] << Option("evalsave"); +#endif } @@ -187,4 +205,6 @@ Option& Option::operator=(const string& v) { return *this; } +// ]֐ǂݍ񂾂̃tOBevaldir̕ύXɂƂȂfalseɂB +bool load_eval_finished = false; } // namespace UCI