Files
Stockfish/src/tools/training_data_generator.cpp
Tomasz Sobczyk a4605860c6 Post-merge fixes.
2021-05-24 11:45:21 +02:00

900 lines
32 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "training_data_generator.h"
#include "sfen_writer.h"
#include "packed_sfen.h"
#include "opening_book.h"
#include "misc.h"
#include "position.h"
#include "thread.h"
#include "tt.h"
#include "uci.h"
#include "extra/nnue_data_binpack_format.h"
#include "nnue/evaluate_nnue.h"
#include "syzygy/tbprobe.h"
#include <atomic>
#include <chrono>
#include <climits>
#include <cmath>
#include <cstring>
#include <fstream>
#include <iomanip>
#include <limits>
#include <list>
#include <memory>
#include <optional>
#include <random>
#include <shared_mutex>
#include <sstream>
#include <unordered_set>
using namespace std;
namespace Stockfish::Tools
{
// Class to generate sfen with multiple threads
struct TrainingDataGenerator
{
struct Params
{
// Min and max depths for search during gensfen
int search_depth_min = 3;
int search_depth_max = -1;
// Number of the nodes to be searched.
// 0 represents no limits.
uint64_t nodes = 0;
// Upper limit of evaluation value of generated situation
int eval_limit = 3000;
// minimum ply with random move
// maximum ply with random move
// Number of random moves in one station
int random_move_minply = 1;
int random_move_maxply = 24;
int random_move_count = 5;
// Move kings with a probability of 1/N when randomly moving like Apery software.
// When you move the king again, there is a 1/N chance that it will randomly moved
// once in the opponent's turn.
// Apery has N=2. Specifying 0 here disables this function.
int random_move_like_apery = 0;
// For when using multi pv instead of random move.
// random_multi_pv is the number of candidates for MultiPV.
// When adopting the move of the candidate move, the difference
// between the evaluation value of the move of the 1st place
// and the evaluation value of the move of the Nth place is.
// Must be in the range random_multi_pv_diff.
// random_multi_pv_depth is the search depth for MultiPV.
int random_multi_pv = 0;
int random_multi_pv_diff = 32000;
int random_multi_pv_depth = -1;
// The minimum and maximum ply (number of steps from
// the initial phase) of the sfens to write out.
int write_minply = 16;
int write_maxply = 400;
uint64_t save_every = std::numeric_limits<uint64_t>::max();
std::string output_file_name = "training_data";
SfenOutputType sfen_format = SfenOutputType::Binpack;
std::string seed;
bool write_out_draw_game_in_training_data_generation = true;
bool detect_draw_by_consecutive_low_score = true;
bool detect_draw_by_insufficient_mating_material = true;
uint64_t num_threads;
std::string book;
void enforce_constraints()
{
search_depth_max = std::max(search_depth_min, search_depth_max);
// Limit the maximum to a one-stop score. (Otherwise you might not end the loop)
eval_limit = std::min(eval_limit, (int)mate_in(2));
save_every = std::max(save_every, REPORT_STATS_EVERY);
num_threads = Options["Threads"];
random_multi_pv_depth = std::max(search_depth_max, random_multi_pv_depth);
}
};
// Hash to limit the export of identical sfens
static constexpr uint64_t GENSFEN_HASH_SIZE = 64 * 1024 * 1024;
// It must be 2**N because it will be used as the mask to calculate hash_index.
static_assert((GENSFEN_HASH_SIZE& (GENSFEN_HASH_SIZE - 1)) == 0);
static constexpr uint64_t REPORT_DOT_EVERY = 5000;
static constexpr uint64_t REPORT_STATS_EVERY = 200000;
static_assert(REPORT_STATS_EVERY % REPORT_DOT_EVERY == 0);
TrainingDataGenerator(
const Params& prm
) :
params(prm),
sfen_writer(prm.output_file_name, prm.num_threads, prm.save_every, prm.sfen_format)
{
hash.resize(GENSFEN_HASH_SIZE);
prngs.reserve(prm.num_threads);
auto seed = prm.seed;
for (uint64_t i = 0; i < prm.num_threads; ++i)
{
prngs.emplace_back(seed);
seed = prngs.back().next_random_seed();
}
if (!prm.book.empty())
{
opening_book = open_opening_book(prm.book, prngs[0]);
if (opening_book == nullptr)
{
std::cout << "WARNING: Failed to open opening book " << prm.book << ". Falling back to startpos.\n";
}
}
// Output seed to veryfy by the user if it's not identical by chance.
std::cout << prngs[0] << std::endl;
}
void generate(uint64_t limit);
private:
Params params;
std::vector<PRNG> prngs;
std::mutex stats_mutex;
TimePoint last_stats_report_time;
// sfen exporter
SfenWriter sfen_writer;
SynchronizedRegionLogger::Region out;
vector<Key> hash; // 64MB*sizeof(HASH_KEY) = 512MB
std::unique_ptr<OpeningBook> opening_book;
static void set_gensfen_search_limits();
void generate_worker(
Thread& th,
std::atomic<uint64_t>& counter,
uint64_t limit);
bool was_seen_before(const Position& pos);
optional<int8_t> get_current_game_result(
Position& pos,
const vector<int>& move_hist_scores) const;
vector<uint8_t> generate_random_move_flags(PRNG& prng);
optional<Move> choose_random_move(
PRNG& prng,
Position& pos,
std::vector<uint8_t>& random_move_flag,
int ply,
int& random_move_c);
bool commit_psv(
Thread& th,
PSVector& sfens,
int8_t lastTurnIsWin,
std::atomic<uint64_t>& counter,
uint64_t limit,
Color result_color);
void report(uint64_t done, uint64_t new_done);
void maybe_report(uint64_t done);
};
void TrainingDataGenerator::set_gensfen_search_limits()
{
// About Search::Limits
// Be careful because this member variable is global and affects other threads.
auto& limits = Search::Limits;
// Make the search equivalent to the "go infinite" command. (Because it is troublesome if time management is done)
limits.infinite = true;
// Since PV is an obstacle when displayed, erase it.
limits.silent = true;
// If you use this, it will be compared with the accumulated nodes of each thread. Therefore, do not use it.
limits.nodes = 0;
// depth is also processed by the one passed as an argument of Tools::search().
limits.depth = 0;
}
void TrainingDataGenerator::generate(uint64_t limit)
{
last_stats_report_time = 0;
set_gensfen_search_limits();
std::atomic<uint64_t> counter{0};
Threads.execute_with_workers([&counter, limit, this](Thread& th) {
generate_worker(th, counter, limit);
});
Threads.wait_for_workers_finished();
sfen_writer.flush();
if (limit % REPORT_STATS_EVERY != 0)
{
report(limit, limit % REPORT_STATS_EVERY);
}
std::cout << std::endl;
}
void TrainingDataGenerator::generate_worker(
Thread& th,
std::atomic<uint64_t>& counter,
uint64_t limit)
{
// For the time being, it will be treated as a draw
// at the maximum number of steps to write.
// Maximum StateInfo + Search PV to advance to leaf buffer
std::vector<StateInfo, AlignedAllocator<StateInfo>> states(
params.write_maxply + MAX_PLY /* == search_depth_min + α */);
StateInfo si;
auto& prng = prngs[th.id()];
// end flag
bool quit = false;
// repeat until the specified number of times
while (!quit)
{
// It is necessary to set a dependent thread for Position.
// When parallelizing, Threads (since this is a vector<Thread*>,
// Do the same for up to Threads[0]...Threads[thread_num-1].
auto& pos = th.rootPos;
if (opening_book != nullptr)
{
auto& fen = opening_book->next_fen();
pos.set(fen, false, &si, &th);
}
else
{
pos.set(StartFEN, false, &si, &th);
}
int resign_counter = 0;
bool should_resign = prng.rand(10) > 1;
// Vector for holding the sfens in the current simulated game.
PSVector packed_sfens;
packed_sfens.reserve(params.write_maxply + MAX_PLY);
// Precomputed flags. Used internally by choose_random_move.
vector<uint8_t> random_move_flag = generate_random_move_flags(prng);
// A counter that keeps track of the number of random moves
// When random_move_minply == -1, random moves are
// performed continuously, so use it at this time.
// Used internally by choose_random_move.
int actual_random_move_count = 0;
// Save history of move scores for adjudication
vector<int> move_hist_scores;
auto flush_psv = [&](int8_t result) {
quit = commit_psv(th, packed_sfens, result, counter, limit, pos.side_to_move());
};
for (int ply = 0; ; ++ply)
{
// Current search depth
const int depth = params.search_depth_min + (int)prng.rand(params.search_depth_max - params.search_depth_min + 1);
// Starting search calls init_for_search
auto [search_value, search_pv] = Search::search(pos, depth, 1, params.nodes);
// This has to be performed after search because it needs to know
// rootMoves which are filled in init_for_search.
const auto result = get_current_game_result(pos, move_hist_scores);
if (result.has_value())
{
flush_psv(result.value());
break;
}
// Always adjudivate by eval limit.
// Also because of this we don't have to check for TB/MATE scores
if (abs(search_value) >= params.eval_limit)
{
resign_counter++;
if ((should_resign && resign_counter >= 4) || abs(search_value) >= VALUE_KNOWN_WIN) {
flush_psv((search_value >= params.eval_limit) ? 1 : -1);
break;
}
}
else
{
resign_counter = 0;
}
// In case there is no PV and the game was not ended here
// there is nothing we can do, we can't continue the game,
// we don't know the result, so discard this game.
if (search_pv.empty())
{
break;
}
// Save the move score for adjudication.
move_hist_scores.push_back(search_value);
// Discard stuff before write_minply is reached
// because it can harm training due to overfitting.
// Initial positions would be too common.
if (ply >= params.write_minply && !was_seen_before(pos))
{
auto& psv = packed_sfens.emplace_back();
// Here we only write the position data.
// Result is added after the whole game is done.
pos.sfen_pack(psv.sfen);
psv.score = search_value;
psv.move = search_pv[0];
psv.gamePly = ply;
}
// Update the next move according to best search result or random move.
auto random_move = choose_random_move(prng, pos, random_move_flag, ply, actual_random_move_count);
const Move next_move = random_move.has_value() ? *random_move : search_pv[0];
// We don't have the whole game yet, but it ended,
// so the writing process ends and the next game starts.
// This shouldn't really happen.
if (!is_ok(next_move))
{
break;
}
// Do move.
pos.do_move(next_move, states[ply]);
}
}
}
bool TrainingDataGenerator::was_seen_before(const Position& pos)
{
// Look into the position hashtable to see if the same
// position was seen before.
// This is a good heuristic to exlude already seen
// positions without many false positives.
auto key = pos.key();
auto hash_index = (size_t)(key & (GENSFEN_HASH_SIZE - 1));
auto old_key = hash[hash_index];
if (key == old_key)
{
return true;
}
else
{
// Replace with the current key.
hash[hash_index] = key;
return false;
}
}
optional<int8_t> TrainingDataGenerator::get_current_game_result(
Position& pos,
const vector<int>& move_hist_scores) const
{
// Variables for draw adjudication.
// Todo: Make this as an option.
// start the adjudication when ply reaches this value
constexpr int adj_draw_ply = 80;
// 4 move scores for each side have to be checked
constexpr int adj_draw_cnt = 8;
// move score in CP
constexpr int adj_draw_score = 0;
// For the time being, it will be treated as a
// draw at the maximum number of steps to write.
const int ply = move_hist_scores.size();
// has it reached the max length or is a draw by fifty-move rule
// or by 3-fold repetition
if (ply >= params.write_maxply
|| pos.is_fifty_move_draw()
|| pos.is_three_fold_repetition())
{
return 0;
}
if(pos.this_thread()->rootMoves.empty())
{
// If there is no legal move
return pos.checkers()
? -1 /* mate */
: 0 /* stalemate */;
}
// Adjudicate game to a draw if the last 4 scores of each engine is 0.
if (params.detect_draw_by_consecutive_low_score)
{
if (ply >= adj_draw_ply)
{
int num_cons_plies_within_draw_score = 0;
bool is_adj_draw = false;
for (auto it = move_hist_scores.rbegin();
it != move_hist_scores.rend(); ++it)
{
if (abs(*it) <= adj_draw_score)
{
num_cons_plies_within_draw_score++;
}
else
{
// Draw scores must happen on consecutive plies
break;
}
if (num_cons_plies_within_draw_score >= adj_draw_cnt)
{
is_adj_draw = true;
break;
}
}
if (is_adj_draw)
{
return 0;
}
}
}
// Draw by insufficient mating material
if (params.detect_draw_by_insufficient_mating_material)
{
if (pos.count<ALL_PIECES>() <= 4)
{
int num_pieces = pos.count<ALL_PIECES>();
// (1) KvK
if (num_pieces == 2)
{
return 0;
}
// (2) KvK + 1 minor piece
if (num_pieces == 3)
{
int minor_pc = pos.count<BISHOP>(WHITE) + pos.count<KNIGHT>(WHITE) +
pos.count<BISHOP>(BLACK) + pos.count<KNIGHT>(BLACK);
if (minor_pc == 1)
{
return 0;
}
}
// (3) KBvKB, bishops of the same color
else if (num_pieces == 4)
{
if (pos.count<BISHOP>(WHITE) == 1 && pos.count<BISHOP>(BLACK) == 1)
{
// Color of bishops is black.
if ((pos.pieces(WHITE, BISHOP) & DarkSquares)
&& (pos.pieces(BLACK, BISHOP) & DarkSquares))
{
return 0;
}
// Color of bishops is white.
if ((pos.pieces(WHITE, BISHOP) & ~DarkSquares)
&& (pos.pieces(BLACK, BISHOP) & ~DarkSquares))
{
return 0;
}
}
}
}
}
return nullopt;
}
vector<uint8_t> TrainingDataGenerator::generate_random_move_flags(PRNG& prng)
{
vector<uint8_t> random_move_flag;
// Depending on random move selection parameters setup
// the array of flags that indicates whether a random move
// be taken at a given ply.
// Make an array like a[0] = 0 ,a[1] = 1, ...
// Fisher-Yates shuffle and take out the first N items.
// Actually, I only want N pieces, so I only need
// to shuffle the first N pieces with Fisher-Yates.
vector<int> a;
a.reserve((size_t)params.random_move_maxply);
// random_move_minply ,random_move_maxply is specified by 1 origin,
// Note that we are handling 0 origin here.
for (int i = std::max(params.random_move_minply - 1, 0); i < params.random_move_maxply; ++i)
{
a.push_back(i);
}
// In case of Apery random move, insert() may be called random_move_count times.
// Reserve only the size considering it.
random_move_flag.resize((size_t)params.random_move_maxply + params.random_move_count);
// A random move that exceeds the size() of a[] cannot be applied, so limit it.
for (int i = 0; i < std::min(params.random_move_count, (int)a.size()); ++i)
{
swap(a[i], a[prng.rand((uint64_t)a.size() - i) + i]);
random_move_flag[a[i]] = true;
}
return random_move_flag;
}
optional<Move> TrainingDataGenerator::choose_random_move(
PRNG& prng,
Position& pos,
std::vector<uint8_t>& random_move_flag,
int ply,
int& random_move_c)
{
optional<Move> random_move;
// Randomly choose one from legal move
if (
// 1. Random move of random_move_count times from random_move_minply to random_move_maxply
(params.random_move_minply != -1 && ply < (int)random_move_flag.size() && random_move_flag[ply]) ||
// 2. A mode to perform random move of random_move_count times after leaving the startpos
(params.random_move_minply == -1 && random_move_c < params.random_move_count))
{
++random_move_c;
// It's not a mate, so there should be one legal move...
if (params.random_multi_pv == 0)
{
// Normal random move
MoveList<LEGAL> list(pos);
// I don't really know the goodness and badness of making this the Apery method.
if (params.random_move_like_apery == 0
|| prng.rand(params.random_move_like_apery) != 0)
{
// Normally one move from legal move
random_move = list.at((size_t)prng.rand((uint64_t)list.size()));
}
else
{
// if you can move the king, move the king
Move moves[8]; // Near 8
Move* p = &moves[0];
for (auto& m : list)
{
if (type_of(pos.moved_piece(m)) == KING)
{
*(p++) = m;
}
}
size_t n = p - &moves[0];
if (n != 0)
{
// move to move the king
random_move = moves[prng.rand(n)];
// In Apery method, at this time there is a 1/2 chance
// that the opponent will also move randomly
if (prng.rand(2) == 0)
{
// Is it a simple hack to add a "1" next to random_move_flag[ply]?
random_move_flag.insert(random_move_flag.begin() + ply + 1, 1, true);
}
}
else
{
// Normally one move from legal move
random_move = list.at((size_t)prng.rand((uint64_t)list.size()));
}
}
}
else
{
Search::search(pos, params.random_multi_pv_depth, params.random_multi_pv);
// Select one from the top N hands of root Moves
auto& rm = pos.this_thread()->rootMoves;
uint64_t s = min((uint64_t)rm.size(), (uint64_t)params.random_multi_pv);
for (uint64_t i = 1; i < s; ++i)
{
// The difference from the evaluation value of rm[0] must
// be within the range of random_multi_pv_diff.
// It can be assumed that rm[x].score is arranged in descending order.
if (rm[0].score > rm[i].score + params.random_multi_pv_diff)
{
s = i;
break;
}
}
random_move = rm[prng.rand(s)].pv[0];
}
}
return random_move;
}
// Write out the phases loaded in sfens to a file.
// result: win/loss in the next phase after the final phase in sfens
// 1 when winning. -1 when losing. Pass 0 for a draw.
// Return value: true if the specified number of
// sfens has already been reached and the process ends.
bool TrainingDataGenerator::commit_psv(
Thread& th,
PSVector& sfens,
int8_t result,
std::atomic<uint64_t>& counter,
uint64_t limit,
Color result_color)
{
if (!params.write_out_draw_game_in_training_data_generation && result == 0)
{
// We didn't write anything so why quit.
return false;
}
auto side_to_move_from_sfen = [](auto& sfen){
return (Color)(sfen.sfen.data[0] & 1);
};
// From the final stage (one step before) to the first stage, give information on the outcome of the game for each stage.
// The phases stored in sfens are assumed to be continuous (in order).
for (auto it = sfens.rbegin(); it != sfens.rend(); ++it)
{
// The side to move is packed as the lowest bit of the first byte
const Color side_to_move = side_to_move_from_sfen(*it);
it->game_result = side_to_move == result_color ? result : -result;
}
// Write sfens in move order to make potential compression easier
for (auto& sfen : sfens)
{
// Return true if there is already enough data generated.
const auto iter = counter.fetch_add(1);
if (iter >= limit)
return true;
// because `iter` was done, now we do one more
maybe_report(iter + 1);
// Write out one sfen.
sfen_writer.write(th.id(), sfen);
}
return false;
}
void TrainingDataGenerator::report(uint64_t done, uint64_t new_done)
{
const auto now_time = now();
const TimePoint elapsed = now_time - last_stats_report_time + 1;
out
<< endl
<< done << " sfens, "
<< new_done * 1000 / elapsed << " sfens/second, "
<< "at " << now_string() << sync_endl;
last_stats_report_time = now_time;
out = sync_region_cout.new_region();
}
void TrainingDataGenerator::maybe_report(uint64_t done)
{
if (done % REPORT_DOT_EVERY == 0)
{
std::lock_guard lock(stats_mutex);
if (last_stats_report_time == 0)
{
last_stats_report_time = now();
out = sync_region_cout.new_region();
}
if (done != 0)
{
out << '.';
if (done % REPORT_STATS_EVERY == 0)
{
report(done, REPORT_STATS_EVERY);
}
}
}
}
// Command to generate a game record
void generate_training_data(istringstream& is)
{
// Number of generated game records default = 8 billion phases (Ponanza specification)
uint64_t loop_max = 8000000000UL;
TrainingDataGenerator::Params params;
// Add a random number to the end of the file name.
bool random_file_name = false;
std::string sfen_format = "binpack";
string token;
while (true)
{
token = "";
is >> token;
if (token == "")
break;
if (token == "depth")
{
is >> params.search_depth_min;
params.search_depth_max = params.search_depth_min;
}
else if (token == "min_depth")
is >> params.search_depth_min;
else if (token == "max_depth")
is >> params.search_depth_min;
else if (token == "nodes")
is >> params.nodes;
else if (token == "count")
is >> loop_max;
else if (token == "output_file_name")
is >> params.output_file_name;
else if (token == "eval_limit")
is >> params.eval_limit;
else if (token == "random_move_min_ply")
is >> params.random_move_minply;
else if (token == "random_move_max_ply")
is >> params.random_move_maxply;
else if (token == "random_move_count")
is >> params.random_move_count;
else if (token == "random_move_like_apery")
is >> params.random_move_like_apery;
else if (token == "random_multi_pv")
is >> params.random_multi_pv;
else if (token == "random_multi_pv_diff")
is >> params.random_multi_pv_diff;
else if (token == "random_multi_pv_depth")
is >> params.random_multi_pv_depth;
else if (token == "write_min_ply")
is >> params.write_minply;
else if (token == "write_max_ply")
is >> params.write_maxply;
else if (token == "save_every")
is >> params.save_every;
else if (token == "book")
is >> params.book;
else if (token == "random_file_name")
is >> random_file_name;
else if (token == "keep_draws")
is >> params.write_out_draw_game_in_training_data_generation;
else if (token == "adjudicate_draws_by_score")
is >> params.detect_draw_by_consecutive_low_score;
else if (token == "adjudicate_draws_by_insufficient_material")
is >> params.detect_draw_by_insufficient_mating_material;
else if (token == "data_format")
is >> sfen_format;
else if (token == "seed")
is >> params.seed;
else if (token == "set_recommended_uci_options")
{
UCI::setoption("Contempt", "0");
UCI::setoption("Skill Level", "20");
UCI::setoption("UCI_Chess960", "false");
UCI::setoption("UCI_AnalyseMode", "false");
UCI::setoption("UCI_LimitStrength", "false");
UCI::setoption("PruneAtShallowDepth", "false");
UCI::setoption("EnableTranspositionTable", "true");
}
else
{
cout << "ERROR: Unknown option " << token << ". Exiting...\n";
return;
}
}
if (!sfen_format.empty())
{
if (sfen_format == "bin")
params.sfen_format = SfenOutputType::Bin;
else if (sfen_format == "binpack")
params.sfen_format = SfenOutputType::Binpack;
else
cout << "WARNING: Unknown sfen format `" << sfen_format << "`. Using bin\n";
}
if (random_file_name)
{
// Give a random number to output_file_name at this point.
// Do not use std::random_device(). Because it always the same integers on MinGW.
PRNG r(params.seed);
// Just in case, reassign the random numbers.
for (int i = 0; i < 10; ++i)
r.rand(1);
auto to_hex = [](uint64_t u) {
std::stringstream ss;
ss << std::hex << u;
return ss.str();
};
// I don't want to wear 64bit numbers by accident, so I'next_move going to make a 64bit number 2 just in case.
params.output_file_name += "_" + to_hex(r.rand<uint64_t>()) + to_hex(r.rand<uint64_t>());
}
params.enforce_constraints();
std::cout << "INFO: Executing generate_training_data command\n";
std::cout << "INFO: Parameters:\n";
std::cout
<< " - search_depth_min = " << params.search_depth_min << endl
<< " - search_depth_max = " << params.search_depth_max << endl
<< " - nodes = " << params.nodes << endl
<< " - count = " << loop_max << endl
<< " - eval_limit = " << params.eval_limit << endl
<< " - num threads (UCI) = " << params.num_threads << endl
<< " - random_move_min_ply = " << params.random_move_minply << endl
<< " - random_move_max_ply = " << params.random_move_maxply << endl
<< " - random_move_count = " << params.random_move_count << endl
<< " - random_move_like_apery = " << params.random_move_like_apery << endl
<< " - random_multi_pv = " << params.random_multi_pv << endl
<< " - random_multi_pv_diff = " << params.random_multi_pv_diff << endl
<< " - random_multi_pv_depth = " << params.random_multi_pv_depth << endl
<< " - write_min_ply = " << params.write_minply << endl
<< " - write_max_ply = " << params.write_maxply << endl
<< " - book = " << params.book << endl
<< " - output_file_name = " << params.output_file_name << endl
<< " - save_every = " << params.save_every << endl
<< " - random_file_name = " << random_file_name << endl
<< " - write_drawn_games = " << params.write_out_draw_game_in_training_data_generation << endl
<< " - draw by low score = " << params.detect_draw_by_consecutive_low_score << endl
<< " - draw by insuff. mat. = " << params.detect_draw_by_insufficient_mating_material << endl;
// Show if the training data generator uses NNUE.
Eval::NNUE::verify();
Threads.main()->ponder = false;
TrainingDataGenerator gensfen(params);
gensfen.generate(loop_max);
std::cout << "INFO: generate_training_data finished." << endl;
}
}