diff --git a/src/eval/evaluate_common.h b/src/eval/evaluate_common.h new file mode 100644 index 00000000..889fda7a --- /dev/null +++ b/src/eval/evaluate_common.h @@ -0,0 +1,83 @@ +#ifndef _EVALUATE_COMMON_H_ +#define _EVALUATE_COMMON_H_ + +// いまどきの手番つき評価関数(EVAL_KPPTとEVAL_KPP_KKPT)の共用header的なもの。 + +#if defined (EVAL_KPPT) || defined(EVAL_KPP_KKPT) || defined(EVAL_NNUE) +#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 + +#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..a0cf7461 --- /dev/null +++ b/src/eval/evaluate_mir_inv_tools.cpp @@ -0,0 +1,186 @@ +#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 + } + +} diff --git a/src/eval/evaluate_mir_inv_tools.h b/src/eval/evaluate_mir_inv_tools.h new file mode 100644 index 00000000..6e82ce58 --- /dev/null +++ b/src/eval/evaluate_mir_inv_tools.h @@ -0,0 +1,43 @@ +#ifndef _EVALUATE_MIR_INV_TOOLS_ +#define _EVALUATE_MIR_INV_TOOLS_ + +// 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 diff --git a/src/eval/nnue/evaluate_nnue.cpp b/src/eval/nnue/evaluate_nnue.cpp index 15d9194b..a19b2a0e 100644 --- a/src/eval/nnue/evaluate_nnue.cpp +++ b/src/eval/nnue/evaluate_nnue.cpp @@ -224,8 +224,8 @@ EvaluateHashTable g_evalTable; // prefetchする関数も用意しておく。 void prefetch_evalhash(const Key key) { - constexpr auto mask = ~((u64)0x1f); - prefetch((void*)((u64)g_evalTable[key] & mask)); + constexpr auto mask = ~((uint64_t)0x1f); + prefetch((void*)((uint64_t)g_evalTable[key] & mask)); } #endif @@ -269,7 +269,7 @@ Value compute_eval(const Position& pos) { } // 評価関数 -Value NNUE::evaluate(const Position& pos) { +Value evaluate(const Position& pos) { const auto& accumulator = pos.state()->accumulator; if (accumulator.computed_score) { return accumulator.score; diff --git a/src/eval/nnue/evaluate_nnue.h b/src/eval/nnue/evaluate_nnue.h index 1ca48d5b..a95f2bd9 100644 --- a/src/eval/nnue/evaluate_nnue.h +++ b/src/eval/nnue/evaluate_nnue.h @@ -55,8 +55,6 @@ bool ReadParameters(std::istream& stream); // 評価関数パラメータを書き込む bool WriteParameters(std::ostream& stream); -Value evaluate(const Position& pos); - } // namespace NNUE } // namespace Eval diff --git a/src/eval/nnue/evaluate_nnue_learner.cpp b/src/eval/nnue/evaluate_nnue_learner.cpp index cd3ae72a..0e558f39 100644 --- a/src/eval/nnue/evaluate_nnue_learner.cpp +++ b/src/eval/nnue/evaluate_nnue_learner.cpp @@ -9,8 +9,9 @@ #include "../../learn/learning_tools.h" #include "../../position.h" -#include "../../usi.h" +#include "../../uci.h" #include "../../misc.h" +#include "../../thread_win32_osx.h" #include "../evaluate_common.h" @@ -37,7 +38,7 @@ std::vector examples; Mutex examples_mutex; // ミニバッチのサンプル数 -u64 batch_size; +uint64_t batch_size; // 乱数生成器 std::mt19937 rng; @@ -57,20 +58,20 @@ double GetGlobalLearningRateScale() { void SendMessages(std::vector messages) { for (auto& message : messages) { trainer->SendMessage(&message); - ASSERT_LV3(message.num_receivers > 0); + assert(message.num_receivers > 0); } } } // namespace // 学習の初期化を行う -void InitializeTraining(double eta1, u64 eta1_epoch, - double eta2, u64 eta2_epoch, double eta3) { +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); + assert(feature_transformer); + assert(network); trainer = Trainer::Create(network.get(), feature_transformer.get()); if (Options["SkipLoadingEval"]) { @@ -82,8 +83,8 @@ void InitializeTraining(double eta1, u64 eta1_epoch, } // ミニバッチのサンプル数を設定する -void SetBatchSize(u64 size) { - ASSERT_LV3(size > 0); +void SetBatchSize(uint64_t size) { + assert(size > 0); batch_size = size; } @@ -97,7 +98,7 @@ void SetOptions(const std::string& options) { std::vector messages; for (const auto& option : Split(options, ',')) { const auto fields = Split(option, '='); - ASSERT_LV3(fields.size() == 1 || fields.size() == 2); + assert(fields.size() == 1 || fields.size() == 2); if (fields.size() == 1) { messages.emplace_back(fields[0]); } else { @@ -112,7 +113,7 @@ 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); + assert(result); SendMessages({{"reset"}}); } @@ -136,7 +137,7 @@ void AddExample(Position& pos, Color rootColor, if (pos.side_to_move() != BLACK) { active_indices[0].swap(active_indices[1]); } - for (const auto color : COLOR) { + for (const auto color : Colors) { std::vector training_features; for (const auto base_index : active_indices[color]) { static_assert(Features::Factorizer::GetDimensions() < @@ -162,8 +163,8 @@ void AddExample(Position& pos, Color rootColor, } // 評価関数パラメーターを更新する -void UpdateParameters(u64 epoch) { - ASSERT_LV3(batch_size > 0); +void UpdateParameters(uint64_t epoch) { + assert(batch_size > 0); EvalLearningTools::Weight::calc_eta(epoch); const auto learning_rate = static_cast( @@ -215,7 +216,7 @@ void save_eval(std::string dir_name) { 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); + assert(result); std::cout << "save_eval() finished. folder = " << eval_dir << std::endl; } diff --git a/src/eval/nnue/evaluate_nnue_learner.h b/src/eval/nnue/evaluate_nnue_learner.h index 130ce376..e2e68738 100644 --- a/src/eval/nnue/evaluate_nnue_learner.h +++ b/src/eval/nnue/evaluate_nnue_learner.h @@ -3,8 +3,6 @@ #ifndef _EVALUATE_NNUE_LEARNER_H_ #define _EVALUATE_NNUE_LEARNER_H_ -#include "../../config.h" - #if defined(EVAL_LEARN) && defined(EVAL_NNUE) #include "../../learn/learn.h" @@ -14,11 +12,11 @@ namespace Eval { namespace NNUE { // 学習の初期化を行う -void InitializeTraining(double eta1, u64 eta1_epoch, - double eta2, u64 eta2_epoch, double eta3); +void InitializeTraining(double eta1, uint64_t eta1_epoch, + double eta2, uint64_t eta2_epoch, double eta3); // ミニバッチのサンプル数を設定する -void SetBatchSize(u64 size); +void SetBatchSize(uint64_t size); // 学習率のスケールを設定する void SetGlobalLearningRateScale(double scale); @@ -34,7 +32,7 @@ void AddExample(Position& pos, Color rootColor, const Learner::PackedSfenValue& psv, double weight); // 評価関数パラメータを更新する -void UpdateParameters(u64 epoch); +void UpdateParameters(uint64_t epoch); // 学習に問題が生じていないかチェックする void CheckHealth(); diff --git a/src/eval/nnue/layers/sum.h b/src/eval/nnue/layers/sum.h index 216de458..3fe000cf 100644 --- a/src/eval/nnue/layers/sum.h +++ b/src/eval/nnue/layers/sum.h @@ -3,8 +3,6 @@ #ifndef _NNUE_LAYERS_SUM_H_ #define _NNUE_LAYERS_SUM_H_ -#include "../../../config.h" - #if defined(EVAL_NNUE) #include "../nnue_common.h" diff --git a/src/eval/nnue/trainer/features/factorizer.h b/src/eval/nnue/trainer/features/factorizer.h index e31c9976..3bc59260 100644 --- a/src/eval/nnue/trainer/features/factorizer.h +++ b/src/eval/nnue/trainer/features/factorizer.h @@ -3,8 +3,6 @@ #ifndef _NNUE_TRAINER_FEATURES_FACTORIZER_H_ #define _NNUE_TRAINER_FEATURES_FACTORIZER_H_ -#include "../../../../config.h" - #if defined(EVAL_NNUE) #include "../../nnue_common.h" @@ -29,7 +27,7 @@ class Factorizer { // 学習用特徴量のインデックスと学習率のスケールを取得する static void AppendTrainingFeatures( IndexType base_index, std::vector* training_features) { - ASSERT_LV5(base_index < FeatureType::kDimensions); + assert(base_index < FeatureType::kDimensions); training_features->emplace_back(base_index); } }; @@ -45,8 +43,8 @@ template IndexType AppendBaseFeature( FeatureProperties properties, IndexType base_index, std::vector* training_features) { - ASSERT_LV5(properties.dimensions == FeatureType::kDimensions); - ASSERT_LV5(base_index < FeatureType::kDimensions); + assert(properties.dimensions == FeatureType::kDimensions); + assert(base_index < FeatureType::kDimensions); training_features->emplace_back(base_index); return properties.dimensions; } @@ -59,14 +57,14 @@ IndexType InheritFeaturesIfRequired( if (!properties.active) { return 0; } - ASSERT_LV5(properties.dimensions == Factorizer::GetDimensions()); - ASSERT_LV5(base_index < FeatureType::kDimensions); + 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_LV5(feature.GetIndex() < Factorizer::GetDimensions()); + assert(feature.GetIndex() < Factorizer::GetDimensions()); feature.ShiftIndex(index_offset); } return properties.dimensions; diff --git a/src/eval/nnue/trainer/features/factorizer_feature_set.h b/src/eval/nnue/trainer/features/factorizer_feature_set.h index e2db79b1..111678e4 100644 --- a/src/eval/nnue/trainer/features/factorizer_feature_set.h +++ b/src/eval/nnue/trainer/features/factorizer_feature_set.h @@ -3,8 +3,6 @@ #ifndef _NNUE_TRAINER_FEATURES_FACTORIZER_FEATURE_SET_H_ #define _NNUE_TRAINER_FEATURES_FACTORIZER_FEATURE_SET_H_ -#include "../../../../config.h" - #if defined(EVAL_NNUE) #include "../../features/feature_set.h" @@ -38,7 +36,7 @@ class Factorizer> { static void AppendTrainingFeatures( IndexType base_index, std::vector* training_features, IndexType base_dimensions = kBaseDimensions) { - ASSERT_LV5(base_index < kBaseDimensions); + assert(base_index < kBaseDimensions); constexpr auto boundary = FeatureSet::kDimensions; if (base_index < boundary) { Tail::AppendTrainingFeatures( @@ -50,7 +48,7 @@ class Factorizer> { for (auto i = start; i < training_features->size(); ++i) { auto& feature = (*training_features)[i]; const auto index = feature.GetIndex(); - ASSERT_LV5(index < Head::GetDimensions() || + assert(index < Head::GetDimensions() || (index >= base_dimensions && index < base_dimensions + Head::GetDimensions() - Head::kBaseDimensions)); @@ -81,13 +79,13 @@ public: static void AppendTrainingFeatures( IndexType base_index, std::vector* training_features, IndexType base_dimensions = kBaseDimensions) { - ASSERT_LV5(base_index < 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_LV5(feature.GetIndex() < Factorizer::GetDimensions()); + assert(feature.GetIndex() < Factorizer::GetDimensions()); if (feature.GetIndex() >= kBaseDimensions) { feature.ShiftIndex(base_dimensions - kBaseDimensions); } diff --git a/src/eval/nnue/trainer/features/factorizer_half_kp.h b/src/eval/nnue/trainer/features/factorizer_half_kp.h index 5682e8e6..36f53edc 100644 --- a/src/eval/nnue/trainer/features/factorizer_half_kp.h +++ b/src/eval/nnue/trainer/features/factorizer_half_kp.h @@ -3,8 +3,6 @@ #ifndef _NNUE_TRAINER_FEATURES_FACTORIZER_HALF_KP_H_ #define _NNUE_TRAINER_FEATURES_FACTORIZER_HALF_KP_H_ -#include "../../../../config.h" - #if defined(EVAL_NNUE) #include "../../features/half_kp.h" diff --git a/src/eval/nnue/trainer/trainer.h b/src/eval/nnue/trainer/trainer.h index 1b322703..630f1a3d 100644 --- a/src/eval/nnue/trainer/trainer.h +++ b/src/eval/nnue/trainer/trainer.h @@ -3,8 +3,6 @@ #ifndef _NNUE_TRAINER_H_ #define _NNUE_TRAINER_H_ -#include "../../../config.h" - #if defined(EVAL_LEARN) && defined(EVAL_NNUE) #include "../nnue_common.h" @@ -36,11 +34,11 @@ class TrainingFeature { explicit TrainingFeature(IndexType index) : index_and_count_((index << kCountBits) | 1) { - ASSERT_LV3(index < (1 << kIndexBits)); + assert(index < (1 << kIndexBits)); } TrainingFeature& operator+=(const TrainingFeature& other) { - ASSERT_LV3(other.GetIndex() == GetIndex()); - ASSERT_LV3(other.GetCount() + GetCount() < (1 << kCountBits)); + assert(other.GetIndex() == GetIndex()); + assert(other.GetCount() + GetCount() < (1 << kCountBits)); index_and_count_ += other.GetCount(); return *this; } @@ -48,7 +46,7 @@ class TrainingFeature { return static_cast(index_and_count_ >> kCountBits); } void ShiftIndex(IndexType offset) { - ASSERT_LV3(GetIndex() + offset < (1 << kIndexBits)); + assert(GetIndex() + offset < (1 << kIndexBits)); index_and_count_ += offset << kCountBits; } IndexType GetCount() const { diff --git a/src/eval/nnue/trainer/trainer_affine_transform.h b/src/eval/nnue/trainer/trainer_affine_transform.h index 197beec3..34c4816b 100644 --- a/src/eval/nnue/trainer/trainer_affine_transform.h +++ b/src/eval/nnue/trainer/trainer_affine_transform.h @@ -3,8 +3,6 @@ #ifndef _NNUE_TRAINER_AFFINE_TRANSFORM_H_ #define _NNUE_TRAINER_AFFINE_TRANSFORM_H_ -#include "../../../config.h" - #if defined(EVAL_LEARN) && defined(EVAL_NNUE) #include "../../../learn/learn.h" diff --git a/src/eval/nnue/trainer/trainer_clipped_relu.h b/src/eval/nnue/trainer/trainer_clipped_relu.h index d7cc96e6..bd894769 100644 --- a/src/eval/nnue/trainer/trainer_clipped_relu.h +++ b/src/eval/nnue/trainer/trainer_clipped_relu.h @@ -3,8 +3,6 @@ #ifndef _NNUE_TRAINER_CLIPPED_RELU_H_ #define _NNUE_TRAINER_CLIPPED_RELU_H_ -#include "../../../config.h" - #if defined(EVAL_LEARN) && defined(EVAL_NNUE) #include "../../../learn/learn.h" diff --git a/src/eval/nnue/trainer/trainer_feature_transformer.h b/src/eval/nnue/trainer/trainer_feature_transformer.h index ff4da717..742da440 100644 --- a/src/eval/nnue/trainer/trainer_feature_transformer.h +++ b/src/eval/nnue/trainer/trainer_feature_transformer.h @@ -3,8 +3,6 @@ #ifndef _NNUE_TRAINER_FEATURE_TRANSFORMER_H_ #define _NNUE_TRAINER_FEATURE_TRANSFORMER_H_ -#include "../../../config.h" - #if defined(EVAL_LEARN) && defined(EVAL_NNUE) #include "../../../learn/learn.h" diff --git a/src/eval/nnue/trainer/trainer_input_slice.h b/src/eval/nnue/trainer/trainer_input_slice.h index c6df775f..0660e987 100644 --- a/src/eval/nnue/trainer/trainer_input_slice.h +++ b/src/eval/nnue/trainer/trainer_input_slice.h @@ -3,8 +3,6 @@ #ifndef _NNUE_TRAINER_INPUT_SLICE_H_ #define _NNUE_TRAINER_INPUT_SLICE_H_ -#include "../../../config.h" - #if defined(EVAL_LEARN) && defined(EVAL_NNUE) #include "../../../learn/learn.h" @@ -35,7 +33,7 @@ class SharedInputTrainer { current_operation_ = Operation::kSendMessage; feature_transformer_trainer_->SendMessage(message); } - ASSERT_LV3(current_operation_ == Operation::kSendMessage); + assert(current_operation_ == Operation::kSendMessage); if (++num_calls_ == num_referrers_) { num_calls_ = 0; current_operation_ = Operation::kNone; @@ -49,7 +47,7 @@ class SharedInputTrainer { current_operation_ = Operation::kInitialize; feature_transformer_trainer_->Initialize(rng); } - ASSERT_LV3(current_operation_ == Operation::kInitialize); + assert(current_operation_ == Operation::kInitialize); if (++num_calls_ == num_referrers_) { num_calls_ = 0; current_operation_ = Operation::kNone; @@ -66,7 +64,7 @@ class SharedInputTrainer { current_operation_ = Operation::kPropagate; output_ = feature_transformer_trainer_->Propagate(batch); } - ASSERT_LV3(current_operation_ == Operation::kPropagate); + assert(current_operation_ == Operation::kPropagate); if (++num_calls_ == num_referrers_) { num_calls_ = 0; current_operation_ = Operation::kNone; @@ -90,7 +88,7 @@ class SharedInputTrainer { } } } - ASSERT_LV3(current_operation_ == Operation::kBackPropagate); + 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) { diff --git a/src/eval/nnue/trainer/trainer_sum.h b/src/eval/nnue/trainer/trainer_sum.h index 4095482a..76f6073f 100644 --- a/src/eval/nnue/trainer/trainer_sum.h +++ b/src/eval/nnue/trainer/trainer_sum.h @@ -3,8 +3,6 @@ #ifndef _NNUE_TRAINER_SUM_H_ #define _NNUE_TRAINER_SUM_H_ -#include "../../../config.h" - #if defined(EVAL_LEARN) && defined(EVAL_NNUE) #include "../../../learn/learn.h" diff --git a/src/evaluate.cpp b/src/evaluate.cpp index 29ea65dc..65c7155a 100644 --- a/src/evaluate.cpp +++ b/src/evaluate.cpp @@ -864,13 +864,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) { -#if defined(EVAL_NNUE) - return Eval::NNUE::evaluate(pos); -#else return Evaluation(pos).value(); -#endif // defined(EVAL_NNUE) } +#endif // defined(EVAL_NNUE) /// trace() is like evaluate(), but instead of returning a value, it returns diff --git a/src/evaluate.h b/src/evaluate.h index 1b114179..f31ea142 100644 --- a/src/evaluate.h +++ b/src/evaluate.h @@ -35,6 +35,8 @@ std::string trace(const Position& pos); Value evaluate(const Position& pos); +void evaluate_with_no_return(const Position& pos); + #if defined(EVAL_NNUE) // ]֐t@CǂݍށB // ́A"is_ready"R}h̉1xĂяoB2xĂяoƂ͑z肵ĂȂB @@ -85,6 +87,13 @@ enum BonaPiece : int32_t 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 @@ -132,7 +141,7 @@ struct EvalList // Տ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 + inverse(sq)), sq); + 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 @@ -181,8 +190,8 @@ public: static const int MAX_LENGTH = 40; // Տ̋ɑ΂āA̋ԍ(PieceNumber)ێĂz - // ʂSQ_NBɈړĂƂp+1܂ŕێĂA - // SQ_NB̋ʂړȂ̂ŁA̒lgƂ͂Ȃ͂B + // ʂSQUARE_NBɈړĂƂp+1܂ŕێĂA + // SQUARE_NB̋ʂړȂ̂ŁA̒lgƂ͂Ȃ͂B PieceNumber piece_no_list_board[SQUARE_NB_PLUS1]; private: diff --git a/src/extra/sfen_packer.cpp b/src/extra/sfen_packer.cpp new file mode 100644 index 00000000..df095ce1 --- /dev/null +++ b/src/extra/sfen_packer.cpp @@ -0,0 +1,444 @@ +#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[] = +{ + {0b000,1}, // NO_PIECE + {0b001,3}, // PAWN + {0b011,3}, // KNIGHT + {0b101,3}, // BISHOP + {0b111,3}, // ROOK +}; + +// 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(pos.game_ply(), 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)); + st = si; + + // Active color + sideToMove = (Color)stream.read_one_bit(); + + // evalListのclear。上でmemsetでゼロクリアしたときにクリアされているが…。 + evalList.clear(); + + // PieceListを更新する上で、どの駒がどこにあるかを設定しなければならないが、 + // それぞれの駒をどこまで使ったかのカウンター + PieceNumber piece_no_count[KING] = { + PIECE_NUMBER_ZERO, + PIECE_NUMBER_PAWN, + PIECE_NUMBER_KNIGHT, + PIECE_NUMBER_BISHOP, + PIECE_NUMBER_ROOK, + }; + + 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(7))] = make_piece(c, KING); + } + else + { + for (auto c : Colors) + board[stream.read_n_bit(7)] = 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 : // 後手玉 + piece_no_count[type_of(pc)]++; // それ以外 + + 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) != W_ROOK; --rsq) {} + set_castling_right(BLACK, rsq); + } + if (stream.read_one_bit()) { + Square rsq; + for (rsq = relative_square(BLACK, SQ_A1); piece_on(rsq) != W_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)); + st->epSquare = ep_square; + + if (!(attackers_to(st->epSquare) & pieces(sideToMove, PAWN)) + || !(pieces(~sideToMove, PAWN) & (st->epSquare + pawn_push(~sideToMove)))) + 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); + + 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..0e904650 --- /dev/null +++ b/src/learn/learner.cpp @@ -0,0 +1,2922 @@ +// 学習関係のルーチン +// +// 1) 棋譜の自動生成 +// → "gensfen"コマンド +// 2) 生成した棋譜からの評価関数パラメーターの学習 +// → "learn"コマンド +// → 教師局面のshuffleもこのコマンドの拡張として行なう。 +// 例) "learn shuffle" +// 3) 定跡の自動生成 +// → "makebook think"コマンド +// → extra/book/book.cppで実装 +// 4) 局後自動検討モード +// → GUIが補佐すべき問題なのでエンジンでは関与しないことにする。 +// etc.. + +#if defined(EVAL_LEARN) + +#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 + 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); + + // 探索部で定義されている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_mated()) + { + if (pos.checkers()) { + // (この局面の一つ前の局面までは書き出す) + // Write the packed fens if checkmate. + // Do not write if stalemate. + 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); + + // 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 + 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に行けないので学習から除外しておく。 + // (そのような教師局面自体を書き出すべきではないのだが古い生成ルーチンで書き出しているかも知れないので) + if (pos.is_mated()) + 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がそんなに長くなることはありえない。 + for (auto m : pv) + { + // 非合法手はやってこないはずなのだが。 + if (!pos.pseudo_legal(m) || !pos.legal(m)) + { + cout << pos << m << endl; + assert(false); + } + + // 各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); + } + + // 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::tr2::sys; + 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 // 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..4d4e0daf --- /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 + 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 + 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 + Mutex task_mutex; +}; + +#endif // defined(EVAL_LEARN) && defined(YANEURAOU_2018_OTAFUKU_ENGINE) + +#endif diff --git a/src/misc.cpp b/src/misc.cpp index 69c6bacc..3bf4fddc 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 @@ -316,6 +317,27 @@ void bindThisThread(size_t idx) { } // 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)); @@ -331,3 +353,127 @@ void* aligned_malloc(size_t size, size_t align) } 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 5b63ef1c..6ce75a4d 100644 --- a/src/misc.h +++ b/src/misc.h @@ -24,11 +24,13 @@ #include #include #include +#include #include #include #include #include "types.h" +#include "thread_win32_osx.h" const std::string engine_info(bool to_uci = false); void prefetch(void* addr); @@ -98,8 +100,20 @@ 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; +} /// 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 @@ -114,6 +128,9 @@ namespace WinProcGroup { // 指定されたミリ秒だけsleepする。 extern void sleep(int ms); +// 現在時刻を文字列化したもを返す。(評価関数の学習時などにログ出力のために用いる) +std::string now_string(); + // 途中での終了処理のためのwrapper static void my_exit() { @@ -121,6 +138,54 @@ static void my_exit() 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: + 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 // -------------------- @@ -176,4 +241,39 @@ struct Path 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 aeba93ad..5dda654a 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 23ce5168..a3f05a87 100644 --- a/src/position.cpp +++ b/src/position.cpp @@ -1480,3 +1480,12 @@ PieceNumber Position::piece_no_of(Square sq) const return n; } #endif // defined(EVAL_NNUE) + +#if defined(EVAL_LEARN) +// ǖʂŎw肪ȂeXgBw萶[`p̂őȂBTɂ͎gȂƁB +bool Position::is_mated() const +{ + // sŋl߂łp^[͂Ȃ̂LEGAL_ALLłKv͂ȂB + return MoveList(*this).size() == 0; +} +#endif // EVAL_LEARN diff --git a/src/position.h b/src/position.h index c6e4f9c9..2387dd1c 100644 --- a/src/position.h +++ b/src/position.h @@ -80,6 +80,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(); @@ -187,6 +190,29 @@ public: const Eval::EvalList* eval_list() const { return &evalList; } #endif // defined(EVAL_NNUE) +#if defined(EVAL_LEARN) + // ǖʂŎw肪ȂeXgBw萶[`p̂őȂBTɂ͎gȂƁB + bool is_mated() const; + + // -- 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); diff --git a/src/search.cpp b/src/search.cpp index a3ce4c2d..279c6d8a 100644 --- a/src/search.cpp +++ b/src/search.cpp @@ -1721,3 +1721,283 @@ 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 + + memset(ss - 4, 0, 7 * 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 = DEPTH_ZERO; + th->selDepth = 0; + th->rootDepth = DEPTH_ZERO; + + // Tm[h̃[ + th->nodes = 0; + + // historyނSNAB̏͏Ԃ邵AT̐x͂ނ뉺̂őP͂悭킩ȂB + // th->clear(); + + for (int i = 4; i > 0; i--) + (ss - i)->continuationHistory = &th->continuationHistory[SQUARE_ZERO][NO_PIECE]; + + // 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 + 7], * ss = stack + 4; + Move pv[MAX_PLY + 1]; + std::vector pvs; + + init_for_search(pos, ss); + ss->pv = pv; // Ƃ肠_~[łǂobt@ȂƂȂB + + // l܂Ă̂ + if (pos.is_mated()) + { + pvs.push_back(MOVE_NONE); + return ValueAndPV(mated_in(/*ss->ply*/ 0 + 1), pvs); + } + + auto bestValue = ::qsearch(pos, ss, -VALUE_INFINITE, VALUE_INFINITE, DEPTH_ZERO); + + // ꂽPVԂB + 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_ * ONE_PLY; + if (depth < DEPTH_ZERO) + return std::pair>(Eval::evaluate(pos), std::vector()); + + if (depth == DEPTH_ZERO) + return qsearch(pos); + + Stack stack[MAX_PLY + 7], * ss = stack + 4; + 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& 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 += ONE_PLY) <= 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; + + // MultiPV + for (pvIdx = 0; pvIdx < multiPV && !Threads.stop; ++pvIdx) + { + // ꂼdepthPV lineɑ΂USI infoŏo͂selDepth + selDepth = 0; + + // depth 5ȏɂĂaspiration searchɐ؂ւB + if (rootDepth >= 5 * ONE_PLY) + { + 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(ONE_PLY, rootDepth - failedHighCnt * ONE_PLY); + 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/types.h b/src/types.h index 5270ccd6..9b06ac0d 100644 --- a/src/types.h +++ b/src/types.h @@ -235,8 +235,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_NB_PLUS1 = SQUARE_NB + 1, // ʂȂꍇASQ_NBɈړ̂ƂĈ߁AzSQ_NB+1ŊmۂȂƂȂƂ̂ł̒萔pB + SQUARE_ZERO = 0, SQUARE_NB = 64, + SQUARE_NB_PLUS1 = SQUARE_NB + 1, // ʂȂꍇASQUARE_NBɈړ̂ƂĈ߁AzSQUARE_NB+1ŊmۂȂƂȂƂ̂ł̒萔pB }; enum Direction : int { @@ -362,10 +362,6 @@ constexpr Square operator~(Square s) { return Square(s ^ SQ_A8); // Vertical flip SQ_A1 -> SQ_A8 } -constexpr Square inverse(Square s) { - return static_cast(static_cast(SQUARE_NB) - s - 1); -} - constexpr File operator~(File f) { return File(f ^ FILE_H); // Horizontal flip FILE_A -> FILE_H } @@ -464,6 +460,12 @@ 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) // -------------------- //  diff --git a/src/uci.cpp b/src/uci.cpp index b47398ad..9a2f90ec 100644 --- a/src/uci.cpp +++ b/src/uci.cpp @@ -37,12 +37,10 @@ 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"; + 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 @@ -179,74 +177,74 @@ namespace { // check sumvZƂAۑĂĂƂŎȍ~Ã`FbNsȂB uint64_t eval_sum; +} // namespace - // is_ready_cmd()OĂяo悤ɂĂB(benchR}hȂǂĂяo) - // ǖʂ͏Ȃ̂ŒӁB - void is_ready(Position& pos, istringstream& is, StateListPtr& states) - { +// 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 + // "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) + 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 */) { - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - if (++count >= 50 /* 5b */) - { - count = 0; - sync_cout << sync_endl; // s𑗐MB - } + 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 (eval_sum != Eval::calc_check_sum()) - sync_cout << "Error! : EVAL memory is corrupted" << sync_endl; } + }); - // isreadyɑ΂ĂreadyokԂ܂Ŏ̃R}hȂƂ͖񑩂Ă̂ - // ̃^C~OŊeϐ̏ĂB + // ]֐̓ǂݍ݂ȂǎԂ̂ł낤͂̃^C~OōsȂB + // NɎԂ̂鏈Ă܂Ə^CAEgāAvlGWƂĂ̔F^CAĂ܂B + if (!UCI::load_eval_finished) + { + // ]֐̓ǂݍ + Eval::load_eval(); - TT.resize(Options["Hash"]); - Search::clear(); - Time.availableNodes = 0; + // `FbNŤvZƕۑ(̌̃j̃`FbN̂) + eval_sum = Eval::calc_check_sum(); - Threads.stop = false; + // \tg̕\ + Eval::print_softname(eval_sum); - // keep alive𑗐M邽߂ɐXbhIAҋ@B - ended = true; - th.join(); + 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; - } -} // namespace + sync_cout << "readyok" << sync_endl; +} /// UCI::loop() waits for a command from stdin, parses it and calls the appropriate @@ -296,7 +294,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") is_ready(pos, is, states); + else if (token == "isready") is_ready(); // Additional custom non-UCI commands, mainly for debugging else if (token == "flip") pos.flip(); diff --git a/src/uci.h b/src/uci.h index 4a7771ca..dac881c1 100644 --- a/src/uci.h +++ b/src/uci.h @@ -81,4 +81,12 @@ extern bool load_eval_finished; // = false; 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