1
0
Fork 0
mirror of https://github.com/sockspls/badfish synced 2025-07-11 19:49:14 +00:00

Merge branch 'master' of https://github.com/nodchip/Stockfish into sf-nnue-nodchip

This commit is contained in:
joergoster 2020-06-24 17:47:55 +02:00
commit 89bbe86800
70 changed files with 12108 additions and 27 deletions

View file

@ -38,9 +38,24 @@ PGOBENCH = ./$(EXE) bench
### Source and object files
SRCS = benchmark.cpp bitbase.cpp bitboard.cpp endgame.cpp evaluate.cpp main.cpp \
material.cpp misc.cpp movegen.cpp movepick.cpp pawns.cpp position.cpp psqt.cpp \
search.cpp thread.cpp timeman.cpp tt.cpp uci.cpp ucioption.cpp tune.cpp syzygy/tbprobe.cpp
search.cpp thread.cpp timeman.cpp tt.cpp uci.cpp ucioption.cpp tune.cpp syzygy/tbprobe.cpp \
eval/evaluate_mir_inv_tools.cpp \
eval/nnue/evaluate_nnue.cpp \
eval/nnue/evaluate_nnue_learner.cpp \
eval/nnue/features/half_kp.cpp \
eval/nnue/features/half_relative_kp.cpp \
eval/nnue/features/k.cpp \
eval/nnue/features/p.cpp \
eval/nnue/features/castling_right.cpp \
eval/nnue/features/enpassant.cpp \
eval/nnue/nnue_test_command.cpp \
extra/sfen_packer.cpp \
learn/gensfen2019.cpp \
learn/learner.cpp \
learn/learning_tools.cpp \
learn/multi_think.cpp
OBJS = $(notdir $(SRCS:.cpp=.o))
OBJS = $(SRCS:.cpp=.o)
VPATH = syzygy
@ -81,6 +96,7 @@ bits = 32
prefetch = no
popcnt = no
sse = no
avx2 = no
pext = no
### 2.2 Architecture specific
@ -119,12 +135,22 @@ ifeq ($(ARCH),x86-64-modern)
sse = yes
endif
ifeq ($(ARCH),x86-64-avx2)
arch = x86_64
bits = 64
prefetch = yes
popcnt = yes
sse = yes
avx2 = yes
endif
ifeq ($(ARCH),x86-64-bmi2)
arch = x86_64
bits = 64
prefetch = yes
popcnt = yes
sse = yes
avx2 = yes
pext = yes
endif
@ -151,8 +177,8 @@ endif
### 3.1 Selecting compiler (default = gcc)
CXXFLAGS += -Wall -Wcast-qual -fno-exceptions -std=c++11 $(EXTRACXXFLAGS)
DEPENDFLAGS += -std=c++11
CXXFLAGS += -Wall -Wcast-qual -fno-exceptions -std=c++17 $(EXTRACXXFLAGS)
DEPENDFLAGS += -std=c++17
LDFLAGS += $(EXTRALDFLAGS)
ifeq ($(COMP),)
@ -232,6 +258,28 @@ ifeq ($(COMP),clang)
endif
endif
ifeq ($(COMP),msys2)
comp=gcc
CXX=g++
CXXFLAGS += -pedantic -Wextra -Wshadow
ifeq ($(ARCH),armv7)
ifeq ($(OS),Android)
CXXFLAGS += -m$(bits)
LDFLAGS += -m$(bits)
endif
else
CXXFLAGS += -m$(bits)
LDFLAGS += -m$(bits)
endif
ifneq ($(KERNEL),Darwin)
LDFLAGS += -Wl,--no-as-needed
endif
LDFLAGS += -static -Wl,-s
endif
ifeq ($(comp),icc)
profile_make = icc-profile-make
profile_use = icc-profile-use
@ -320,19 +368,26 @@ endif
### 3.6 popcnt
ifeq ($(popcnt),yes)
ifeq ($(arch),ppc64)
CXXFLAGS += -DUSE_POPCNT
CXXFLAGS += -DUSE_POPCNT -DUSE_SSE2
else ifeq ($(comp),icc)
CXXFLAGS += -msse3 -DUSE_POPCNT
CXXFLAGS += -msse3 -DUSE_POPCNT -DUSE_SSE2
else
CXXFLAGS += -msse3 -mpopcnt -DUSE_POPCNT
CXXFLAGS += -msse3 -mpopcnt -DUSE_POPCNT -DUSE_SSE2
endif
endif
ifeq ($(avx2),yes)
CXXFLAGS += -DUSE_AVX2
ifeq ($(comp),$(filter $(comp),gcc clang mingw msys2))
CXXFLAGS += -mavx2
endif
endif
### 3.7 pext
ifeq ($(pext),yes)
CXXFLAGS += -DUSE_PEXT
ifeq ($(comp),$(filter $(comp),gcc clang mingw))
CXXFLAGS += -msse4 -mbmi2
ifeq ($(comp),$(filter $(comp),gcc clang mingw msys2))
CXXFLAGS += -mbmi2
endif
endif
@ -341,7 +396,7 @@ endif
### needs access to the optimization flags.
ifeq ($(optimize),yes)
ifeq ($(debug), no)
ifeq ($(comp),$(filter $(comp),gcc clang))
ifeq ($(comp),$(filter $(comp),gcc clang msys2))
CXXFLAGS += -flto
LDFLAGS += $(CXXFLAGS)
endif
@ -384,6 +439,7 @@ help:
@echo "Supported archs:"
@echo ""
@echo "x86-64-bmi2 > x86 64-bit with pext support (also enables SSE4)"
@echo "x86-64-avx2 > x86 64-bit with avx2 support (also enables SSE4)"
@echo "x86-64-modern > x86 64-bit with popcnt support (also enables SSE3)"
@echo "x86-64 > x86 64-bit generic"
@echo "x86-32 > x86 32-bit (also enables SSE)"
@ -400,6 +456,7 @@ help:
@echo "mingw > Gnu compiler with MinGW under Windows"
@echo "clang > LLVM Clang compiler"
@echo "icc > Intel compiler"
@echo "msys2 > MSYS2"
@echo ""
@echo "Simple examples. If you don't know what to do, you likely want to run: "
@echo ""
@ -449,7 +506,7 @@ clean: objclean profileclean
# clean binaries and objects
objclean:
@rm -f $(EXE) *.o ./syzygy/*.o
@rm -f $(EXE) *.o ./syzygy/*.o ./learn/*.o ./extra/*.o ./eval/*.o ./eval/nnue/*.o ./eval/nnue/features/*.o
# clean auxiliary profiling files
profileclean:
@ -479,6 +536,7 @@ config-sanity:
@echo "prefetch: '$(prefetch)'"
@echo "popcnt: '$(popcnt)'"
@echo "sse: '$(sse)'"
@echo "avx2: '$(avx2)'"
@echo "pext: '$(pext)'"
@echo ""
@echo "Flags:"
@ -539,8 +597,20 @@ icc-profile-use:
EXTRACXXFLAGS='-prof_use -prof_dir ./profdir' \
all
nnue: config-sanity
$(MAKE) CXXFLAGS='$(CXXFLAGS) -DEVAL_NNUE -DUSE_EVAL_HASH -DENABLE_TEST_CMD -fopenmp' LDFLAGS='$(LDFLAGS) -fopenmp' build
nnue-gen-sfen-from-original-eval: config-sanity
$(MAKE) CXXFLAGS='$(CXXFLAGS) -DEVAL_LEARN -DUSE_EVAL_HASH -DENABLE_TEST_CMD -fopenmp' LDFLAGS='$(LDFLAGS) -fopenmp' build
nnue-learn: config-sanity
$(MAKE) CXXFLAGS='$(CXXFLAGS) -DEVAL_LEARN -DEVAL_NNUE -DUSE_EVAL_HASH -DENABLE_TEST_CMD -fopenmp' LDFLAGS='$(LDFLAGS) -fopenmp' build
nnue-learn-use-blas: config-sanity
$(MAKE) CXXFLAGS='$(CXXFLAGS) -DEVAL_LEARN -DEVAL_NNUE -DUSE_EVAL_HASH -DENABLE_TEST_CMD -DUSE_BLAS -I/mingw64/include/OpenBLAS -fopenmp' LDFLAGS='$(LDFLAGS) -lopenblas -fopenmp' build
.depend:
-@$(CXX) $(DEPENDFLAGS) -MM $(SRCS) > $@ 2> /dev/null
-@$(CXX) $(DEPENDFLAGS) -MM $(OBJS:.o=.cpp) > $@ 2> /dev/null
-include .depend

View file

@ -0,0 +1,82 @@
#ifndef _EVALUATE_COMMON_H_
#define _EVALUATE_COMMON_H_
// いまどきの手番つき評価関数(EVAL_KPPTとEVAL_KPP_KKPT)の共用header的なもの。
#if defined(EVAL_NNUE) || defined(EVAL_LEARN)
#include <functional>
// 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::function<void(int32_t, int32_t)>f, 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<bool, 4>& 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<bool,4>& freeze);
// 評価関数パラメーターをファイルに保存する。
// ファイルの末尾につける拡張子を指定できる。
void save_eval(std::string suffix);
// 現在のetaを取得する。
double get_eta();
// -- 学習に関連したコマンド
// KKを正規化する関数。元の評価関数と完全に等価にはならないので注意。
// kkp,kppの値をなるべくゼロに近づけることで、学習中に出現しなかった特徴因子の値(ゼロになっている)が
// 妥当であることを保証しようという考え。
void regularize_kk();
#endif
}
#endif // defined(EVAL_NNUE) || defined(EVAL_LEARN)
#endif // _EVALUATE_KPPT_COMMON_H_

View file

@ -0,0 +1,190 @@
#if defined(EVAL_NNUE) || defined(EVAL_LEARN)
#include "evaluate_mir_inv_tools.h"
namespace Eval
{
// --- tables
// あるBonaPieceを相手側から見たときの値
// BONA_PIECE_INITが-1なので符号型で持つ必要がある。
// KPPTを拡張しても当面、BonaPieceが2^15を超えることはないのでint16_tで良しとする。
int16_t inv_piece_[Eval::fe_end];
// 盤面上のあるBonaPieceをミラーした位置にあるものを返す。
int16_t mir_piece_[Eval::fe_end];
// --- methods
// あるBonaPieceを相手側から見たときの値を返す
Eval::BonaPiece inv_piece(Eval::BonaPiece p) { return (Eval::BonaPiece)inv_piece_[p]; }
// 盤面上のあるBonaPieceをミラーした位置にあるものを返す。
Eval::BonaPiece mir_piece(Eval::BonaPiece p) { return (Eval::BonaPiece)mir_piece_[p]; }
std::function<void()> 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<BonaPiece> s;
vector<int> a = {
f_hand_pawn - 1,e_hand_pawn - 1,
f_hand_lance - 1, e_hand_lance - 1,
f_hand_knight - 1, e_hand_knight - 1,
f_hand_silver - 1, e_hand_silver - 1,
f_hand_gold - 1, e_hand_gold - 1,
f_hand_bishop - 1, e_hand_bishop - 1,
f_hand_rook - 1, e_hand_rook - 1,
};
for (auto b : a)
s.insert((BonaPiece)b);
// さらに出現しない升の盤上の歩、香、桂も除外(Aperyはここにもゴミが入っている)
for (Rank r = RANK_1; r <= RANK_2; ++r)
for (File f = FILE_1; f <= FILE_9; ++f)
{
if (r == RANK_1)
{
// 1段目の歩
BonaPiece b1 = BonaPiece(f_pawn + (f | r));
s.insert(b1);
s.insert(inv_piece[b1]);
// 1段目の香
BonaPiece b2 = BonaPiece(f_lance + (f | r));
s.insert(b2);
s.insert(inv_piece[b2]);
}
// 1,2段目の桂
BonaPiece b = BonaPiece(f_knight + (f | r));
s.insert(b);
s.insert(inv_piece[b]);
}
cout << "\nchecking kpp_write()..";
for (auto sq : SQ)
{
cout << sq << ' ';
for (BonaPiece p1 = BONA_PIECE_ZERO; p1 < fe_end; ++p1)
for (BonaPiece p2 = BONA_PIECE_ZERO; p2 < fe_end; ++p2)
if (!s.count(p1) && !s.count(p2))
kpp_write(sq, p1, p2, kpp[sq][p1][p2]);
}
cout << "\nchecking kkp_write()..";
for (auto sq1 : SQ)
{
cout << sq1 << ' ';
for (auto sq2 : SQ)
for (BonaPiece p1 = BONA_PIECE_ZERO; p1 < fe_end; ++p1)
if (!s.count(p1))
kkp_write(sq1, sq2, p1, kkp[sq1][sq2][p1]);
}
cout << "..done!" << endl;
#endif
}
}
#endif // defined(EVAL_NNUE) || defined(EVAL_LEARN)

View file

@ -0,0 +1,47 @@
#ifndef _EVALUATE_MIR_INV_TOOLS_
#define _EVALUATE_MIR_INV_TOOLS_
#if defined(EVAL_NNUE) || defined(EVAL_LEARN)
// BonaPieceのmirror(左右反転)やinverse(盤上の180度回転)させた駒を得るためのツール類。
#include "../types.h"
#include "../evaluate.h"
#include <functional>
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<void()> mir_piece_init_function;
extern int16_t mir_piece_[Eval::fe_end];
extern int16_t inv_piece_[Eval::fe_end];
// この関数を明示的に呼び出すか、init()を呼び出すかしたときに、上のテーブルが初期化される。
extern void init_mir_inv_tables();
}
#endif // defined(EVAL_NNUE) || defined(EVAL_LEARN)
#endif

View file

@ -0,0 +1,38 @@
// NNUE評価関数で用いる入力特徴量とネットワーク構造の定義
#include "../features/feature_set.h"
#include "../features/half_kp.h"
#include "../features/castling_right.h"
#include "../features/enpassant.h"
#include "../layers/input_slice.h"
#include "../layers/affine_transform.h"
#include "../layers/clipped_relu.h"
namespace Eval {
namespace NNUE {
// 評価関数で用いる入力特徴量
using RawFeatures = Features::FeatureSet<
Features::HalfKP<Features::Side::kFriend>, Features::CastlingRight,
Features::EnPassant>;
// 変換後の入力特徴量の次元数
constexpr IndexType kTransformedFeatureDimensions = 256;
namespace Layers {
// ネットワーク構造の定義
using InputLayer = InputSlice<kTransformedFeatureDimensions * 2>;
using HiddenLayer1 = ClippedReLU<AffineTransform<InputLayer, 32>>;
using HiddenLayer2 = ClippedReLU<AffineTransform<HiddenLayer1, 32>>;
using OutputLayer = AffineTransform<HiddenLayer2, 1>;
} // namespace Layers
using Network = Layers::OutputLayer;
} // namespace NNUE
} // namespace Eval

View file

@ -0,0 +1,35 @@
// NNUE評価関数で用いる入力特徴量とネットワーク構造の定義
#include "../features/feature_set.h"
#include "../features/half_kp.h"
#include "../layers/input_slice.h"
#include "../layers/affine_transform.h"
#include "../layers/clipped_relu.h"
namespace Eval {
namespace NNUE {
// 評価関数で用いる入力特徴量
using RawFeatures = Features::FeatureSet<
Features::HalfKP<Features::Side::kFriend>>;
// 変換後の入力特徴量の次元数
constexpr IndexType kTransformedFeatureDimensions = 256;
namespace Layers {
// ネットワーク構造の定義
using InputLayer = InputSlice<kTransformedFeatureDimensions * 2>;
using HiddenLayer1 = ClippedReLU<AffineTransform<InputLayer, 32>>;
using HiddenLayer2 = ClippedReLU<AffineTransform<HiddenLayer1, 32>>;
using OutputLayer = AffineTransform<HiddenLayer2, 1>;
} // namespace Layers
using Network = Layers::OutputLayer;
} // namespace NNUE
} // namespace Eval

View file

@ -0,0 +1,38 @@
// NNUE評価関数で用いる入力特徴量とネットワーク構造の定義
#include "../features/feature_set.h"
#include "../features/k.h"
#include "../features/p.h"
#include "../features/castling_right.h"
#include "../features/enpassant.h"
#include "../layers/input_slice.h"
#include "../layers/affine_transform.h"
#include "../layers/clipped_relu.h"
namespace Eval {
namespace NNUE {
// 評価関数で用いる入力特徴量
using RawFeatures = Features::FeatureSet<Features::K, Features::P,
Features::CastlingRight, Features::EnPassant>;
// 変換後の入力特徴量の次元数
constexpr IndexType kTransformedFeatureDimensions = 256;
namespace Layers {
// ネットワーク構造の定義
using InputLayer = InputSlice<kTransformedFeatureDimensions * 2>;
using HiddenLayer1 = ClippedReLU<AffineTransform<InputLayer, 32>>;
using HiddenLayer2 = ClippedReLU<AffineTransform<HiddenLayer1, 32>>;
using OutputLayer = AffineTransform<HiddenLayer2, 1>;
} // namespace Layers
using Network = Layers::OutputLayer;
} // namespace NNUE
} // namespace Eval

View file

@ -0,0 +1,37 @@
// NNUE評価関数で用いる入力特徴量とネットワーク構造の定義
#include "../features/feature_set.h"
#include "../features/k.h"
#include "../features/p.h"
#include "../features/castling_right.h"
#include "../layers/input_slice.h"
#include "../layers/affine_transform.h"
#include "../layers/clipped_relu.h"
namespace Eval {
namespace NNUE {
// 評価関数で用いる入力特徴量
using RawFeatures = Features::FeatureSet<Features::K, Features::P,
Features::CastlingRight>;
// 変換後の入力特徴量の次元数
constexpr IndexType kTransformedFeatureDimensions = 256;
namespace Layers {
// ネットワーク構造の定義
using InputLayer = InputSlice<kTransformedFeatureDimensions * 2>;
using HiddenLayer1 = ClippedReLU<AffineTransform<InputLayer, 32>>;
using HiddenLayer2 = ClippedReLU<AffineTransform<HiddenLayer1, 32>>;
using OutputLayer = AffineTransform<HiddenLayer2, 1>;
} // namespace Layers
using Network = Layers::OutputLayer;
} // namespace NNUE
} // namespace Eval

View file

@ -0,0 +1,35 @@
// NNUE評価関数で用いる入力特徴量とネットワーク構造の定義
#include "../features/feature_set.h"
#include "../features/k.h"
#include "../features/p.h"
#include "../layers/input_slice.h"
#include "../layers/affine_transform.h"
#include "../layers/clipped_relu.h"
namespace Eval {
namespace NNUE {
// 評価関数で用いる入力特徴量
using RawFeatures = Features::FeatureSet<Features::K, Features::P>;
// 変換後の入力特徴量の次元数
constexpr IndexType kTransformedFeatureDimensions = 256;
namespace Layers {
// ネットワーク構造の定義
using InputLayer = InputSlice<kTransformedFeatureDimensions * 2>;
using HiddenLayer1 = ClippedReLU<AffineTransform<InputLayer, 32>>;
using HiddenLayer2 = ClippedReLU<AffineTransform<HiddenLayer1, 32>>;
using OutputLayer = AffineTransform<HiddenLayer2, 1>;
} // namespace Layers
using Network = Layers::OutputLayer;
} // namespace NNUE
} // namespace Eval

View file

@ -0,0 +1,322 @@
// NNUE評価関数の計算に関するコード
#if defined(EVAL_NNUE)
#include <fstream>
#include <iostream>
#include "../../evaluate.h"
#include "../../position.h"
#include "../../misc.h"
#include "../../uci.h"
#include "evaluate_nnue.h"
namespace Eval {
namespace NNUE {
// 入力特徴量変換器
AlignedPtr<FeatureTransformer> feature_transformer;
// 評価関数
AlignedPtr<Network> network;
// 評価関数ファイル名
const char* const kFileName = "nn.bin";
// 評価関数の構造を表す文字列を取得する
std::string GetArchitectureString() {
return "Features=" + FeatureTransformer::GetStructureString() +
",Network=" + Network::GetStructureString();
}
namespace {
namespace Detail {
// 評価関数パラメータを初期化する
template <typename T>
void Initialize(AlignedPtr<T>& pointer) {
pointer.reset(reinterpret_cast<T*>(aligned_malloc(sizeof(T), alignof(T))));
std::memset(pointer.get(), 0, sizeof(T));
}
// 評価関数パラメータを読み込む
template <typename T>
bool ReadParameters(std::istream& stream, const AlignedPtr<T>& pointer) {
std::uint32_t header;
stream.read(reinterpret_cast<char*>(&header), sizeof(header));
if (!stream || header != T::GetHashValue()) return false;
return pointer->ReadParameters(stream);
}
// 評価関数パラメータを書き込む
template <typename T>
bool WriteParameters(std::ostream& stream, const AlignedPtr<T>& pointer) {
constexpr std::uint32_t header = T::GetHashValue();
stream.write(reinterpret_cast<const char*>(&header), sizeof(header));
return pointer->WriteParameters(stream);
}
} // namespace Detail
// 評価関数パラメータを初期化する
void Initialize() {
Detail::Initialize(feature_transformer);
Detail::Initialize(network);
}
} // namespace
// ヘッダを読み込む
bool ReadHeader(std::istream& stream,
std::uint32_t* hash_value, std::string* architecture) {
std::uint32_t version, size;
stream.read(reinterpret_cast<char*>(&version), sizeof(version));
stream.read(reinterpret_cast<char*>(hash_value), sizeof(*hash_value));
stream.read(reinterpret_cast<char*>(&size), sizeof(size));
if (!stream || version != kVersion) return false;
architecture->resize(size);
stream.read(&(*architecture)[0], size);
return !stream.fail();
}
// ヘッダを書き込む
bool WriteHeader(std::ostream& stream,
std::uint32_t hash_value, const std::string& architecture) {
stream.write(reinterpret_cast<const char*>(&kVersion), sizeof(kVersion));
stream.write(reinterpret_cast<const char*>(&hash_value), sizeof(hash_value));
const std::uint32_t size = static_cast<std::uint32_t>(architecture.size());
stream.write(reinterpret_cast<const char*>(&size), sizeof(size));
stream.write(architecture.data(), size);
return !stream.fail();
}
// 評価関数パラメータを読み込む
bool ReadParameters(std::istream& stream) {
std::uint32_t hash_value;
std::string architecture;
if (!ReadHeader(stream, &hash_value, &architecture)) return false;
if (hash_value != kHashValue) return false;
if (!Detail::ReadParameters(stream, feature_transformer)) return false;
if (!Detail::ReadParameters(stream, network)) return false;
return stream && stream.peek() == std::ios::traits_type::eof();
}
// 評価関数パラメータを書き込む
bool WriteParameters(std::ostream& stream) {
if (!WriteHeader(stream, kHashValue, GetArchitectureString())) return false;
if (!Detail::WriteParameters(stream, feature_transformer)) return false;
if (!Detail::WriteParameters(stream, network)) return false;
return !stream.fail();
}
// 差分計算ができるなら進める
static void UpdateAccumulatorIfPossible(const Position& pos) {
feature_transformer->UpdateAccumulatorIfPossible(pos);
}
// 評価値を計算する
static Value ComputeScore(const Position& pos, bool refresh = false) {
auto& accumulator = pos.state()->accumulator;
if (!refresh && accumulator.computed_score) {
return accumulator.score;
}
alignas(kCacheLineSize) TransformedFeatureType
transformed_features[FeatureTransformer::kBufferSize];
feature_transformer->Transform(pos, transformed_features, refresh);
alignas(kCacheLineSize) char buffer[Network::kBufferSize];
const auto output = network->Propagate(transformed_features, buffer);
// VALUE_MAX_EVALより大きな値が返ってくるとaspiration searchがfail highして
// 探索が終わらなくなるのでVALUE_MAX_EVAL以下であることを保証すべき。
// この現象が起きても、対局時に秒固定などだとそこで探索が打ち切られるので、
// 1つ前のiterationのときの最善手がbestmoveとして指されるので見かけ上、
// 問題ない。このVALUE_MAX_EVALが返ってくるような状況は、ほぼ詰みの局面であり、
// そのような詰みの局面が出現するのは終盤で形勢に大差がついていることが多いので
// 勝敗にはあまり影響しない。
// しかし、教師生成時などdepth固定で探索するときに探索から戻ってこなくなるので
// そのスレッドの計算時間を無駄にする。またdepth固定対局でtime-outするようになる。
auto score = static_cast<Value>(output[0] / FV_SCALE);
// 1) ここ、下手にclipすると学習時には影響があるような気もするが…。
// 2) accumulator.scoreは、差分計算の時に用いないので書き換えて問題ない。
score = Math::clamp(score , -VALUE_MAX_EVAL , VALUE_MAX_EVAL);
accumulator.score = score;
accumulator.computed_score = true;
return accumulator.score;
}
} // namespace NNUE
#if defined(USE_EVAL_HASH)
// HashTableに評価値を保存するために利用するクラス
struct alignas(16) ScoreKeyValue {
#if defined(USE_SSE2)
ScoreKeyValue() = default;
ScoreKeyValue(const ScoreKeyValue& other) {
static_assert(sizeof(ScoreKeyValue) == sizeof(__m128i),
"sizeof(ScoreKeyValue) should be equal to sizeof(__m128i)");
_mm_store_si128(&as_m128i, other.as_m128i);
}
ScoreKeyValue& operator=(const ScoreKeyValue& other) {
_mm_store_si128(&as_m128i, other.as_m128i);
return *this;
}
#endif
// evaluate hashでatomicに操作できる必要があるのでそのための操作子
void encode() {
#if defined(USE_SSE2)
// ScoreKeyValue は atomic にコピーされるので key が合っていればデータも合っている。
#else
key ^= score;
#endif
}
// decode()はencode()の逆変換だが、xorなので逆変換も同じ変換。
void decode() { encode(); }
union {
struct {
std::uint64_t key;
std::uint64_t score;
};
#if defined(USE_SSE2)
__m128i as_m128i;
#endif
};
};
// シンプルなHashTableの実装。
// Sizeは2のべき乗。
template <typename T, size_t Size>
struct HashTable {
HashTable() { clear(); }
T* operator [] (const Key k) { return entries_ + (static_cast<size_t>(k) & (Size - 1)); }
void clear() { memset(entries_, 0, sizeof(T)*Size); }
// Size が 2のべき乗であることのチェック
static_assert((Size & (Size - 1)) == 0, "");
private:
T entries_[Size];
};
// evaluateしたものを保存しておくHashTable(俗にいうehash)
#if !defined(USE_LARGE_EVAL_HASH)
// 134MB(魔女のAVX2以外の時の設定)
struct EvaluateHashTable : HashTable<ScoreKeyValue, 0x800000> {};
#else
// prefetch有りなら大きいほうが良いのでは…。
// → あまり変わらないし、メモリもったいないのでデフォルトでは↑の設定で良いか…。
// 1GB(魔女のAVX2の時の設定)
struct EvaluateHashTable : HashTable<ScoreKeyValue, 0x4000000> {};
#endif
EvaluateHashTable g_evalTable;
// prefetchする関数も用意しておく。
void prefetch_evalhash(const Key key) {
constexpr auto mask = ~((uint64_t)0x1f);
prefetch((void*)((uint64_t)g_evalTable[key] & mask));
}
#endif
// 評価関数ファイルを読み込む
// benchコマンドなどでOptionsを保存して復元するのでこのときEvalDirが変更されたことになって、
// 評価関数の再読込の必要があるというフラグを立てるため、この関数は2度呼び出されることがある。
void load_eval() {
NNUE::Initialize();
if (!Options["SkipLoadingEval"])
{
const std::string dir_name = Options["EvalDir"];
const std::string file_name = Path::Combine(dir_name, NNUE::kFileName);
//{
// std::ofstream stream(file_name, std::ios::binary);
// NNUE::WriteParameters(stream);
//}
std::ifstream stream(file_name, std::ios::binary);
const bool result = NNUE::ReadParameters(stream);
// ASSERT(result);
if (!result)
{
// 読み込みエラーのとき終了してくれないと困る。
std::cout << "Error! : failed to read " << NNUE::kFileName << std::endl;
my_exit();
}
}
}
// 初期化
void init() {
}
// 評価関数。差分計算ではなく全計算する。
// Position::set()で一度だけ呼び出される。(以降は差分計算)
// 手番側から見た評価値を返すので注意。(他の評価関数とは設計がこの点において異なる)
// なので、この関数の最適化は頑張らない。
Value compute_eval(const Position& pos) {
return NNUE::ComputeScore(pos, true);
}
// 評価関数
Value evaluate(const Position& pos) {
const auto& accumulator = pos.state()->accumulator;
if (accumulator.computed_score) {
return accumulator.score;
}
#if defined(USE_GLOBAL_OPTIONS)
// GlobalOptionsでeval hashを用いない設定になっているなら
// eval hashへの照会をskipする。
if (!GlobalOptions.use_eval_hash) {
ASSERT_LV5(pos.state()->materialValue == Eval::material(pos));
return NNUE::ComputeScore(pos);
}
#endif
#if defined(USE_EVAL_HASH)
// evaluate hash tableにはあるかも。
const Key key = pos.key();
ScoreKeyValue entry = *g_evalTable[key];
entry.decode();
if (entry.key == key) {
// あった!
return Value(entry.score);
}
#endif
Value score = NNUE::ComputeScore(pos);
#if defined(USE_EVAL_HASH)
// せっかく計算したのでevaluate hash tableに保存しておく。
entry.key = key;
entry.score = score;
entry.encode();
*g_evalTable[key] = entry;
#endif
return score;
}
// 差分計算ができるなら進める
void evaluate_with_no_return(const Position& pos) {
NNUE::UpdateAccumulatorIfPossible(pos);
}
// 現在の局面の評価値の内訳を表示する
void print_eval_stat(Position& /*pos*/) {
std::cout << "--- EVAL STAT: not implemented" << std::endl;
}
} // namespace Eval
#endif // defined(EVAL_NNUE)

View file

@ -0,0 +1,64 @@
// NNUE評価関数で用いるheader
#ifndef _EVALUATE_NNUE_H_
#define _EVALUATE_NNUE_H_
#if defined(EVAL_NNUE)
#include "nnue_feature_transformer.h"
#include "nnue_architecture.h"
#include <memory>
namespace Eval {
namespace NNUE {
// 評価関数の構造のハッシュ値
constexpr std::uint32_t kHashValue =
FeatureTransformer::GetHashValue() ^ Network::GetHashValue();
// メモリ領域の解放を自動化するためのデリータ
template <typename T>
struct AlignedDeleter {
void operator()(T* ptr) const {
ptr->~T();
aligned_free(ptr);
}
};
template <typename T>
using AlignedPtr = std::unique_ptr<T, AlignedDeleter<T>>;
// 入力特徴量変換器
extern AlignedPtr<FeatureTransformer> feature_transformer;
// 評価関数
extern AlignedPtr<Network> network;
// 評価関数ファイル名
extern const char* const kFileName;
// 評価関数の構造を表す文字列を取得する
std::string GetArchitectureString();
// ヘッダを読み込む
bool ReadHeader(std::istream& stream,
std::uint32_t* hash_value, std::string* architecture);
// ヘッダを書き込む
bool WriteHeader(std::ostream& stream,
std::uint32_t hash_value, const std::string& architecture);
// 評価関数パラメータを読み込む
bool ReadParameters(std::istream& stream);
// 評価関数パラメータを書き込む
bool WriteParameters(std::ostream& stream);
} // namespace NNUE
} // namespace Eval
#endif // defined(EVAL_NNUE)
#endif

View file

