diff options
| author | tslil <tslil@posteo.de> | 2026-09-05 09:54:11 +0100 |
|---|---|---|
| committer | tslil <tslil@posteo.de> | 2026-09-05 16:02:41 +0100 |
| commit | f6ea541ce762b190c7d68f221a24b555ebf0c80b (patch) | |
| tree | e45e3d1fe740ed2c893cb7ee374ea99bd90f9d40 /src/match.c | |
| parent | 1ca3309dad9d412870b2b793f33af7a7ccabd514 (diff) | |
training rework, best weights so far*
After quite a bit of experimentation it would appear that a far better way to train on self-play data in this setup is to randomise not the moves but the opening. The new selfplay and match binaries have lost the epsilon parameter and now play a random opening of some length before switching to pure, deterministic engine inference. This also resulted in a cleanup of nn_train.py.
As an added bonus, in this configuration it makes sense to harvest training data from both sides in self-play and consequently the two sides are given distinct game ids (this was almost surely an oversight in the original version of TD(lambda) that i had). I also noticed that rand32 was not using the top 32bits of xorshitf*.
*Under default settings nn_train.py produces a 5x5 weight set that doesn't lose a single game to any other cttei that i cared to look at, when measured using `racetrack` (which has a diverse balanced opening book).
Diffstat (limited to 'src/match.c')
| -rw-r--r-- | src/match.c | 52 |
1 files changed, 18 insertions, 34 deletions
diff --git a/src/match.c b/src/match.c index 49ff9c9..eff1931 100644 --- a/src/match.c +++ b/src/match.c @@ -73,31 +73,27 @@ void negamax_display_progress(const uint8_t cur_depth, // Game driver // =================================================================== -// Returns the winner's colour, or -1 for a draw / abort static int play_game(const nn_function current_fn, const nn_function cand_fn, - const enum COLOUR current_fn_colour, const uint32_t eps10000) { + const enum COLOUR current_fn_colour, + const uint8_t random_opening_length) { reset_state(state, state->board_size); char ptn[16]; for (uint32_t ply = 0; ply < MAX_PLY; ply++) { - // Whose evaluator is it? - const nn_function fn = - (state->current_colour == current_fn_colour) ? current_fn : cand_fn; - negamax_set_evaluator(fn); - - if (rand32() % 10000 < eps10000) { - // Epsilon: uniform random legal move + if (ply < random_opening_length) { uint32_t n_actions; action_t *actions = actions_generate(state, &n_actions); action_to_ptn(actions[rand32() % n_actions], ptn); } else { - // Greedy: the side's best move + const nn_function fn = + (state->current_colour == current_fn_colour) ? current_fn : cand_fn; + negamax_set_evaluator(fn); negamax_generate(state); strcpy(ptn, negamax_ptn); } - const enum ACT_RESULT r = do_ptn(state, ptn); - if (r == GAME_END) + const enum ACT_RESULT r = do_ptn(state, ptn); + if (r == GAME_END) break; if (r != ACT_OK) { fprintf(stderr, "match: unexpected result %d for ptn %s\n", r, ptn); @@ -125,18 +121,12 @@ static int play_game(const nn_function current_fn, const nn_function cand_fn, // =================================================================== const char *usage = - "match, one Tak game of the current weights against the candidate.\n\ -\n \ -usage: match <size> <eps> <depth>\n\ -\n\ -This binary is linked with both the include/weight_* files and the include/cand_weight_* files. It plays a single game, with a random side choice and both sides using epsilon-greedy move noise. The result format is:\n\ -\n\ - <current|candidate|draw> <plies>\n\ -\n\ -nn_train.py runs many of these in parallel to score the candidate against the current weights.\n\ -\n\ -Copyright (C) 2026, tslil clingman\n\ -\n\ + "match, one Tak game of the current weights against the candidate.\n\n\ +usage: match <size> <random_opening_length> <depth>\n\n\ +This binary is linked with both the include/weight_* files and the include/cand_weight_* files. It plays a single <size>x<size> game, with a random side choice and both sides using <random_opening_length> random moves at the start of the game. The result format is:\n\n\ +\t<current|candidate|draw> <plies>\n\n\ +nn_train.py runs many of these in parallel to score the candidate against the current weights.\n\n\ +Copyright (C) 2026, tslil clingman\n\n\ This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version."; int main(int argc, char **argv) { @@ -146,22 +136,16 @@ int main(int argc, char **argv) { } const uint8_t size = (uint8_t)atoi(argv[1]); - const uint32_t eps10000 = (uint32_t)(atof(argv[2]) * 10000.0 + 0.5); + const uint8_t random_opening_length = (uint8_t)atoi(argv[2]); negamax_search_depth = (uint8_t)atoi(argv[3]); if (size != 5 && size != 6) { fprintf(stderr, "match: size must be 5 or 6\n"); return EXIT_FAILURE; } - if (eps10000 >= 10000) { - fprintf(stderr, "match: eps must be < 1\n"); - return EXIT_FAILURE; - } - - // Processes launched in the same second must not share a seed xors = ((uint64_t)time(NULL) ^ ((uint64_t)getpid() << 32)) * - 0x9E3779B97F4A7C15ULL + - 0xDEADBEEF; + 0x9E3779B97F4A7C15ULL + + 0xC0FFEE; state = new_tak_state(size); negamax_init(size); @@ -173,7 +157,7 @@ int main(int argc, char **argv) { : &six_cand; const enum COLOUR current_fn_colour = rand32() % 2 ? C_WHITE : C_BLACK; - const int winner = play_game(current_fn, cand_fn, current_fn_colour, eps10000); + const int winner = play_game(current_fn, cand_fn, current_fn_colour, random_opening_length); const char *name; if (winner == -1) |