@ -0,0 +1,231 @@
// NNUE評価関数の学習時用のコード
#if defined(EVAL_LEARN) && defined(EVAL_NNUE)
#include <random>
#include <fstream>
#include "../../learn/learn.h"
#include "../../learn/learning_tools.h"
#include "../../position.h"
#include "../../uci.h"
#include "../../misc.h"
#include "../../thread_win32_osx.h"
#include "../evaluate_common.h"
#include "evaluate_nnue.h"
#include "evaluate_nnue_learner.h"
#include "trainer/features/factorizer_feature_set.h"
#include "trainer/features/factorizer_half_kp.h"
#include "trainer/trainer_feature_transformer.h"
#include "trainer/trainer_input_slice.h"
#include "trainer/trainer_affine_transform.h"
#include "trainer/trainer_clipped_relu.h"
#include "trainer/trainer_sum.h"
namespace Eval {
namespace NNUE {
namespace {
// 学習データ
std::vector<Example> examples;
// examplesの排他制御をするMutex
std::mutex examples_mutex;
// ミニバッチのサンプル数
uint64_t batch_size;
// 乱数生成器
std::mt19937 rng;
// 学習器
std::shared_ptr<Trainer<Network>> trainer;
// 学習率のスケール
double global_learning_rate_scale;
// 学習率のスケールを取得する
double GetGlobalLearningRateScale() {
return global_learning_rate_scale;
}
// ハイパーパラメータなどのオプションを学習器に伝える
void SendMessages(std::vector<Message> messages) {
for (auto& message : messages) {
trainer->SendMessage(&message);
assert(message.num_receivers > 0);
}
}
} // namespace
// 学習の初期化を行う
void InitializeTraining(double eta1, uint64_t eta1_epoch,
double eta2, uint64_t eta2_epoch, double eta3) {
std::cout << "Initializing NN training for "
<< GetArchitectureString() << std::endl;
assert(feature_transformer);
assert(network);
trainer = Trainer<Network>::Create(network.get(), feature_transformer.get());
if (Options["SkipLoadingEval"]) {
trainer->Initialize(rng);
}
global_learning_rate_scale = 1.0;
EvalLearningTools::Weight::init_eta(eta1, eta2, eta3, eta1_epoch, eta2_epoch);
}
// ミニバッチのサンプル数を設定する
void SetBatchSize(uint64_t size) {
assert(size > 0);
batch_size = size;
}
// 学習率のスケールを設定する
void SetGlobalLearningRateScale(double scale) {
global_learning_rate_scale = scale;
}
// ハイパーパラメータなどのオプションを設定する
void SetOptions(const std::string& options) {
std::vector<Message> messages;
for (const auto& option : Split(options, ',')) {
const auto fields = Split(option, '=');
assert(fields.size() == 1 || fields.size() == 2);
if (fields.size() == 1) {
messages.emplace_back(fields[0]);
} else {
messages.emplace_back(fields[0], fields[1]);
}
}
SendMessages(std::move(messages));
}
// 学習用評価関数パラメータをファイルから読み直す
void RestoreParameters(const std::string& dir_name) {
const std::string file_name = Path::Combine(dir_name, NNUE::kFileName);
std::ifstream stream(file_name, std::ios::binary);
bool result = ReadParameters(stream);
assert(result);
SendMessages({{"reset"}});
}
// 学習データを1サンプル追加する
void AddExample(Position& pos, Color rootColor,
const Learner::PackedSfenValue& psv, double weight) {
Example example;
if (rootColor == pos.side_to_move()) {
example.sign = 1;
} else {
example.sign = -1;
}
example.psv = psv;
example.weight = weight;
Features::IndexList active_indices[2];
for (const auto trigger : kRefreshTriggers) {
RawFeatures::AppendActiveIndices(pos, trigger, active_indices);
}
if (pos.side_to_move() != WHITE) {
active_indices[0].swap(active_indices[1]);
}
for (const auto color : Colors) {
std::vector<TrainingFeature> training_features;
for (const auto base_index : active_indices[color]) {
static_assert(Features::Factorizer<RawFeatures>::GetDimensions() <
(1 << TrainingFeature::kIndexBits), "");
Features::Factorizer<RawFeatures>::AppendTrainingFeatures(
base_index, &training_features);
}
std::sort(training_features.begin(), training_features.end());
auto& unique_features = example.training_features[color];
for (const auto& feature : training_features) {
if (!unique_features.empty() &&
feature.GetIndex() == unique_features.back().GetIndex()) {
unique_features.back() += feature;
} else {
unique_features.push_back(feature);
}
}
}
std::lock_guard<std::mutex> lock(examples_mutex);
examples.push_back(std::move(example));
}
// 評価関数パラメーターを更新する
void UpdateParameters(uint64_t epoch) {
assert(batch_size > 0);
EvalLearningTools::Weight::calc_eta(epoch);
const auto learning_rate = static_cast<LearnFloatType>(
get_eta() / batch_size);
std::lock_guard<std::mutex> lock(examples_mutex);
std::shuffle(examples.begin(), examples.end(), rng);
while (examples.size() >= batch_size) {
std::vector<Example> batch(examples.end() - batch_size, examples.end());
examples.resize(examples.size() - batch_size);
const auto network_output = trainer->Propagate(batch);
std::vector<LearnFloatType> gradients(batch.size());
for (std::size_t b = 0; b < batch.size(); ++b) {
const auto shallow = static_cast<Value>(Round<std::int32_t>(
batch[b].sign * network_output[b] * kPonanzaConstant));
const auto& psv = batch[b].psv;
const double gradient = batch[b].sign * Learner::calc_grad(shallow, psv);
gradients[b] = static_cast<LearnFloatType>(gradient * batch[b].weight);
}
trainer->Backpropagate(gradients.data(), learning_rate);
}
SendMessages({{"quantize_parameters"}});
}
// 学習に問題が生じていないかチェックする
void CheckHealth() {
SendMessages({{"check_health"}});
}
} // namespace NNUE
// 評価関数パラメーターをファイルに保存する
void save_eval(std::string dir_name) {
auto eval_dir = Path::Combine(Options["EvalSaveDir"], dir_name);
std::cout << "save_eval() start. folder = " << eval_dir << std::endl;
// すでにこのフォルダがあるならmkdir()に失敗するが、
// 別にそれは構わない。なければ作って欲しいだけ。
// また、EvalSaveDirまでのフォルダは掘ってあるものとする。
Dependency::mkdir(eval_dir);
if (Options["SkipLoadingEval"] && NNUE::trainer) {
NNUE::SendMessages({{"clear_unobserved_feature_weights"}});
}
const std::string file_name = Path::Combine(eval_dir, NNUE::kFileName);
std::ofstream stream(file_name, std::ios::binary);
const bool result = NNUE::WriteParameters(stream);
assert(result);
std::cout << "save_eval() finished. folder = " << eval_dir << std::endl;
}
// 現在のetaを取得する
double get_eta() {
return NNUE::GetGlobalLearningRateScale() * EvalLearningTools::Weight::eta;
}
} // namespace Eval
#endif // defined(EVAL_LEARN) && defined(EVAL_NNUE)

View file

@ -0,0 +1,46 @@
// NNUE評価関数の学習で用いるインターフェイス
#ifndef _EVALUATE_NNUE_LEARNER_H_
#define _EVALUATE_NNUE_LEARNER_H_
#if defined(EVAL_LEARN) && defined(EVAL_NNUE)
#include "../../learn/learn.h"
namespace Eval {
namespace NNUE {
// 学習の初期化を行う
void InitializeTraining(double eta1, uint64_t eta1_epoch,
double eta2, uint64_t eta2_epoch, double eta3);
// ミニバッチのサンプル数を設定する
void SetBatchSize(uint64_t size);
// 学習率のスケールを設定する
void SetGlobalLearningRateScale(double scale);
// ハイパーパラメータなどのオプションを設定する
void SetOptions(const std::string& options);
// 学習用評価関数パラメータをファイルから読み直す
void RestoreParameters(const std::string& dir_name);
// 学習データを1サンプル追加する
void AddExample(Position& pos, Color rootColor,
const Learner::PackedSfenValue& psv, double weight);
// 評価関数パラメータを更新する
void UpdateParameters(uint64_t epoch);
// 学習に問題が生じていないかチェックする
void CheckHealth();
} // namespace NNUE
} // namespace Eval
#endif // defined(EVAL_LEARN) && defined(EVAL_NNUE)
#endif

View file

@ -0,0 +1,73 @@
// NNUE評価関数の入力特徴量Kの定義
#if defined(EVAL_NNUE)
#include "castling_right.h"
#include "index_list.h"
namespace Eval {
namespace NNUE {
namespace Features {
// 特徴量のうち、値が1であるインデックスのリストを取得する
void CastlingRight::AppendActiveIndices(
const Position& pos, Color perspective, IndexList* active) {
// コンパイラの警告を回避するため、配列サイズが小さい場合は何もしない
if (RawFeatures::kMaxActiveDimensions < kMaxActiveDimensions) return;
int castling_rights = pos.state()->castlingRights;
int relative_castling_rights;
if (perspective == WHITE) {
relative_castling_rights = castling_rights;
}
else {
// Invert the perspective.
relative_castling_rights = ((castling_rights & 3) << 2)
& ((castling_rights >> 2) & 3);
}
for (int i = 0; i < kDimensions; ++i) {
if (relative_castling_rights & (i << 1)) {
active->push_back(i);
}
}
}
// 特徴量のうち、一手前から値が変化したインデックスのリストを取得する
void CastlingRight::AppendChangedIndices(
const Position& pos, Color perspective,
IndexList* removed, IndexList* added) {
int previous_castling_rights = pos.state()->previous->castlingRights;
int current_castling_rights = pos.state()->castlingRights;
int relative_previous_castling_rights;
int relative_current_castling_rights;
if (perspective == WHITE) {
relative_previous_castling_rights = previous_castling_rights;
relative_current_castling_rights = current_castling_rights;
}
else {
// Invert the perspective.
relative_previous_castling_rights = ((previous_castling_rights & 3) << 2)
& ((previous_castling_rights >> 2) & 3);
relative_current_castling_rights = ((current_castling_rights & 3) << 2)
& ((current_castling_rights >> 2) & 3);
}
for (int i = 0; i < kDimensions; ++i) {
if ((relative_previous_castling_rights & (i << 1)) &&
(relative_current_castling_rights & (i << 1)) == 0) {
removed->push_back(i);
}
}
}
} // namespace Features
} // namespace NNUE
} // namespace Eval
#endif // defined(EVAL_NNUE)

View file

@ -0,0 +1,48 @@
// NNUE評価関数の入力特徴量Kの定義
#ifndef _NNUE_FEATURES_CASTLING_RIGHT_H_
#define _NNUE_FEATURES_CASTLING_RIGHT_H_
#if defined(EVAL_NNUE)
#include "../../../evaluate.h"
#include "features_common.h"
namespace Eval {
namespace NNUE {
namespace Features {
// 特徴量K玉の位置
class CastlingRight {
public:
// 特徴量名
static constexpr const char* kName = "CastlingRight";
// 評価関数ファイルに埋め込むハッシュ値
static constexpr std::uint32_t kHashValue = 0x913968AAu;
// 特徴量の次元数
static constexpr IndexType kDimensions = 4;
// 特徴量のうち、同時に値が1となるインデックスの数の最大値
static constexpr IndexType kMaxActiveDimensions = 4;
// 差分計算の代わりに全計算を行うタイミング
static constexpr TriggerEvent kRefreshTrigger = TriggerEvent::kNone;
// 特徴量のうち、値が1であるインデックスのリストを取得する
static void AppendActiveIndices(const Position& pos, Color perspective,
IndexList* active);
// 特徴量のうち、一手前から値が変化したインデックスのリストを取得する
static void AppendChangedIndices(const Position& pos, Color perspective,
IndexList* removed, IndexList* added);
};
} // namespace Features
} // namespace NNUE
} // namespace Eval
#endif // defined(EVAL_NNUE)
#endif

View file

@ -0,0 +1,47 @@
// NNUE評価関数の入力特徴量Kの定義
#if defined(EVAL_NNUE)
#include "enpassant.h"
#include "index_list.h"
namespace Eval {
namespace NNUE {
namespace Features {
// 特徴量のうち、値が1であるインデックスのリストを取得する
void EnPassant::AppendActiveIndices(
const Position& pos, Color perspective, IndexList* active) {
// コンパイラの警告を回避するため、配列サイズが小さい場合は何もしない
if (RawFeatures::kMaxActiveDimensions < kMaxActiveDimensions) return;
auto epSquare = pos.state()->epSquare;
if (epSquare == SQ_NONE) {
return;
}
if (perspective == BLACK) {
epSquare = Inv(epSquare);
}
auto file = file_of(epSquare);
active->push_back(file);
}
// 特徴量のうち、一手前から値が変化したインデックスのリストを取得する
void EnPassant::AppendChangedIndices(
const Position& pos, Color perspective,
IndexList* removed, IndexList* added) {
// Not implemented.
assert(false);
}
} // namespace Features
} // namespace NNUE
} // namespace Eval
#endif // defined(EVAL_NNUE)

View file

@ -0,0 +1,48 @@
// NNUE評価関数の入力特徴量Kの定義
#ifndef _NNUE_FEATURES_ENPASSANT_H_
#define _NNUE_FEATURES_ENPASSANT_H_
#if defined(EVAL_NNUE)
#include "../../../evaluate.h"
#include "features_common.h"
namespace Eval {
namespace NNUE {
namespace Features {
// 特徴量K玉の位置
class EnPassant {
public:
// 特徴量名
static constexpr const char* kName = "EnPassant";
// 評価関数ファイルに埋め込むハッシュ値
static constexpr std::uint32_t kHashValue = 0x02924F91u;
// 特徴量の次元数
static constexpr IndexType kDimensions = 8;
// 特徴量のうち、同時に値が1となるインデックスの数の最大値
static constexpr IndexType kMaxActiveDimensions = 1;
// 差分計算の代わりに全計算を行うタイミング
static constexpr TriggerEvent kRefreshTrigger = TriggerEvent::kAnyPieceMoved;
// 特徴量のうち、値が1であるインデックスのリストを取得する
static void AppendActiveIndices(const Position& pos, Color perspective,
IndexList* active);
// 特徴量のうち、一手前から値が変化したインデックスのリストを取得する
static void AppendChangedIndices(const Position& pos, Color perspective,
IndexList* removed, IndexList* added);
};
} // namespace Features
} // namespace NNUE
} // namespace Eval
#endif // defined(EVAL_NNUE)
#endif

View file

@ -0,0 +1,249 @@
// NNUE評価関数の入力特徴量セットを表すクラステンプレート
#ifndef _NNUE_FEATURE_SET_H_
#define _NNUE_FEATURE_SET_H_
#if defined(EVAL_NNUE)
#include "features_common.h"
#include <array>
namespace Eval {
namespace NNUE {
namespace Features {
// 値のリストを表すクラステンプレート
template <typename T, T... Values>
struct CompileTimeList;
template <typename T, T First, T... Remaining>
struct CompileTimeList<T, First, Remaining...> {
static constexpr bool Contains(T value) {
return value == First || CompileTimeList<T, Remaining...>::Contains(value);
}
static constexpr std::array<T, sizeof...(Remaining) + 1>
kValues = {{First, Remaining...}};
};
template <typename T, T First, T... Remaining>
constexpr std::array<T, sizeof...(Remaining) + 1>
CompileTimeList<T, First, Remaining...>::kValues;
template <typename T>
struct CompileTimeList<T> {
static constexpr bool Contains(T /*value*/) {
return false;
}
static constexpr std::array<T, 0> kValues = {{}};
};
// リストの先頭への追加を行うクラステンプレート
template <typename T, typename ListType, T Value>
struct AppendToList;
template <typename T, T... Values, T AnotherValue>
struct AppendToList<T, CompileTimeList<T, Values...>, AnotherValue> {
using Result = CompileTimeList<T, AnotherValue, Values...>;
};
// ソートされた重複のないリストへの追加を行うクラステンプレート
template <typename T, typename ListType, T Value>
struct InsertToSet;
template <typename T, T First, T... Remaining, T AnotherValue>
struct InsertToSet<T, CompileTimeList<T, First, Remaining...>, AnotherValue> {
using Result = std::conditional_t<
CompileTimeList<T, First, Remaining...>::Contains(AnotherValue),
CompileTimeList<T, First, Remaining...>,
std::conditional_t<(AnotherValue < First),
CompileTimeList<T, AnotherValue, First, Remaining...>,
typename AppendToList<T, typename InsertToSet<
T, CompileTimeList<T, Remaining...>, AnotherValue>::Result,
First>::Result>>;
};
template <typename T, T Value>
struct InsertToSet<T, CompileTimeList<T>, Value> {
using Result = CompileTimeList<T, Value>;
};
// 特徴量セットの基底クラス
template <typename Derived>
class FeatureSetBase {
public:
// 特徴量のうち、値が1であるインデックスのリストを取得する
template <typename IndexListType>
static void AppendActiveIndices(
const Position& pos, TriggerEvent trigger, IndexListType active[2]) {
for (const auto perspective : Colors) {
Derived::CollectActiveIndices(
pos, trigger, perspective, &active[perspective]);
}
}
// 特徴量のうち、一手前から値が変化したインデックスのリストを取得する
template <typename PositionType, typename IndexListType>
static void AppendChangedIndices(
const PositionType& pos, TriggerEvent trigger,
IndexListType removed[2], IndexListType added[2], bool reset[2]) {
const auto& dp = pos.state()->dirtyPiece;
if (dp.dirty_num == 0) return;
for (const auto perspective : Colors) {
reset[perspective] = false;
switch (trigger) {
case TriggerEvent::kNone:
break;
case TriggerEvent::kFriendKingMoved:
reset[perspective] =
dp.pieceNo[0] == PIECE_NUMBER_KING + perspective;
break;
case TriggerEvent::kEnemyKingMoved:
reset[perspective] =
dp.pieceNo[0] == PIECE_NUMBER_KING + ~perspective;
break;
case TriggerEvent::kAnyKingMoved:
reset[perspective] = dp.pieceNo[0] >= PIECE_NUMBER_KING;
break;
case TriggerEvent::kAnyPieceMoved:
reset[perspective] = true;
break;
default:
assert(false);
break;
}
if (reset[perspective]) {
Derived::CollectActiveIndices(
pos, trigger, perspective, &added[perspective]);
} else {
Derived::CollectChangedIndices(
pos, trigger, perspective,
&removed[perspective], &added[perspective]);
}
}
}
};
// 特徴量セットを表すクラステンプレート
// 実行時の計算量を線形にするために、内部の処理はテンプレート引数の逆順に行う
template <typename FirstFeatureType, typename... RemainingFeatureTypes>
class FeatureSet<FirstFeatureType, RemainingFeatureTypes...> :
public FeatureSetBase<
FeatureSet<FirstFeatureType, RemainingFeatureTypes...>> {
private:
using Head = FirstFeatureType;
using Tail = FeatureSet<RemainingFeatureTypes...>;
public:
// 評価関数ファイルに埋め込むハッシュ値
static constexpr std::uint32_t kHashValue =
Head::kHashValue ^ (Tail::kHashValue << 1) ^ (Tail::kHashValue >> 31);
// 特徴量の次元数
static constexpr IndexType kDimensions =
Head::kDimensions + Tail::kDimensions;
// 特徴量のうち、同時に値が1となるインデックスの数の最大値
static constexpr IndexType kMaxActiveDimensions =
Head::kMaxActiveDimensions + Tail::kMaxActiveDimensions;
// 差分計算の代わりに全計算を行うタイミングのリスト
using SortedTriggerSet = typename InsertToSet<TriggerEvent,
typename Tail::SortedTriggerSet, Head::kRefreshTrigger>::Result;
static constexpr auto kRefreshTriggers = SortedTriggerSet::kValues;
// 特徴量名を取得する
static std::string GetName() {
return std::string(Head::kName) + "+" + Tail::GetName();
}
private:
// 特徴量のうち、値が1であるインデックスのリストを取得する
template <typename IndexListType>
static void CollectActiveIndices(
const Position& pos, const TriggerEvent trigger, const Color perspective,
IndexListType* const active) {
Tail::CollectActiveIndices(pos, trigger, perspective, active);
if (Head::kRefreshTrigger == trigger) {
const auto start = active->size();
Head::AppendActiveIndices(pos, perspective, active);
for (auto i = start; i < active->size(); ++i) {
(*active)[i] += Tail::kDimensions;
}
}
}
// 特徴量のうち、一手前から値が変化したインデックスのリストを取得する
template <typename IndexListType>
static void CollectChangedIndices(
const Position& pos, const TriggerEvent trigger, const Color perspective,
IndexListType* const removed, IndexListType* const added) {
Tail::CollectChangedIndices(pos, trigger, perspective, removed, added);
if (Head::kRefreshTrigger == trigger) {
const auto start_removed = removed->size();
const auto start_added = added->size();
Head::AppendChangedIndices(pos, perspective, removed, added);
for (auto i = start_removed; i < removed->size(); ++i) {
(*removed)[i] += Tail::kDimensions;
}
for (auto i = start_added; i < added->size(); ++i) {
(*added)[i] += Tail::kDimensions;
}
}
}
// 基底クラスと、自身を再帰的に利用するクラステンプレートをfriendにする
friend class FeatureSetBase<FeatureSet>;
template <typename... FeatureTypes>
friend class FeatureSet;
};
// 特徴量セットを表すクラステンプレート
// テンプレート引数が1つの場合の特殊化
template <typename FeatureType>
class FeatureSet<FeatureType> : public FeatureSetBase<FeatureSet<FeatureType>> {
public:
// 評価関数ファイルに埋め込むハッシュ値
static constexpr std::uint32_t kHashValue = FeatureType::kHashValue;
// 特徴量の次元数
static constexpr IndexType kDimensions = FeatureType::kDimensions;
// 特徴量のうち、同時に値が1となるインデックスの数の最大値
static constexpr IndexType kMaxActiveDimensions =
FeatureType::kMaxActiveDimensions;
// 差分計算の代わりに全計算を行うタイミングのリスト
using SortedTriggerSet =
CompileTimeList<TriggerEvent, FeatureType::kRefreshTrigger>;
static constexpr auto kRefreshTriggers = SortedTriggerSet::kValues;
// 特徴量名を取得する
static std::string GetName() {
return FeatureType::kName;
}
private:
// 特徴量のうち、値が1であるインデックスのリストを取得する
static void CollectActiveIndices(
const Position& pos, const TriggerEvent trigger, const Color perspective,
IndexList* const active) {
if (FeatureType::kRefreshTrigger == trigger) {
FeatureType::AppendActiveIndices(pos, perspective, active);
}
}
// 特徴量のうち、一手前から値が変化したインデックスのリストを取得する
static void CollectChangedIndices(
const Position& pos, const TriggerEvent trigger, const Color perspective,
IndexList* const removed, IndexList* const added) {
if (FeatureType::kRefreshTrigger == trigger) {
FeatureType::AppendChangedIndices(pos, perspective, removed, added);
}
}
// 基底クラスと、自身を再帰的に利用するクラステンプレートをfriendにする
friend class FeatureSetBase<FeatureSet>;
template <typename... FeatureTypes>
friend class FeatureSet;
};
} // namespace Features
} // namespace NNUE
} // namespace Eval
#endif // defined(EVAL_NNUE)
#endif

View file

@ -0,0 +1,47 @@
// NNUE評価関数の入力特徴量の共通ヘッダ
#ifndef _NNUE_FEATURES_COMMON_H_
#define _NNUE_FEATURES_COMMON_H_
#if defined(EVAL_NNUE)
#include "../../../evaluate.h"
#include "../nnue_common.h"
namespace Eval {
namespace NNUE {
namespace Features {
// インデックスリストの型
class IndexList;
// 特徴量セットを表すクラステンプレート
template <typename... FeatureTypes>
class FeatureSet;
// 差分計算の代わりに全計算を行うタイミングの種類
enum class TriggerEvent {
kNone, // 可能な場合は常に差分計算する
kFriendKingMoved, // 自玉が移動した場合に全計算する
kEnemyKingMoved, // 敵玉が移動した場合に全計算する
kAnyKingMoved, // どちらかの玉が移動した場合に全計算する
kAnyPieceMoved, // 常に全計算する
};
// 手番側or相手側
enum class Side {
kFriend, // 手番側
kEnemy, // 相手側
};
} // namespace Features
} // namespace NNUE
} // namespace Eval
#endif // defined(EVAL_NNUE)
#endif

View file

@ -0,0 +1,84 @@
// NNUE評価関数の入力特徴量HalfKPの定義
#if defined(EVAL_NNUE)
#include "half_kp.h"
#include "index_list.h"
namespace Eval {
namespace NNUE {
namespace Features {
// 玉の位置とBonaPieceから特徴量のインデックスを求める
template <Side AssociatedKing>
inline IndexType HalfKP<AssociatedKing>::MakeIndex(Square sq_k, BonaPiece p) {
return static_cast<IndexType>(fe_end) * static_cast<IndexType>(sq_k) + p;
}
// 駒の情報を取得する
template <Side AssociatedKing>
inline void HalfKP<AssociatedKing>::GetPieces(
const Position& pos, Color perspective,
BonaPiece** pieces, Square* sq_target_k) {
*pieces = (perspective == BLACK) ?
pos.eval_list()->piece_list_fb() :
pos.eval_list()->piece_list_fw();
const PieceNumber target = (AssociatedKing == Side::kFriend) ?
static_cast<PieceNumber>(PIECE_NUMBER_KING + perspective) :
static_cast<PieceNumber>(PIECE_NUMBER_KING + ~perspective);
*sq_target_k = static_cast<Square>(((*pieces)[target] - f_king) % SQUARE_NB);
}
// 特徴量のうち、値が1であるインデックスのリストを取得する
template <Side AssociatedKing>
void HalfKP<AssociatedKing>::AppendActiveIndices(
const Position& pos, Color perspective, IndexList* active) {
// コンパイラの警告を回避するため、配列サイズが小さい場合は何もしない
if (RawFeatures::kMaxActiveDimensions < kMaxActiveDimensions) return;
BonaPiece* pieces;
Square sq_target_k;
GetPieces(pos, perspective, &pieces, &sq_target_k);
for (PieceNumber i = PIECE_NUMBER_ZERO; i < PIECE_NUMBER_KING; ++i) {
if (pieces[i] != Eval::BONA_PIECE_ZERO) {
active->push_back(MakeIndex(sq_target_k, pieces[i]));
}
}
}
// 特徴量のうち、一手前から値が変化したインデックスのリストを取得する
template <Side AssociatedKing>
void HalfKP<AssociatedKing>::AppendChangedIndices(
const Position& pos, Color perspective,
IndexList* removed, IndexList* added) {
BonaPiece* pieces;
Square sq_target_k;
GetPieces(pos, perspective, &pieces, &sq_target_k);
const auto& dp = pos.state()->dirtyPiece;
for (int i = 0; i < dp.dirty_num; ++i) {
if (dp.pieceNo[i] >= PIECE_NUMBER_KING) continue;
const auto old_p = static_cast<BonaPiece>(
dp.changed_piece[i].old_piece.from[perspective]);
if (old_p != Eval::BONA_PIECE_ZERO) {
removed->push_back(MakeIndex(sq_target_k, old_p));
}
const auto new_p = static_cast<BonaPiece>(
dp.changed_piece[i].new_piece.from[perspective]);
if (new_p != Eval::BONA_PIECE_ZERO) {
added->push_back(MakeIndex(sq_target_k, new_p));
}
}
}
template class HalfKP<Side::kFriend>;
template class HalfKP<Side::kEnemy>;
} // namespace Features
} // namespace NNUE
} // namespace Eval
#endif // defined(EVAL_NNUE)

View file

@ -0,0 +1,62 @@
// NNUE評価関数の入力特徴量HalfKPの定義
#ifndef _NNUE_FEATURES_HALF_KP_H_
#define _NNUE_FEATURES_HALF_KP_H_
#if defined(EVAL_NNUE)
#include "../../../evaluate.h"
#include "features_common.h"
namespace Eval {
namespace NNUE {
namespace Features {
// 特徴量HalfKP自玉または敵玉の位置と、玉以外の駒の位置の組み合わせ
template <Side AssociatedKing>
class HalfKP {
public:
// 特徴量名
static constexpr const char* kName =
(AssociatedKing == Side::kFriend) ? "HalfKP(Friend)" : "HalfKP(Enemy)";
// 評価関数ファイルに埋め込むハッシュ値
static constexpr std::uint32_t kHashValue =
0x5D69D5B9u ^ (AssociatedKing == Side::kFriend);
// 特徴量の次元数
static constexpr IndexType kDimensions =
static_cast<IndexType>(SQUARE_NB) * static_cast<IndexType>(fe_end);
// 特徴量のうち、同時に値が1となるインデックスの数の最大値
static constexpr IndexType kMaxActiveDimensions = PIECE_NUMBER_KING;
// 差分計算の代わりに全計算を行うタイミング
static constexpr TriggerEvent kRefreshTrigger =
(AssociatedKing == Side::kFriend) ?
TriggerEvent::kFriendKingMoved : TriggerEvent::kEnemyKingMoved;
// 特徴量のうち、値が1であるインデックスのリストを取得する
static void AppendActiveIndices(const Position& pos, Color perspective,
IndexList* active);
// 特徴量のうち、一手前から値が変化したインデックスのリストを取得する
static void AppendChangedIndices(const Position& pos, Color perspective,
IndexList* removed, IndexList* added);
// 玉の位置とBonaPieceから特徴量のインデックスを求める
static IndexType MakeIndex(Square sq_k, BonaPiece p);
private:
// 駒の情報を取得する
static void GetPieces(const Position& pos, Color perspective,
BonaPiece** pieces, Square* sq_target_k);
};
} // namespace Features
} // namespace NNUE
} // namespace Eval
#endif // defined(EVAL_NNUE)
#endif

View file

@ -0,0 +1,97 @@
// NNUE評価関数の入力特徴量HalfRelativeKPの定義
#if defined(EVAL_NNUE)
#include "half_relative_kp.h"
#include "index_list.h"
namespace Eval {
namespace NNUE {
namespace Features {
// 玉の位置とBonaPieceから特徴量のインデックスを求める
template <Side AssociatedKing>
inline IndexType HalfRelativeKP<AssociatedKing>::MakeIndex(
Square sq_k, BonaPiece p) {
constexpr IndexType W = kBoardWidth;
constexpr IndexType H = kBoardHeight;
const IndexType piece_index = (p - fe_hand_end) / SQUARE_NB;
const Square sq_p = static_cast<Square>((p - fe_hand_end) % SQUARE_NB);
const IndexType relative_file = file_of(sq_p) - file_of(sq_k) + (W / 2);
const IndexType relative_rank = rank_of(sq_p) - rank_of(sq_k) + (H / 2);
return H * W * piece_index + H * relative_file + relative_rank;
}
// 駒の情報を取得する
template <Side AssociatedKing>
inline void HalfRelativeKP<AssociatedKing>::GetPieces(
const Position& pos, Color perspective,
BonaPiece** pieces, Square* sq_target_k) {
*pieces = (perspective == BLACK) ?
pos.eval_list()->piece_list_fb() :
pos.eval_list()->piece_list_fw();
const PieceNumber target = (AssociatedKing == Side::kFriend) ?
static_cast<PieceNumber>(PIECE_NUMBER_KING + perspective) :
static_cast<PieceNumber>(PIECE_NUMBER_KING + ~perspective);
*sq_target_k = static_cast<Square>(((*pieces)[target] - f_king) % SQUARE_NB);
}
// 特徴量のうち、値が1であるインデックスのリストを取得する
template <Side AssociatedKing>
void HalfRelativeKP<AssociatedKing>::AppendActiveIndices(
const Position& pos, Color perspective, IndexList* active) {
// コンパイラの警告を回避するため、配列サイズが小さい場合は何もしない
if (RawFeatures::kMaxActiveDimensions < kMaxActiveDimensions) return;
BonaPiece* pieces;
Square sq_target_k;
GetPieces(pos, perspective, &pieces, &sq_target_k);
for (PieceNumber i = PIECE_NUMBER_ZERO; i < PIECE_NUMBER_KING; ++i) {
if (pieces[i] >= fe_hand_end) {
if (pieces[i] != Eval::BONA_PIECE_ZERO) {
active->push_back(MakeIndex(sq_target_k, pieces[i]));
}
}
}
}
// 特徴量のうち、一手前から値が変化したインデックスのリストを取得する
template <Side AssociatedKing>
void HalfRelativeKP<AssociatedKing>::AppendChangedIndices(
const Position& pos, Color perspective,
IndexList* removed, IndexList* added) {
BonaPiece* pieces;
Square sq_target_k;
GetPieces(pos, perspective, &pieces, &sq_target_k);
const auto& dp = pos.state()->dirtyPiece;
for (int i = 0; i < dp.dirty_num; ++i) {
if (dp.pieceNo[i] >= PIECE_NUMBER_KING) continue;
const auto old_p = static_cast<BonaPiece>(
dp.changed_piece[i].old_piece.from[perspective]);
if (old_p >= fe_hand_end) {
if (old_p != Eval::BONA_PIECE_ZERO) {
removed->push_back(MakeIndex(sq_target_k, old_p));
}
}
const auto new_p = static_cast<BonaPiece>(
dp.changed_piece[i].new_piece.from[perspective]);
if (new_p >= fe_hand_end) {
if (new_p != Eval::BONA_PIECE_ZERO) {
added->push_back(MakeIndex(sq_target_k, new_p));
}
}
}
}
template class HalfRelativeKP<Side::kFriend>;
template class HalfRelativeKP<Side::kEnemy>;
} // namespace Features
} // namespace NNUE
} // namespace Eval
#endif // defined(EVAL_NNUE)

View file

@ -0,0 +1,68 @@
// NNUE評価関数の入力特徴量HalfRelativeKPの定義
#ifndef _NNUE_FEATURES_HALF_RELATIVE_KP_H_
#define _NNUE_FEATURES_HALF_RELATIVE_KP_H_
#if defined(EVAL_NNUE)
#include "../../../evaluate.h"
#include "features_common.h"
namespace Eval {
namespace NNUE {
namespace Features {
// 特徴量HalfRelativeKP自玉または敵玉を基準とした、玉以外の各駒の相対位置
template <Side AssociatedKing>
class HalfRelativeKP {
public:
// 特徴量名
static constexpr const char* kName = (AssociatedKing == Side::kFriend) ?
"HalfRelativeKP(Friend)" : "HalfRelativeKP(Enemy)";
// 評価関数ファイルに埋め込むハッシュ値
static constexpr std::uint32_t kHashValue =
0xF9180919u ^ (AssociatedKing == Side::kFriend);
// 玉を除いた駒種
static constexpr IndexType kNumPieceKinds = (fe_end - fe_hand_end) / SQUARE_NB;
// 玉を中央に置いた仮想的な盤の幅
static constexpr IndexType kBoardWidth = FILE_NB * 2 - 1;
// 玉を中央に置いた仮想的な盤の高さ
static constexpr IndexType kBoardHeight = RANK_NB * 2 - 1;
// 特徴量の次元数
static constexpr IndexType kDimensions =
kNumPieceKinds * kBoardHeight * kBoardWidth;
// 特徴量のうち、同時に値が1となるインデックスの数の最大値
static constexpr IndexType kMaxActiveDimensions = PIECE_NUMBER_KING;
// 差分計算の代わりに全計算を行うタイミング
static constexpr TriggerEvent kRefreshTrigger =
(AssociatedKing == Side::kFriend) ?
TriggerEvent::kFriendKingMoved : TriggerEvent::kEnemyKingMoved;
// 特徴量のうち、値が1であるインデックスのリストを取得する
static void AppendActiveIndices(const Position& pos, Color perspective,
IndexList* active);
// 特徴量のうち、一手前から値が変化したインデックスのリストを取得する
static void AppendChangedIndices(const Position& pos, Color perspective,
IndexList* removed, IndexList* added);
// 玉の位置とBonaPieceから特徴量のインデックスを求める
static IndexType MakeIndex(Square sq_k, BonaPiece p);
private:
// 駒の情報を取得する
static void GetPieces(const Position& pos, Color perspective,
BonaPiece** pieces, Square* sq_target_k);
};
} // namespace Features
} // namespace NNUE
} // namespace Eval
#endif // defined(EVAL_NNUE)
#endif

View file

@ -0,0 +1,55 @@
// 入力特徴量のインデックスリストの定義
#ifndef _NNUE_FEATURES_INDEX_LIST_H_
#define _NNUE_FEATURES_INDEX_LIST_H_
#if defined(EVAL_NNUE)
#include "../../../position.h"
#include "../nnue_architecture.h"
namespace Eval {
namespace NNUE {
namespace Features {
// 特徴量のインデックスリストに使うクラステンプレート
template <typename T, std::size_t MaxSize>
class ValueList {
public:
std::size_t size() const { return size_; }
void resize(std::size_t size) { size_ = size; }
void push_back(const T& value) { values_[size_++] = value; }
T& operator[](std::size_t index) { return values_[index]; }
T* begin() { return values_; }
T* end() { return values_ + size_; }
const T& operator[](std::size_t index) const { return values_[index]; }
const T* begin() const { return values_; }
const T* end() const { return values_ + size_; }
void swap(ValueList& other) {
const std::size_t max_size = std::max(size_, other.size_);
for (std::size_t i = 0; i < max_size; ++i) {
std::swap(values_[i], other.values_[i]);
}
std::swap(size_, other.size_);
}
private:
T values_[MaxSize];
std::size_t size_ = 0;
};
// 特徴量のインデックスリストの型
class IndexList
: public ValueList<IndexType, RawFeatures::kMaxActiveDimensions> {
};
} // namespace Features
} // namespace NNUE
} // namespace Eval
#endif // defined(EVAL_NNUE)
#endif

View file

@ -0,0 +1,49 @@
// NNUE評価関数の入力特徴量Kの定義
#if defined(EVAL_NNUE)
#include "k.h"
#include "index_list.h"
namespace Eval {
namespace NNUE {
namespace Features {
// 特徴量のうち、値が1であるインデックスのリストを取得する
void K::AppendActiveIndices(
const Position& pos, Color perspective, IndexList* active) {
// コンパイラの警告を回避するため、配列サイズが小さい場合は何もしない
if (RawFeatures::kMaxActiveDimensions < kMaxActiveDimensions) return;
const BonaPiece* pieces = (perspective == BLACK) ?
pos.eval_list()->piece_list_fb() :
pos.eval_list()->piece_list_fw();
assert(pieces[PIECE_NUMBER_BKING] != BONA_PIECE_ZERO);
assert(pieces[PIECE_NUMBER_WKING] != BONA_PIECE_ZERO);
for (PieceNumber i = PIECE_NUMBER_KING; i < PIECE_NUMBER_NB; ++i) {
active->push_back(pieces[i] - fe_end);
}
}
// 特徴量のうち、一手前から値が変化したインデックスのリストを取得する
void K::AppendChangedIndices(
const Position& pos, Color perspective,
IndexList* removed, IndexList* added) {
const auto& dp = pos.state()->dirtyPiece;
if (dp.pieceNo[0] >= PIECE_NUMBER_KING) {
removed->push_back(
dp.changed_piece[0].old_piece.from[perspective] - fe_end);
added->push_back(
dp.changed_piece[0].new_piece.from[perspective] - fe_end);
}
}
} // namespace Features
} // namespace NNUE
} // namespace Eval
#endif // defined(EVAL_NNUE)

View file

@ -0,0 +1,48 @@
// NNUE評価関数の入力特徴量Kの定義
#ifndef _NNUE_FEATURES_K_H_
#define _NNUE_FEATURES_K_H_
#if defined(EVAL_NNUE)
#include "../../../evaluate.h"
#include "features_common.h"
namespace Eval {
namespace NNUE {
namespace Features {
// 特徴量K玉の位置
class K {
public:
// 特徴量名
static constexpr const char* kName = "K";
// 評価関数ファイルに埋め込むハッシュ値
static constexpr std::uint32_t kHashValue = 0xD3CEE169u;
// 特徴量の次元数
static constexpr IndexType kDimensions = SQUARE_NB * 2;
// 特徴量のうち、同時に値が1となるインデックスの数の最大値
static constexpr IndexType kMaxActiveDimensions = 2;
// 差分計算の代わりに全計算を行うタイミング
static constexpr TriggerEvent kRefreshTrigger = TriggerEvent::kNone;
// 特徴量のうち、値が1であるインデックスのリストを取得する
static void AppendActiveIndices(const Position& pos, Color perspective,
IndexList* active);
// 特徴量のうち、一手前から値が変化したインデックスのリストを取得する
static void AppendChangedIndices(const Position& pos, Color perspective,
IndexList* removed, IndexList* added);
};
} // namespace Features
} // namespace NNUE
} // namespace Eval
#endif // defined(EVAL_NNUE)
#endif

View file

@ -0,0 +1,52 @@
// NNUE評価関数の入力特徴量Pの定義
#if defined(EVAL_NNUE)
#include "p.h"
#include "index_list.h"
namespace Eval {
namespace NNUE {
namespace Features {
// 特徴量のうち、値が1であるインデックスのリストを取得する
void P::AppendActiveIndices(
const Position& pos, Color perspective, IndexList* active) {
// コンパイラの警告を回避するため、配列サイズが小さい場合は何もしない
if (RawFeatures::kMaxActiveDimensions < kMaxActiveDimensions) return;
const BonaPiece* pieces = (perspective == BLACK) ?
pos.eval_list()->piece_list_fb() :
pos.eval_list()->piece_list_fw();
for (PieceNumber i = PIECE_NUMBER_ZERO; i < PIECE_NUMBER_KING; ++i) {
if (pieces[i] != Eval::BONA_PIECE_ZERO) {
active->push_back(pieces[i]);
}
}
}
// 特徴量のうち、一手前から値が変化したインデックスのリストを取得する
void P::AppendChangedIndices(
const Position& pos, Color perspective,
IndexList* removed, IndexList* added) {
const auto& dp = pos.state()->dirtyPiece;
for (int i = 0; i < dp.dirty_num; ++i) {
if (dp.pieceNo[i] >= PIECE_NUMBER_KING) continue;
if (dp.changed_piece[i].old_piece.from[perspective] != Eval::BONA_PIECE_ZERO) {
removed->push_back(dp.changed_piece[i].old_piece.from[perspective]);
}
if (dp.changed_piece[i].new_piece.from[perspective] != Eval::BONA_PIECE_ZERO) {
added->push_back(dp.changed_piece[i].new_piece.from[perspective]);
}
}
}
} // namespace Features
} // namespace NNUE
} // namespace Eval
#endif // defined(EVAL_NNUE)

View file

@ -0,0 +1,48 @@
// NNUE評価関数の入力特徴量Pの定義
#ifndef _NNUE_FEATURES_P_H_
#define _NNUE_FEATURES_P_H_
#if defined(EVAL_NNUE)
#include "../../../evaluate.h"
#include "features_common.h"
namespace Eval {
namespace NNUE {
namespace Features {
// 特徴量P玉以外の駒のBonaPiece
class P {
public:
// 特徴量名
static constexpr const char* kName = "P";
// 評価関数ファイルに埋め込むハッシュ値
static constexpr std::uint32_t kHashValue = 0x764CFB4Bu;
// 特徴量の次元数
static constexpr IndexType kDimensions = fe_end;
// 特徴量のうち、同時に値が1となるインデックスの数の最大値
static constexpr IndexType kMaxActiveDimensions = PIECE_NUMBER_KING;
// 差分計算の代わりに全計算を行うタイミング
static constexpr TriggerEvent kRefreshTrigger = TriggerEvent::kNone;
// 特徴量のうち、値が1であるインデックスのリストを取得する
static void AppendActiveIndices(const Position& pos, Color perspective,
IndexList* active);
// 特徴量のうち、一手前から値が変化したインデックスのリストを取得する
static void AppendChangedIndices(const Position& pos, Color perspective,
IndexList* removed, IndexList* added);
};
} // namespace Features
} // namespace NNUE
} // namespace Eval
#endif // defined(EVAL_NNUE)
#endif

View file

@ -0,0 +1,178 @@
// NNUE評価関数の層AffineTransformの定義
#ifndef _NNUE_LAYERS_AFFINE_TRANSFORM_H_
#define _NNUE_LAYERS_AFFINE_TRANSFORM_H_
#if defined(EVAL_NNUE)
#include "../nnue_common.h"
namespace Eval {
namespace NNUE {
namespace Layers {
// アフィン変換層
template <typename PreviousLayer, IndexType OutputDimensions>
class AffineTransform {
public:
// 入出力の型
using InputType = typename PreviousLayer::OutputType;
using OutputType = std::int32_t;
static_assert(std::is_same<InputType, std::uint8_t>::value, "");
// 入出力の次元数
static constexpr IndexType kInputDimensions =
PreviousLayer::kOutputDimensions;
static constexpr IndexType kOutputDimensions = OutputDimensions;
static constexpr IndexType kPaddedInputDimensions =
CeilToMultiple<IndexType>(kInputDimensions, kMaxSimdWidth);
// この層で使用する順伝播用バッファのサイズ
static constexpr std::size_t kSelfBufferSize =
CeilToMultiple(kOutputDimensions * sizeof(OutputType), kCacheLineSize);
// 入力層からこの層までで使用する順伝播用バッファのサイズ
static constexpr std::size_t kBufferSize =
PreviousLayer::kBufferSize + kSelfBufferSize;
// 評価関数ファイルに埋め込むハッシュ値
static constexpr std::uint32_t GetHashValue() {
std::uint32_t hash_value = 0xCC03DAE4u;
hash_value += kOutputDimensions;
hash_value ^= PreviousLayer::GetHashValue() >> 1;
hash_value ^= PreviousLayer::GetHashValue() << 31;
return hash_value;
}
// 入力層からこの層までの構造を表す文字列
static std::string GetStructureString() {
return "AffineTransform[" +
std::to_string(kOutputDimensions) + "<-" +
std::to_string(kInputDimensions) + "](" +
PreviousLayer::GetStructureString() + ")";
}
// パラメータを読み込む
bool ReadParameters(std::istream& stream) {
if (!previous_layer_.ReadParameters(stream)) return false;
stream.read(reinterpret_cast<char*>(biases_),
kOutputDimensions * sizeof(BiasType));
stream.read(reinterpret_cast<char*>(weights_),
kOutputDimensions * kPaddedInputDimensions *
sizeof(WeightType));
return !stream.fail();
}
// パラメータを書き込む
bool WriteParameters(std::ostream& stream) const {
if (!previous_layer_.WriteParameters(stream)) return false;
stream.write(reinterpret_cast<const char*>(biases_),
kOutputDimensions * sizeof(BiasType));
stream.write(reinterpret_cast<const char*>(weights_),
kOutputDimensions * kPaddedInputDimensions *
sizeof(WeightType));
return !stream.fail();
}
// 順伝播
const OutputType* Propagate(
const TransformedFeatureType* transformed_features, char* buffer) const {
const auto input = previous_layer_.Propagate(
transformed_features, buffer + kSelfBufferSize);
const auto output = reinterpret_cast<OutputType*>(buffer);
#if defined(USE_AVX2)
constexpr IndexType kNumChunks = kPaddedInputDimensions / kSimdWidth;
const __m256i kOnes = _mm256_set1_epi16(1);
const auto input_vector = reinterpret_cast<const __m256i*>(input);
#elif defined(USE_SSE41)
constexpr IndexType kNumChunks = kPaddedInputDimensions / kSimdWidth;
const __m128i kOnes = _mm_set1_epi16(1);
const auto input_vector = reinterpret_cast<const __m128i*>(input);
#elif defined(IS_ARM)
constexpr IndexType kNumChunks = kPaddedInputDimensions / kSimdWidth;
const auto input_vector = reinterpret_cast<const int8x8_t*>(input);
#endif
for (IndexType i = 0; i < kOutputDimensions; ++i) {
const IndexType offset = i * kPaddedInputDimensions;
#if defined(USE_AVX2)
__m256i sum = _mm256_set_epi32(0, 0, 0, 0, 0, 0, 0, biases_[i]);
const auto row = reinterpret_cast<const __m256i*>(&weights_[offset]);
for (IndexType j = 0; j < kNumChunks; ++j) {
__m256i product = _mm256_maddubs_epi16(
#if defined(__MINGW32__) || defined(__MINGW64__)
// HACK: Use _mm256_loadu_si256() instead of _mm256_load_si256. Because the binary
// compiled with g++ in MSYS2 crashes here because the output memory is not aligned
// even though alignas is specified.
_mm256_loadu_si256
#else
_mm256_load_si256
#endif
(&input_vector[j]), _mm256_load_si256(&row[j]));
product = _mm256_madd_epi16(product, kOnes);
sum = _mm256_add_epi32(sum, product);
}
sum = _mm256_hadd_epi32(sum, sum);
sum = _mm256_hadd_epi32(sum, sum);
const __m128i lo = _mm256_extracti128_si256(sum, 0);
const __m128i hi = _mm256_extracti128_si256(sum, 1);
output[i] = _mm_cvtsi128_si32(lo) + _mm_cvtsi128_si32(hi);
#elif defined(USE_SSE41)
__m128i sum = _mm_cvtsi32_si128(biases_[i]);
const auto row = reinterpret_cast<const __m128i*>(&weights_[offset]);
for (IndexType j = 0; j < kNumChunks; ++j) {
__m128i product = _mm_maddubs_epi16(
_mm_load_si128(&input_vector[j]), _mm_load_si128(&row[j]));
product = _mm_madd_epi16(product, kOnes);
sum = _mm_add_epi32(sum, product);
}
sum = _mm_hadd_epi32(sum, sum);
sum = _mm_hadd_epi32(sum, sum);
output[i] = _mm_cvtsi128_si32(sum);
#elif defined(IS_ARM)
int32x4_t sum = {biases_[i]};
const auto row = reinterpret_cast<const int8x8_t*>(&weights_[offset]);
for (IndexType j = 0; j < kNumChunks; ++j) {
int16x8_t product = vmull_s8(input_vector[j * 2], row[j * 2]);
product = vmlal_s8(product, input_vector[j * 2 + 1], row[j * 2 + 1]);
sum = vpadalq_s16(sum, product);
}
output[i] = sum[0] + sum[1] + sum[2] + sum[3];
#else
OutputType sum = biases_[i];
for (IndexType j = 0; j < kInputDimensions; ++j) {
sum += weights_[offset + j] * input[j];
}
output[i] = sum;
#endif
}
return output;
}
private:
// パラメータの型
using BiasType = OutputType;
using WeightType = std::int8_t;
// 学習用クラスをfriendにする
friend class Trainer<AffineTransform>;
// この層の直前の層
PreviousLayer previous_layer_;
// パラメータ
alignas(kCacheLineSize) BiasType biases_[kOutputDimensions];
alignas(kCacheLineSize)
WeightType weights_[kOutputDimensions * kPaddedInputDimensions];
};
} // namespace Layers
} // namespace NNUE
} // namespace Eval
#endif // defined(EVAL_NNUE)
#endif

View file

@ -0,0 +1,168 @@
// NNUE評価関数の層ClippedReLUの定義
#ifndef _NNUE_LAYERS_CLIPPED_RELU_H_
#define _NNUE_LAYERS_CLIPPED_RELU_H_
#if defined(EVAL_NNUE)
#include "../nnue_common.h"
namespace Eval {
namespace NNUE {
namespace Layers {
// Clipped ReLU
template <typename PreviousLayer>
class ClippedReLU {
public:
// 入出力の型
using InputType = typename PreviousLayer::OutputType;
using OutputType = std::uint8_t;
static_assert(std::is_same<InputType, std::int32_t>::value, "");
// 入出力の次元数
static constexpr IndexType kInputDimensions =
PreviousLayer::kOutputDimensions;
static constexpr IndexType kOutputDimensions = kInputDimensions;
// この層で使用する順伝播用バッファのサイズ
static constexpr std::size_t kSelfBufferSize =
CeilToMultiple(kOutputDimensions * sizeof(OutputType), kCacheLineSize);
// 入力層からこの層までで使用する順伝播用バッファのサイズ
static constexpr std::size_t kBufferSize =
PreviousLayer::kBufferSize + kSelfBufferSize;
// 評価関数ファイルに埋め込むハッシュ値
static constexpr std::uint32_t GetHashValue() {
std::uint32_t hash_value = 0x538D24C7u;
hash_value += PreviousLayer::GetHashValue();
return hash_value;
}
// 入力層からこの層までの構造を表す文字列
static std::string GetStructureString() {
return "ClippedReLU[" +
std::to_string(kOutputDimensions) + "](" +
PreviousLayer::GetStructureString() + ")";
}
// パラメータを読み込む
bool ReadParameters(std::istream& stream) {
return previous_layer_.ReadParameters(stream);
}
// パラメータを書き込む
bool WriteParameters(std::ostream& stream) const {
return previous_layer_.WriteParameters(stream);
}
// 順伝播
const OutputType* Propagate(
const TransformedFeatureType* transformed_features, char* buffer) const {
const auto input = previous_layer_.Propagate(
transformed_features, buffer + kSelfBufferSize);
const auto output = reinterpret_cast<OutputType*>(buffer);
#if defined(USE_AVX2)
constexpr IndexType kNumChunks = kInputDimensions / kSimdWidth;
const __m256i kZero = _mm256_setzero_si256();
const __m256i kOffsets = _mm256_set_epi32(7, 3, 6, 2, 5, 1, 4, 0);
const auto in = reinterpret_cast<const __m256i*>(input);
const auto out = reinterpret_cast<__m256i*>(output);
for (IndexType i = 0; i < kNumChunks; ++i) {
const __m256i words0 = _mm256_srai_epi16(_mm256_packs_epi32(
#if defined(__MINGW32__) || defined(__MINGW64__)
// HACK: Use _mm256_loadu_si256() instead of _mm256_load_si256. Because the binary
// compiled with g++ in MSYS2 crashes here because the output memory is not aligned
// even though alignas is specified.
_mm256_loadu_si256
#else
_mm256_load_si256
#endif
(&in[i * 4 + 0]),
#if defined(__MINGW32__) || defined(__MINGW64__)
_mm256_loadu_si256
#else
_mm256_load_si256
#endif
(&in[i * 4 + 1])), kWeightScaleBits);
const __m256i words1 = _mm256_srai_epi16(_mm256_packs_epi32(
#if defined(__MINGW32__) || defined(__MINGW64__)
_mm256_loadu_si256
#else
_mm256_load_si256
#endif
(&in[i * 4 + 2]),
#if defined(__MINGW32__) || defined(__MINGW64__)
_mm256_loadu_si256
#else
_mm256_load_si256
#endif
(&in[i * 4 + 3])), kWeightScaleBits);
#if defined(__MINGW32__) || defined(__MINGW64__)
_mm256_storeu_si256
#else
_mm256_store_si256
#endif
(&out[i], _mm256_permutevar8x32_epi32(_mm256_max_epi8(
_mm256_packs_epi16(words0, words1), kZero), kOffsets));
}
constexpr IndexType kStart = kNumChunks * kSimdWidth;
#elif defined(USE_SSE41)
constexpr IndexType kNumChunks = kInputDimensions / kSimdWidth;
const __m128i kZero = _mm_setzero_si128();
const auto in = reinterpret_cast<const __m128i*>(input);
const auto out = reinterpret_cast<__m128i*>(output);
for (IndexType i = 0; i < kNumChunks; ++i) {
const __m128i words0 = _mm_srai_epi16(_mm_packs_epi32(
_mm_load_si128(&in[i * 4 + 0]),
_mm_load_si128(&in[i * 4 + 1])), kWeightScaleBits);
const __m128i words1 = _mm_srai_epi16(_mm_packs_epi32(
_mm_load_si128(&in[i * 4 + 2]),
_mm_load_si128(&in[i * 4 + 3])), kWeightScaleBits);
_mm_store_si128(&out[i], _mm_max_epi8(
_mm_packs_epi16(words0, words1), kZero));
}
constexpr IndexType kStart = kNumChunks * kSimdWidth;
#elif defined(IS_ARM)
constexpr IndexType kNumChunks = kInputDimensions / (kSimdWidth / 2);
const int8x8_t kZero = {0};
const auto in = reinterpret_cast<const int32x4_t*>(input);
const auto out = reinterpret_cast<int8x8_t*>(output);
for (IndexType i = 0; i < kNumChunks; ++i) {
int16x8_t shifted;
const auto pack = reinterpret_cast<int16x4_t*>(&shifted);
pack[0] = vqshrn_n_s32(in[i * 2 + 0], kWeightScaleBits);
pack[1] = vqshrn_n_s32(in[i * 2 + 1], kWeightScaleBits);
out[i] = vmax_s8(vqmovn_s16(shifted), kZero);
}
constexpr IndexType kStart = kNumChunks * (kSimdWidth / 2);
#else
constexpr IndexType kStart = 0;
#endif
for (IndexType i = kStart; i < kInputDimensions; ++i) {
output[i] = static_cast<OutputType>(
std::max(0, std::min(127, input[i] >> kWeightScaleBits)));
}
return output;
}
private:
// 学習用クラスをfriendにする
friend class Trainer<ClippedReLU>;
// この層の直前の層
PreviousLayer previous_layer_;
};
} // namespace Layers
} // namespace NNUE
} // namespace Eval
#endif // defined(EVAL_NNUE)
#endif

View file

@ -0,0 +1,74 @@
// NNUE評価関数の層InputSliceの定義
#ifndef _NNUE_LAYERS_INPUT_SLICE_H_
#define _NNUE_LAYERS_INPUT_SLICE_H_
#if defined(EVAL_NNUE)
#include "../nnue_common.h"
namespace Eval {
namespace NNUE {
namespace Layers {
// 入力層
template <IndexType OutputDimensions, IndexType Offset = 0>
class InputSlice {
public:
// アライメントを維持する必要がある
static_assert(Offset % kMaxSimdWidth == 0, "");
// 出力の型
using OutputType = TransformedFeatureType;
// 出力の次元数
static constexpr IndexType kOutputDimensions = OutputDimensions;
// 入力層からこの層までで使用する順伝播用バッファのサイズ
static constexpr std::size_t kBufferSize = 0;
// 評価関数ファイルに埋め込むハッシュ値
static constexpr std::uint32_t GetHashValue() {
std::uint32_t hash_value = 0xEC42E90Du;
hash_value ^= kOutputDimensions ^ (Offset << 10);
return hash_value;
}
// 入力層からこの層までの構造を表す文字列
static std::string GetStructureString() {
return "InputSlice[" + std::to_string(kOutputDimensions) + "(" +
std::to_string(Offset) + ":" +
std::to_string(Offset + kOutputDimensions) + ")]";
}
// パラメータを読み込む
bool ReadParameters(std::istream& /*stream*/) {
return true;
}
// パラメータを書き込む
bool WriteParameters(std::ostream& /*stream*/) const {
return true;
}
// 順伝播
const OutputType* Propagate(
const TransformedFeatureType* transformed_features,
char* /*buffer*/) const {
return transformed_features + Offset;
}
private:
};
} // namespace Layers
} // namespace NNUE
} // namespace Eval
#endif // defined(EVAL_NNUE)
#endif

163
src/eval/nnue/layers/sum.h Normal file
View file

@ -0,0 +1,163 @@
// NNUE評価関数の層Sumの定義
#ifndef _NNUE_LAYERS_SUM_H_
#define _NNUE_LAYERS_SUM_H_
#if defined(EVAL_NNUE)
#include "../nnue_common.h"
namespace Eval {
namespace NNUE {
namespace Layers {
// 複数の層の出力の和を取る層
template <typename FirstPreviousLayer, typename... RemainingPreviousLayers>
class Sum : public Sum<RemainingPreviousLayers...> {
private:
using Head = FirstPreviousLayer;
using Tail = Sum<RemainingPreviousLayers...>;
public:
// 入出力の型
using InputType = typename Head::OutputType;
using OutputType = InputType;
static_assert(std::is_same<InputType, typename Tail::InputType>::value, "");
// 入出力の次元数
static constexpr IndexType kInputDimensions = Head::kOutputDimensions;
static constexpr IndexType kOutputDimensions = kInputDimensions;
static_assert(kInputDimensions == Tail::kInputDimensions , "");
// この層で使用する順伝播用バッファのサイズ
static constexpr std::size_t kSelfBufferSize =
CeilToMultiple(kOutputDimensions * sizeof(OutputType), kCacheLineSize);
// 入力層からこの層までで使用する順伝播用バッファのサイズ
static constexpr std::size_t kBufferSize =
std::max(Head::kBufferSize + kSelfBufferSize, Tail::kBufferSize);
// 評価関数ファイルに埋め込むハッシュ値
static constexpr std::uint32_t GetHashValue() {
std::uint32_t hash_value = 0xBCE400B4u;
hash_value ^= Head::GetHashValue() >> 1;
hash_value ^= Head::GetHashValue() << 31;
hash_value ^= Tail::GetHashValue() >> 2;
hash_value ^= Tail::GetHashValue() << 30;
return hash_value;
}
// 入力層からこの層までの構造を表す文字列
static std::string GetStructureString() {
return "Sum[" +
std::to_string(kOutputDimensions) + "](" + GetSummandsString() + ")";
}
// パラメータを読み込む
bool ReadParameters(std::istream& stream) {
if (!Tail::ReadParameters(stream)) return false;
return previous_layer_.ReadParameters(stream);
}
// パラメータを書き込む
bool WriteParameters(std::ostream& stream) const {
if (!Tail::WriteParameters(stream)) return false;
return previous_layer_.WriteParameters(stream);
}
// 順伝播
const OutputType* Propagate(
const TransformedFeatureType* transformed_features, char* buffer) const {
Tail::Propagate(transformed_features, buffer);
const auto head_output = previous_layer_.Propagate(
transformed_features, buffer + kSelfBufferSize);
const auto output = reinterpret_cast<OutputType*>(buffer);
for (IndexType i = 0; i < kOutputDimensions; ++i) {
output[i] += head_output[i];
}
return output;
}
protected:
// 和を取る対象となる層のリストを表す文字列
static std::string GetSummandsString() {
return Head::GetStructureString() + "," + Tail::GetSummandsString();
}
// 学習用クラスをfriendにする
friend class Trainer<Sum>;
// この層の直前の層
FirstPreviousLayer previous_layer_;
};
// 複数の層の出力の和を取る層テンプレート引数が1つの場合
template <typename PreviousLayer>
class Sum<PreviousLayer> {
public:
// 入出力の型
using InputType = typename PreviousLayer::OutputType;
using OutputType = InputType;
// 入出力の次元数
static constexpr IndexType kInputDimensions =
PreviousLayer::kOutputDimensions;
static constexpr IndexType kOutputDimensions = kInputDimensions;
// 入力層からこの層までで使用する順伝播用バッファのサイズ
static constexpr std::size_t kBufferSize = PreviousLayer::kBufferSize;
// 評価関数ファイルに埋め込むハッシュ値
static constexpr std::uint32_t GetHashValue() {
std::uint32_t hash_value = 0xBCE400B4u;
hash_value ^= PreviousLayer::GetHashValue() >> 1;
hash_value ^= PreviousLayer::GetHashValue() << 31;
return hash_value;
}
// 入力層からこの層までの構造を表す文字列
static std::string GetStructureString() {
return "Sum[" +
std::to_string(kOutputDimensions) + "](" + GetSummandsString() + ")";
}
// パラメータを読み込む
bool ReadParameters(std::istream& stream) {
return previous_layer_.ReadParameters(stream);
}
// パラメータを書き込む
bool WriteParameters(std::ostream& stream) const {
return previous_layer_.WriteParameters(stream);
}
// 順伝播
const OutputType* Propagate(
const TransformedFeatureType* transformed_features, char* buffer) const {
return previous_layer_.Propagate(transformed_features, buffer);
}
protected:
// 和を取る対象となる層のリストを表す文字列
static std::string GetSummandsString() {
return PreviousLayer::GetStructureString();
}
// 学習用クラスをfriendにする
friend class Trainer<Sum>;
// この層の直前の層
PreviousLayer previous_layer_;
};
} // namespace Layers
} // namespace NNUE
} // namespace Eval
#endif // defined(EVAL_NNUE)
#endif

View file

@ -0,0 +1,30 @@
// NNUE評価関数の差分計算用のクラス
#ifndef _NNUE_ACCUMULATOR_H_
#define _NNUE_ACCUMULATOR_H_
#if defined(EVAL_NNUE)
#include "nnue_architecture.h"
namespace Eval {
namespace NNUE {
// 入力特徴量をアフィン変換した結果を保持するクラス
// 最終的な出力である評価値も一緒に持たせておく
struct alignas(32) Accumulator {
std::int16_t
accumulation[2][kRefreshTriggers.size()][kTransformedFeatureDimensions];
Value score = VALUE_ZERO;
bool computed_accumulation = false;
bool computed_score = false;
};
} // namespace NNUE
} // namespace Eval
#endif // defined(EVAL_NNUE)
#endif

View file

@ -0,0 +1,32 @@
// NNUE評価関数で用いる入力特徴量とネットワーク構造
#ifndef _NNUE_ARCHITECTURE_H_
#define _NNUE_ARCHITECTURE_H_
#if defined(EVAL_NNUE)
// 入力特徴量とネットワーク構造が定義されたヘッダをincludeする
//#include "architectures/k-p_256x2-32-32.h"
//#include "architectures/k-p-cr_256x2-32-32.h"
//#include "architectures/k-p-cr-ep_256x2-32-32.h"
#include "architectures/halfkp_256x2-32-32.h"
//#include "architectures/halfkp-cr-ep_256x2-32-32.h"
namespace Eval {
namespace NNUE {
static_assert(kTransformedFeatureDimensions % kMaxSimdWidth == 0, "");
static_assert(Network::kOutputDimensions == 1, "");
static_assert(std::is_same<Network::OutputType, std::int32_t>::value, "");
// 差分計算の代わりに全計算を行うタイミングのリスト
constexpr auto kRefreshTriggers = RawFeatures::kRefreshTriggers;
} // namespace NNUE
} // namespace Eval
#endif // defined(EVAL_NNUE)
#endif

View file

@ -0,0 +1,60 @@
// NNUE評価関数で用いる定数など
#ifndef _NNUE_COMMON_H_
#define _NNUE_COMMON_H_
#if defined(EVAL_NNUE)
#if defined(USE_AVX2)
#include <immintrin.h>
#elif defined(USE_SSE2)
#include <emmintrin.h>
#endif
namespace Eval {
namespace NNUE {
// 評価関数ファイルのバージョンを表す定数
constexpr std::uint32_t kVersion = 0x7AF32F16u;
// 評価値の計算で利用する定数
constexpr int FV_SCALE = 16;
constexpr int kWeightScaleBits = 6;
// キャッシュラインのサイズ(バイト単位)
constexpr std::size_t kCacheLineSize = 64;
// SIMD幅バイト単位
#if defined(USE_AVX2)
constexpr std::size_t kSimdWidth = 32;
#elif defined(USE_SSE2)
constexpr std::size_t kSimdWidth = 16;
#elif defined(IS_ARM)
constexpr std::size_t kSimdWidth = 16;
#endif
constexpr std::size_t kMaxSimdWidth = 32;
// 変換後の入力特徴量の型
using TransformedFeatureType = std::uint8_t;
// インデックスの型
using IndexType = std::uint32_t;
// 学習用クラステンプレートの前方宣言
template <typename Layer>
class Trainer;
// n以上で最小のbaseの倍数を求める
template <typename IntType>
constexpr IntType CeilToMultiple(IntType n, IntType base) {
return (n + base - 1) / base * base;
}
} // namespace NNUE
} // namespace Eval
#endif // defined(EVAL_NNUE)
#endif

View file

@ -0,0 +1,347 @@
// NNUE評価関数の入力特徴量の変換を行うクラス
#ifndef _NNUE_FEATURE_TRANSFORMER_H_
#define _NNUE_FEATURE_TRANSFORMER_H_
#if defined(EVAL_NNUE)
#include "nnue_common.h"
#include "nnue_architecture.h"
#include "features/index_list.h"
#include <cstring> // std::memset()
namespace Eval {
namespace NNUE {
// 入力特徴量変換器
class FeatureTransformer {
private:
// 片側分の出力の次元数
static constexpr IndexType kHalfDimensions = kTransformedFeatureDimensions;
public:
// 出力の型
using OutputType = TransformedFeatureType;
// 入出力の次元数
static constexpr IndexType kInputDimensions = RawFeatures::kDimensions;
static constexpr IndexType kOutputDimensions = kHalfDimensions * 2;
// 順伝播用バッファのサイズ
static constexpr std::size_t kBufferSize =
kOutputDimensions * sizeof(OutputType);
// 評価関数ファイルに埋め込むハッシュ値
static constexpr std::uint32_t GetHashValue() {
return RawFeatures::kHashValue ^ kOutputDimensions;
}
// 構造を表す文字列
static std::string GetStructureString() {
return RawFeatures::GetName() + "[" +
std::to_string(kInputDimensions) + "->" +
std::to_string(kHalfDimensions) + "x2]";
}
// パラメータを読み込む
bool ReadParameters(std::istream& stream) {
stream.read(reinterpret_cast<char*>(biases_),
kHalfDimensions * sizeof(BiasType));
stream.read(reinterpret_cast<char*>(weights_),
kHalfDimensions * kInputDimensions * sizeof(WeightType));
return !stream.fail();
}
// パラメータを書き込む
bool WriteParameters(std::ostream& stream) const {
stream.write(reinterpret_cast<const char*>(biases_),
kHalfDimensions * sizeof(BiasType));
stream.write(reinterpret_cast<const char*>(weights_),
kHalfDimensions * kInputDimensions * sizeof(WeightType));
return !stream.fail();
}
// 可能なら差分計算を進める
bool UpdateAccumulatorIfPossible(const Position& pos) const {
const auto now = pos.state();
if (now->accumulator.computed_accumulation) {
return true;
}
const auto prev = now->previous;
if (prev && prev->accumulator.computed_accumulation) {
UpdateAccumulator(pos);
return true;
}
return false;
}
// 入力特徴量を変換する
void Transform(const Position& pos, OutputType* output, bool refresh) const {
if (refresh || !UpdateAccumulatorIfPossible(pos)) {
RefreshAccumulator(pos);
}
const auto& accumulation = pos.state()->accumulator.accumulation;
#if defined(USE_AVX2)
constexpr IndexType kNumChunks = kHalfDimensions / kSimdWidth;
constexpr int kControl = 0b11011000;
const __m256i kZero = _mm256_setzero_si256();
#elif defined(USE_SSE41)
constexpr IndexType kNumChunks = kHalfDimensions / kSimdWidth;
const __m128i kZero = _mm_setzero_si128();
#elif defined(IS_ARM)
constexpr IndexType kNumChunks = kHalfDimensions / (kSimdWidth / 2);
const int8x8_t kZero = {0};
#endif
const Color perspectives[2] = {pos.side_to_move(), ~pos.side_to_move()};
for (IndexType p = 0; p < 2; ++p) {
const IndexType offset = kHalfDimensions * p;
#if defined(USE_AVX2)
auto out = reinterpret_cast<__m256i*>(&output[offset]);
for (IndexType j = 0; j < kNumChunks; ++j) {
__m256i sum0 =
#if defined(__MINGW32__) || defined(__MINGW64__)
// HACK: Use _mm256_loadu_si256() instead of _mm256_load_si256. Because the binary
// compiled with g++ in MSYS2 crashes here because the output memory is not aligned
// even though alignas is specified.
_mm256_loadu_si256
#else
_mm256_load_si256
#endif
(&reinterpret_cast<const __m256i*>(
accumulation[perspectives[p]][0])[j * 2 + 0]);
__m256i sum1 =
#if defined(__MINGW32__) || defined(__MINGW64__)
_mm256_loadu_si256
#else
_mm256_load_si256
#endif
(&reinterpret_cast<const __m256i*>(
accumulation[perspectives[p]][0])[j * 2 + 1]);
for (IndexType i = 1; i < kRefreshTriggers.size(); ++i) {
sum0 = _mm256_add_epi16(sum0, reinterpret_cast<const __m256i*>(
accumulation[perspectives[p]][i])[j * 2 + 0]);
sum1 = _mm256_add_epi16(sum1, reinterpret_cast<const __m256i*>(
accumulation[perspectives[p]][i])[j * 2 + 1]);
}
#if defined(__MINGW32__) || defined(__MINGW64__)
_mm256_storeu_si256
#else
_mm256_store_si256
#endif
(&out[j], _mm256_permute4x64_epi64(_mm256_max_epi8(
_mm256_packs_epi16(sum0, sum1), kZero), kControl));
}
#elif defined(USE_SSE41)
auto out = reinterpret_cast<__m128i*>(&output[offset]);
for (IndexType j = 0; j < kNumChunks; ++j) {
__m128i sum0 = _mm_load_si128(&reinterpret_cast<const __m128i*>(
accumulation[perspectives[p]][0])[j * 2 + 0]);
__m128i sum1 = _mm_load_si128(&reinterpret_cast<const __m128i*>(
accumulation[perspectives[p]][0])[j * 2 + 1]);
for (IndexType i = 1; i < kRefreshTriggers.size(); ++i) {
sum0 = _mm_add_epi16(sum0, reinterpret_cast<const __m128i*>(
accumulation[perspectives[p]][i])[j * 2 + 0]);
sum1 = _mm_add_epi16(sum1, reinterpret_cast<const __m128i*>(
accumulation[perspectives[p]][i])[j * 2 + 1]);
}
_mm_store_si128(&out[j], _mm_max_epi8(
_mm_packs_epi16(sum0, sum1), kZero));
}
#elif defined(IS_ARM)
const auto out = reinterpret_cast<int8x8_t*>(&output[offset]);
for (IndexType j = 0; j < kNumChunks; ++j) {
int16x8_t sum = reinterpret_cast<const int16x8_t*>(
accumulation[perspectives[p]][0])[j];
for (IndexType i = 1; i < kRefreshTriggers.size(); ++i) {
sum = vaddq_s16(sum, reinterpret_cast<const int16x8_t*>(
accumulation[perspectives[p]][i])[j]);
}
out[j] = vmax_s8(vqmovn_s16(sum), kZero);
}
#else
for (IndexType j = 0; j < kHalfDimensions; ++j) {
BiasType sum = accumulation[static_cast<int>(perspectives[p])][0][j];
for (IndexType i = 1; i < kRefreshTriggers.size(); ++i) {
sum += accumulation[static_cast<int>(perspectives[p])][i][j];
}
output[offset + j] = static_cast<OutputType>(
std::max<int>(0, std::min<int>(127, sum)));
}
#endif
}
}
private:
// 差分計算を用いずに累積値を計算する
void RefreshAccumulator(const Position& pos) const {
auto& accumulator = pos.state()->accumulator;
for (IndexType i = 0; i < kRefreshTriggers.size(); ++i) {
Features::IndexList active_indices[2];
RawFeatures::AppendActiveIndices(pos, kRefreshTriggers[i],
active_indices);
for (const auto perspective : Colors) {
if (i == 0) {
std::memcpy(accumulator.accumulation[perspective][i], biases_,
kHalfDimensions * sizeof(BiasType));
} else {
std::memset(accumulator.accumulation[perspective][i], 0,
kHalfDimensions * sizeof(BiasType));
}
for (const auto index : active_indices[perspective]) {
const IndexType offset = kHalfDimensions * index;
#if defined(USE_AVX2)
auto accumulation = reinterpret_cast<__m256i*>(
&accumulator.accumulation[perspective][i][0]);
auto column = reinterpret_cast<const __m256i*>(&weights_[offset]);
constexpr IndexType kNumChunks = kHalfDimensions / (kSimdWidth / 2);
for (IndexType j = 0; j < kNumChunks; ++j) {
#if defined(__MINGW32__) || defined(__MINGW64__)
_mm256_storeu_si256(&accumulation[j], _mm256_add_epi16(_mm256_loadu_si256(&accumulation[j]), column[j]));
#else
accumulation[j] = _mm256_add_epi16(accumulation[j], column[j]);
#endif
}
#elif defined(USE_SSE2)
auto accumulation = reinterpret_cast<__m128i*>(
&accumulator.accumulation[perspective][i][0]);
auto column = reinterpret_cast<const __m128i*>(&weights_[offset]);
constexpr IndexType kNumChunks = kHalfDimensions / (kSimdWidth / 2);
for (IndexType j = 0; j < kNumChunks; ++j) {
accumulation[j] = _mm_add_epi16(accumulation[j], column[j]);
}
#elif defined(IS_ARM)
auto accumulation = reinterpret_cast<int16x8_t*>(
&accumulator.accumulation[perspective][i][0]);
auto column = reinterpret_cast<const int16x8_t*>(&weights_[offset]);
constexpr IndexType kNumChunks = kHalfDimensions / (kSimdWidth / 2);
for (IndexType j = 0; j < kNumChunks; ++j) {
accumulation[j] = vaddq_s16(accumulation[j], column[j]);
}
#else
for (IndexType j = 0; j < kHalfDimensions; ++j) {
accumulator.accumulation[perspective][i][j] += weights_[offset + j];
}
#endif
}
}
}
accumulator.computed_accumulation = true;
accumulator.computed_score = false;
}
// 差分計算を用いて累積値を計算する
void UpdateAccumulator(const Position& pos) const {
const auto prev_accumulator = pos.state()->previous->accumulator;
auto& accumulator = pos.state()->accumulator;
for (IndexType i = 0; i < kRefreshTriggers.size(); ++i) {
Features::IndexList removed_indices[2], added_indices[2];
bool reset[2];
RawFeatures::AppendChangedIndices(pos, kRefreshTriggers[i],
removed_indices, added_indices, reset);
for (const auto perspective : Colors) {
#if defined(USE_AVX2)
constexpr IndexType kNumChunks = kHalfDimensions / (kSimdWidth / 2);
auto accumulation = reinterpret_cast<__m256i*>(
&accumulator.accumulation[perspective][i][0]);
#elif defined(USE_SSE2)
constexpr IndexType kNumChunks = kHalfDimensions / (kSimdWidth / 2);
auto accumulation = reinterpret_cast<__m128i*>(
&accumulator.accumulation[perspective][i][0]);
#elif defined(IS_ARM)
constexpr IndexType kNumChunks = kHalfDimensions / (kSimdWidth / 2);
auto accumulation = reinterpret_cast<int16x8_t*>(
&accumulator.accumulation[perspective][i][0]);
#endif
if (reset[perspective]) {
if (i == 0) {
std::memcpy(accumulator.accumulation[perspective][i], biases_,
kHalfDimensions * sizeof(BiasType));
} else {
std::memset(accumulator.accumulation[perspective][i], 0,
kHalfDimensions * sizeof(BiasType));
}
} else { // 1から0に変化した特徴量に関する差分計算
std::memcpy(accumulator.accumulation[perspective][i],
prev_accumulator.accumulation[perspective][i],
kHalfDimensions * sizeof(BiasType));
for (const auto index : removed_indices[perspective]) {
const IndexType offset = kHalfDimensions * index;
#if defined(USE_AVX2)
auto column = reinterpret_cast<const __m256i*>(&weights_[offset]);
for (IndexType j = 0; j < kNumChunks; ++j) {
accumulation[j] = _mm256_sub_epi16(accumulation[j], column[j]);
}
#elif defined(USE_SSE2)
auto column = reinterpret_cast<const __m128i*>(&weights_[offset]);
for (IndexType j = 0; j < kNumChunks; ++j) {
accumulation[j] = _mm_sub_epi16(accumulation[j], column[j]);
}
#elif defined(IS_ARM)
auto column = reinterpret_cast<const int16x8_t*>(&weights_[offset]);
for (IndexType j = 0; j < kNumChunks; ++j) {
accumulation[j] = vsubq_s16(accumulation[j], column[j]);
}
#else
for (IndexType j = 0; j < kHalfDimensions; ++j) {
accumulator.accumulation[perspective][i][j] -=
weights_[offset + j];
}
#endif
}
}
{ // 0から1に変化した特徴量に関する差分計算
for (const auto index : added_indices[perspective]) {
const IndexType offset = kHalfDimensions * index;
#if defined(USE_AVX2)
auto column = reinterpret_cast<const __m256i*>(&weights_[offset]);
for (IndexType j = 0; j < kNumChunks; ++j) {
accumulation[j] = _mm256_add_epi16(accumulation[j], column[j]);
}
#elif defined(USE_SSE2)
auto column = reinterpret_cast<const __m128i*>(&weights_[offset]);
for (IndexType j = 0; j < kNumChunks; ++j) {
accumulation[j] = _mm_add_epi16(accumulation[j], column[j]);
}
#elif defined(IS_ARM)
auto column = reinterpret_cast<const int16x8_t*>(&weights_[offset]);
for (IndexType j = 0; j < kNumChunks; ++j) {
accumulation[j] = vaddq_s16(accumulation[j], column[j]);
}
#else
for (IndexType j = 0; j < kHalfDimensions; ++j) {
accumulator.accumulation[perspective][i][j] +=
weights_[offset + j];
}
#endif
}
}
}
}
accumulator.computed_accumulation = true;
accumulator.computed_score = false;
}
// パラメータの型
using BiasType = std::int16_t;
using WeightType = std::int16_t;
// 学習用クラスをfriendにする
friend class Trainer<FeatureTransformer>;
// パラメータ
alignas(kCacheLineSize) BiasType biases_[kHalfDimensions];
alignas(kCacheLineSize)
WeightType weights_[kHalfDimensions * kInputDimensions];
};
} // namespace NNUE
} // namespace Eval
#endif // defined(EVAL_NNUE)
#endif

View file

@ -0,0 +1,201 @@
// NNUE評価関数に関するUSI拡張コマンド
#if defined(ENABLE_TEST_CMD) && defined(EVAL_NNUE)
#include "../../thread.h"
#include "../../uci.h"
#include "evaluate_nnue.h"
#include "nnue_test_command.h"
#include <set>
#include <fstream>
#define ASSERT(X) { if (!(X)) { std::cout << "\nError : ASSERT(" << #X << "), " << __FILE__ << "(" << __LINE__ << "): " << __func__ << std::endl; \
std::this_thread::sleep_for(std::chrono::microseconds(3000)); *(int*)1 =0;} }
namespace Eval {
namespace NNUE {
namespace {
// 主に差分計算に関するRawFeaturesのテスト
void TestFeatures(Position& pos) {
const std::uint64_t num_games = 1000;
StateInfo si;
pos.set(StartFEN, false, &si, Threads.main());
const int MAX_PLY = 256; // 256手までテスト
StateInfo state[MAX_PLY]; // StateInfoを最大手数分だけ
int ply; // 初期局面からの手数
PRNG prng(20171128);
std::uint64_t num_moves = 0;
std::vector<std::uint64_t> num_updates(kRefreshTriggers.size() + 1);
std::vector<std::uint64_t> num_resets(kRefreshTriggers.size());
constexpr IndexType kUnknown = -1;
std::vector<IndexType> trigger_map(RawFeatures::kDimensions, kUnknown);
auto make_index_sets = [&](const Position& pos) {
std::vector<std::vector<std::set<IndexType>>> index_sets(
kRefreshTriggers.size(), std::vector<std::set<IndexType>>(2));
for (IndexType i = 0; i < kRefreshTriggers.size(); ++i) {
Features::IndexList active_indices[2];
RawFeatures::AppendActiveIndices(pos, kRefreshTriggers[i],
active_indices);
for (const auto perspective : Colors) {
for (const auto index : active_indices[perspective]) {
ASSERT(index < RawFeatures::kDimensions);
ASSERT(index_sets[i][perspective].count(index) == 0);
ASSERT(trigger_map[index] == kUnknown || trigger_map[index] == i);
index_sets[i][perspective].insert(index);
trigger_map[index] = i;
}
}
}
return index_sets;
};
auto update_index_sets = [&](const Position& pos, auto* index_sets) {
for (IndexType i = 0; i < kRefreshTriggers.size(); ++i) {
Features::IndexList removed_indices[2], added_indices[2];
bool reset[2];
RawFeatures::AppendChangedIndices(pos, kRefreshTriggers[i],
removed_indices, added_indices, reset);
for (const auto perspective : Colors) {
if (reset[perspective]) {
(*index_sets)[i][perspective].clear();
++num_resets[i];
} else {
for (const auto index : removed_indices[perspective]) {
ASSERT(index < RawFeatures::kDimensions);
ASSERT((*index_sets)[i][perspective].count(index) == 1);
ASSERT(trigger_map[index] == kUnknown || trigger_map[index] == i);
(*index_sets)[i][perspective].erase(index);
++num_updates.back();
++num_updates[i];
trigger_map[index] = i;
}
}
for (const auto index : added_indices[perspective]) {
ASSERT(index < RawFeatures::kDimensions);
ASSERT((*index_sets)[i][perspective].count(index) == 0);
ASSERT(trigger_map[index] == kUnknown || trigger_map[index] == i);
(*index_sets)[i][perspective].insert(index);
++num_updates.back();
++num_updates[i];
trigger_map[index] = i;
}
}
}
};
std::cout << "feature set: " << RawFeatures::GetName()
<< "[" << RawFeatures::kDimensions << "]" << std::endl;
std::cout << "start testing with random games";
for (std::uint64_t i = 0; i < num_games; ++i) {
auto index_sets = make_index_sets(pos);
for (ply = 0; ply < MAX_PLY; ++ply) {
MoveList<LEGAL> mg(pos); // 全合法手の生成
// 合法な指し手がなかった == 詰み
if (mg.size() == 0)
break;
// 生成された指し手のなかからランダムに選び、その指し手で局面を進める。
Move m = mg.begin()[prng.rand(mg.size())];
pos.do_move(m, state[ply]);
++num_moves;
update_index_sets(pos, &index_sets);
ASSERT(index_sets == make_index_sets(pos));
}
pos.set(StartFEN, false, &si, Threads.main());
// 100回に1回ごとに'.'を出力(進んでいることがわかるように)
if ((i % 100) == 0)
std::cout << "." << std::flush;
}
std::cout << "passed." << std::endl;
std::cout << num_games << " games, " << num_moves << " moves, "
<< num_updates.back() << " updates, "
<< (1.0 * num_updates.back() / num_moves)
<< " updates per move" << std::endl;
std::size_t num_observed_indices = 0;
for (IndexType i = 0; i < kRefreshTriggers.size(); ++i) {
const auto count = std::count(trigger_map.begin(), trigger_map.end(), i);
num_observed_indices += count;
std::cout << "TriggerEvent(" << static_cast<int>(kRefreshTriggers[i])
<< "): " << count << " features ("
<< (100.0 * count / RawFeatures::kDimensions) << "%), "
<< num_updates[i] << " updates ("
<< (1.0 * num_updates[i] / num_moves) << " per move), "
<< num_resets[i] << " resets ("
<< (100.0 * num_resets[i] / num_moves) << "%)"
<< std::endl;
}
std::cout << "observed " << num_observed_indices << " ("
<< (100.0 * num_observed_indices / RawFeatures::kDimensions)
<< "% of " << RawFeatures::kDimensions
<< ") features" << std::endl;
}
// 評価関数の構造を表す文字列を出力する
void PrintInfo(std::istream& stream) {
std::cout << "network architecture: " << GetArchitectureString() << std::endl;
while (true) {
std::string file_name;
stream >> file_name;
if (file_name.empty()) break;
std::uint32_t hash_value;
std::string architecture;
const bool success = [&]() {
std::ifstream file_stream(file_name, std::ios::binary);
if (!file_stream) return false;
if (!ReadHeader(file_stream, &hash_value, &architecture)) return false;
return true;
}();
std::cout << file_name << ": ";
if (success) {
if (hash_value == kHashValue) {
std::cout << "matches with this binary";
if (architecture != GetArchitectureString()) {
std::cout << ", but architecture string differs: " << architecture;
}
std::cout << std::endl;
} else {
std::cout << architecture << std::endl;
}
} else {
std::cout << "failed to read header" << std::endl;
}
}
}
} // namespace
// NNUE評価関数に関するUSI拡張コマンド
void TestCommand(Position& pos, std::istream& stream) {
std::string sub_command;
stream >> sub_command;
if (sub_command == "test_features") {
TestFeatures(pos);
} else if (sub_command == "info") {
PrintInfo(stream);
} else {
std::cout << "usage:" << std::endl;
std::cout << " test nnue test_features" << std::endl;
std::cout << " test nnue info [path/to/" << kFileName << "...]" << std::endl;
}
}
} // namespace NNUE
} // namespace Eval
#endif // defined(ENABLE_TEST_CMD) && defined(EVAL_NNUE)

View file

@ -0,0 +1,21 @@
// NNUE評価関数に関するUSI拡張コマンドのインターフェイス
#ifndef _NNUE_TEST_COMMAND_H_
#define _NNUE_TEST_COMMAND_H_
#if defined(ENABLE_TEST_CMD) && defined(EVAL_NNUE)
namespace Eval {
namespace NNUE {
// NNUE評価関数に関するUSI拡張コマンド
void TestCommand(Position& pos, std::istream& stream);
} // namespace NNUE
} // namespace Eval
#endif // defined(ENABLE_TEST_CMD) && defined(EVAL_NNUE)
#endif

View file

@ -0,0 +1,110 @@
// NNUE評価関数の特徴量変換クラステンプレート
#ifndef _NNUE_TRAINER_FEATURES_FACTORIZER_H_
#define _NNUE_TRAINER_FEATURES_FACTORIZER_H_
#if defined(EVAL_NNUE)
#include "../../nnue_common.h"
#include "../trainer.h"
namespace Eval {
namespace NNUE {
namespace Features {
// 入力特徴量を学習用特徴量に変換するクラステンプレート
// デフォルトでは学習用特徴量は元の入力特徴量と同じとし、必要に応じて特殊化する
template <typename FeatureType>
class Factorizer {
public:
// 学習用特徴量の次元数を取得する
static constexpr IndexType GetDimensions() {
return FeatureType::kDimensions;
}
// 学習用特徴量のインデックスと学習率のスケールを取得する
static void AppendTrainingFeatures(
IndexType base_index, std::vector<TrainingFeature>* training_features) {
assert(base_index < FeatureType::kDimensions);
training_features->emplace_back(base_index);
}
};
// 学習用特徴量の情報
struct FeatureProperties {
bool active;
IndexType dimensions;
};
// 元の入力特徴量を学習用特徴量に追加する
template <typename FeatureType>
IndexType AppendBaseFeature(
FeatureProperties properties, IndexType base_index,
std::vector<TrainingFeature>* training_features) {
assert(properties.dimensions == FeatureType::kDimensions);
assert(base_index < FeatureType::kDimensions);
training_features->emplace_back(base_index);
return properties.dimensions;
}
// 学習率のスケールが0でなければ他の種類の学習用特徴量を引き継ぐ
template <typename FeatureType>
IndexType InheritFeaturesIfRequired(
IndexType index_offset, FeatureProperties properties, IndexType base_index,
std::vector<TrainingFeature>* training_features) {
if (!properties.active) {
return 0;
}
assert(properties.dimensions == Factorizer<FeatureType>::GetDimensions());
assert(base_index < FeatureType::kDimensions);
const auto start = training_features->size();
Factorizer<FeatureType>::AppendTrainingFeatures(
base_index, training_features);
for (auto i = start; i < training_features->size(); ++i) {
auto& feature = (*training_features)[i];
assert(feature.GetIndex() < Factorizer<FeatureType>::GetDimensions());
feature.ShiftIndex(index_offset);
}
return properties.dimensions;
}
// 学習用特徴量を追加せず、必要に応じてインデックスの差分を返す
// 対応する特徴量がない場合にInheritFeaturesIfRequired()の代わりに呼ぶ
IndexType SkipFeatures(FeatureProperties properties) {
if (!properties.active) {
return 0;
}
return properties.dimensions;
}
// 学習用特徴量の次元数を取得する
template <std::size_t N>
constexpr IndexType GetActiveDimensions(
const FeatureProperties (&properties)[N]) {
static_assert(N > 0, "");
IndexType dimensions = properties[0].dimensions;
for (std::size_t i = 1; i < N; ++i) {
if (properties[i].active) {
dimensions += properties[i].dimensions;
}
}
return dimensions;
}
// 配列の要素数を取得する
template <typename T, std::size_t N>
constexpr std::size_t GetArrayLength(const T (&/*array*/)[N]) {
return N;
}
} // namespace Features
} // namespace NNUE
} // namespace Eval
#endif // defined(EVAL_NNUE)
#endif

View file

@ -0,0 +1,104 @@
// NNUE評価関数の特徴量変換クラステンプレートのFeatureSet用特殊化
#ifndef _NNUE_TRAINER_FEATURES_FACTORIZER_FEATURE_SET_H_
#define _NNUE_TRAINER_FEATURES_FACTORIZER_FEATURE_SET_H_
#if defined(EVAL_NNUE)
#include "../../features/feature_set.h"
#include "factorizer.h"
namespace Eval {
namespace NNUE {
namespace Features {
// 入力特徴量を学習用特徴量に変換するクラステンプレート
// FeatureSet用特殊化
template <typename FirstFeatureType, typename... RemainingFeatureTypes>
class Factorizer<FeatureSet<FirstFeatureType, RemainingFeatureTypes...>> {
private:
using Head = Factorizer<FeatureSet<FirstFeatureType>>;
using Tail = Factorizer<FeatureSet<RemainingFeatureTypes...>>;
public:
// 元の入力特徴量の次元数
static constexpr IndexType kBaseDimensions =
FeatureSet<FirstFeatureType, RemainingFeatureTypes...>::kDimensions;
// 学習用特徴量の次元数を取得する
static constexpr IndexType GetDimensions() {
return Head::GetDimensions() + Tail::GetDimensions();
}
// 学習用特徴量のインデックスと学習率のスケールを取得する
static void AppendTrainingFeatures(
IndexType base_index, std::vector<TrainingFeature>* training_features,
IndexType base_dimensions = kBaseDimensions) {
assert(base_index < kBaseDimensions);
constexpr auto boundary = FeatureSet<RemainingFeatureTypes...>::kDimensions;
if (base_index < boundary) {
Tail::AppendTrainingFeatures(
base_index, training_features, base_dimensions);
} else {
const auto start = training_features->size();
Head::AppendTrainingFeatures(
base_index - boundary, training_features, base_dimensions);
for (auto i = start; i < training_features->size(); ++i) {
auto& feature = (*training_features)[i];
const auto index = feature.GetIndex();
assert(index < Head::GetDimensions() ||
(index >= base_dimensions &&
index < base_dimensions +
Head::GetDimensions() - Head::kBaseDimensions));
if (index < Head::kBaseDimensions) {
feature.ShiftIndex(Tail::kBaseDimensions);
} else {
feature.ShiftIndex(Tail::GetDimensions() - Tail::kBaseDimensions);
}
}
}
}
};
// 入力特徴量を学習用特徴量に変換するクラステンプレート
// FeatureSetのテンプレート引数が1つの場合の特殊化
template <typename FeatureType>
class Factorizer<FeatureSet<FeatureType>> {
public:
// 元の入力特徴量の次元数
static constexpr IndexType kBaseDimensions = FeatureType::kDimensions;
// 学習用特徴量の次元数を取得する
static constexpr IndexType GetDimensions() {
return Factorizer<FeatureType>::GetDimensions();
}
// 学習用特徴量のインデックスと学習率のスケールを取得する
static void AppendTrainingFeatures(
IndexType base_index, std::vector<TrainingFeature>* training_features,
IndexType base_dimensions = kBaseDimensions) {
assert(base_index < kBaseDimensions);
const auto start = training_features->size();
Factorizer<FeatureType>::AppendTrainingFeatures(
base_index, training_features);
for (auto i = start; i < training_features->size(); ++i) {
auto& feature = (*training_features)[i];
assert(feature.GetIndex() < Factorizer<FeatureType>::GetDimensions());
if (feature.GetIndex() >= kBaseDimensions) {
feature.ShiftIndex(base_dimensions - kBaseDimensions);
}
}
}
};
} // namespace Features
} // namespace NNUE
} // namespace Eval
#endif // defined(EVAL_NNUE)
#endif

View file

@ -0,0 +1,103 @@
// NNUE評価関数の特徴量変換クラステンプレートのHalfKP用特殊化
#ifndef _NNUE_TRAINER_FEATURES_FACTORIZER_HALF_KP_H_
#define _NNUE_TRAINER_FEATURES_FACTORIZER_HALF_KP_H_
#if defined(EVAL_NNUE)
#include "../../features/half_kp.h"
#include "../../features/p.h"
#include "../../features/half_relative_kp.h"
#include "factorizer.h"
namespace Eval {
namespace NNUE {
namespace Features {
// 入力特徴量を学習用特徴量に変換するクラステンプレート
// HalfKP用特殊化
template <Side AssociatedKing>
class Factorizer<HalfKP<AssociatedKing>> {
private:
using FeatureType = HalfKP<AssociatedKing>;
// 特徴量のうち、同時に値が1となるインデックスの数の最大値
static constexpr IndexType kMaxActiveDimensions =
FeatureType::kMaxActiveDimensions;
// 学習用特徴量の種類
enum TrainingFeatureType {
kFeaturesHalfKP,
kFeaturesHalfK,
kFeaturesP,
kFeaturesHalfRelativeKP,
kNumTrainingFeatureTypes,
};
// 学習用特徴量の情報
static constexpr FeatureProperties kProperties[] = {
// kFeaturesHalfKP
{true, FeatureType::kDimensions},
// kFeaturesHalfK
{true, SQUARE_NB},
// kFeaturesP
{true, Factorizer<P>::GetDimensions()},
// kFeaturesHalfRelativeKP
{true, Factorizer<HalfRelativeKP<AssociatedKing>>::GetDimensions()},
};
static_assert(GetArrayLength(kProperties) == kNumTrainingFeatureTypes, "");
public:
// 学習用特徴量の次元数を取得する
static constexpr IndexType GetDimensions() {
return GetActiveDimensions(kProperties);
}
// 学習用特徴量のインデックスと学習率のスケールを取得する
static void AppendTrainingFeatures(
IndexType base_index, std::vector<TrainingFeature>* training_features) {
// kFeaturesHalfKP
IndexType index_offset = AppendBaseFeature<FeatureType>(
kProperties[kFeaturesHalfKP], base_index, training_features);
const auto sq_k = static_cast<Square>(base_index / fe_end);
const auto p = static_cast<BonaPiece>(base_index % fe_end);
// kFeaturesHalfK
{
const auto& properties = kProperties[kFeaturesHalfK];
if (properties.active) {
training_features->emplace_back(index_offset + sq_k);
index_offset += properties.dimensions;
}
}
// kFeaturesP
index_offset += InheritFeaturesIfRequired<P>(
index_offset, kProperties[kFeaturesP], p, training_features);
// kFeaturesHalfRelativeKP
if (p >= fe_hand_end) {
index_offset += InheritFeaturesIfRequired<HalfRelativeKP<AssociatedKing>>(
index_offset, kProperties[kFeaturesHalfRelativeKP],
HalfRelativeKP<AssociatedKing>::MakeIndex(sq_k, p),
training_features);
} else {
index_offset += SkipFeatures(kProperties[kFeaturesHalfRelativeKP]);
}
assert(index_offset == GetDimensions());
}
};
template <Side AssociatedKing>
constexpr FeatureProperties Factorizer<HalfKP<AssociatedKing>>::kProperties[];
} // namespace Features
} // namespace NNUE
} // namespace Eval
#endif // defined(EVAL_NNUE)
#endif

View file

@ -0,0 +1,125 @@
// NNUE評価関数の学習用クラステンプレートの共通ヘッダ
#ifndef _NNUE_TRAINER_H_
#define _NNUE_TRAINER_H_
#if defined(EVAL_LEARN) && defined(EVAL_NNUE)
#include "../nnue_common.h"
#include "../features/index_list.h"
#include <sstream>
#if defined(USE_BLAS)
static_assert(std::is_same<LearnFloatType, float>::value, "");
#include <cblas.h>
#endif
namespace Eval {
namespace NNUE {
// 評価値と勝率の関係式で用いるPonanza定数
constexpr double kPonanzaConstant = 600.0;
// 学習用特徴量のインデックス1つを表すクラス
class TrainingFeature {
using StorageType = std::uint32_t;
static_assert(std::is_unsigned<StorageType>::value, "");
public:
static constexpr std::uint32_t kIndexBits = 24;
static_assert(kIndexBits < std::numeric_limits<StorageType>::digits, "");
static constexpr std::uint32_t kCountBits =
std::numeric_limits<StorageType>::digits - kIndexBits;
explicit TrainingFeature(IndexType index) :
index_and_count_((index << kCountBits) | 1) {
assert(index < (1 << kIndexBits));
}
TrainingFeature& operator+=(const TrainingFeature& other) {
assert(other.GetIndex() == GetIndex());
assert(other.GetCount() + GetCount() < (1 << kCountBits));
index_and_count_ += other.GetCount();
return *this;
}
IndexType GetIndex() const {
return static_cast<IndexType>(index_and_count_ >> kCountBits);
}
void ShiftIndex(IndexType offset) {
assert(GetIndex() + offset < (1 << kIndexBits));
index_and_count_ += offset << kCountBits;
}
IndexType GetCount() const {
return static_cast<IndexType>(index_and_count_ & ((1 << kCountBits) - 1));
}
bool operator<(const TrainingFeature& other) const {
return index_and_count_ < other.index_and_count_;
}
private:
StorageType index_and_count_;
};
// 学習データ1サンプルを表す構造体
struct Example {
std::vector<TrainingFeature> training_features[2];
Learner::PackedSfenValue psv;
int sign;
double weight;
};
// ハイパーパラメータの設定などに使用するメッセージ
struct Message {
Message(const std::string& name, const std::string& value = "") :
name(name), value(value), num_peekers(0), num_receivers(0) {}
const std::string name;
const std::string value;
std::uint32_t num_peekers;
std::uint32_t num_receivers;
};
// メッセージを受理するかどうかを判定する
bool ReceiveMessage(const std::string& name, Message* message) {
const auto subscript = "[" + std::to_string(message->num_peekers) + "]";
if (message->name.substr(0, name.size() + 1) == name + "[") {
++message->num_peekers;
}
if (message->name == name || message->name == name + subscript) {
++message->num_receivers;
return true;
}
return false;
}
// 文字列を分割する
std::vector<std::string> Split(const std::string& input, char delimiter) {
std::istringstream stream(input);
std::string field;
std::vector<std::string> fields;
while (std::getline(stream, field, delimiter)) {
fields.push_back(field);
}
return fields;
}
// 浮動小数点数を整数に丸める
template <typename IntType>
IntType Round(double value) {
return static_cast<IntType>(std::floor(value + 0.5));
}
// アライメント付きmake_shared
template <typename T, typename... ArgumentTypes>
std::shared_ptr<T> MakeAlignedSharedPtr(ArgumentTypes&&... arguments) {
const auto ptr = new(aligned_malloc(sizeof(T), alignof(T)))
T(std::forward<ArgumentTypes>(arguments)...);
return std::shared_ptr<T>(ptr, AlignedDeleter<T>());
}
} // namespace NNUE
} // namespace Eval
#endif // defined(EVAL_LEARN) && defined(EVAL_NNUE)
#endif

View file

@ -0,0 +1,301 @@
// NNUE評価関数の学習クラステンプレートのAffineTransform用特殊化
#ifndef _NNUE_TRAINER_AFFINE_TRANSFORM_H_
#define _NNUE_TRAINER_AFFINE_TRANSFORM_H_
#if defined(EVAL_LEARN) && defined(EVAL_NNUE)
#include "../../../learn/learn.h"
#include "../layers/affine_transform.h"
#include "trainer.h"
#include <random>
namespace Eval {
namespace NNUE {
// 学習:アフィン変換層
template <typename PreviousLayer, IndexType OutputDimensions>
class Trainer<Layers::AffineTransform<PreviousLayer, OutputDimensions>> {
private:
// 学習対象の層の型
using LayerType = Layers::AffineTransform<PreviousLayer, OutputDimensions>;
public:
// ファクトリ関数
static std::shared_ptr<Trainer> Create(
LayerType* target_layer, FeatureTransformer* feature_transformer) {
return std::shared_ptr<Trainer>(
new Trainer(target_layer, feature_transformer));
}
// ハイパーパラメータなどのオプションを設定する
void SendMessage(Message* message) {
previous_layer_trainer_->SendMessage(message);
if (ReceiveMessage("momentum", message)) {
momentum_ = static_cast<LearnFloatType>(std::stod(message->value));
}
if (ReceiveMessage("learning_rate_scale", message)) {
learning_rate_scale_ =
static_cast<LearnFloatType>(std::stod(message->value));
}
if (ReceiveMessage("reset", message)) {
DequantizeParameters();
}
if (ReceiveMessage("quantize_parameters", message)) {
QuantizeParameters();
}
}
// パラメータを乱数で初期化する
template <typename RNG>
void Initialize(RNG& rng) {
previous_layer_trainer_->Initialize(rng);
if (kIsOutputLayer) {
// 出力層は0で初期化する
std::fill(std::begin(biases_), std::end(biases_),
static_cast<LearnFloatType>(0.0));
std::fill(std::begin(weights_), std::end(weights_),
static_cast<LearnFloatType>(0.0));
} else {
// 入力の分布が各ユニット平均0.5、等分散であることを仮定し、
// 出力の分布が各ユニット平均0.5、入力と同じ等分散になるように初期化する
const double kSigma = 1.0 / std::sqrt(kInputDimensions);
auto distribution = std::normal_distribution<double>(0.0, kSigma);
for (IndexType i = 0; i < kOutputDimensions; ++i) {
double sum = 0.0;
for (IndexType j = 0; j < kInputDimensions; ++j) {
const auto weight = static_cast<LearnFloatType>(distribution(rng));
weights_[kInputDimensions * i + j] = weight;
sum += weight;
}
biases_[i] = static_cast<LearnFloatType>(0.5 - 0.5 * sum);
}
}
QuantizeParameters();
}
// 順伝播
const LearnFloatType* Propagate(const std::vector<Example>& batch) {
if (output_.size() < kOutputDimensions * batch.size()) {
output_.resize(kOutputDimensions * batch.size());
gradients_.resize(kInputDimensions * batch.size());
}
batch_size_ = static_cast<IndexType>(batch.size());
batch_input_ = previous_layer_trainer_->Propagate(batch);
#if defined(USE_BLAS)
for (IndexType b = 0; b < batch_size_; ++b) {
const IndexType batch_offset = kOutputDimensions * b;
cblas_scopy(kOutputDimensions, biases_, 1, &output_[batch_offset], 1);
}
cblas_sgemm(CblasColMajor, CblasTrans, CblasNoTrans,
kOutputDimensions, batch_size_, kInputDimensions, 1.0,
weights_, kInputDimensions,
batch_input_, kInputDimensions,
1.0, &output_[0], kOutputDimensions);
#else
for (IndexType b = 0; b < batch_size_; ++b) {
const IndexType input_batch_offset = kInputDimensions * b;
const IndexType output_batch_offset = kOutputDimensions * b;
for (IndexType i = 0; i < kOutputDimensions; ++i) {
double sum = biases_[i];
for (IndexType j = 0; j < kInputDimensions; ++j) {
const IndexType index = kInputDimensions * i + j;
sum += weights_[index] * batch_input_[input_batch_offset + j];
}
output_[output_batch_offset + i] = static_cast<LearnFloatType>(sum);
}
}
#endif
return output_.data();
}
// 逆伝播
void Backpropagate(const LearnFloatType* gradients,
LearnFloatType learning_rate) {
const LearnFloatType local_learning_rate =
learning_rate * learning_rate_scale_;
#if defined(USE_BLAS)
// backpropagate
cblas_sgemm(CblasColMajor, CblasNoTrans, CblasNoTrans,
kInputDimensions, batch_size_, kOutputDimensions, 1.0,
weights_, kInputDimensions,
gradients, kOutputDimensions,
0.0, &gradients_[0], kInputDimensions);
// update
cblas_sscal(kOutputDimensions, momentum_, biases_diff_, 1);
for (IndexType b = 0; b < batch_size_; ++b) {
const IndexType batch_offset = kOutputDimensions * b;
cblas_saxpy(kOutputDimensions, 1.0,
&gradients[batch_offset], 1, biases_diff_, 1);
}
cblas_saxpy(kOutputDimensions, -local_learning_rate,
biases_diff_, 1, biases_, 1);
cblas_sgemm(CblasRowMajor, CblasTrans, CblasNoTrans,
kOutputDimensions, kInputDimensions, batch_size_, 1.0,
gradients, kOutputDimensions,
batch_input_, kInputDimensions,
momentum_, weights_diff_, kInputDimensions);
cblas_saxpy(kOutputDimensions * kInputDimensions, -local_learning_rate,
weights_diff_, 1, weights_, 1);
#else
// backpropagate
for (IndexType b = 0; b < batch_size_; ++b) {
const IndexType input_batch_offset = kInputDimensions * b;
const IndexType output_batch_offset = kOutputDimensions * b;
for (IndexType j = 0; j < kInputDimensions; ++j) {
double sum = 0.0;
for (IndexType i = 0; i < kOutputDimensions; ++i) {
const IndexType index = kInputDimensions * i + j;
sum += weights_[index] * gradients[output_batch_offset + i];
}
gradients_[input_batch_offset + j] = static_cast<LearnFloatType>(sum);
}
}
// update
for (IndexType i = 0; i < kOutputDimensions; ++i) {
biases_diff_[i] *= momentum_;
}
for (IndexType i = 0; i < kOutputDimensions * kInputDimensions; ++i) {
weights_diff_[i] *= momentum_;
}
for (IndexType b = 0; b < batch_size_; ++b) {
const IndexType input_batch_offset = kInputDimensions * b;
const IndexType output_batch_offset = kOutputDimensions * b;
for (IndexType i = 0; i < kOutputDimensions; ++i) {
biases_diff_[i] += gradients[output_batch_offset + i];
}
for (IndexType i = 0; i < kOutputDimensions; ++i) {
for (IndexType j = 0; j < kInputDimensions; ++j) {
const IndexType index = kInputDimensions * i + j;
weights_diff_[index] += gradients[output_batch_offset + i] *
batch_input_[input_batch_offset + j];
}
}
}
for (IndexType i = 0; i < kOutputDimensions; ++i) {
biases_[i] -= local_learning_rate * biases_diff_[i];
}
for (IndexType i = 0; i < kOutputDimensions * kInputDimensions; ++i) {
weights_[i] -= local_learning_rate * weights_diff_[i];
}
#endif
previous_layer_trainer_->Backpropagate(gradients_.data(), learning_rate);
}
private:
// コンストラクタ
Trainer(LayerType* target_layer, FeatureTransformer* feature_transformer) :
batch_size_(0),
batch_input_(nullptr),
previous_layer_trainer_(Trainer<PreviousLayer>::Create(
&target_layer->previous_layer_, feature_transformer)),
target_layer_(target_layer),
biases_(),
weights_(),
biases_diff_(),
weights_diff_(),
momentum_(0.0),
learning_rate_scale_(1.0) {
DequantizeParameters();
}
// 重みの飽和とパラメータの整数化
void QuantizeParameters() {
for (IndexType i = 0; i < kOutputDimensions * kInputDimensions; ++i) {
weights_[i] = std::max(-kMaxWeightMagnitude,
std::min(+kMaxWeightMagnitude, weights_[i]));
}
for (IndexType i = 0; i < kOutputDimensions; ++i) {
target_layer_->biases_[i] =
Round<typename LayerType::BiasType>(biases_[i] * kBiasScale);
}
for (IndexType i = 0; i < kOutputDimensions; ++i) {
const auto offset = kInputDimensions * i;
const auto padded_offset = LayerType::kPaddedInputDimensions * i;
for (IndexType j = 0; j < kInputDimensions; ++j) {
target_layer_->weights_[padded_offset + j] =
Round<typename LayerType::WeightType>(
weights_[offset + j] * kWeightScale);
}
}
}
// 整数化されたパラメータの読み込み
void DequantizeParameters() {
for (IndexType i = 0; i < kOutputDimensions; ++i) {
biases_[i] = static_cast<LearnFloatType>(
target_layer_->biases_[i] / kBiasScale);
}
for (IndexType i = 0; i < kOutputDimensions; ++i) {
const auto offset = kInputDimensions * i;
const auto padded_offset = LayerType::kPaddedInputDimensions * i;
for (IndexType j = 0; j < kInputDimensions; ++j) {
weights_[offset + j] = static_cast<LearnFloatType>(
target_layer_->weights_[padded_offset + j] / kWeightScale);
}
}
std::fill(std::begin(biases_diff_), std::end(biases_diff_),
static_cast<LearnFloatType>(0.0));
std::fill(std::begin(weights_diff_), std::end(weights_diff_),
static_cast<LearnFloatType>(0.0));
}
// 入出力の次元数
static constexpr IndexType kInputDimensions = LayerType::kInputDimensions;
static constexpr IndexType kOutputDimensions = LayerType::kOutputDimensions;
// 出力の次元数が1なら出力層
static constexpr bool kIsOutputLayer = kOutputDimensions == 1;
// パラメータの整数化で用いる係数
static constexpr LearnFloatType kActivationScale =
std::numeric_limits<std::int8_t>::max();
static constexpr LearnFloatType kBiasScale = kIsOutputLayer ?
(kPonanzaConstant * FV_SCALE) :
((1 << kWeightScaleBits) * kActivationScale);
static constexpr LearnFloatType kWeightScale = kBiasScale / kActivationScale;
// パラメータの整数化でオーバーフローさせないために用いる重みの絶対値の上限
static constexpr LearnFloatType kMaxWeightMagnitude =
std::numeric_limits<typename LayerType::WeightType>::max() / kWeightScale;
// ミニバッチのサンプル数
IndexType batch_size_;
// ミニバッチの入力
const LearnFloatType* batch_input_;
// 直前の層のTrainer
const std::shared_ptr<Trainer<PreviousLayer>> previous_layer_trainer_;
// 学習対象の層
LayerType* const target_layer_;
// パラメータ
LearnFloatType biases_[kOutputDimensions];
LearnFloatType weights_[kOutputDimensions * kInputDimensions];
// パラメータの更新で用いるバッファ
LearnFloatType biases_diff_[kOutputDimensions];
LearnFloatType weights_diff_[kOutputDimensions * kInputDimensions];
// 順伝播用バッファ
std::vector<LearnFloatType> output_;
// 逆伝播用バッファ
std::vector<LearnFloatType> gradients_;
// ハイパーパラメータ
LearnFloatType momentum_;
LearnFloatType learning_rate_scale_;
};
} // namespace NNUE
} // namespace Eval
#endif // defined(EVAL_LEARN) && defined(EVAL_NNUE)
#endif

View file

@ -0,0 +1,142 @@
// NNUE評価関数の学習クラステンプレートのClippedReLU用特殊化
#ifndef _NNUE_TRAINER_CLIPPED_RELU_H_
#define _NNUE_TRAINER_CLIPPED_RELU_H_
#if defined(EVAL_LEARN) && defined(EVAL_NNUE)
#include "../../../learn/learn.h"
#include "../layers/clipped_relu.h"
#include "trainer.h"
namespace Eval {
namespace NNUE {
// 学習:アフィン変換層
template <typename PreviousLayer>
class Trainer<Layers::ClippedReLU<PreviousLayer>> {
private:
// 学習対象の層の型
using LayerType = Layers::ClippedReLU<PreviousLayer>;
public:
// ファクトリ関数
static std::shared_ptr<Trainer> Create(
LayerType* target_layer, FeatureTransformer* feature_transformer) {
return std::shared_ptr<Trainer>(
new Trainer(target_layer, feature_transformer));
}
// ハイパーパラメータなどのオプションを設定する
void SendMessage(Message* message) {
previous_layer_trainer_->SendMessage(message);
if (ReceiveMessage("check_health", message)) {
CheckHealth();
}
}
// パラメータを乱数で初期化する
template <typename RNG>
void Initialize(RNG& rng) {
previous_layer_trainer_->Initialize(rng);
}
// 順伝播
const LearnFloatType* Propagate(const std::vector<Example>& batch) {
if (output_.size() < kOutputDimensions * batch.size()) {
output_.resize(kOutputDimensions * batch.size());
gradients_.resize(kInputDimensions * batch.size());
}
const auto input = previous_layer_trainer_->Propagate(batch);
batch_size_ = static_cast<IndexType>(batch.size());
for (IndexType b = 0; b < batch_size_; ++b) {
const IndexType batch_offset = kOutputDimensions * b;
for (IndexType i = 0; i < kOutputDimensions; ++i) {
const IndexType index = batch_offset + i;
output_[index] = std::max(+kZero, std::min(+kOne, input[index]));
min_activations_[i] = std::min(min_activations_[i], output_[index]);
max_activations_[i] = std::max(max_activations_[i], output_[index]);
}
}
return output_.data();
}
// 逆伝播
void Backpropagate(const LearnFloatType* gradients,
LearnFloatType learning_rate) {
for (IndexType b = 0; b < batch_size_; ++b) {
const IndexType batch_offset = kOutputDimensions * b;
for (IndexType i = 0; i < kOutputDimensions; ++i) {
const IndexType index = batch_offset + i;
gradients_[index] = gradients[index] *
(output_[index] > kZero) * (output_[index] < kOne);
}
}
previous_layer_trainer_->Backpropagate(gradients_.data(), learning_rate);
}
private:
// コンストラクタ
Trainer(LayerType* target_layer, FeatureTransformer* feature_transformer) :
batch_size_(0),
previous_layer_trainer_(Trainer<PreviousLayer>::Create(
&target_layer->previous_layer_, feature_transformer)),
target_layer_(target_layer) {
std::fill(std::begin(min_activations_), std::end(min_activations_),
std::numeric_limits<LearnFloatType>::max());
std::fill(std::begin(max_activations_), std::end(max_activations_),
std::numeric_limits<LearnFloatType>::lowest());
}
// 学習に問題が生じていないかチェックする
void CheckHealth() {
const auto largest_min_activation = *std::max_element(
std::begin(min_activations_), std::end(min_activations_));
const auto smallest_max_activation = *std::min_element(
std::begin(max_activations_), std::end(max_activations_));
std::cout << "INFO: largest min activation = " << largest_min_activation
<< ", smallest max activation = " << smallest_max_activation
<< std::endl;
std::fill(std::begin(min_activations_), std::end(min_activations_),
std::numeric_limits<LearnFloatType>::max());
std::fill(std::begin(max_activations_), std::end(max_activations_),
std::numeric_limits<LearnFloatType>::lowest());
}
// 入出力の次元数
static constexpr IndexType kInputDimensions = LayerType::kOutputDimensions;
static constexpr IndexType kOutputDimensions = LayerType::kOutputDimensions;
// LearnFloatTypeの定数
static constexpr LearnFloatType kZero = static_cast<LearnFloatType>(0.0);
static constexpr LearnFloatType kOne = static_cast<LearnFloatType>(1.0);
// ミニバッチのサンプル数
IndexType batch_size_;
// 直前の層のTrainer
const std::shared_ptr<Trainer<PreviousLayer>> previous_layer_trainer_;
// 学習対象の層
LayerType* const target_layer_;
// 順伝播用バッファ
std::vector<LearnFloatType> output_;
// 逆伝播用バッファ
std::vector<LearnFloatType> gradients_;
// ヘルスチェック用統計値
LearnFloatType min_activations_[kOutputDimensions];
LearnFloatType max_activations_[kOutputDimensions];
};
} // namespace NNUE
} // namespace Eval
#endif // defined(EVAL_LEARN) && defined(EVAL_NNUE)
#endif

View file

@ -0,0 +1,377 @@
// NNUE評価関数の学習クラステンプレートのFeatureTransformer用特殊化
#ifndef _NNUE_TRAINER_FEATURE_TRANSFORMER_H_
#define _NNUE_TRAINER_FEATURE_TRANSFORMER_H_
#if defined(EVAL_LEARN) && defined(EVAL_NNUE)
#include "../../../learn/learn.h"
#include "../nnue_feature_transformer.h"
#include "trainer.h"
#include "features/factorizer_feature_set.h"
#include <array>
#include <bitset>
#include <numeric>
#include <random>
#include <set>
#if defined(_OPENMP)
#include <omp.h>
#endif
namespace Eval {
namespace NNUE {
// 学習:入力特徴量変換器
template <>
class Trainer<FeatureTransformer> {
private:
// 学習対象の層の型
using LayerType = FeatureTransformer;
public:
template <typename T>
friend struct AlignedDeleter;
template <typename T, typename... ArgumentTypes>
friend std::shared_ptr<T> MakeAlignedSharedPtr(ArgumentTypes&&... arguments);
// ファクトリ関数
static std::shared_ptr<Trainer> Create(LayerType* target_layer) {
return MakeAlignedSharedPtr<Trainer>(target_layer);
}
// ハイパーパラメータなどのオプションを設定する
void SendMessage(Message* message) {
if (ReceiveMessage("momentum", message)) {
momentum_ = static_cast<LearnFloatType>(std::stod(message->value));
}
if (ReceiveMessage("learning_rate_scale", message)) {
learning_rate_scale_ =
static_cast<LearnFloatType>(std::stod(message->value));
}
if (ReceiveMessage("reset", message)) {
DequantizeParameters();
}
if (ReceiveMessage("quantize_parameters", message)) {
QuantizeParameters();
}
if (ReceiveMessage("clear_unobserved_feature_weights", message)) {
ClearUnobservedFeatureWeights();
}
if (ReceiveMessage("check_health", message)) {
CheckHealth();
}
}
// パラメータを乱数で初期化する
template <typename RNG>
void Initialize(RNG& rng) {
std::fill(std::begin(weights_), std::end(weights_), +kZero);
const double kSigma = 0.1 / std::sqrt(RawFeatures::kMaxActiveDimensions);
auto distribution = std::normal_distribution<double>(0.0, kSigma);
for (IndexType i = 0; i < kHalfDimensions * RawFeatures::kDimensions; ++i) {
const auto weight = static_cast<LearnFloatType>(distribution(rng));
weights_[i] = weight;
}
for (IndexType i = 0; i < kHalfDimensions; ++i) {
biases_[i] = static_cast<LearnFloatType>(0.5);
}
QuantizeParameters();
}
// 順伝播
const LearnFloatType* Propagate(const std::vector<Example>& batch) {
if (output_.size() < kOutputDimensions * batch.size()) {
output_.resize(kOutputDimensions * batch.size());
gradients_.resize(kOutputDimensions * batch.size());
}
batch_ = &batch;
// affine transform
#pragma omp parallel for
for (IndexType b = 0; b < batch.size(); ++b) {
const IndexType batch_offset = kOutputDimensions * b;
for (IndexType c = 0; c < 2; ++c) {
const IndexType output_offset = batch_offset + kHalfDimensions * c;
#if defined(USE_BLAS)
cblas_scopy(kHalfDimensions, biases_, 1, &output_[output_offset], 1);
for (const auto& feature : batch[b].training_features[c]) {
const IndexType weights_offset = kHalfDimensions * feature.GetIndex();
cblas_saxpy(kHalfDimensions, (float)feature.GetCount(),
&weights_[weights_offset], 1, &output_[output_offset], 1);
}
#else
for (IndexType i = 0; i < kHalfDimensions; ++i) {
output_[output_offset + i] = biases_[i];
}
for (const auto& feature : batch[b].training_features[c]) {
const IndexType weights_offset = kHalfDimensions * feature.GetIndex();
for (IndexType i = 0; i < kHalfDimensions; ++i) {
output_[output_offset + i] +=
feature.GetCount() * weights_[weights_offset + i];
}
}
#endif
}
}
// clipped ReLU
for (IndexType b = 0; b < batch.size(); ++b) {
const IndexType batch_offset = kOutputDimensions * b;
for (IndexType i = 0; i < kOutputDimensions; ++i) {
const IndexType index = batch_offset + i;
min_pre_activation_ = std::min(min_pre_activation_, output_[index]);
max_pre_activation_ = std::max(max_pre_activation_, output_[index]);
output_[index] = std::max(+kZero, std::min(+kOne, output_[index]));
const IndexType t = i % kHalfDimensions;
min_activations_[t] = std::min(min_activations_[t], output_[index]);
max_activations_[t] = std::max(max_activations_[t], output_[index]);
}
}
return output_.data();
}
// 逆伝播
void Backpropagate(const LearnFloatType* gradients,
LearnFloatType learning_rate) {
const LearnFloatType local_learning_rate =
learning_rate * learning_rate_scale_;
for (IndexType b = 0; b < batch_->size(); ++b) {
const IndexType batch_offset = kOutputDimensions * b;
for (IndexType i = 0; i < kOutputDimensions; ++i) {
const IndexType index = batch_offset + i;
gradients_[index] = gradients[index] *
((output_[index] > kZero) * (output_[index] < kOne));
}
}
// 重み行列は入力に出現した特徴量に対応する列のみを更新するため、
// momentumを使用せず、学習率を補正してスケールを合わせる
const LearnFloatType effective_learning_rate =
static_cast<LearnFloatType>(local_learning_rate / (1.0 - momentum_));
#if defined(USE_BLAS)
cblas_sscal(kHalfDimensions, momentum_, biases_diff_, 1);
for (IndexType b = 0; b < batch_->size(); ++b) {
const IndexType batch_offset = kOutputDimensions * b;
for (IndexType c = 0; c < 2; ++c) {
const IndexType output_offset = batch_offset + kHalfDimensions * c;
cblas_saxpy(kHalfDimensions, 1.0,
&gradients_[output_offset], 1, biases_diff_, 1);
}
}
cblas_saxpy(kHalfDimensions, -local_learning_rate,
biases_diff_, 1, biases_, 1);
#pragma omp parallel
{
#if defined(_OPENMP)
const IndexType num_threads = omp_get_num_threads();
const IndexType thread_index = omp_get_thread_num();
#endif
for (IndexType b = 0; b < batch_->size(); ++b) {
const IndexType batch_offset = kOutputDimensions * b;
for (IndexType c = 0; c < 2; ++c) {
const IndexType output_offset = batch_offset + kHalfDimensions * c;
for (const auto& feature : (*batch_)[b].training_features[c]) {
#if defined(_OPENMP)
if (feature.GetIndex() % num_threads != thread_index) continue;
#endif
const IndexType weights_offset =
kHalfDimensions * feature.GetIndex();
const auto scale = static_cast<LearnFloatType>(
effective_learning_rate / feature.GetCount());
cblas_saxpy(kHalfDimensions, -scale,
&gradients_[output_offset], 1,
&weights_[weights_offset], 1);
}
}
}
}
#else
for (IndexType i = 0; i < kHalfDimensions; ++i) {
biases_diff_[i] *= momentum_;
}
for (IndexType b = 0; b < batch_->size(); ++b) {
const IndexType batch_offset = kOutputDimensions * b;
for (IndexType c = 0; c < 2; ++c) {
const IndexType output_offset = batch_offset + kHalfDimensions * c;
for (IndexType i = 0; i < kHalfDimensions; ++i) {
biases_diff_[i] += gradients_[output_offset + i];
}
}
}
for (IndexType i = 0; i < kHalfDimensions; ++i) {
biases_[i] -= local_learning_rate * biases_diff_[i];
}
for (IndexType b = 0; b < batch_->size(); ++b) {
const IndexType batch_offset = kOutputDimensions * b;
for (IndexType c = 0; c < 2; ++c) {
const IndexType output_offset = batch_offset + kHalfDimensions * c;
for (const auto& feature : (*batch_)[b].training_features[c]) {
const IndexType weights_offset = kHalfDimensions * feature.GetIndex();
const auto scale = static_cast<LearnFloatType>(
effective_learning_rate / feature.GetCount());
for (IndexType i = 0; i < kHalfDimensions; ++i) {
weights_[weights_offset + i] -=
scale * gradients_[output_offset + i];
}
}
}
}
#endif
for (IndexType b = 0; b < batch_->size(); ++b) {
for (IndexType c = 0; c < 2; ++c) {
for (const auto& feature : (*batch_)[b].training_features[c]) {
observed_features.set(feature.GetIndex());
}
}
}
}
private:
// コンストラクタ
Trainer(LayerType* target_layer) :
batch_(nullptr),
target_layer_(target_layer),
biases_(),
weights_(),
biases_diff_(),
momentum_(0.0),
learning_rate_scale_(1.0) {
min_pre_activation_ = std::numeric_limits<LearnFloatType>::max();
max_pre_activation_ = std::numeric_limits<LearnFloatType>::lowest();
std::fill(std::begin(min_activations_), std::end(min_activations_),
std::numeric_limits<LearnFloatType>::max());
std::fill(std::begin(max_activations_), std::end(max_activations_),
std::numeric_limits<LearnFloatType>::lowest());
DequantizeParameters();
}
// 重みの飽和とパラメータの整数化
void QuantizeParameters() {
for (IndexType i = 0; i < kHalfDimensions; ++i) {
target_layer_->biases_[i] =
Round<typename LayerType::BiasType>(biases_[i] * kBiasScale);
}
std::vector<TrainingFeature> training_features;
#pragma omp parallel for private(training_features)
for (IndexType j = 0; j < RawFeatures::kDimensions; ++j) {
training_features.clear();
Features::Factorizer<RawFeatures>::AppendTrainingFeatures(
j, &training_features);
for (IndexType i = 0; i < kHalfDimensions; ++i) {
double sum = 0.0;
for (const auto& feature : training_features) {
sum += weights_[kHalfDimensions * feature.GetIndex() + i];
}
target_layer_->weights_[kHalfDimensions * j + i] =
Round<typename LayerType::WeightType>(sum * kWeightScale);
}
}
}
// 整数化されたパラメータの読み込み
void DequantizeParameters() {
for (IndexType i = 0; i < kHalfDimensions; ++i) {
biases_[i] = static_cast<LearnFloatType>(
target_layer_->biases_[i] / kBiasScale);
}
std::fill(std::begin(weights_), std::end(weights_), +kZero);
for (IndexType i = 0; i < kHalfDimensions * RawFeatures::kDimensions; ++i) {
weights_[i] = static_cast<LearnFloatType>(
target_layer_->weights_[i] / kWeightScale);
}
std::fill(std::begin(biases_diff_), std::end(biases_diff_), +kZero);
}
// 学習データに出現していない特徴量に対応する重みを0にする
void ClearUnobservedFeatureWeights() {
for (IndexType i = 0; i < kInputDimensions; ++i) {
if (!observed_features.test(i)) {
std::fill(std::begin(weights_) + kHalfDimensions * i,
std::begin(weights_) + kHalfDimensions * (i + 1), +kZero);
}
}
QuantizeParameters();
}
// 学習に問題が生じていないかチェックする
void CheckHealth() {
std::cout << "INFO: observed " << observed_features.count()
<< " (out of " << kInputDimensions << ") features" << std::endl;
constexpr LearnFloatType kPreActivationLimit =
std::numeric_limits<typename LayerType::WeightType>::max() /
kWeightScale;
std::cout << "INFO: (min, max) of pre-activations = "
<< min_pre_activation_ << ", "
<< max_pre_activation_ << " (limit = "
<< kPreActivationLimit << ")" << std::endl;
const auto largest_min_activation = *std::max_element(
std::begin(min_activations_), std::end(min_activations_));
const auto smallest_max_activation = *std::min_element(
std::begin(max_activations_), std::end(max_activations_));
std::cout << "INFO: largest min activation = " << largest_min_activation
<< ", smallest max activation = " << smallest_max_activation
<< std::endl;
std::fill(std::begin(min_activations_), std::end(min_activations_),
std::numeric_limits<LearnFloatType>::max());
std::fill(std::begin(max_activations_), std::end(max_activations_),
std::numeric_limits<LearnFloatType>::lowest());
}
// 入出力の次元数
static constexpr IndexType kInputDimensions =
Features::Factorizer<RawFeatures>::GetDimensions();
static constexpr IndexType kOutputDimensions = LayerType::kOutputDimensions;
static constexpr IndexType kHalfDimensions = LayerType::kHalfDimensions;
// パラメータの整数化で用いる係数
static constexpr LearnFloatType kActivationScale =
std::numeric_limits<std::int8_t>::max();
static constexpr LearnFloatType kBiasScale = kActivationScale;
static constexpr LearnFloatType kWeightScale = kActivationScale;
// LearnFloatTypeの定数
static constexpr LearnFloatType kZero = static_cast<LearnFloatType>(0.0);
static constexpr LearnFloatType kOne = static_cast<LearnFloatType>(1.0);
// ミニバッチ
const std::vector<Example>* batch_;
// 学習対象の層
LayerType* const target_layer_;
// パラメータ
alignas(kCacheLineSize) LearnFloatType biases_[kHalfDimensions];
alignas(kCacheLineSize)
LearnFloatType weights_[kHalfDimensions * kInputDimensions];
// パラメータの更新で用いるバッファ
LearnFloatType biases_diff_[kHalfDimensions];
std::vector<LearnFloatType> gradients_;
// 順伝播用バッファ
std::vector<LearnFloatType> output_;
// 学習データに出現した特徴量
std::bitset<kInputDimensions> observed_features;
// ハイパーパラメータ
LearnFloatType momentum_;
LearnFloatType learning_rate_scale_;
// ヘルスチェック用統計値
LearnFloatType min_pre_activation_;
LearnFloatType max_pre_activation_;
LearnFloatType min_activations_[kHalfDimensions];
LearnFloatType max_activations_[kHalfDimensions];
};
} // namespace NNUE
} // namespace Eval
#endif // defined(EVAL_LEARN) && defined(EVAL_NNUE)
#endif

View file

@ -0,0 +1,251 @@
// NNUE評価関数の学習クラステンプレートのInputSlice用特殊化
#ifndef _NNUE_TRAINER_INPUT_SLICE_H_
#define _NNUE_TRAINER_INPUT_SLICE_H_
#if defined(EVAL_LEARN) && defined(EVAL_NNUE)
#include "../../../learn/learn.h"
#include "../layers/input_slice.h"
#include "trainer.h"
namespace Eval {
namespace NNUE {
// 学習:入力層
class SharedInputTrainer {
public:
// ファクトリ関数
static std::shared_ptr<SharedInputTrainer> Create(
FeatureTransformer* feature_transformer) {
static std::shared_ptr<SharedInputTrainer> instance;
if (!instance) {
instance.reset(new SharedInputTrainer(feature_transformer));
}
++instance->num_referrers_;
return instance;
}
// ハイパーパラメータなどのオプションを設定する
void SendMessage(Message* message) {
if (num_calls_ == 0) {
current_operation_ = Operation::kSendMessage;
feature_transformer_trainer_->SendMessage(message);
}
assert(current_operation_ == Operation::kSendMessage);
if (++num_calls_ == num_referrers_) {
num_calls_ = 0;
current_operation_ = Operation::kNone;
}
}
// パラメータを乱数で初期化する
template <typename RNG>
void Initialize(RNG& rng) {
if (num_calls_ == 0) {
current_operation_ = Operation::kInitialize;
feature_transformer_trainer_->Initialize(rng);
}
assert(current_operation_ == Operation::kInitialize);
if (++num_calls_ == num_referrers_) {
num_calls_ = 0;
current_operation_ = Operation::kNone;
}
}
// 順伝播
const LearnFloatType* Propagate(const std::vector<Example>& batch) {
if (gradients_.size() < kInputDimensions * batch.size()) {
gradients_.resize(kInputDimensions * batch.size());
}
batch_size_ = static_cast<IndexType>(batch.size());
if (num_calls_ == 0) {
current_operation_ = Operation::kPropagate;
output_ = feature_transformer_trainer_->Propagate(batch);
}
assert(current_operation_ == Operation::kPropagate);
if (++num_calls_ == num_referrers_) {
num_calls_ = 0;
current_operation_ = Operation::kNone;
}
return output_;
}
// 逆伝播
void Backpropagate(const LearnFloatType* gradients,
LearnFloatType learning_rate) {
if (num_referrers_ == 1) {
feature_transformer_trainer_->Backpropagate(gradients, learning_rate);
return;
}
if (num_calls_ == 0) {
current_operation_ = Operation::kBackPropagate;
for (IndexType b = 0; b < batch_size_; ++b) {
const IndexType batch_offset = kInputDimensions * b;
for (IndexType i = 0; i < kInputDimensions; ++i) {
gradients_[batch_offset + i] = static_cast<LearnFloatType>(0.0);
}
}
}
assert(current_operation_ == Operation::kBackPropagate);
for (IndexType b = 0; b < batch_size_; ++b) {
const IndexType batch_offset = kInputDimensions * b;
for (IndexType i = 0; i < kInputDimensions; ++i) {
gradients_[batch_offset + i] += gradients[batch_offset + i];
}
}
if (++num_calls_ == num_referrers_) {
feature_transformer_trainer_->Backpropagate(
gradients_.data(), learning_rate);
num_calls_ = 0;
current_operation_ = Operation::kNone;
}
}
private:
// コンストラクタ
SharedInputTrainer(FeatureTransformer* feature_transformer) :
batch_size_(0),
num_referrers_(0),
num_calls_(0),
current_operation_(Operation::kNone),
feature_transformer_trainer_(Trainer<FeatureTransformer>::Create(
feature_transformer)),
output_(nullptr) {
}
// 入出力の次元数
static constexpr IndexType kInputDimensions =
FeatureTransformer::kOutputDimensions;
// 処理の種類
enum class Operation {
kNone,
kSendMessage,
kInitialize,
kPropagate,
kBackPropagate,
};
// ミニバッチのサンプル数
IndexType batch_size_;
// この層を入力として共有する層の数
std::uint32_t num_referrers_;
// 現在の処理が呼び出された回数
std::uint32_t num_calls_;
// 現在の処理の種類
Operation current_operation_;
// 入力特徴量変換器のTrainer
const std::shared_ptr<Trainer<FeatureTransformer>>
feature_transformer_trainer_;
// 順伝播用に共有する出力のポインタ
const LearnFloatType* output_;
// 逆伝播用バッファ
std::vector<LearnFloatType> gradients_;
};
// 学習:入力層
template <IndexType OutputDimensions, IndexType Offset>
class Trainer<Layers::InputSlice<OutputDimensions, Offset>> {
private:
// 学習対象の層の型
using LayerType = Layers::InputSlice<OutputDimensions, Offset>;
public:
// ファクトリ関数
static std::shared_ptr<Trainer> Create(
LayerType* /*target_layer*/, FeatureTransformer* feature_transformer) {
return std::shared_ptr<Trainer>(new Trainer(feature_transformer));
}
// ハイパーパラメータなどのオプションを設定する
void SendMessage(Message* message) {
shared_input_trainer_->SendMessage(message);
}
// パラメータを乱数で初期化する
template <typename RNG>
void Initialize(RNG& rng) {
shared_input_trainer_->Initialize(rng);
}
// 順伝播
const LearnFloatType* Propagate(const std::vector<Example>& batch) {
if (output_.size() < kOutputDimensions * batch.size()) {
output_.resize(kOutputDimensions * batch.size());
gradients_.resize(kInputDimensions * batch.size());
}
batch_size_ = static_cast<IndexType>(batch.size());
const auto input = shared_input_trainer_->Propagate(batch);
for (IndexType b = 0; b < batch_size_; ++b) {
const IndexType input_offset = kInputDimensions * b;
const IndexType output_offset = kOutputDimensions * b;
#if defined(USE_BLAS)
cblas_scopy(kOutputDimensions, &input[input_offset + Offset], 1,
&output_[output_offset], 1);
#else
for (IndexType i = 0; i < kOutputDimensions; ++i) {
output_[output_offset + i] = input[input_offset + Offset + i];
}
#endif
}
return output_.data();
}
// 逆伝播
void Backpropagate(const LearnFloatType* gradients,
LearnFloatType learning_rate) {
for (IndexType b = 0; b < batch_size_; ++b) {
const IndexType input_offset = kInputDimensions * b;
const IndexType output_offset = kOutputDimensions * b;
for (IndexType i = 0; i < kInputDimensions; ++i) {
if (i < Offset || i >= Offset + kOutputDimensions) {
gradients_[input_offset + i] = static_cast<LearnFloatType>(0.0);
} else {
gradients_[input_offset + i] = gradients[output_offset + i - Offset];
}
}
}
shared_input_trainer_->Backpropagate(gradients_.data(), learning_rate);
}
private:
// コンストラクタ
Trainer(FeatureTransformer* feature_transformer) :
batch_size_(0),
shared_input_trainer_(SharedInputTrainer::Create(feature_transformer)) {
}
// 入出力の次元数
static constexpr IndexType kInputDimensions =
FeatureTransformer::kOutputDimensions;
static constexpr IndexType kOutputDimensions = OutputDimensions;
static_assert(Offset + kOutputDimensions <= kInputDimensions, "");
// ミニバッチのサンプル数
IndexType batch_size_;
// 共有入力層のTrainer
const std::shared_ptr<SharedInputTrainer> shared_input_trainer_;
// 順伝播用バッファ
std::vector<LearnFloatType> output_;
// 逆伝播用バッファ
std::vector<LearnFloatType> gradients_;
};
} // namespace NNUE
} // namespace Eval
#endif // defined(EVAL_LEARN) && defined(EVAL_NNUE)
#endif

View file

@ -0,0 +1,190 @@
// NNUE評価関数の学習クラステンプレートのSum用特殊化
#ifndef _NNUE_TRAINER_SUM_H_
#define _NNUE_TRAINER_SUM_H_
#if defined(EVAL_LEARN) && defined(EVAL_NNUE)
#include "../../../learn/learn.h"
#include "../layers/sum.h"
#include "trainer.h"
namespace Eval {
namespace NNUE {
// 学習:複数の層の出力の和を取る層
template <typename FirstPreviousLayer, typename... RemainingPreviousLayers>
class Trainer<Layers::Sum<FirstPreviousLayer, RemainingPreviousLayers...>> :
Trainer<Layers::Sum<RemainingPreviousLayers...>> {
private:
// 学習対象の層の型
using LayerType = Layers::Sum<FirstPreviousLayer, RemainingPreviousLayers...>;
using Tail = Trainer<Layers::Sum<RemainingPreviousLayers...>>;
public:
// ファクトリ関数
static std::shared_ptr<Trainer> Create(
LayerType* target_layer, FeatureTransformer* feature_transformer) {
return std::shared_ptr<Trainer>(
new Trainer(target_layer, feature_transformer));
}
// ハイパーパラメータなどのオプションを設定する
void SendMessage(Message* message) {
// 他のメンバ関数の結果は処理の順番に依存しないため、
// 実装をシンプルにすることを目的としてTailを先に処理するが、
// SendMessageは添字の対応を分かりやすくするためにHeadを先に処理する
previous_layer_trainer_->SendMessage(message);
Tail::SendMessage(message);
}
// パラメータを乱数で初期化する
template <typename RNG>
void Initialize(RNG& rng) {
Tail::Initialize(rng);
previous_layer_trainer_->Initialize(rng);
}
// 順伝播
/*const*/ LearnFloatType* Propagate(const std::vector<Example>& batch) {
batch_size_ = static_cast<IndexType>(batch.size());
auto output = Tail::Propagate(batch);
const auto head_output = previous_layer_trainer_->Propagate(batch);
#if defined(USE_BLAS)
cblas_saxpy(kOutputDimensions * batch_size_, 1.0,
head_output, 1, output, 1);
#else
for (IndexType b = 0; b < batch_size_; ++b) {
const IndexType batch_offset = kOutputDimensions * b;
for (IndexType i = 0; i < kOutputDimensions; ++i) {
output[batch_offset + i] += head_output[batch_offset + i];
}
}
#endif
return output;
}
// 逆伝播
void Backpropagate(const LearnFloatType* gradients,
LearnFloatType learning_rate) {
Tail::Backpropagate(gradients, learning_rate);
previous_layer_trainer_->Backpropagate(gradients, learning_rate);
}
private:
// コンストラクタ
Trainer(LayerType* target_layer, FeatureTransformer* feature_transformer) :
Tail(target_layer, feature_transformer),
batch_size_(0),
previous_layer_trainer_(Trainer<FirstPreviousLayer>::Create(
&target_layer->previous_layer_, feature_transformer)),
target_layer_(target_layer) {
}
// 入出力の次元数
static constexpr IndexType kOutputDimensions = LayerType::kOutputDimensions;
// サブクラスをfriendにする
template <typename SumLayer>
friend class Trainer;
// ミニバッチのサンプル数
IndexType batch_size_;
// 直前の層のTrainer
const std::shared_ptr<Trainer<FirstPreviousLayer>> previous_layer_trainer_;
// 学習対象の層
LayerType* const target_layer_;
};
// 学習複数の層の出力の和を取る層テンプレート引数が1つの場合
template <typename PreviousLayer>
class Trainer<Layers::Sum<PreviousLayer>> {
private:
// 学習対象の層の型
using LayerType = Layers::Sum<PreviousLayer>;
public:
// ファクトリ関数
static std::shared_ptr<Trainer> Create(
LayerType* target_layer, FeatureTransformer* feature_transformer) {
return std::shared_ptr<Trainer>(
new Trainer(target_layer, feature_transformer));
}
// ハイパーパラメータなどのオプションを設定する
void SendMessage(Message* message) {
previous_layer_trainer_->SendMessage(message);
}
// パラメータを乱数で初期化する
template <typename RNG>
void Initialize(RNG& rng) {
previous_layer_trainer_->Initialize(rng);
}
// 順伝播
/*const*/ LearnFloatType* Propagate(const std::vector<Example>& batch) {
if (output_.size() < kOutputDimensions * batch.size()) {
output_.resize(kOutputDimensions * batch.size());
}
batch_size_ = static_cast<IndexType>(batch.size());
const auto output = previous_layer_trainer_->Propagate(batch);
#if defined(USE_BLAS)
cblas_scopy(kOutputDimensions * batch_size_, output, 1, &output_[0], 1);
#else
for (IndexType b = 0; b < batch_size_; ++b) {
const IndexType batch_offset = kOutputDimensions * b;
for (IndexType i = 0; i < kOutputDimensions; ++i) {
output_[batch_offset + i] = output[batch_offset + i];
}
}
#endif
return output_.data();
}
// 逆伝播
void Backpropagate(const LearnFloatType* gradients,
LearnFloatType learning_rate) {
previous_layer_trainer_->Backpropagate(gradients, learning_rate);
}
private:
// コンストラクタ
Trainer(LayerType* target_layer, FeatureTransformer* feature_transformer) :
batch_size_(0),
previous_layer_trainer_(Trainer<PreviousLayer>::Create(
&target_layer->previous_layer_, feature_transformer)),
target_layer_(target_layer) {
}
// 入出力の次元数
static constexpr IndexType kOutputDimensions = LayerType::kOutputDimensions;
// サブクラスをfriendにする
template <typename SumLayer>
friend class Trainer;
// ミニバッチのサンプル数
IndexType batch_size_;
// 直前の層のTrainer
const std::shared_ptr<Trainer<PreviousLayer>> previous_layer_trainer_;
// 学習対象の層
LayerType* const target_layer_;
// 順伝播用バッファ
std::vector<LearnFloatType> output_;
};
} // namespace NNUE
} // namespace Eval
#endif // defined(EVAL_LEARN) && defined(EVAL_NNUE)
#endif

View file

@ -22,6 +22,7 @@
#include <cassert>
#include <cstring> // For std::memset
#include <iomanip>
#include <set>
#include <sstream>
#include "bitboard.h"
@ -29,6 +30,7 @@
#include "material.h"
#include "pawns.h"
#include "thread.h"
#include "eval/nnue/evaluate_nnue.h"
namespace Trace {
@ -877,9 +879,11 @@ namespace {
/// evaluate() is the evaluator for the outer world. It returns a static
/// evaluation of the position from the point of view of the side to move.
#if !defined(EVAL_NNUE)
Value Eval::evaluate(const Position& pos) {
return Evaluation<NO_TRACE>(pos).value();
}
#endif // defined(EVAL_NNUE)
/// trace() is like evaluate(), but instead of returning a value, it returns
@ -924,3 +928,138 @@ std::string Eval::trace(const Position& pos) {
return ss.str();
}
#if defined(EVAL_NNUE) || defined(EVAL_LEARN)
namespace Eval {
ExtBonaPiece kpp_board_index[PIECE_NB] = {
{ BONA_PIECE_ZERO, BONA_PIECE_ZERO },
{ f_pawn, e_pawn },
{ f_knight, e_knight },
{ f_bishop, e_bishop },
{ f_rook, e_rook },
{ f_queen, e_queen },
{ f_king, e_king },
{ BONA_PIECE_ZERO, BONA_PIECE_ZERO },
// 後手から見た場合。fとeが入れ替わる。
{ BONA_PIECE_ZERO, BONA_PIECE_ZERO },
{ e_pawn, f_pawn },
{ e_knight, f_knight },
{ e_bishop, f_bishop },
{ e_rook, f_rook },
{ e_queen, f_queen },
{ e_king, f_king },
{ BONA_PIECE_ZERO, BONA_PIECE_ZERO }, // 金の成りはない
};
// 内部で保持しているpieceListFw[]が正しいBonaPieceであるかを検査する。
// 注 : デバッグ用。遅い。
bool EvalList::is_valid(const Position& pos)
{
std::set<PieceNumber> piece_numbers;
for (Square sq = SQ_A1; sq != SQUARE_NB; ++sq) {
auto piece_number = piece_no_of_board(sq);
if (piece_number == PIECE_NUMBER_NB) {
continue;
}
assert(!piece_numbers.count(piece_number));
piece_numbers.insert(piece_number);
}
for (int i = 0; i < length(); ++i)
{
BonaPiece fw = pieceListFw[i];
// このfwが本当に存在するかをPositionクラスのほうに調べに行く。
if (fw == Eval::BONA_PIECE_ZERO) {
continue;
}
// 範囲外
if (!(0 <= fw && fw < fe_end))
return false;
// 盤上の駒なのでこの駒が本当に存在するか調べにいく。
for (Piece pc = NO_PIECE; pc < PIECE_NB; ++pc)
{
auto pt = type_of(pc);
if (pt == NO_PIECE || pt == 7) // 存在しない駒
continue;
// 駒pcのBonaPieceの開始番号
auto s = BonaPiece(kpp_board_index[pc].fw);
if (s <= fw && fw < s + SQUARE_NB)
{
// 見つかったのでこの駒がsqの地点にあるかを調べる。
Square sq = (Square)(fw - s);
Piece pc2 = pos.piece_on(sq);
if (pc2 != pc)
return false;
goto Found;
}
}
// 何故か存在しない駒であった..
return false;
Found:;
}
// Validate piece_no_list_board
for (auto sq = SQUARE_ZERO; sq < SQUARE_NB; ++sq) {
Piece expected_piece = pos.piece_on(sq);
PieceNumber piece_number = piece_no_list_board[sq];
if (piece_number == PIECE_NUMBER_NB) {
assert(expected_piece == NO_PIECE);
if (expected_piece != NO_PIECE) {
return false;
}
continue;
}
BonaPiece bona_piece_white = pieceListFw[piece_number];
Piece actual_piece;
for (actual_piece = NO_PIECE; actual_piece < PIECE_NB; ++actual_piece) {
if (kpp_board_index[actual_piece].fw == BONA_PIECE_ZERO) {
continue;
}
if (kpp_board_index[actual_piece].fw <= bona_piece_white
&& bona_piece_white < kpp_board_index[actual_piece].fw + SQUARE_NB) {
break;
}
}
assert(actual_piece != PIECE_NB);
if (actual_piece == PIECE_NB) {
return false;
}
assert(actual_piece == expected_piece);
if (actual_piece != expected_piece) {
return false;
}
Square actual_square = static_cast<Square>(
bona_piece_white - kpp_board_index[actual_piece].fw);
assert(sq == actual_square);
if (sq != actual_square) {
return false;
}
}
return true;
}
}
#endif // defined(EVAL_NNUE) || defined(EVAL_LEARN)
#if !defined(EVAL_NNUE)
namespace Eval {
void evaluate_with_no_return(const Position& pos) {}
void update_weights(uint64_t epoch, const std::array<bool, 4> & freeze) {}
void init_grad(double eta1, uint64_t eta_epoch, double eta2, uint64_t eta2_epoch, double eta3) {}
void add_grad(Position& pos, Color rootColor, double delt_grad, const std::array<bool, 4> & freeze) {}
void save_eval(std::string suffix) {}
double get_eta() { return 0.0; }
}
#endif // defined(EVAL_NNUE)

View file

@ -32,6 +32,191 @@ namespace Eval {
std::string trace(const Position& pos);
Value evaluate(const Position& pos);
void evaluate_with_no_return(const Position& pos);
Value compute_eval(const Position& pos);
#if defined(EVAL_NNUE) || defined(EVAL_LEARN)
// 評価関数ファイルを読み込む。
// これは、"is_ready"コマンドの応答時に1度だけ呼び出される。2度呼び出すことは想定していない。
// (ただし、EvalDir(評価関数フォルダ)が変更になったあと、isreadyが再度送られてきたら読みなおす。)
void load_eval();
static uint64_t calc_check_sum() { return 0; }
static void print_softname(uint64_t check_sum) {}
// --- 評価関数で使う定数 KPP(玉と任意2駒)のPに相当するenum
// (評価関数の実験のときには、BonaPieceは自由に定義したいのでここでは定義しない。)
// BonanzaでKKP/KPPと言うときのP(Piece)を表現する型。
// Σ KPPを求めるときに、39の地点の歩のように、升×駒種に対して一意な番号が必要となる。
enum BonaPiece : int32_t
{
// f = friend(≒先手)の意味。e = enemy(≒後手)の意味
// 未初期化の時の値
BONA_PIECE_NOT_INIT = -1,
// 無効な駒。駒落ちのときなどは、不要な駒をここに移動させる。
BONA_PIECE_ZERO = 0,
fe_hand_end = BONA_PIECE_ZERO + 1,
// Bonanzaのように盤上のありえない升の歩や香の番号を詰めない。
// 理由1) 学習のときに相対PPで1段目に香がいるときがあって、それを逆変換において正しく表示するのが難しい。
// 理由2) 縦型BitboardだとSquareからの変換に困る。
// --- 盤上の駒
f_pawn = fe_hand_end,
e_pawn = f_pawn + SQUARE_NB,
f_knight = e_pawn + SQUARE_NB,
e_knight = f_knight + SQUARE_NB,
f_bishop = e_knight + SQUARE_NB,
e_bishop = f_bishop + SQUARE_NB,
f_rook = e_bishop + SQUARE_NB,
e_rook = f_rook + SQUARE_NB,
f_queen = e_rook + SQUARE_NB,
e_queen = f_queen + SQUARE_NB,
fe_end = e_queen + SQUARE_NB,
f_king = fe_end,
e_king = f_king + SQUARE_NB,
fe_end2 = e_king + SQUARE_NB, // 玉も含めた末尾の番号。
};
#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の歩)の番号とを
// ペアにしたものをExtBonaPiece型と呼ぶことにする。
union ExtBonaPiece
{
struct {
BonaPiece fw; // from white
BonaPiece fb; // from black
};
BonaPiece from[2];
ExtBonaPiece() {}
ExtBonaPiece(BonaPiece fw_, BonaPiece fb_) : fw(fw_), fb(fb_) {}
};
// 駒が今回の指し手によってどこからどこに移動したのかの情報。
// 駒はExtBonaPiece表現であるとする。
struct ChangedBonaPiece
{
ExtBonaPiece old_piece;
ExtBonaPiece new_piece;
};
// KPPテーブルの盤上の駒pcに対応するBonaPieceを求めるための配列。
// 例)
// BonaPiece fb = kpp_board_index[pc].fb + sq; // 先手から見たsqにあるpcに対応するBonaPiece
// BonaPiece fw = kpp_board_index[pc].fw + sq; // 後手から見たsqにあるpcに対応するBonaPiece
extern ExtBonaPiece kpp_board_index[PIECE_NB];
// 評価関数で用いる駒リスト。どの駒(PieceNumber)がどこにあるのか(BonaPiece)を保持している構造体
struct EvalList
{
// 評価関数(FV38型)で用いる駒番号のリスト
BonaPiece* piece_list_fw() const { return const_cast<BonaPiece*>(pieceListFw); }
BonaPiece* piece_list_fb() const { return const_cast<BonaPiece*>(pieceListFb); }
// 指定されたpiece_noの駒をExtBonaPiece型に変換して返す。
ExtBonaPiece bona_piece(PieceNumber piece_no) const
{
ExtBonaPiece bp;
bp.fw = pieceListFw[piece_no];
bp.fb = pieceListFb[piece_no];
return bp;
}
// 盤上のsqの升にpiece_noのpcの駒を配置する
void put_piece(PieceNumber piece_no, Square sq, Piece pc) {
set_piece_on_board(piece_no, BonaPiece(kpp_board_index[pc].fw + sq), BonaPiece(kpp_board_index[pc].fb + Inv(sq)), sq);
}
// 盤上のある升sqに対応するPieceNumberを返す。
PieceNumber piece_no_of_board(Square sq) const { return piece_no_list_board[sq]; }
// pieceListを初期化する。
// 駒落ちに対応させる時のために、未使用の駒の値はBONA_PIECE_ZEROにしておく。
// 通常の評価関数を駒落ちの評価関数として流用できる。
// piece_no_listのほうはデバッグが捗るようにPIECE_NUMBER_NBで初期化。
void clear()
{
for (auto& p : pieceListFw)
p = BONA_PIECE_ZERO;
for (auto& p : pieceListFb)
p = BONA_PIECE_ZERO;
for (auto& v : piece_no_list_board)
v = PIECE_NUMBER_NB;
}
// 内部で保持しているpieceListFw[]が正しいBonaPieceであるかを検査する。
// 注 : デバッグ用。遅い。
bool is_valid(const Position& pos);
// 盤上sqにあるpiece_noの駒のBonaPieceがfb,fwであることを設定する。
inline void set_piece_on_board(PieceNumber piece_no, BonaPiece fw, BonaPiece fb, Square sq)
{
assert(is_ok(piece_no));
pieceListFw[piece_no] = fw;
pieceListFb[piece_no] = fb;
piece_no_list_board[sq] = piece_no;
}
// 駒リスト。駒番号(PieceNumber)いくつの駒がどこにあるのか(BonaPiece)を示す。FV38などで用いる。
// 駒リストの長さ
// 38固定
public:
int length() const { return PIECE_NUMBER_KING; }
// VPGATHERDDを使う都合、4の倍数でなければならない。
// また、KPPT型評価関数などは、39,40番目の要素がゼロであることを前提とした
// アクセスをしている箇所があるので注意すること。
static const int MAX_LENGTH = 32;
// 盤上の駒に対して、その駒番号(PieceNumber)を保持している配列
// 玉がSQUARE_NBに移動しているとき用に+1まで保持しておくが、
// SQUARE_NBの玉を移動させないので、この値を使うことはないはず。
PieceNumber piece_no_list_board[SQUARE_NB_PLUS1];
private:
BonaPiece pieceListFw[MAX_LENGTH];
BonaPiece pieceListFb[MAX_LENGTH];
};
// 評価値の差分計算の管理用
// 前の局面から移動した駒番号を管理するための構造体
// 動く駒は、最大で2個。
struct DirtyPiece
{
// その駒番号の駒が何から何に変わったのか
Eval::ChangedBonaPiece changed_piece[2];
// dirtyになった駒番号
PieceNumber pieceNo[2];
// dirtyになった個数。
// null moveだと0ということもありうる。
// 動く駒と取られる駒とで最大で2つ。
int dirty_num;
};
#endif // defined(EVAL_NNUE) || defined(EVAL_LEARN)
}
#endif // #ifndef EVALUATE_H_INCLUDED

448
src/extra/sfen_packer.cpp Normal file
View file

@ -0,0 +1,448 @@
#if defined (EVAL_LEARN)
#include "../misc.h"
#include "../position.h"
#include <sstream>
#include <fstream>
#include <cstring> // 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以外) = 26bit ( + 成りフラグ1bit+ 先後1bit )
// 手駒の1枚 = 15bit ( + 成りフラグ1bit+ 先後1bit )
//
// 空 xxxxx0 + 0 (none)
// 歩 xxxx01 + 2 xxxx0 + 2
// 香 xx0011 + 2 xx001 + 2
// 桂 xx1011 + 2 xx101 + 2
// 銀 xx0111 + 2 xx011 + 2
// 金 x01111 + 1 x0111 + 1 // 金は成りフラグはない。
// 角 011111 + 2 01111 + 2
// 飛 111111 + 2 11111 + 2
//
// すべての駒が盤上にあるとして、
// 空 81 - 40駒 = 41升 = 41bit
// 歩 4bit*18駒 = 72bit
// 香 6bit* 4駒 = 24bit
// 桂 6bit* 4駒 = 24bit
// 銀 6bit* 4駒 = 24bit
// 金 6bit* 4駒 = 24bit
// 角 8bit* 2駒 = 16bit
// 飛 8bit* 2駒 = 16bit
// -------
// 241bit + 1bit(手番) + 7bit×2(王の位置先後) = 256bit
//
// 盤上の駒が手駒に移動すると盤上の駒が空になるので盤上のその升は1bitで表現でき、
// 手駒は、盤上の駒より1bit少なく表現できるので結局、全体のbit数に変化はない。
// ゆえに、この表現において、どんな局面でもこのbit数で表現できる。
// 手駒に成りフラグは不要だが、これも含めておくと盤上の駒のbit数-1になるので
// 全体のbit数が固定化できるのでこれも含めておくことにする。
// Huffman Encoding
//
// Empty xxxxxxx0
// Pawn xxxxx001 + 1 bit (Side to move)
// Knight xxxxx011 + 1 bit (Side to move)
// Bishop xxxxx101 + 1 bit (Side to move)
// Rook xxxxx111 + 1 bit (Side to move)
struct HuffmanedPiece
{
int code; // どうコード化されるか
int bits; // 何bit専有するのか
};
HuffmanedPiece huffman_table[] =
{
{0b0000,1}, // NO_PIECE
{0b0001,4}, // PAWN
{0b0011,4}, // KNIGHT
{0b0101,4}, // BISHOP
{0b0111,4}, // ROOK
{0b1001,4}, // QUEEN
};
// sfenを圧縮/解凍するためのクラス
// sfenはハフマン符号化をすることで256bit(32bytes)にpackできる。
// このことはなのはminiにより証明された。上のハフマン符号化である。
//
// 内部フォーマット = 手番1bit+王の位置7bit*2 + 盤上の駒(ハフマン符号化) + 手駒(ハフマン符号化)
// Side to move (White = 0, Black = 1) (1bit)
// White King Position (6 bits)
// Black King Position (6 bits)
// Huffman Encoding of the board
// Castling availability (1 bit x 4)
// En passant square (1 or 1 + 6 bits)
// Rule 50 (6 bits)
// Game play (8 bits)
//
// TODO(someone): Rename SFEN to FEN.
//
struct SfenPacker
{
// sfenをpackしてdata[32]に格納する。
void pack(const Position& pos)
{
// cout << pos;
memset(data, 0, 32 /* 256bit */);
stream.set_data(data);
// 手番
// Side to move.
stream.write_one_bit((int)(pos.side_to_move()));
// 先手玉、後手玉の位置、それぞれ7bit
// White king and black king, 6 bits for each.
for(auto c : Colors)
stream.write_n_bit(pos.king_square(c), 6);
// Write the pieces on the board other than the kings.
for (Rank r = RANK_8; r >= RANK_1; --r)
{
for (File f = FILE_A; f <= FILE_H; ++f)
{
Piece pc = pos.piece_on(make_square(f, r));
if (type_of(pc) == KING)
continue;
write_board_piece_to_stream(pc);
}
}
// TODO(someone): Support chess960.
stream.write_one_bit(pos.can_castle(WHITE_OO));
stream.write_one_bit(pos.can_castle(WHITE_OOO));
stream.write_one_bit(pos.can_castle(BLACK_OO));
stream.write_one_bit(pos.can_castle(BLACK_OOO));
if (pos.ep_square() == SQ_NONE) {
stream.write_one_bit(0);
}
else {
stream.write_one_bit(1);
stream.write_n_bit(static_cast<int>(pos.ep_square()), 6);
}
stream.write_n_bit(pos.state()->rule50, 6);
stream.write_n_bit(1 + (pos.game_ply() - (pos.side_to_move() == BLACK)) / 2, 8);
assert(stream.get_cursor() <= 256);
}
// pack()でpackされたsfen(256bit = 32bytes)
// もしくはunpack()でdecodeするsfen
uint8_t *data; // uint8_t[32];
//private:
// Position::set_from_packed_sfen(uint8_t data[32])でこれらの関数を使いたいので筋は悪いがpublicにしておく。
BitStream stream;
// 盤面の駒をstreamに出力する。
void write_board_piece_to_stream(Piece pc)
{
// 駒種
PieceType pr = type_of(pc);
auto c = huffman_table[pr];
stream.write_n_bit(c.code, c.bits);
if (pc == NO_PIECE)
return;
// 先後フラグ
stream.write_one_bit(color_of(pc));
}
// 盤面の駒を1枚streamから読み込む
Piece read_board_piece_from_stream()
{
PieceType pr = NO_PIECE_TYPE;
int code = 0, bits = 0;
while (true)
{
code |= stream.read_one_bit() << bits;
++bits;
assert(bits <= 6);
for (pr = NO_PIECE_TYPE; pr < KING; ++pr)
if (huffman_table[pr].code == code
&& huffman_table[pr].bits == bits)
goto Found;
}
Found:;
if (pr == NO_PIECE_TYPE)
return NO_PIECE;
// 先後フラグ
Color c = (Color)stream.read_one_bit();
return make_piece(c, pr);
}
};
// -----------------------------------
// Positionクラスに追加
// -----------------------------------
// 高速化のために直接unpackする関数を追加。かなりしんどい。
// packer::unpack()とPosition::set()とを合体させて書く。
// 渡された局面に問題があって、エラーのときは非0を返す。
int Position::set_from_packed_sfen(const PackedSfen& sfen , StateInfo * si, Thread* th, bool mirror)
{
SfenPacker packer;
auto& stream = packer.stream;
stream.set_data((uint8_t*)&sfen);
std::memset(this, 0, sizeof(Position));
std::memset(si, 0, sizeof(StateInfo));
std::fill_n(&pieceList[0][0], sizeof(pieceList) / sizeof(Square), SQ_NONE);
st = si;
// Active color
sideToMove = (Color)stream.read_one_bit();
// evalListのclear。上でmemsetでゼロクリアしたときにクリアされているが…。
evalList.clear();
// PieceListを更新する上で、どの駒がどこにあるかを設定しなければならないが、
// それぞれの駒をどこまで使ったかのカウンター
PieceNumber next_piece_number = PIECE_NUMBER_ZERO;
pieceList[W_KING][0] = SQUARE_NB;
pieceList[B_KING][0] = SQUARE_NB;
// まず玉の位置
if (mirror)
{
for (auto c : Colors)
board[Mir((Square)stream.read_n_bit(6))] = make_piece(c, KING);
}
else
{
for (auto c : Colors)
board[stream.read_n_bit(6)] = make_piece(c, KING);
}
// Piece placement
for (Rank r = RANK_8; r >= RANK_1; --r)
{
for (File f = FILE_A; f <= FILE_H; ++f)
{
auto sq = make_square(f, r);
if (mirror) {
sq = Mir(sq);
}
// すでに玉がいるようだ
Piece pc;
if (type_of(board[sq]) != KING)
{
assert(board[sq] == NO_PIECE);
pc = packer.read_board_piece_from_stream();
}
else
{
pc = board[sq];
board[sq] = NO_PIECE; // いっかい取り除いておかないとput_piece()でASSERTに引っかかる。
}
// 駒がない場合もあるのでその場合はスキップする。
if (pc == NO_PIECE)
continue;
put_piece(Piece(pc), sq);
// evalListの更新
PieceNumber piece_no =
(pc == B_KING) ? PIECE_NUMBER_BKING : // 先手玉
(pc == W_KING) ? PIECE_NUMBER_WKING : // 後手玉
next_piece_number++; // それ以外
evalList.put_piece(piece_no, sq, pc); // sqの升にpcの駒を配置する
//cout << sq << ' ' << board[sq] << ' ' << stream.get_cursor() << endl;
if (stream.get_cursor() > 256)
return 1;
//assert(stream.get_cursor() <= 256);
}
}
// Castling availability.
// TODO(someone): Support chess960.
st->castlingRights = 0;
if (stream.read_one_bit()) {
Square rsq;
for (rsq = relative_square(WHITE, SQ_H1); piece_on(rsq) != W_ROOK; --rsq) {}
set_castling_right(WHITE, rsq);
}
if (stream.read_one_bit()) {
Square rsq;
for (rsq = relative_square(WHITE, SQ_A1); piece_on(rsq) != W_ROOK; ++rsq) {}
set_castling_right(WHITE, rsq);
}
if (stream.read_one_bit()) {
Square rsq;
for (rsq = relative_square(BLACK, SQ_H1); piece_on(rsq) != B_ROOK; --rsq) {}
set_castling_right(BLACK, rsq);
}
if (stream.read_one_bit()) {
Square rsq;
for (rsq = relative_square(BLACK, SQ_A1); piece_on(rsq) != B_ROOK; ++rsq) {}
set_castling_right(BLACK, rsq);
}
// En passant square. Ignore if no pawn capture is possible
if (stream.read_one_bit()) {
Square ep_square = static_cast<Square>(stream.read_n_bit(6));
if (mirror) {
ep_square = Mir(ep_square);
}
st->epSquare = ep_square;
if (!(attackers_to(st->epSquare) & pieces(sideToMove, PAWN))
|| !(pieces(~sideToMove, PAWN) & (st->epSquare + pawn_push(~sideToMove))))
st->epSquare = SQ_NONE;
}
else {
st->epSquare = SQ_NONE;
}
// Halfmove clock
st->rule50 = static_cast<Square>(stream.read_n_bit(6));
// Fullmove number
gamePly = static_cast<Square>(stream.read_n_bit(8));
// Convert from fullmove starting from 1 to gamePly starting from 0,
// handle also common incorrect FEN with fullmove = 0.
gamePly = std::max(2 * (gamePly - 1), 0) + (sideToMove == BLACK);
assert(stream.get_cursor() <= 256);
chess960 = false;
thisThread = th;
set_state(st);
//std::cout << *this << std::endl;
assert(pos_is_ok());
#if defined(EVAL_NNUE)
assert(evalList.is_valid(*this));
#endif // defined(EVAL_NNUE)
return 0;
}
// 盤面と手駒、手番を与えて、そのsfenを返す。
//std::string Position::sfen_from_rawdata(Piece board[81], Hand hands[2], Color turn, int gamePly_)
//{
// // 内部的な構造体にコピーして、sfen()を呼べば、変換過程がそこにしか依存していないならば
// // これで正常に変換されるのでは…。
// Position pos;
//
// memcpy(pos.board, board, sizeof(Piece) * 81);
// memcpy(pos.hand, hands, sizeof(Hand) * 2);
// pos.sideToMove = turn;
// pos.gamePly = gamePly_;
//
// return pos.sfen();
//
// // ↑の実装、美しいが、いかんせん遅い。
// // 棋譜を大量に読み込ませて学習させるときにここがボトルネックになるので直接unpackする関数を書く。
//}
// packされたsfenを得る。引数に指定したバッファに返す。
void Position::sfen_pack(PackedSfen& sfen)
{
SfenPacker sp;
sp.data = (uint8_t*)&sfen;
sp.pack(*this);
}
//// packされたsfenを解凍する。sfen文字列が返る。
//std::string Position::sfen_unpack(const PackedSfen& sfen)
//{
// SfenPacker sp;
// sp.data = (uint8_t*)&sfen;
// return sp.unpack();
//}
#endif // USE_SFEN_PACKER

View file

@ -0,0 +1 @@
// just a place holder

133
src/learn/half_float.h Normal file
View file

@ -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__

237
src/learn/learn.h Normal file
View file

@ -0,0 +1,237 @@
#ifndef _LEARN_H_
#define _LEARN_H_
#if defined(EVAL_LEARN)
#include <vector>
// =====================
// 学習時の設定
// =====================
// 以下のいずれかを選択すれば、そのあとの細々したものは自動的に選択される。
// いずれも選択しない場合は、そのあとの細々したものをひとつひとつ設定する必要がある。
// 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<Value, std::vector<Move> > 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_

2952
src/learn/learner.cpp Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,256 @@
#include "learning_tools.h"
#if defined (EVAL_LEARN)
#if defined(_OPENMP)
#include <omp.h>
#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<bool> 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がつあるときに、論理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<bool> 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<SQUARE_NB; ++k)
for (int i = 0; i<10000; ++i) // 試しに、かなり大きなfe_endを想定して10000で回してみる。
for (int j = 0; j < i; ++j)
{
auto kkpp = g_kkpp.fromKKPP(k, (BonaPiece)i, (BonaPiece)j);
auto r = kkpp.toRawIndex();
assert(n++ == r);
auto kkpp2 = g_kkpp.fromIndex(r + g_kkpp.min_index());
assert(kkpp2.king() == k && kkpp2.piece0() == i && kkpp2.piece1() == j);
}
}
// このEvalLearningTools全体の初期化
void init()
{
// 初期化は、起動後1回限りで良いのでそのためのフラグ。
static bool first = true;
if (first)
{
std::cout << "EvalLearningTools init..";
// mir_piece()とinv_piece()を利用可能にする。
// このあとmin_index_flagの初期化を行なうが、そこが
// これに依存しているので、こちらを先に行なう必要がある。
init_mir_inv_tables();
//learning_tools_unit_test_kpp();
//learning_tools_unit_test_kppp();
//learning_tools_unit_test_kkpp();
// UnitTestを実行するの最後でも良いのだが、init_min_index_flag()にとても時間がかかるので
// デバッグ時はこのタイミングで行いたい。
init_min_index_flag();
std::cout << "done." << std::endl;
first = false;
}
}
}
#endif

1034
src/learn/learning_tools.h Normal file

File diff suppressed because it is too large Load diff

123
src/learn/multi_think.cpp Normal file
View file

@ -0,0 +1,123 @@
#include "../types.h"
#if defined(EVAL_LEARN)
#include "multi_think.h"
#include "../tt.h"
#include "../uci.h"
#include <thread>
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<std::thread> 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)

151
src/learn/multi_think.h Normal file
View file

@ -0,0 +1,151 @@
#ifndef _MULTI_THINK_
#define _MULTI_THINK_
#if defined(EVAL_LEARN)
#include <functional>
#include "../misc.h"
#include "../learn/learn.h"
#include "../thread_win32_osx.h"
#include <atomic>
// 棋譜からの学習や、自ら思考させて定跡を生成するときなど、
// 複数スレッドが個別に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<void()> 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<std::mutex> lk(loop_mutex);
if (loop_count >= loop_max)
return UINT64_MAX;
return loop_count++;
}
// [ASYNC] 処理した個数を返す用。呼び出されるごとにインクリメントされたカウンターが返る。
uint64_t get_done_count() {
std::unique_lock<std::mutex> lk(loop_mutex);
return ++done_count;
}
// worker threadがI/Oにアクセスするときのmutex
std::mutex io_mutex;
protected:
// 乱数発生器本体
AsyncPRNG prng;
private:
// workerが処理する(Search::think()を呼び出す)回数
std::atomic<uint64_t> loop_max;
// workerが処理した(Search::think()を呼び出した)回数
std::atomic<uint64_t> loop_count;
// 処理した回数を返す用。
std::atomic<uint64_t> done_count;
// ↑の変数を変更するときのmutex
std::mutex loop_mutex;
// スレッドの終了フラグ。
// vector<bool>にすると複数スレッドから書き換えようとしたときに正しく反映されないことがある…はず。
typedef uint8_t Flag;
std::vector<Flag> thread_finished;
};
// idle時間にtaskを処理する仕組み。
// masterは好きなときにpush_task_async()でtaskを渡す。
// slaveは暇なときにon_idle()を実行すると、taskを一つ取り出してqueueがなくなるまで実行を続ける。
// MultiThinkのthread workerをmaster-slave方式で書きたいときに用いると便利。
struct TaskDispatcher
{
typedef std::function<void(size_t /* thread_id */)> 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<std::mutex> lk(task_mutex);
tasks.push_back(task);
}
// task用の配列の要素をsize分だけ事前に確保する。
void task_reserve(size_t size)
{
tasks.reserve(size);
}
protected:
// taskの集合
std::vector<Task> tasks;
// [ASYNC] taskを一つ取り出す。on_idle()から呼び出される。
Task get_task_async()
{
std::unique_lock<std::mutex> lk(task_mutex);
if (tasks.size() == 0)
return nullptr;
Task task = *tasks.rbegin();
tasks.pop_back();
return task;
}
// tasksにアクセスするとき用のmutex
std::mutex task_mutex;
};
#endif // defined(EVAL_LEARN) && defined(YANEURAOU_2018_OTAFUKU_ENGINE)
#endif

View file

@ -42,6 +42,7 @@ typedef bool(*fun3_t)(HANDLE, CONST GROUP_AFFINITY*, PGROUP_AFFINITY);
#endif
#include <fstream>
#include <functional>
#include <iomanip>
#include <iostream>
#include <sstream>
@ -522,3 +523,164 @@ void bindThisThread(size_t idx) {
#endif
} // namespace WinProcGroup
// 現在時刻を文字列化したもを返す。(評価関数の学習時などに用いる)
std::string now_string()
{
// std::ctime(), localtime()を使うと、MSVCでセキュアでないという警告が出る。
// C++標準的にはそんなことないはずなのだが…。
#if defined(_MSC_VER)
// C4996 : 'ctime' : This function or variable may be unsafe.Consider using ctime_s instead.
#pragma warning(disable : 4996)
#endif
auto now = std::chrono::system_clock::now();
auto tp = std::chrono::system_clock::to_time_t(now);
auto result = string(std::ctime(&tp));
// 末尾に改行コードが含まれているならこれを除去する
while (*result.rbegin() == '\n' || (*result.rbegin() == '\r'))
result.pop_back();
return result;
}
void sleep(int ms)
{
std::this_thread::sleep_for(std::chrono::milliseconds(ms));
}
void* aligned_malloc(size_t size, size_t align)
{
void* p = _mm_malloc(size, align);
if (p == nullptr)
{
std::cout << "info string can't allocate memory. sise = " << size << std::endl;
exit(1);
}
return p;
}
int read_file_to_memory(std::string filename, std::function<void* (uint64_t)> 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 <codecvt> // mkdirするのにwstringが欲しいのでこれが必要
#include <locale> // wstring_convertにこれが必要。
namespace Dependency {
int mkdir(std::string dir_name)
{
std::wstring_convert<std::codecvt_utf8<wchar_t>, 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 <direct.h>
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

View file

@ -21,13 +21,20 @@
#ifndef MISC_H_INCLUDED
#define MISC_H_INCLUDED
#include <algorithm>
#include <cassert>
#include <chrono>
#include <functional>
#include <mutex>
#include <ostream>
#include <string>
#include <vector>
#ifndef _MSC_VER
#include <mm_malloc.h>
#endif
#include "types.h"
#include "thread_win32_osx.h"
const std::string engine_info(bool to_uci = false);
const std::string compiler_info();
@ -108,8 +115,21 @@ public:
/// Output values only have 1/8th of their bits set on average.
template<typename T> T sparse_rand()
{ return T(rand64() & rand64() & rand64()); }
// 0からn-1までの乱数を返す。(一様分布ではないが現実的にはこれで十分)
uint64_t rand(uint64_t n) { return rand<uint64_t>() % n; }
// 内部で使用している乱数seedを返す。
uint64_t get_seed() const { return s; }
};
// 乱数のseedを表示する。(デバッグ用)
inline std::ostream& operator<<(std::ostream& os, PRNG& prng)
{
os << "PRNG::seed = " << std::hex << prng.get_seed() << std::dec;
return os;
}
inline uint64_t mul_hi64(uint64_t a, uint64_t b) {
#if defined(__GNUC__) && defined(IS_64BIT)
__extension__ typedef unsigned __int128 uint128;
@ -122,7 +142,6 @@ inline uint64_t mul_hi64(uint64_t a, uint64_t b) {
uint64_t c3 = aL * bH + (uint32_t)c2;
return aH * bH + (c2 >> 32) + (c3 >> 32);
#endif
}
/// Under Windows it is not possible for a process to run on more than one
/// logical processor group. This usually means to be limited to use max 64
@ -134,4 +153,155 @@ namespace WinProcGroup {
void bindThisThread(size_t idx);
}
// 指定されたミリ秒だけsleepする。
extern void sleep(int ms);
// 現在時刻を文字列化したもを返す。(評価関数の学習時などにログ出力のために用いる)
std::string now_string();
// 途中での終了処理のためのwrapper
static void my_exit()
{
sleep(3000); // エラーメッセージが出力される前に終了するのはまずいのでwaitを入れておく。
exit(EXIT_FAILURE);
}
// msys2、Windows Subsystem for Linuxなどのgcc/clangでコンパイルした場合、
// C++のstd::ifstreamで::read()は、一発で2GB以上のファイルの読み書きが出来ないのでそのためのwrapperである。
//
// read_file_to_memory()の引数のcallback_funcは、ファイルがオープン出来た時点でそのファイルサイズを引数として
// callbackされるので、バッファを確保して、その先頭ポインタを返す関数を渡すと、そこに読み込んでくれる。
// これらの関数は、ファイルが見つからないときなどエラーの際には非0を返す。
//
// また、callbackされた関数のなかでバッファが確保できなかった場合や、想定していたファイルサイズと異なった場合は、
// nullptrを返せば良い。このとき、read_file_to_memory()は、読み込みを中断し、エラーリターンする。
int read_file_to_memory(std::string filename, std::function<void* (uint64_t)> 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<typename T> T rand() {
std::unique_lock<std::mutex> lk(mutex);
return prng.rand<T>();
}
// [ASYNC] 0からn-1までの乱数を返す。(一様分布ではないが現実的にはこれで十分)
uint64_t rand(uint64_t n) {
std::unique_lock<std::mutex> lk(mutex);
return prng.rand(n);
}
// 内部で使用している乱数seedを返す。
uint64_t get_seed() const { return prng.get_seed(); }
protected:
std::mutex mutex;
PRNG prng;
};
// 乱数のseedを表示する。(デバッグ用)
inline std::ostream& operator<<(std::ostream& os, AsyncPRNG& prng)
{
os << "AsyncPRNG::seed = " << std::hex << prng.get_seed() << std::dec;
return os;
}
// --------------------
// Math
// --------------------
// 進行度の計算や学習で用いる数学的な関数
namespace Math {
// シグモイド関数
// = 1.0 / (1.0 + std::exp(-x))
double sigmoid(double x);
// シグモイド関数の微分
// = sigmoid(x) * (1.0 - sigmoid(x))
double dsigmoid(double x);
// vを[lo,hi]の間に収まるようにクリップする。
// ※ Stockfishではこの関数、bitboard.hに書いてある。
template<class T> constexpr const T& clamp(const T& v, const T& lo, const T& hi) {
return v < lo ? lo : v > hi ? hi : v;
}
}
// --------------------
// Path
// --------------------
// C#にあるPathクラス的なもの。ファイル名の操作。
// C#のメソッド名に合わせておく。
struct Path
{
// path名とファイル名を結合して、それを返す。
// folder名のほうは空文字列でないときに、末尾に'/'か'\\'がなければそれを付与する。
static std::string Combine(const std::string& folder, const std::string& filename)
{
if (folder.length() >= 1 && *folder.rbegin() != '/' && *folder.rbegin() != '\\')
return folder + "/" + filename;
return folder + filename;
}
// full path表現から、(フォルダ名を除いた)ファイル名の部分を取得する。
static std::string GetFileName(const std::string& path)
{
// "\"か"/"か、どちらを使ってあるかはわからない。
auto path_index1 = path.find_last_of("\\") + 1;
auto path_index2 = path.find_last_of("/") + 1;
auto path_index = std::max(path_index1, path_index2);
return path.substr(path_index);
}
};
extern void* aligned_malloc(size_t size, size_t align);
static void aligned_free(void* ptr) { _mm_free(ptr); }
// alignasを指定しているのにnewのときに無視されるSTLのコンテナがメモリ確保するときに無視するので、
// そのために用いるカスタムアロケーター。
template <typename T>
class AlignedAllocator {
public:
using value_type = T;
AlignedAllocator() {}
AlignedAllocator(const AlignedAllocator&) {}
AlignedAllocator(AlignedAllocator&&) {}
template <typename U> AlignedAllocator(const AlignedAllocator<U>&) {}
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

View file

@ -68,6 +68,9 @@ struct MoveList {
return std::find(begin(), end(), move) != end();
}
// i番目の要素を返す
const ExtMove at(size_t i) const { assert(0 <= i && i < size()); return begin()[i]; }
private:
ExtMove moveList[MAX_MOVES], *last;
};

View file

@ -209,6 +209,15 @@ Position& Position::set(const string& fenStr, bool isChess960, StateInfo* si, Th
std::fill_n(&pieceList[0][0], sizeof(pieceList) / sizeof(Square), SQ_NONE);
st = si;
#if defined(EVAL_NNUE)
// evalListのclear。上でmemsetでゼロクリアしたときにクリアされているが…。
evalList.clear();
// PieceListを更新する上で、どの駒がどこにあるかを設定しなければならないが、
// それぞれの駒をどこまで使ったかのカウンター
PieceNumber next_piece_number = PIECE_NUMBER_ZERO;
#endif // defined(EVAL_NNUE)
ss >> std::noskipws;
// 1. Piece placement
@ -222,7 +231,17 @@ Position& Position::set(const string& fenStr, bool isChess960, StateInfo* si, Th
else if ((idx = PieceToChar.find(token)) != string::npos)
{
put_piece(Piece(idx), sq);
auto pc = Piece(idx);
put_piece(pc, sq);
#if defined(EVAL_NNUE)
PieceNumber piece_no =
(idx == W_KING) ? PIECE_NUMBER_WKING : // 先手玉
(idx == B_KING) ? PIECE_NUMBER_BKING : // 後手玉
next_piece_number++; // それ以外
evalList.put_piece(piece_no, sq, pc); // sqの升にpcの駒を配置する
#endif // defined(EVAL_NNUE)
++sq;
}
}
@ -285,6 +304,9 @@ Position& Position::set(const string& fenStr, bool isChess960, StateInfo* si, Th
set_state(st);
assert(pos_is_ok());
#if defined(EVAL_NNUE)
assert(evalList.is_valid(*this));
#endif // defined(EVAL_NNUE)
return *this;
}
@ -706,6 +728,11 @@ void Position::do_move(Move m, StateInfo& newSt, bool givesCheck) {
++st->rule50;
++st->pliesFromNull;
#if defined(EVAL_NNUE)
st->accumulator.computed_accumulation = false;
st->accumulator.computed_score = false;
#endif // defined(EVAL_NNUE)
Color us = sideToMove;
Color them = ~us;
Square from = from_sq(m);
@ -713,10 +740,20 @@ void Position::do_move(Move m, StateInfo& newSt, bool givesCheck) {
Piece pc = piece_on(from);
Piece captured = type_of(m) == ENPASSANT ? make_piece(them, PAWN) : piece_on(to);
#if defined(EVAL_NNUE)
PieceNumber piece_no0 = PIECE_NUMBER_NB;
PieceNumber piece_no1 = PIECE_NUMBER_NB;
#endif // defined(EVAL_NNUE)
assert(color_of(pc) == us);
assert(captured == NO_PIECE || color_of(captured) == (type_of(m) != CASTLING ? them : us));
assert(type_of(captured) != KING);
#if defined(EVAL_NNUE)
auto& dp = st->dirtyPiece;
dp.dirty_num = 1;
#endif // defined(EVAL_NNUE)
if (type_of(m) == CASTLING)
{
assert(pc == make_piece(us, KING));
@ -746,13 +783,32 @@ void Position::do_move(Move m, StateInfo& newSt, bool givesCheck) {
assert(relative_rank(us, to) == RANK_6);
assert(piece_on(to) == NO_PIECE);
assert(piece_on(capsq) == make_piece(them, PAWN));
#if defined(EVAL_NNUE)
piece_no1 = piece_no_of(capsq);
#endif // defined(EVAL_NNUE)
//board[capsq] = NO_PIECE; // Not done by remove_piece()
#if defined(EVAL_NNUE)
evalList.piece_no_list_board[capsq] = PIECE_NUMBER_NB;
#endif // defined(EVAL_NNUE)
}
else {
#if defined(EVAL_NNUE)
piece_no1 = piece_no_of(capsq);
#endif // defined(EVAL_NNUE)
}
st->pawnKey ^= Zobrist::psq[captured][capsq];
}
else
else {
st->nonPawnMaterial[them] -= PieceValue[MG][captured];
#if defined(EVAL_NNUE)
piece_no1 = piece_no_of(capsq);
#endif // defined(EVAL_NNUE)
}
// Update board and piece lists
remove_piece(capsq);
@ -766,6 +822,21 @@ void Position::do_move(Move m, StateInfo& newSt, bool givesCheck) {
// Reset rule 50 counter
st->rule50 = 0;
#if defined(EVAL_NNUE)
dp.dirty_num = 2; // 動いた駒は2個
dp.pieceNo[1] = piece_no1;
dp.changed_piece[1].old_piece = evalList.bona_piece(piece_no1);
// Do not use Eval::EvalList::put_piece() because the piece is removed
// from the game, and the corresponding elements of the piece lists
// needs to be Eval::BONA_PIECE_ZERO.
evalList.set_piece_on_board(piece_no1, Eval::BONA_PIECE_ZERO, Eval::BONA_PIECE_ZERO, capsq);
// Set PIECE_NUMBER_NB to piece_no_of_board[capsq] directly because it
// will not be overritten to pc if the move type is enpassant.
evalList.piece_no_list_board[capsq] = PIECE_NUMBER_NB;
dp.changed_piece[1].new_piece = evalList.bona_piece(piece_no1);
#endif // defined(EVAL_NNUE)
}
// Update hash key
@ -787,8 +858,21 @@ void Position::do_move(Move m, StateInfo& newSt, bool givesCheck) {
}
// Move the piece. The tricky Chess960 castling is handled earlier
if (type_of(m) != CASTLING)
move_piece(from, to);
if (type_of(m) != CASTLING) {
#if defined(EVAL_NNUE)
piece_no0 = piece_no_of(from);
#endif // defined(EVAL_NNUE)
move_piece(from, to);
#if defined(EVAL_NNUE)
dp.pieceNo[0] = piece_no0;
dp.changed_piece[0].old_piece = evalList.bona_piece(piece_no0);
evalList.piece_no_list_board[from] = PIECE_NUMBER_NB;
evalList.put_piece(piece_no0, to, pc);
dp.changed_piece[0].new_piece = evalList.bona_piece(piece_no0);
#endif // defined(EVAL_NNUE)
}
// If the moving piece is a pawn do some special extra work
if (type_of(pc) == PAWN)
@ -811,6 +895,15 @@ void Position::do_move(Move m, StateInfo& newSt, bool givesCheck) {
remove_piece(to);
put_piece(promotion, to);
#if defined(EVAL_NNUE)
piece_no0 = piece_no_of(to);
//dp.pieceNo[0] = piece_no0;
//dp.changed_piece[0].old_piece = evalList.bona_piece(piece_no0);
assert(evalList.piece_no_list_board[from] == PIECE_NUMBER_NB);
evalList.put_piece(piece_no0, to, promotion);
dp.changed_piece[0].new_piece = evalList.bona_piece(piece_no0);
#endif // defined(EVAL_NNUE)
// Update hash keys
k ^= Zobrist::psq[pc][to] ^ Zobrist::psq[promotion][to];
st->pawnKey ^= Zobrist::psq[pc][to];
@ -861,7 +954,12 @@ void Position::do_move(Move m, StateInfo& newSt, bool givesCheck) {
}
}
//std::cout << *this << std::endl;
assert(pos_is_ok());
#if defined(EVAL_NNUE)
assert(evalList.is_valid(*this));
#endif // defined(EVAL_NNUE)
}
@ -891,6 +989,11 @@ void Position::undo_move(Move m) {
remove_piece(to);
pc = make_piece(us, PAWN);
put_piece(pc, to);
#if defined(EVAL_NNUE)
PieceNumber piece_no0 = st->dirtyPiece.pieceNo[0];
evalList.put_piece(piece_no0, to, pc);
#endif // defined(EVAL_NNUE)
}
if (type_of(m) == CASTLING)
@ -900,8 +1003,15 @@ void Position::undo_move(Move m) {
}
else
{
move_piece(to, from); // Put the piece back at the source square
#if defined(EVAL_NNUE)
PieceNumber piece_no0 = st->dirtyPiece.pieceNo[0];
evalList.put_piece(piece_no0, from, pc);
evalList.piece_no_list_board[to] = PIECE_NUMBER_NB;
#endif // defined(EVAL_NNUE)
if (st->capturedPiece)
{
Square capsq = to;
@ -918,6 +1028,13 @@ void Position::undo_move(Move m) {
}
put_piece(st->capturedPiece, capsq); // Restore the captured piece
#if defined(EVAL_NNUE)
PieceNumber piece_no1 = st->dirtyPiece.pieceNo[1];
assert(evalList.bona_piece(piece_no1).fw == Eval::BONA_PIECE_ZERO);
assert(evalList.bona_piece(piece_no1).fb == Eval::BONA_PIECE_ZERO);
evalList.put_piece(piece_no1, capsq, st->capturedPiece);
#endif // defined(EVAL_NNUE)
}
}
@ -926,6 +1043,9 @@ void Position::undo_move(Move m) {
--gamePly;
assert(pos_is_ok());
#if defined(EVAL_NNUE)
assert(evalList.is_valid(*this));
#endif // defined(EVAL_NNUE)
}
@ -933,18 +1053,60 @@ void Position::undo_move(Move m) {
/// is a bit tricky in Chess960 where from/to squares can overlap.
template<bool Do>
void Position::do_castling(Color us, Square from, Square& to, Square& rfrom, Square& rto) {
#if defined(EVAL_NNUE)
auto& dp = st->dirtyPiece;
// 差分計算のために移動した駒をStateInfoに記録しておく。
dp.dirty_num = 2; // 動いた駒は2個
PieceNumber piece_no0;
PieceNumber piece_no1;
if (Do) {
piece_no0 = piece_no_of(from);
piece_no1 = piece_no_of(to);
}
#endif // defined(EVAL_NNUE)
bool kingSide = to > from;
rfrom = to; // Castling is encoded as "king captures friendly rook"
rto = relative_square(us, kingSide ? SQ_F1 : SQ_D1);
to = relative_square(us, kingSide ? SQ_G1 : SQ_C1);
#if defined(EVAL_NNUE)
if (!Do) {
piece_no0 = piece_no_of(to);
piece_no1 = piece_no_of(rto);
}
#endif // defined(EVAL_NNUE)
// Remove both pieces first since squares could overlap in Chess960
remove_piece(Do ? from : to);
remove_piece(Do ? rfrom : rto);
board[Do ? from : to] = board[Do ? rfrom : rto] = NO_PIECE; // Since remove_piece doesn't do this for us
put_piece(make_piece(us, KING), Do ? to : from);
put_piece(make_piece(us, ROOK), Do ? rto : rfrom);
#if defined(EVAL_NNUE)
if (Do) {
dp.pieceNo[0] = piece_no0;
dp.changed_piece[0].old_piece = evalList.bona_piece(piece_no0);
evalList.piece_no_list_board[from] = PIECE_NUMBER_NB;
evalList.put_piece(piece_no0, to, make_piece(us, KING));
dp.changed_piece[0].new_piece = evalList.bona_piece(piece_no0);
dp.pieceNo[1] = piece_no1;
dp.changed_piece[1].old_piece = evalList.bona_piece(piece_no1);
evalList.piece_no_list_board[rfrom] = PIECE_NUMBER_NB;
evalList.put_piece(piece_no1, rto, make_piece(us, ROOK));
dp.changed_piece[1].new_piece = evalList.bona_piece(piece_no1);
}
else {
evalList.piece_no_list_board[to] = PIECE_NUMBER_NB;
evalList.put_piece(piece_no0, from, make_piece(us, KING));
evalList.piece_no_list_board[rto] = PIECE_NUMBER_NB;
evalList.put_piece(piece_no1, rfrom, make_piece(us, ROOK));
}
#endif // defined(EVAL_NNUE)
}
@ -969,6 +1131,10 @@ void Position::do_null_move(StateInfo& newSt) {
st->key ^= Zobrist::side;
prefetch(TT.first_entry(st->key));
#if defined(EVAL_NNUE)
st->accumulator.computed_score = false;
#endif
++st->rule50;
st->pliesFromNull = 0;
@ -1297,3 +1463,13 @@ bool Position::pos_is_ok() const {
return true;
}
#if defined(EVAL_NNUE)
PieceNumber Position::piece_no_of(Square sq) const
{
assert(piece_on(sq) != NO_PIECE);
PieceNumber n = evalList.piece_no_of_board(sq);
assert(is_ok(n));
return n;
}
#endif // defined(EVAL_NNUE)

View file

@ -23,12 +23,17 @@
#include <cassert>
#include <deque>
#include <iostream>
#include <memory> // For std::unique_ptr
#include <string>
#include "bitboard.h"
#include "evaluate.h"
#include "misc.h"
#include "types.h"
#include "eval/nnue/nnue_accumulator.h"
/// StateInfo struct stores information needed to restore a Position object to
/// its previous state when we retract a move. Whenever a move is made on the
@ -54,6 +59,13 @@ struct StateInfo {
Bitboard pinners[COLOR_NB];
Bitboard checkSquares[PIECE_TYPE_NB];
int repetition;
#if defined(EVAL_NNUE)
Eval::NNUE::Accumulator accumulator;
// 評価値の差分計算の管理用
Eval::DirtyPiece dirtyPiece;
#endif // defined(EVAL_NNUE)
};
/// A list to keep track of the position states along the setup moves (from the
@ -69,6 +81,9 @@ typedef std::unique_ptr<std::deque<StateInfo>> StateListPtr;
/// traversing the search tree.
class Thread;
// packされたsfen
struct PackedSfen { uint8_t data[32]; };
class Position {
public:
static void init();
@ -162,6 +177,37 @@ public:
bool pos_is_ok() const;
void flip();
#if defined(EVAL_NNUE) || defined(EVAL_LEARN)
// --- StateInfo
// 現在の局面に対応するStateInfoを返す。
// たとえば、state()->capturedPieceであれば、前局面で捕獲された駒が格納されている。
StateInfo* state() const { return st; }
// 評価関数で使うための、どの駒番号の駒がどこにあるかなどの情報。
const Eval::EvalList* eval_list() const { return &evalList; }
#endif // defined(EVAL_NNUE) || defined(EVAL_LEARN)
#if defined(EVAL_LEARN)
// -- sfen化ヘルパ
// packされたsfenを得る。引数に指定したバッファに返す。
// gamePlyはpackに含めない。
void sfen_pack(PackedSfen& sfen);
// ↑sfenを経由すると遅いので直接packされたsfenをセットする関数を作った。
// pos.set(sfen_unpack(data),si,th); と等価。
// 渡された局面に問題があって、エラーのときは非0を返す。
// PackedSfenにgamePlyは含まないので復元できない。そこを設定したいのであれば引数で指定すること。
int set_from_packed_sfen(const PackedSfen& sfen, StateInfo* si, Thread* th, bool mirror = false);
// 盤面と手駒、手番を与えて、そのsfenを返す。
//static std::string sfen_from_rawdata(Piece board[81], Hand hands[2], Color turn, int gamePly);
// c側の玉の位置を返す。
Square king_square(Color c) const { return pieceList[make_piece(c, KING)][0]; }
#endif // EVAL_LEARN
private:
// Initialization helpers (used while setting up a position)
void set_castling_right(Color c, Square rfrom);
@ -175,6 +221,11 @@ private:
template<bool Do>
void do_castling(Color us, Square from, Square& to, Square& rfrom, Square& rto);
#if defined(EVAL_NNUE)
// 盤上のsqの升にある駒のPieceNumberを返す。
PieceNumber piece_no_of(Square sq) const;
#endif // defined(EVAL_NNUE)
// Data members
Piece board[SQUARE_NB];
Bitboard byTypeBB[PIECE_TYPE_NB];
@ -191,6 +242,11 @@ private:
Thread* thisThread;
StateInfo* st;
bool chess960;
#if defined(EVAL_NNUE) || defined(EVAL_LEARN)
// 評価関数で用いる駒のリスト
Eval::EvalList evalList;
#endif // defined(EVAL_NNUE) || defined(EVAL_LEARN)
};
namespace PSQT {

View file

@ -965,7 +965,7 @@ moves_loop: // When in check, search starts from here
ss->moveCount = ++moveCount;
if (rootNode && thisThread == Threads.main() && Time.elapsed() > 3000)
if (rootNode && thisThread == Threads.main() && Time.elapsed() > 3000 && !Limits.silent)
sync_cout << "info depth " << depth
<< " currmove " << UCI::move(move, pos.is_chess960())
<< " currmovenumber " << moveCount + thisThread->pvIdx << sync_endl;
@ -1531,7 +1531,13 @@ moves_loop: // When in check, search starts from here
prefetch(TT.first_entry(pos.key_after(move)));
// Check for legality just before making the move
if (!pos.legal(move))
if (
#if defined(EVAL_LEARN)
// HACK: pos.piece_on(from_sq(m)) sometimes will be NO_PIECE during machine learning.
!pos.pseudo_legal(move) ||
#endif // EVAL_LEARN
!pos.legal(move)
)
{
moveCount--;
continue;
@ -1927,3 +1933,315 @@ void Tablebases::rank_root_moves(Position& pos, Search::RootMoves& rootMoves) {
m.tbRank = 0;
}
}
// --- 学習時に用いる、depth固定探索などの関数を外部に対して公開
#if defined (EVAL_LEARN)
namespace Learner
{
// 学習用に、1つのスレッドからsearch,qsearch()を呼び出せるようなスタブを用意する。
// いまにして思えば、AperyのようにSearcherを持ってスレッドごとに置換表などを用意するほうが
// 良かったかも知れない。
// 学習のための初期化。
// Learner::search(),Learner::qsearch()から呼び出される。
void init_for_search(Position& pos, Stack* ss)
{
// RootNodeはss->ply == 0がその条件。
// ゼロクリアするので、ss->ply == 0となるので大丈夫…。
std::memset(ss - 7, 0, 10 * sizeof(Stack));
// Search::Limitsに関して
// このメンバー変数はglobalなので他のスレッドに影響を及ぼすので気をつけること。
{
auto& limits = Search::Limits;
// 探索を"go infinite"コマンド相当にする。(time managementされると困るため)
limits.infinite = true;
// PVを表示されると邪魔なので消しておく。
limits.silent = true;
// これを用いると各スレッドのnodesを積算したものと比較されてしまう。ゆえに使用しない。
limits.nodes = 0;
// depthも、Learner::search()の引数として渡されたもので処理する。
limits.depth = 0;
// 引き分け付近の手数で引き分けの値が返るのを防ぐために大きな値にしておく。
//limits.max_game_ply = 1 << 16;
// 入玉ルールも入れておかないと引き分けになって決着つきにくい。
//limits.enteringKingRule = EnteringKingRule::EKR_27_POINT;
}
// DrawValueの設定
{
// スレッドごとに用意してないので
// 他のスレッドで上書きされかねない。仕方がないが。
// どうせそうなるなら、0にすべきだと思う。
//drawValueTable[REPETITION_DRAW][BLACK] = VALUE_ZERO;
//drawValueTable[REPETITION_DRAW][WHITE] = VALUE_ZERO;
}
// this_threadに関して。
{
auto th = pos.this_thread();
th->completedDepth = 0;
th->selDepth = 0;
th->rootDepth = 0;
// 探索ノード数のゼロ初期化
th->nodes = 0;
// history類を全部クリアする。この初期化は少し時間がかかるし、探索の精度はむしろ下がるので善悪はよくわからない。
// th->clear();
int ct = int(Options["Contempt"]) * PawnValueEg / 100; // From centipawns
Color us = pos.side_to_move();
// In analysis mode, adjust contempt in accordance with user preference
if (Limits.infinite || Options["UCI_AnalyseMode"])
ct = Options["Analysis Contempt"] == "Off" ? 0
: Options["Analysis Contempt"] == "Both" ? ct
: Options["Analysis Contempt"] == "White" && us == BLACK ? -ct
: Options["Analysis Contempt"] == "Black" && us == WHITE ? -ct
: ct;
// Evaluation score is from the white point of view
th->contempt = (us == WHITE ? make_score(ct, ct / 2)
: -make_score(ct, ct / 2));
for (int i = 7; i > 0; i--)
(ss - i)->continuationHistory = &th->continuationHistory[0][0][NO_PIECE][0]; // Use as a sentinel
// rootMovesの設定
auto& rootMoves = th->rootMoves;
rootMoves.clear();
for (auto m : MoveList<LEGAL>(pos))
rootMoves.push_back(Search::RootMove(m));
assert(!rootMoves.empty());
//#if defined(USE_GLOBAL_OPTIONS)
// 探索スレッドごとの置換表の世代を管理しているはずなので、
// 新規の探索であるから、このスレッドに対する置換表の世代を増やす。
//TT.new_search(th->thread_id());
// ↑ここでnew_searchを呼び出すと1手前の探索結果が使えなくて損ということはあるのでは…。
// ここでこれはやらずに、呼び出し側で1局ごとにTT.new_search(th->thread_id())をやるべきでは…。
// → 同一の終局図に至るのを回避したいので、教師生成時には置換表は全スレ共通で使うようにする。
//#endif
}
}
// 読み筋と評価値のペア。Learner::search(),Learner::qsearch()が返す。
typedef std::pair<Value, std::vector<Move> > ValueAndPV;
// 静止探索。
//
// 前提条件) pos.set_this_thread(Threads[thread_id])で探索スレッドが設定されていること。
//  また、Threads.stopが来ると探索を中断してしまうので、そのときのPVは正しくない。
//  search()から戻ったあと、Threads.stop == trueなら、その探索結果を用いてはならない。
//  あと、呼び出し前は、Threads.stop == falseの状態で呼び出さないと、探索を中断して返ってしまうので注意。
//
// 詰まされている場合は、PV配列にMOVE_RESIGNが返る。
//
// 引数でalpha,betaを指定できるようにしていたが、これがその窓で探索したときの結果を
// 置換表に書き込むので、その窓に対して枝刈りが出来るような値が書き込まれて学習のときに
// 悪い影響があるので、窓の範囲を指定できるようにするのをやめることにした。
ValueAndPV qsearch(Position& pos)
{
Stack stack[MAX_PLY + 10], * ss = stack + 7;
Move pv[MAX_PLY + 1];
init_for_search(pos, ss);
ss->pv = pv; // とりあえずダミーでどこかバッファがないといけない。
if (pos.is_draw(0)) {
// Return draw value if draw.
return { VALUE_DRAW, {} };
}
// 詰まされているのか
if (MoveList<LEGAL>(pos).size() == 0)
{
// Return the mated value if checkmated.
return { mated_in(/*ss->ply*/ 0 + 1), {} };
}
auto bestValue = ::qsearch<PV>(pos, ss, -VALUE_INFINITE, VALUE_INFINITE, 0);
// 得られたPVを返す。
std::vector<Move> pvs;
for (Move* p = &ss->pv[0]; is_ok(*p); ++p)
pvs.push_back(*p);
return ValueAndPV(bestValue, pvs);
}
// 通常探索。深さdepth(整数で指定)。
// 3手読み時のスコアが欲しいなら、
// auto v = search(pos,3);
// のようにすべし。
// v.firstに評価値、v.secondにPVが得られる。
// multi pvが有効のときは、pos.this_thread()->rootMoves[N].pvにそのPV(読み筋)の配列が得られる。
// multi pvの指定はこの関数の引数multiPVで行なう。(Options["MultiPV"]の値は無視される)
//
// rootでの宣言勝ち判定はしないので(扱いが面倒なので)、ここでは行わない。
// 呼び出し側で処理すること。
//
// 前提条件) pos.set_this_thread(Threads[thread_id])で探索スレッドが設定されていること。
//  また、Threads.stopが来ると探索を中断してしまうので、そのときのPVは正しくない。
//  search()から戻ったあと、Threads.stop == trueなら、その探索結果を用いてはならない。
//  あと、呼び出し前は、Threads.stop == falseの状態で呼び出さないと、探索を中断して返ってしまうので注意。
ValueAndPV search(Position& pos, int depth_, size_t multiPV /* = 1 */, uint64_t nodesLimit /* = 0 */)
{
std::vector<Move> pvs;
Depth depth = depth_;
if (depth < 0)
return std::pair<Value, std::vector<Move>>(Eval::evaluate(pos), std::vector<Move>());
if (depth == 0)
return qsearch(pos);
Stack stack[MAX_PLY + 10], * ss = stack + 7;
Move pv[MAX_PLY + 1];
init_for_search(pos, ss);
ss->pv = pv; // とりあえずダミーでどこかバッファがないといけない。
// this_threadに関連する変数の初期化
auto th = pos.this_thread();
auto& rootDepth = th->rootDepth;
auto& pvIdx = th->pvIdx;
auto& pvLast = th->pvLast;
auto& rootMoves = th->rootMoves;
auto& completedDepth = th->completedDepth;
auto& selDepth = th->selDepth;
// bestmoveとしてしこの局面の上位N個を探索する機能
//size_t multiPV = Options["MultiPV"];
// この局面での指し手の数を上回ってはいけない
multiPV = std::min(multiPV, rootMoves.size());
// ード制限にMultiPVの値を掛けておかないと、depth固定、MultiPVありにしたときに1つの候補手に同じnodeだけ思考したことにならない。
nodesLimit *= multiPV;
Value alpha = -VALUE_INFINITE;
Value beta = VALUE_INFINITE;
Value delta = -VALUE_INFINITE;
Value bestValue = -VALUE_INFINITE;
while ((rootDepth += 1) <= depth
// node制限を超えた場合もこのループを抜ける
// 探索ノード数は、この関数の引数で渡されている。
&& !(nodesLimit /*node制限あり*/ && th->nodes.load(std::memory_order_relaxed) >= nodesLimit)
)
{
for (RootMove& rm : rootMoves)
rm.previousScore = rm.score;
size_t pvFirst = 0;
pvLast = 0;
// MultiPV loop. We perform a full root search for each PV line
for (pvIdx = 0; pvIdx < multiPV && !Threads.stop; ++pvIdx)
{
if (pvIdx == pvLast)
{
pvFirst = pvLast;
for (pvLast++; pvLast < rootMoves.size(); pvLast++)
if (rootMoves[pvLast].tbRank != rootMoves[pvFirst].tbRank)
break;
}
// それぞれのdepthとPV lineに対するUSI infoで出力するselDepth
selDepth = 0;
// depth 5以上においてはaspiration searchに切り替える。
if (rootDepth >= 5 * 1)
{
delta = Value(20);
Value p = rootMoves[pvIdx].previousScore;
alpha = std::max(p - delta, -VALUE_INFINITE);
beta = std::min(p + delta, VALUE_INFINITE);
}
// aspiration search
int failedHighCnt = 0;
while (true)
{
Depth adjustedDepth = std::max(1, rootDepth - failedHighCnt * 1);
bestValue = ::search<PV>(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 windowを広げる。
// ただし、引数で指定されていた値になっていたら、もうfail low/high扱いとしてbreakする。
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);
// 暴走チェック
//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;
}
// このPV、途中でNULL_MOVEの可能性があるかも知れないので排除するためにis_ok()を通す。
// → PVなのでNULL_MOVEはしないことになっているはずだし、
// MOVE_WINも突っ込まれていることはない。(いまのところ)
for (Move move : rootMoves[0].pv)
{
if (!is_ok(move))
break;
pvs.push_back(move);
}
//sync_cout << rootDepth << sync_endl;
// multiPV時を考慮して、rootMoves[0]のscoreをbestValueとして返す。
bestValue = rootMoves[0].score;
return ValueAndPV(bestValue, pvs);
}
}
#endif

View file

@ -88,6 +88,7 @@ struct LimitsType {
time[WHITE] = time[BLACK] = inc[WHITE] = inc[BLACK] = npmsec = movetime = TimePoint(0);
movestogo = depth = mate = perft = infinite = 0;
nodes = 0;
silent = false;
}
bool use_time_management() const {
@ -98,6 +99,9 @@ struct LimitsType {
TimePoint time[COLOR_NB], inc[COLOR_NB], npmsec, movetime, startTime;
int movestogo, depth, mate, perft, infinite;
int64_t nodes;
// 画面に出力しないサイレントモード(プロセス内での連続自己対戦のとき用)
// このときPVを出力しない。
bool silent;
};
extern LimitsType Limits;

View file

@ -115,6 +115,9 @@ void TranspositionTable::clear() {
/// TTEntry t2 if its replace value is greater than that of t2.
TTEntry* TranspositionTable::probe(const Key key, bool& found) const {
#if defined(DISABLE_TT)
return found = false, first_entry(0);
#else
TTEntry* const tte = first_entry(key);
const uint16_t key16 = (uint16_t)key; // Use the low 16 bits as key inside the cluster
@ -139,6 +142,7 @@ TTEntry* TranspositionTable::probe(const Key key, bool& found) const {
replace = &tte[i];
return found = false, replace;
#endif
}

View file

@ -131,6 +131,8 @@ enum Color {
WHITE, BLACK, COLOR_NB = 2
};
constexpr Color Colors[2] = { WHITE, BLACK };
enum CastlingRights {
NO_CASTLING,
WHITE_OO,
@ -187,7 +189,10 @@ enum Value : int {
QueenValueMg = 2538, QueenValueEg = 2682,
Tempo = 28,
MidgameLimit = 15258, EndgameLimit = 3915
MidgameLimit = 15258, EndgameLimit = 3915,
// 評価関数の返す値の最大値(2**14ぐらいに収まっていて欲しいところだが..)
VALUE_MAX_EVAL = 27000,
};
enum PieceType {
@ -232,7 +237,8 @@ enum Square : int {
SQ_A8, SQ_B8, SQ_C8, SQ_D8, SQ_E8, SQ_F8, SQ_G8, SQ_H8,
SQ_NONE,
SQUARE_NB = 64
SQUARE_ZERO = 0, SQUARE_NB = 64,
SQUARE_NB_PLUS1 = SQUARE_NB + 1, // 玉がいない場合、SQUARE_NBに移動したものとして扱うため、配列をSQUARE_NB+1で確保しないといけないときがあるのでこの定数を用いる。
};
enum Direction : int {
@ -455,6 +461,44 @@ constexpr bool is_ok(Move m) {
return from_sq(m) != to_sq(m); // Catch MOVE_NULL and MOVE_NONE
}
// 盤面を180°回したときの升目を返す
constexpr Square Inv(Square sq) { return (Square)((SQUARE_NB - 1) - sq); }
// 盤面をミラーしたときの升目を返す
constexpr Square Mir(Square sq) { return make_square(File(7 - (int)file_of(sq)), rank_of(sq)); }
#if defined(EVAL_NNUE) || defined(EVAL_LEARN)
// --------------------
// 駒箱
// --------------------
// Positionクラスで用いる、駒リスト(どの駒がどこにあるのか)を管理するときの番号。
enum PieceNumber : uint8_t
{
PIECE_NUMBER_PAWN = 0,
PIECE_NUMBER_KNIGHT = 16,
PIECE_NUMBER_BISHOP = 20,
PIECE_NUMBER_ROOK = 24,
PIECE_NUMBER_QUEEN = 28,
PIECE_NUMBER_KING = 30,
PIECE_NUMBER_WKING = 30,
PIECE_NUMBER_BKING = 31, // 先手、後手の玉の番号が必要な場合はこっちを用いる
PIECE_NUMBER_ZERO = 0,
PIECE_NUMBER_NB = 32,
};
inline PieceNumber& operator++(PieceNumber& d) { return d = PieceNumber(int8_t(d) + 1); }
inline PieceNumber operator++(PieceNumber& d, int) {
PieceNumber x = d;
d = PieceNumber(int8_t(d) + 1);
return x;
}
inline PieceNumber& operator--(PieceNumber& d) { return d = PieceNumber(int8_t(d) - 1); }
// PieceNumberの整合性の検査。assert用。
constexpr bool is_ok(PieceNumber pn) { return pn < PIECE_NUMBER_NB; }
#endif // defined(EVAL_NNUE) || defined(EVAL_LEARN)
#endif // #ifndef TYPES_H_INCLUDED
#include "tune.h" // Global visibility to tuning setup

View file

@ -33,16 +33,55 @@
#include "uci.h"
#include "syzygy/tbprobe.h"
#if defined(EVAL_NNUE) && defined(ENABLE_TEST_CMD)
#include "eval/nnue/nnue_test_command.h"
#endif
using namespace std;
extern vector<string> 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";
// 棋譜を自動生成するコマンド
#if defined (EVAL_LEARN)
namespace Learner
{
// 教師局面の自動生成
void gen_sfen(Position& pos, istringstream& is);
// 生成した棋譜からの学習
void learn(Position& pos, istringstream& is);
#if defined(GENSFEN2019)
// 開発中の教師局面の自動生成コマンド
void gen_sfen2019(Position& pos, istringstream& is);
#endif
// 読み筋と評価値のペア。Learner::search(),Learner::qsearch()が返す。
typedef std::pair<Value, std::vector<Move> > ValueAndPV;
ValueAndPV qsearch(Position& pos);
ValueAndPV search(Position& pos, int depth_, size_t multiPV = 1, uint64_t nodesLimit = 0);
}
#endif
#if defined(EVAL_NNUE) && defined(ENABLE_TEST_CMD)
void test_cmd(Position& pos, istringstream& is)
{
// 探索をするかも知れないので初期化しておく。
is_ready();
std::string param;
is >> param;
if (param == "nnue") Eval::NNUE::TestCommand(pos, is);
}
#endif
namespace {
// FEN string of the initial position, normal chess
const char* StartFEN = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1";
// position() is called when engine receives the "position" UCI command.
// The function sets up the position described in the given FEN string ("fen")
// or the starting position ("startpos") and then makes the moves given in the
@ -182,8 +221,115 @@ namespace {
<< "\nNodes/second : " << 1000 * nodes / elapsed << endl;
}
// check sumを計算したとき、それを保存しておいてあとで次回以降、整合性のチェックを行なう。
uint64_t eval_sum;
} // namespace
// is_ready_cmd()を外部から呼び出せるようにしておく。(benchコマンドなどから呼び出したいため)
// 局面は初期化されないので注意。
void is_ready(bool skipCorruptCheck)
{
#if defined(EVAL_NNUE)
// "isready"を受け取ったあと、"readyok"を返すまで5秒ごとに改行を送るように修正する。(keep alive的な処理)
// USI2.0の仕様より。
// -"isready"のあとのtime out時間は、30秒程度とする。これを超えて、評価関数の初期化、hashテーブルの確保をしたい場合、
// 思考エンジン側から定期的に何らかのメッセージ(改行可)を送るべきである。
// -ShogiGUIではすでにそうなっているので、MyShogiもそれに追随する。
// -また、やねうら王のエンジン側は、"isready"を受け取ったあと、"readyok"を返すまで5秒ごとに改行を送るように修正する。
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 /* 5秒 */)
{
count = 0;
sync_cout << sync_endl; // 改行を送信する。
}
}
});
// 評価関数の読み込みなど時間のかかるであろう処理はこのタイミングで行なう。
// 起動時に時間のかかる処理をしてしまうと将棋所がタイムアウト判定をして、思考エンジンとしての認識をリタイアしてしまう。
if (!UCI::load_eval_finished)
{
// 評価関数の読み込み
Eval::load_eval();
// チェックサムの計算と保存(その後のメモリ破損のチェックのため)
eval_sum = Eval::calc_check_sum();
// ソフト名の表示
Eval::print_softname(eval_sum);
UCI::load_eval_finished = true;
}
else
{
// メモリが破壊されていないかを調べるためにチェックサムを毎回調べる。
// 時間が少しもったいない気もするが.. 0.1秒ぐらいのことなので良しとする。
if (!skipCorruptCheck && eval_sum != Eval::calc_check_sum())
sync_cout << "Error! : EVAL memory is corrupted" << sync_endl;
}
// isreadyに対してはreadyokを返すまで次のコマンドが来ないことは約束されているので
// このタイミングで各種変数の初期化もしておく。
TT.resize(Options["Hash"]);
Search::clear();
Time.availableNodes = 0;
Threads.stop = false;
// keep aliveを送信するために生成したスレッドを終了させ、待機する。
ended = true;
th.join();
#endif // defined(EVAL_NNUE)
sync_cout << "readyok" << sync_endl;
}
// --------------------
// テスト用にqsearch(),search()を直接呼ぶ
// --------------------
#if defined(EVAL_LEARN)
void qsearch_cmd(Position& pos)
{
cout << "qsearch : ";
auto pv = Learner::qsearch(pos);
cout << "Value = " << pv.first << " , " << UCI::value(pv.first) << " , PV = ";
for (auto m : pv.second)
cout << UCI::move(m, false) << " ";
cout << endl;
}
void search_cmd(Position& pos, istringstream& is)
{
string token;
int depth = 1;
int multi_pv = (int)Options["MultiPV"];
while (is >> token)
{
if (token == "depth")
is >> depth;
if (token == "multipv")
is >> multi_pv;
}
cout << "search depth = " << depth << " , multi_pv = " << multi_pv << " : ";
auto pv = Learner::search(pos, depth, multi_pv);
cout << "Value = " << pv.first << " , " << UCI::value(pv.first) << " , PV = ";
for (auto m : pv.second)
cout << UCI::move(m, false) << " ";
cout << endl;
}
#endif
/// UCI::loop() waits for a command from stdin, parses it and calls the appropriate
/// function. Also intercepts EOF from stdin to ensure gracefully exiting if the
@ -231,7 +377,7 @@ void UCI::loop(int argc, char* argv[]) {
else if (token == "go") go(pos, is, states);
else if (token == "position") position(pos, is, states);
else if (token == "ucinewgame") Search::clear();
else if (token == "isready") sync_cout << "readyok" << sync_endl;
else if (token == "isready") is_ready();
// Additional custom non-UCI commands, mainly for debugging.
// Do not use these commands during a search!
@ -240,6 +386,28 @@ void UCI::loop(int argc, char* argv[]) {
else if (token == "d") sync_cout << pos << sync_endl;
else if (token == "eval") sync_cout << Eval::trace(pos) << sync_endl;
else if (token == "compiler") sync_cout << compiler_info() << sync_endl;
#if defined (EVAL_LEARN)
else if (token == "gensfen") Learner::gen_sfen(pos, is);
else if (token == "learn") Learner::learn(pos, is);
#if defined (GENSFEN2019)
// 開発中の教師局面生成コマンド
else if (token == "gensfen2019") Learner::gen_sfen2019(pos, is);
#endif
// テスト用にqsearch(),search()を直接呼ぶコマンド
else if (token == "qsearch") qsearch_cmd(pos);
else if (token == "search") search_cmd(pos, is);
#endif
#if defined(EVAL_NNUE)
else if (token == "eval_nnue") sync_cout << "eval_nnue = " << Eval::compute_eval(pos) << sync_endl;
#endif
#if defined(EVAL_NNUE) && defined(ENABLE_TEST_CMD)
// テストコマンド
else if (token == "test") test_cmd(pos, is);
#endif
else
sync_cout << "Unknown command: " << cmd << sync_endl;

View file

@ -75,8 +75,18 @@ std::string move(Move m, bool chess960);
std::string pv(const Position& pos, Depth depth, Value alpha, Value beta);
Move to_move(const Position& pos, std::string& str);
// 評価関数を読み込んだかのフラグ。これはevaldirの変更にともなってfalseにする。
extern bool load_eval_finished; // = false;
} // namespace UCI
extern UCI::OptionsMap Options;
// USIの"isready"コマンドが呼び出されたときの処理。このときに評価関数の読み込みなどを行なう。
// benchmarkコマンドのハンドラなどで"isready"が来ていないときに評価関数を読み込ませたいときに用いる。
// skipCorruptCheck == trueのときは評価関数の2度目の読み込みのときのcheck sumによるメモリ破損チェックを省略する。
// ※ この関数は、Stockfishにはないがないと不便なので追加しておく。
void is_ready(bool skipCorruptCheck = false);
extern const char* StartFEN;
#endif // #ifndef UCI_H_INCLUDED

View file

@ -42,6 +42,7 @@ void on_hash_size(const Option& o) { TT.resize(size_t(o)); }
void on_logger(const Option& o) { start_logger(o); }
void on_threads(const Option& o) { Threads.set(size_t(o)); }
void on_tb_path(const Option& o) { Tablebases::init(o); }
void on_eval_dir(const Option& o) { load_eval_finished = false; }
/// Our case insensitive less() function as required by UCI protocol
@ -79,6 +80,23 @@ void init(OptionsMap& o) {
o["SyzygyProbeDepth"] << Option(1, 1, 100);
o["Syzygy50MoveRule"] << Option(true);
o["SyzygyProbeLimit"] << Option(7, 0, 7);
// 評価関数フォルダ。これを変更したとき、評価関数を次のisreadyタイミングで読み直す必要がある。
o["EvalDir"] << Option("eval", on_eval_dir);
// isreadyタイミングで評価関数を読み込まれると、新しい評価関数の変換のために
// test evalconvertコマンドを叩きたいのに、その新しい評価関数がないがために
// このコマンドの実行前に異常終了してしまう。
// そこでこの隠しオプションでisready時の評価関数の読み込みを抑制して、
// test evalconvertコマンドを叩く。
o["SkipLoadingEval"] << Option(false);
// 定跡の指し手を何手目まで用いるか
o["BookMoves"] << Option(16, 0, 10000);
#if defined(EVAL_LEARN)
// 評価関数の学習を行なうときは、評価関数の保存先のフォルダを変更できる。
// デフォルトではevalsave。このフォルダは事前に用意されているものとする。
// このフォルダ配下にフォルダを"0/","1/",…のように自動的に掘り、そこに評価関数ファイルを保存する。
o["EvalSaveDir"] << Option("evalsave");
#endif
}
@ -187,4 +205,6 @@ Option& Option::operator=(const string& v) {
return *this;
}
// 評価関数を読み込んだかのフラグ。これはevaldirの変更にともなってfalseにする。
bool load_eval_finished = false;
} // namespace UCI