├── .gitignore ├── CMakeLists.txt ├── LICENSE ├── README.md ├── compile_data_loader.bat ├── cross_check_eval.py ├── delete_bad_nets.py ├── do_plots.py ├── docs ├── features.md ├── img │ ├── A-768-8-8-1.drawio │ ├── A-768-8-8-1.svg │ ├── HalfKAv2-45056-256x2P1x2-32-32-1.drawio │ ├── HalfKAv2-45056-256x2P1x2-32-32-1.svg │ ├── HalfKAv2-45056-256x2P8x2[-32-32-1]x8.drawio │ ├── HalfKAv2-45056-256x2P8x2[-32-32-1]x8.svg │ ├── HalfKP-40960-256x2-32-32-1.drawio │ ├── HalfKP-40960-256x2-32-32-1.svg │ ├── HalfKP-40960-4x2-8-1.drawio │ ├── HalfKP-40960-4x2-8-1.svg │ ├── SFNNv1_architecture_detailed.drawio │ ├── SFNNv1_architecture_detailed.svg │ ├── SFNNv1_architecture_detailed_v2.drawio │ ├── SFNNv1_architecture_detailed_v2.svg │ ├── SFNNv2_architecture.drawio │ ├── SFNNv2_architecture.svg │ ├── SFNNv2_architecture_detailed.drawio │ ├── SFNNv2_architecture_detailed.svg │ ├── SFNNv2_architecture_detailed_v2.drawio │ ├── SFNNv2_architecture_detailed_v2.svg │ ├── SFNNv3_architecture.drawio │ ├── SFNNv3_architecture.svg │ ├── SFNNv3_architecture_detailed.drawio │ ├── SFNNv3_architecture_detailed.svg │ ├── SFNNv3_architecture_detailed_v2.drawio │ ├── SFNNv3_architecture_detailed_v2.svg │ ├── SFNNv4_architecture.drawio │ ├── SFNNv4_architecture.svg │ ├── SFNNv4_architecture_detailed.drawio │ ├── SFNNv4_architecture_detailed.svg │ ├── SFNNv4_architecture_detailed_v2.drawio │ ├── SFNNv4_architecture_detailed_v2.svg │ ├── SFNNv5_architecture.drawio │ ├── SFNNv5_architecture.svg │ ├── SFNNv5_architecture_detailed.drawio │ ├── SFNNv5_architecture_detailed.svg │ ├── SFNNv5_architecture_detailed_v2.drawio │ ├── SFNNv5_architecture_detailed_v2.svg │ ├── SFNNv6_architecture.drawio │ ├── SFNNv6_architecture.svg │ ├── SFNNv6_architecture_detailed.drawio │ ├── SFNNv6_architecture_detailed.svg │ ├── SFNNv6_architecture_detailed_v2.drawio │ ├── SFNNv6_architecture_detailed_v2.svg │ ├── SFNNv7_architecture.drawio │ ├── SFNNv7_architecture.svg │ ├── SFNNv7_architecture_detailed.drawio │ ├── SFNNv7_architecture_detailed.svg │ ├── SFNNv7_architecture_detailed_v2.drawio │ ├── SFNNv7_architecture_detailed_v2.svg │ ├── SFNNv8_architecture.drawio │ ├── SFNNv8_architecture.svg │ ├── SFNNv8_architecture_detailed.drawio │ ├── SFNNv8_architecture_detailed.svg │ ├── SFNNv8_architecture_detailed_v2.drawio │ ├── SFNNv8_architecture_detailed_v2.svg │ ├── board_0.png │ ├── clipped_relu.png │ ├── crelu16.drawio │ ├── crelu16.svg │ ├── crelu32.drawio │ ├── crelu32.svg │ ├── cross_entropy_loss.png │ ├── cross_entropy_loss_contour.png │ ├── cross_entropy_loss_grad.png │ ├── cross_entropy_loss_grad_contour.png │ ├── fc_input_density.png │ ├── m256_add_dpbusd_epi32.drawio │ ├── m256_add_dpbusd_epi32.svg │ ├── m256_block_sparse_weight_matrix.drawio │ ├── m256_block_sparse_weight_matrix.svg │ ├── m256_haddx4.drawio │ ├── m256_haddx4.svg │ ├── m256_process_chunk.drawio │ ├── m256_process_chunk.svg │ ├── m256_process_chunk_alternative.drawio │ ├── m256_process_chunk_alternative.svg │ ├── mse_loss.png │ ├── mse_loss_contour.png │ ├── mse_loss_grad.png │ ├── mse_loss_grad_contour.png │ ├── mv.drawio │ ├── mv.svg │ ├── mvs.drawio │ ├── mvs.svg │ ├── quantmoid4.png │ ├── quantmoid4_equation.png │ ├── sigmoid.png │ └── sigmoid_wdl_fit.png └── nnue.md ├── feature_block.py ├── feature_set.py ├── feature_transformer.py ├── features.py ├── ftperm.py ├── halfka.py ├── halfka_v2.py ├── halfka_v2_hm.py ├── halfkp.py ├── lib ├── nnue_training_data_formats.h ├── nnue_training_data_stream.h └── rng.h ├── model.py ├── nnue_dataset.py ├── perf_sigmoid_fitter.py ├── ranger.py ├── requirements.txt ├── run_games.py ├── scripts ├── easy_train.py ├── easy_train_example.bat ├── easy_train_example.sh ├── gensfen.sh ├── rename_net.sh └── train.sh ├── serialize.py ├── train.py ├── training_data_loader.cpp ├── visualize.py └── visualize_multi_hist.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | env/ 3 | build/ 4 | logs/ -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.0) 2 | 3 | project(training_data_loader CXX) 4 | 5 | if(NOT DEFINED CMAKE_BUILD_TYPE) 6 | set(CMAKE_BUILD_TYPE RelWithDebInfo) 7 | endif() 8 | 9 | set(CMAKE_CXX_FLAGS_DEBUG "-g") 10 | set(CMAKE_CXX_FLAGS_RELEASE "-O3 -march=native -DNDEBUG") 11 | set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "-g -O3 -march=native -DNDEBUG") 12 | 13 | set(CMAKE_CXX_STANDARD 17) 14 | set(CMAKE_CXX_STANDARD_REQUIRED 17) 15 | 16 | include(CheckCXXCompilerFlag) 17 | 18 | # Function to check if the CPU supports bmi2 19 | function(check_bmi2_support) 20 | execute_process( 21 | COMMAND bash -c "awk '/^vendor_id/{{print \$3; exit}}' /proc/cpuinfo" 22 | OUTPUT_VARIABLE VENDOR_ID 23 | OUTPUT_STRIP_TRAILING_WHITESPACE 24 | ) 25 | execute_process( 26 | COMMAND bash -c "awk '/^cpu family/{{print \$4; exit}}' /proc/cpuinfo" 27 | OUTPUT_VARIABLE CPU_FAMILY 28 | OUTPUT_STRIP_TRAILING_WHITESPACE 29 | ) 30 | execute_process( 31 | COMMAND bash -c "grep -m1 -o 'bmi2' /proc/cpuinfo" 32 | OUTPUT_VARIABLE CPU_BMI2 33 | OUTPUT_STRIP_TRAILING_WHITESPACE 34 | ) 35 | 36 | if(VENDOR_ID STREQUAL "AuthenticAMD") 37 | if(CPU_FAMILY GREATER_EQUAL "23" AND CPU_BMI2 STREQUAL "bmi2") 38 | set(CPU_SUPPORTS_BMI2 TRUE PARENT_SCOPE) 39 | endif() 40 | elseif(CPU_BMI2 STREQUAL "bmi2") 41 | set(CPU_SUPPORTS_BMI2 TRUE PARENT_SCOPE) 42 | else() 43 | set(CPU_SUPPORTS_BMI2 FALSE PARENT_SCOPE) 44 | endif() 45 | endfunction() 46 | 47 | check_bmi2_support() 48 | 49 | if(CPU_SUPPORTS_BMI2) 50 | message(STATUS "Adding BMI2 support") 51 | add_definitions(-DHAS_BMI2) 52 | else() 53 | message(STATUS "No BMI2 support") 54 | endif() 55 | 56 | add_library(training_data_loader SHARED training_data_loader.cpp) 57 | 58 | find_package(Threads REQUIRED) 59 | 60 | target_link_libraries(training_data_loader Threads::Threads) 61 | 62 | install( 63 | TARGETS training_data_loader 64 | RUNTIME DESTINATION . 65 | LIBRARY DESTINATION .) 66 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Setup 2 | 3 | ### Software prerequisites 4 | 5 | #### Python 6 | 7 | If using easy_train.py then at least 3.7 is required. 8 | 9 | Otherwise versions around 3.6 should also work, but updating is recommended. 10 | 11 | Python 3.11 was also tested and works well. 12 | 13 | #### A C++ compiler 14 | 15 | If you're using easy_train.py then at least GCC 9.2 is required for compiling the data loader, Stockfish, and c-chess-cli. This a defensive version requirement as older versions were known to cause problems with Stockfish. 16 | 17 | If you're not using easy_train.py then no automatic compilation will take place; use what you wish. 18 | 19 | If you're on Windows the best way is to probably use [MSYS2](https://www.msys2.org/). It would also make it easy to install Make (next step). 20 | 21 | #### Make 22 | 23 | For compiling Stockfish and c-chess-cli. Not strictly necessary if you're not using easy_train.py, but recommended. 24 | 25 | #### CMake 26 | 27 | Necessary for compiling the data loader. 28 | 29 | https://cmake.org/install/ 30 | 31 | ### Package dependencies 32 | 33 | ``` 34 | python -m venv trainer 35 | pip install -r requirements.txt 36 | ``` 37 | 38 | PyTorch with CUDA 11.8 will be automatically installed, along with the matching CuPy version. 39 | 40 | ### The data loader (if not using easy_train.py) 41 | 42 | This requires a C++17 compiler and cmake. 43 | 44 | Windows: 45 | ``` 46 | compile_data_loader.bat 47 | ``` 48 | 49 | Linux/Mac: 50 | ``` 51 | sh compile_data_loader.bat 52 | ``` 53 | 54 | # Network training and management 55 | 56 | Hard way: [wiki](https://github.com/official-stockfish/nnue-pytorch/wiki/Basic-training-procedure-(train.py)) 57 | 58 | Easier way: [wiki](https://github.com/official-stockfish/nnue-pytorch/wiki/Basic-training-procedure-(easy_train.py)) 59 | 60 | # Logging 61 | 62 | TODO: Move to wiki. Add setup for easy_train.py 63 | 64 | ``` 65 | tensorboard --logdir=logs 66 | ``` 67 | Then, go to http://localhost:6006/ 68 | 69 | # Automatically run matches to determine the best net generated by a (running) training 70 | 71 | TODO: Move to wiki 72 | 73 | ``` 74 | python run_games.py --concurrency 16 --stockfish_exe ./stockfish.master --c_chess_exe ./c-chess-cli --ordo_exe ./ordo --book_file_name ./noob_3moves.epd run96 75 | ``` 76 | 77 | Automatically converts all `.ckpt` found under `run96` to `.nnue` and runs games to find the best net. Games are played using `c-chess-cli` and nets are ranked using `ordo`. 78 | This script runs in a loop, and will monitor the directory for new checkpoints. Can be run in parallel with the training, if idle cores are available. 79 | 80 | 81 | # Thanks 82 | 83 | * Sopel - for the amazing fast sparse data loader 84 | * connormcmonigle - https://github.com/connormcmonigle/seer-nnue, and loss function advice. 85 | * syzygy - http://www.talkchess.com/forum3/viewtopic.php?f=7&t=75506 86 | * https://github.com/DanielUranga/TensorFlowNNUE 87 | * https://hxim.github.io/Stockfish-Evaluation-Guide/ 88 | * dkappe - Suggesting ranger (https://github.com/lessw2020/Ranger-Deep-Learning-Optimizer) -------------------------------------------------------------------------------- /compile_data_loader.bat: -------------------------------------------------------------------------------- 1 | cmake . -Bbuild -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_INSTALL_PREFIX="./" 2 | cmake --build ./build --config RelWithDebInfo --target install -------------------------------------------------------------------------------- /cross_check_eval.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import features 3 | import serialize 4 | import nnue_dataset 5 | import subprocess 6 | import re 7 | import chess 8 | from model import NNUE 9 | 10 | def read_model(nnue_path, feature_set): 11 | with open(nnue_path, 'rb') as f: 12 | reader = serialize.NNUEReader(f, feature_set) 13 | return reader.model 14 | 15 | def make_fen_batch_provider(data_path, batch_size): 16 | return nnue_dataset.FenBatchProvider(data_path, True, 1, batch_size, False, 10) 17 | 18 | def eval_model_batch(model, batch): 19 | us, them, white_indices, white_values, black_indices, black_values, outcome, score, psqt_indices, layer_stack_indices = batch.contents.get_tensors('cuda') 20 | 21 | evals = [v.item() for v in model.forward(us, them, white_indices, white_values, black_indices, black_values, psqt_indices, layer_stack_indices) * 600.0] 22 | for i in range(len(evals)): 23 | if them[i] > 0.5: 24 | evals[i] = -evals[i] 25 | return evals 26 | 27 | re_nnue_eval = re.compile(r'NNUE evaluation:?\s*?([-+]?\d*?\.\d*)') 28 | 29 | def compute_basic_eval_stats(evals): 30 | min_engine_eval = min(evals) 31 | max_engine_eval = max(evals) 32 | avg_engine_eval = sum(evals) / len(evals) 33 | avg_abs_engine_eval = sum(abs(v) for v in evals) / len(evals) 34 | 35 | return min_engine_eval, max_engine_eval, avg_engine_eval, avg_abs_engine_eval 36 | 37 | def compute_correlation(engine_evals, model_evals): 38 | if len(engine_evals) != len(model_evals): 39 | raise Exception("number of engine evals doesn't match the number of model evals") 40 | 41 | min_engine_eval, max_engine_eval, avg_engine_eval, avg_abs_engine_eval = compute_basic_eval_stats(engine_evals) 42 | min_model_eval, max_model_eval, avg_model_eval, avg_abs_model_eval = compute_basic_eval_stats(model_evals) 43 | 44 | print('Min engine/model eval: {} / {}'.format(min_engine_eval, min_model_eval)) 45 | print('Max engine/model eval: {} / {}'.format(max_engine_eval, max_model_eval)) 46 | print('Avg engine/model eval: {} / {}'.format(avg_engine_eval, avg_model_eval)) 47 | print('Avg abs engine/model eval: {} / {}'.format(avg_abs_engine_eval, avg_abs_model_eval)) 48 | 49 | relative_model_error = sum(abs(model - engine) / (abs(engine)+0.001) for model, engine in zip(model_evals, engine_evals)) / len(engine_evals) 50 | relative_engine_error = sum(abs(model - engine) / (abs(model)+0.001) for model, engine in zip(model_evals, engine_evals)) / len(engine_evals) 51 | min_diff = min(abs(model - engine) for model, engine in zip(model_evals, engine_evals)) 52 | max_diff = max(abs(model - engine) for model, engine in zip(model_evals, engine_evals)) 53 | print('Relative engine error: {}'.format(relative_engine_error)) 54 | print('Relative model error: {}'.format(relative_model_error)) 55 | print('Avg abs difference: {}'.format(sum(abs(model - engine) for model, engine in zip(model_evals, engine_evals)) / len(engine_evals))) 56 | print('Min difference: {}'.format(min_diff)) 57 | print('Max difference: {}'.format(max_diff)) 58 | 59 | def eval_engine_batch(engine_path, net_path, fens): 60 | engine = subprocess.Popen([engine_path], stdin=subprocess.PIPE, stdout=subprocess.PIPE, universal_newlines=True) 61 | parts = ['uci', 'setoption name EvalFile value {}'.format(net_path)] 62 | for fen in fens: 63 | parts.append('position fen {}'.format(fen)) 64 | parts.append('eval') 65 | parts.append('quit') 66 | query = '\n'.join(parts) 67 | out = engine.communicate(input=query)[0] 68 | evals = re.findall(re_nnue_eval, out) 69 | return [int(float(v)*208) for v in evals] 70 | 71 | def filter_fens(fens): 72 | # We don't want fens where a king is in check, as these cannot be evaluated by the engine. 73 | filtered_fens = [] 74 | for fen in fens: 75 | board = chess.Board(fen=fen) 76 | if not board.is_check(): 77 | filtered_fens.append(fen) 78 | return filtered_fens 79 | 80 | def main(): 81 | parser = argparse.ArgumentParser(description="") 82 | parser.add_argument("--net", type=str, help="path to a .nnue net") 83 | parser.add_argument("--engine", type=str, help="path to stockfish") 84 | parser.add_argument("--data", type=str, help="path to a .bin or .binpack dataset") 85 | parser.add_argument("--checkpoint", type=str, help="Optional checkpoint (used instead of nnue for local eval)") 86 | parser.add_argument("--count", type=int, default=100, help="number of datapoints to process") 87 | features.add_argparse_args(parser) 88 | args = parser.parse_args() 89 | 90 | batch_size = 1000 91 | 92 | feature_set = features.get_feature_set_from_name(args.features) 93 | if args.checkpoint: 94 | model = NNUE.load_from_checkpoint(args.checkpoint, feature_set=feature_set) 95 | else: 96 | model = read_model(args.net, feature_set) 97 | model.eval() 98 | model.cuda() 99 | fen_batch_provider = make_fen_batch_provider(args.data, batch_size) 100 | 101 | model_evals = [] 102 | engine_evals = [] 103 | 104 | done = 0 105 | print('Processed {} positions.'.format(done)) 106 | while done < args.count: 107 | fens = filter_fens(next(fen_batch_provider)) 108 | 109 | b = nnue_dataset.make_sparse_batch_from_fens(feature_set, fens, [0] * len(fens), [1] * len(fens), [0] * len(fens)) 110 | model_evals += eval_model_batch(model, b) 111 | nnue_dataset.destroy_sparse_batch(b) 112 | 113 | engine_evals += eval_engine_batch(args.engine, args.net, fens) 114 | 115 | done += len(fens) 116 | print('Processed {} positions.'.format(done)) 117 | 118 | compute_correlation(engine_evals, model_evals) 119 | 120 | if __name__ == '__main__': 121 | main() 122 | -------------------------------------------------------------------------------- /delete_bad_nets.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import re 3 | import os 4 | import itertools 5 | 6 | def parse_ordo(ordo_filename): 7 | ordo_scores = [] 8 | 9 | with open(ordo_filename, 'r') as ordo_file: 10 | lines = ordo_file.readlines() 11 | for line in lines: 12 | if 'nn-epoch' in line: 13 | fields = line.split() 14 | net = fields[1] 15 | rating = float(fields[3]) 16 | error = float(fields[4]) 17 | ordo_scores.append((net, rating, error)) 18 | 19 | return ordo_scores 20 | 21 | def find_ckpt_files(root_dir): 22 | p = re.compile('.*\\.ckpt') 23 | ckpt_files = [] 24 | for path, subdirs, files in os.walk(root_dir, followlinks=False): 25 | for filename in files: 26 | m = p.match(filename) 27 | if m: 28 | ckpt_files.append(os.path.join(path, filename)) 29 | return ckpt_files 30 | 31 | def find_nnue_files(root_dir): 32 | p = re.compile('.*\\.nnue') 33 | nnue_files = [] 34 | for path, subdirs, files in os.walk(root_dir, followlinks=False): 35 | for filename in files: 36 | m = p.match(filename) 37 | if m: 38 | nnue_files.append(os.path.join(path, filename)) 39 | return nnue_files 40 | 41 | def get_net_dir(net_path): 42 | return os.path.dirname(net_path) 43 | 44 | def split_nets_by_strength(nets, split_point=16): 45 | nets.sort(key=lambda x: -x[1]) 46 | best_nets = nets[:min(split_point, len(nets))] 47 | worst_nets = nets[min(split_point, len(nets)):] 48 | return best_nets, worst_nets 49 | 50 | def get_nets_by_directory(best_nets, worst_nets, num_best_to_keep=16): 51 | binned_best_nets = dict() 52 | binned_worst_nets = dict() 53 | 54 | for net_name, rating, error in itertools.chain(best_nets, worst_nets): 55 | basedir = get_net_dir(net_name) 56 | if not basedir in binned_best_nets: 57 | binned_best_nets[basedir] = [] 58 | if not basedir in binned_worst_nets: 59 | binned_worst_nets[basedir] = [] 60 | 61 | for net_name, rating, error in worst_nets: 62 | basedir = get_net_dir(net_name) 63 | binned_worst_nets[basedir].append(net_name) 64 | 65 | for net_name, rating, error in best_nets: 66 | basedir = get_net_dir(net_name) 67 | binned_best_nets[basedir].append(net_name) 68 | 69 | return binned_best_nets, binned_worst_nets 70 | 71 | def delete_bad_nets(root_dir, num_best_to_keep=16): 72 | net_epoch_p = re.compile(".*epoch([0-9]*)\\.nnue") 73 | ckpt_epoch_p = re.compile(".*epoch=([0-9]*).*\\.ckpt") 74 | ordo_filename = os.path.join(root_dir, "ordo.out") 75 | if not os.path.exists(ordo_filename): 76 | print('No ordo file found. Exiting.') 77 | return 78 | else: 79 | nets = parse_ordo(ordo_filename) 80 | best_nets, worst_nets = split_nets_by_strength(nets, num_best_to_keep) 81 | 82 | best_nets_by_dir, worst_nets_by_dir = get_nets_by_directory(best_nets, worst_nets, num_best_to_keep) 83 | for basedir, worst_nets_in_dir in worst_nets_by_dir.items(): 84 | ckpt_files = find_ckpt_files(basedir) 85 | nnue_files = find_nnue_files(basedir) 86 | worst_epochs = [net_epoch_p.match(net_name)[1] for net_name in worst_nets_in_dir] 87 | 88 | for ckpt_file in ckpt_files: 89 | try: 90 | ckpt_epoch = ckpt_epoch_p.match(ckpt_file)[1] 91 | if ckpt_epoch in worst_epochs: 92 | print('Delete {}'.format(ckpt_file)) 93 | os.remove(ckpt_file) 94 | except: 95 | pass 96 | 97 | print('Keep {}'.format(ckpt_file)) 98 | 99 | for nnue_file in nnue_files: 100 | try: 101 | nnue_epoch = net_epoch_p.match(nnue_file)[1] 102 | if nnue_epoch in worst_epochs: 103 | print('Delete {}'.format(nnue_file)) 104 | os.remove(nnue_file) 105 | except: 106 | pass 107 | 108 | print('Keep {}'.format(nnue_file)) 109 | 110 | 111 | def show_help(): 112 | print('Usage: python delete_bad_nets.py root_dir [num_best_to_keep]') 113 | print('root_dir - the directory to "cleanup"') 114 | print('num_best_to_keep - the number of best nets to keep. Default: 16') 115 | print('') 116 | print('It expects to find ordo.out somewhere within root_dir.') 117 | print('If the ordo.out is not found nothing is deleted.') 118 | print('It uses the ratings from the ordo file to determine which nets are best.') 119 | print('The engine names must contain the network name in the') 120 | print('following format: "nn-epoch[0-9]*\\.nnue". The network file') 121 | print('can be specified with a parent directory (for example') 122 | print('"run_0/nn-epoch100.nnue"), in which case the .ckpt file corresponding') 123 | print('to this .nnue file will only be searched for in the parent ("run_0") directory.') 124 | print('The .ckpt files must contain "epoch=([0-9]*).*\\.ckpt".') 125 | print('Both ckpt and nnue files are deleted. Only nets listed in the ordo') 126 | print('file can be deleted. Other nets are always kept.') 127 | print('The .nnue and .ckpt files are matched by epoch.') 128 | print('') 129 | print('The directory layout can be for example:') 130 | print('- root_dir') 131 | print(' - run_0') 132 | print(' - a/b/c/d.ckpt') 133 | print(' - *.nnue') 134 | print(' - run_1') 135 | print(' - a/b/c/d.ckpt') 136 | print(' - *.nnue') 137 | print(' - ordo.out') 138 | print(' (in this case ony lines with engine name matching') 139 | print(' "run_[01]/nn-epoch[0-9]*\\.nnue" will be used.)') 140 | 141 | def main(): 142 | if len(sys.argv) < 2: 143 | show_help() 144 | return 145 | 146 | root_dir = sys.argv[1] 147 | num_best_to_keep = sys.argv[2] if len(sys.argv) >= 3 else 16 148 | delete_bad_nets(root_dir, num_best_to_keep) 149 | 150 | if __name__ == '__main__': 151 | main() 152 | -------------------------------------------------------------------------------- /do_plots.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from tensorboard.backend.event_processing.event_accumulator import EventAccumulator 4 | 5 | import matplotlib.pyplot as plt 6 | import argparse 7 | import re 8 | import sys 9 | import os 10 | import collections 11 | 12 | def find_event_files(root_dir): 13 | p = re.compile('events\\.out\\.tfevents.*') 14 | tfevent_files = [] 15 | for path, subdirs, files in os.walk(root_dir, followlinks=False): 16 | for filename in files: 17 | m = p.match(filename) 18 | if m: 19 | tfevent_files.append(os.path.join(path, filename)) 20 | return tfevent_files 21 | 22 | def find_ordo_file(root_dir): 23 | for path, subdirs, files in os.walk(root_dir, followlinks=False): 24 | for filename in files: 25 | if filename == 'ordo.out': 26 | return os.path.join(path, filename) 27 | return None 28 | 29 | def get_list_aggregator(aggregation_mode='avg'): 30 | if aggregation_mode == 'min': 31 | return lambda x: min(x) 32 | elif aggregation_mode == 'max': 33 | return lambda x: max(x) 34 | elif aggregation_mode == 'avg': 35 | return lambda x: sum(x) / len(x) 36 | else: 37 | raise Exception('Invalid aggregation_mode {}'.format(aggregation_mode)) 38 | 39 | def aggregate_dict(values, aggregation_mode='avg'): 40 | ''' 41 | values must be a dict of lists 42 | each list is aggregated to a single scalar 43 | based on the aggregation_mode 44 | can be one of 'min', 'max', 'avg' 45 | ''' 46 | 47 | aggregate_list = get_list_aggregator(aggregation_mode) 48 | 49 | res = dict() 50 | for k, v in values.items(): 51 | res[k] = aggregate_list(v) 52 | return res 53 | 54 | def dict_to_xy(d): 55 | x = [] 56 | y = [] 57 | for k, v in sorted(d.items()): 58 | x.append(k) 59 | y.append(v) 60 | return x, y 61 | 62 | def parse_ordo_file(filename, label): 63 | p = re.compile('.*nn-epoch(\\d*)\\.nnue') 64 | with open(filename, 'r') as ordo_file: 65 | rows = [] 66 | lines = ordo_file.readlines() 67 | for line in lines: 68 | if 'nn-epoch' in line and label in line: 69 | fields = line.split() 70 | net = fields[1] 71 | epoch = int(p.match(net)[1]) 72 | rating = float(fields[3]) 73 | error = float(fields[4]) 74 | rows.append((net, epoch, rating, error)) 75 | 76 | return rows 77 | 78 | def transpose_list_of_tuples(l): 79 | return list(map(list, zip(*l))) 80 | 81 | def do_plots(out_filename, root_dirs, elo_range, loss_range, split): 82 | ''' 83 | 1. Find tfevents files for each root directory 84 | 2. Look for metrics 85 | 2.1. Look for 'val_loss' 86 | 3. Look for ordo.out 87 | 3.1. Parse elo from ordo. 88 | 4. Do plots. 89 | ''' 90 | 91 | tf_size_guidance = { 92 | 'compressedHistograms': 10, 93 | 'images': 0, 94 | 'scalars': 0, 95 | 'histograms': 1 96 | } 97 | 98 | fig = plt.figure() 99 | fig.set_size_inches(18, 10) 100 | ax_train_loss = fig.add_subplot(311) 101 | ax_val_loss = fig.add_subplot(312) 102 | ax_elo = None 103 | 104 | ax_val_loss.set_xlabel('step') 105 | ax_val_loss.set_ylabel('val_loss') 106 | 107 | ax_train_loss.set_xlabel('step') 108 | ax_train_loss.set_ylabel('train_loss') 109 | 110 | 111 | for user_root_dir in root_dirs: 112 | 113 | # if asked to split we split the roto dir into a number of user root dirs, 114 | # i.e. all direct subdirectories containing tfevent files. 115 | # we use the ordo file in the root dir, but split the content. 116 | split_root_dirs = [user_root_dir] 117 | if split: 118 | split_root_dirs = [] 119 | for item in os.listdir(user_root_dir): 120 | if os.path.isdir(os.path.join(user_root_dir, item)): 121 | root_dir = os.path.join(user_root_dir, item) 122 | if len(find_event_files(root_dir)) > 0: 123 | split_root_dirs.append(root_dir) 124 | split_root_dirs.sort() 125 | 126 | for root_dir in split_root_dirs: 127 | print('Processing root_dir {}'.format(root_dir)) 128 | tfevents_files = find_event_files(root_dir) 129 | print('Found {} tfevents files.'.format(len(tfevents_files))) 130 | 131 | val_losses = collections.defaultdict(lambda: []) 132 | train_losses = collections.defaultdict(lambda: []) 133 | for i, tfevents_file in enumerate(tfevents_files): 134 | print('Processing tfevents file {}/{}: {}'.format(i+1, len(tfevents_files), tfevents_file)) 135 | events_acc = EventAccumulator(tfevents_file, tf_size_guidance) 136 | events_acc.Reload() 137 | 138 | vv = events_acc.Scalars('val_loss') 139 | print('Found {} val_loss entries.'.format(len(vv))) 140 | minloss = min([v[2] for v in vv]) 141 | for v in vv: 142 | if v[2] < minloss + loss_range: 143 | step = v[1] 144 | val_losses[step].append(v[2]) 145 | 146 | vv = events_acc.Scalars('train_loss') 147 | minloss = min([v[2] for v in vv]) 148 | print('Found {} train_loss entries.'.format(len(vv))) 149 | for v in vv: 150 | if v[2] < minloss + loss_range: 151 | step = v[1] 152 | train_losses[step].append(v[2]) 153 | 154 | print('Aggregating data...') 155 | 156 | val_loss = aggregate_dict(val_losses, 'min') 157 | x, y = dict_to_xy(val_loss) 158 | ax_val_loss.plot(x, y, label=root_dir) 159 | 160 | train_loss = aggregate_dict(train_losses, 'min') 161 | x, y = dict_to_xy(train_loss) 162 | ax_train_loss.plot(x, y, label=root_dir) 163 | 164 | print('Finished aggregating data.') 165 | 166 | ordo_file = find_ordo_file(user_root_dir) 167 | if ordo_file: 168 | print('Found ordo file {}'.format(ordo_file)) 169 | if ax_elo is None: 170 | ax_elo = fig.add_subplot(313) 171 | ax_elo.set_xlabel('epoch') 172 | ax_elo.set_ylabel('Elo') 173 | 174 | for root_dir in split_root_dirs: 175 | rows = parse_ordo_file(ordo_file, root_dir if split else "nnue") 176 | if len(rows) == 0: 177 | continue 178 | rows = sorted(rows, key=lambda x:x[1]) 179 | epochs = [] 180 | elos = [] 181 | errors = [] 182 | maxelo = max([row[2] for row in rows]) 183 | for row in rows: 184 | epoch = row[1] 185 | elo = row[2] 186 | error = row[3] 187 | if not epoch in epochs: 188 | if elo > maxelo - elo_range: 189 | epochs.append(epoch) 190 | elos.append(elo) 191 | errors.append(error) 192 | 193 | print('Found ordo data for {} epochs'.format(len(epochs))) 194 | 195 | ax_elo.errorbar(epochs, elos, yerr=errors, label=root_dir) 196 | 197 | else: 198 | print('Did not find ordo file. Skipping.') 199 | 200 | 201 | ax_val_loss.legend() 202 | ax_train_loss.legend() 203 | if ax_elo: 204 | ax_elo.legend() 205 | 206 | print('Saving plot at {}'.format(out_filename)) 207 | #plt.show() 208 | plt.savefig(out_filename, dpi=300) 209 | 210 | 211 | def main(): 212 | #do_plots('test_plot_out.png', ['../nnue-pytorch-training/experiment_10', '../nnue-pytorch-training/experiment_11']) 213 | 214 | parser = argparse.ArgumentParser( 215 | description="Generate plots of losses and Elo for experiments run", 216 | formatter_class=argparse.ArgumentDefaultsHelpFormatter, 217 | ) 218 | parser.add_argument( 219 | "root_dirs", 220 | type=str, 221 | nargs='+', 222 | help="multiple root directories (containing ordo.out and tensorflow event files)" 223 | ) 224 | parser.add_argument( 225 | "--output", 226 | type=str, 227 | default="experiment_loss_Elo.png", 228 | help="Filename of the plot generated", 229 | ) 230 | parser.add_argument( 231 | "--elo_range", 232 | type=float, 233 | default=50.0, 234 | help="Limit Elo data shown to the best result - elo_range", 235 | ) 236 | parser.add_argument( 237 | "--loss_range", 238 | type=float, 239 | default=0.004, 240 | help="Limit loss data shown to the best result + loss_range", 241 | ) 242 | parser.add_argument("--split", 243 | action='store_true', 244 | help="Split the root dirs provided, assumes the ordo file is still at the root, and nets in that ordo file match root_dir/sub_dir/", 245 | ) 246 | args = parser.parse_args() 247 | 248 | print(args.root_dirs) 249 | do_plots(args.output, args.root_dirs, elo_range = args.elo_range, loss_range = args.loss_range, split = args.split) 250 | 251 | if __name__ == '__main__': 252 | main() 253 | -------------------------------------------------------------------------------- /docs/features.md: -------------------------------------------------------------------------------- 1 | # Features 2 | 3 | This doc is pretty much a copy-paste from [here](https://github.com/glinscott/nnue-pytorch/pull/30) 4 | 5 | A feature transformer is defined over a `feature_set`. A `feature_set` is a list of `feature_block`s. Each `feature_block` contains a name, hash, num_real_features, num_virtual_features, num_features, and knows some stuff about how to manipulate its indices. The number of real features and virtual features is used to distinguish factorizers, because they have to be handled differently on serialization to .nnue. A `feature_block` consists of `factors` which are an `OrderedDict` of `(str, int)` pairs where the first is the name of the factor and the second is its size. The first factor is assumed to be real, all the following ones are assumed to be virtual. `feature_set` concatenates these blocks and exposes similar operations. A `feature_block` knows how to coalesce its feature to real features. A `feature_block` is identified by it's name, for example `HalfKP` is a block, as is `HalfKP^` which denotes a factorized `HalfKP` (this is just a convention, but is strict and is used in some places for conversion ability discovery). A `feature_set` name is `'+'.join(...)` of names of its blocks. 6 | 7 | From now on the feature set used for learning and serialization/deserialization has to be specified explicitly as a program argument. The new argument `--features=...` takes a name of the feature set. For example `--features="HalfKP^"` or `--features="HalfKP"` or some imaginary `--features="HalfKP^+FancyFeatures+MoreFancyFeatures^"`. This argument is present in both train.py and serialize.py. 8 | 9 | The current semantics are as follows: 10 | 11 | 1. When training a new net from scratch - `--features` specifies the feature set to use for learning. The feature transformer weights are initialized normally for the real features and zero initialized for the virtual features. 12 | 2. When resuming training from a .pt model - `--features` specifies the feature set to use for learning. If the feature set specified doesn't match the feature set from the .pt model a conversion is attempted. Right now only a conversion of feature set with a single block from non-factorized to factorized is supported. The factorized block must have the non-factorized features as the first factor. The virtual feature weights are initialized to zero. 13 | 3. When converting .ckpt to .nnue - `--features` specifies the features as stored in the .ckpt file. The user must pass the correct feature set through `--features` because it can't be inferred from the .ckpt. If the features from `--features` and the saved model don't match it'll likely stack trace on some dimension mismatch. 14 | 4. When converting .pt to .nnue - `--features` is ignored, the `feature_set` from the saved model is used, the weights are coalesced when writing the .nnue file. 15 | 5. When converting .nnue to .pt - `--features` specifies the features in the .nnue file. The resulting .pt model has the same feature_set. Note that when resuming training this model can be converted to a compatible feature_set, see point 2. -------------------------------------------------------------------------------- /docs/img/A-768-8-8-1.drawio: -------------------------------------------------------------------------------- 1 | 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 -------------------------------------------------------------------------------- /docs/img/HalfKAv2-45056-256x2P1x2-32-32-1.drawio: -------------------------------------------------------------------------------- 1 | 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 -------------------------------------------------------------------------------- /docs/img/HalfKAv2-45056-256x2P8x2[-32-32-1]x8.drawio: -------------------------------------------------------------------------------- 1 | 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 -------------------------------------------------------------------------------- /docs/img/HalfKP-40960-256x2-32-32-1.drawio: -------------------------------------------------------------------------------- 1 | 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 -------------------------------------------------------------------------------- /docs/img/HalfKP-40960-4x2-8-1.drawio: -------------------------------------------------------------------------------- 1 | 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 -------------------------------------------------------------------------------- /docs/img/SFNNv1_architecture_detailed.drawio: -------------------------------------------------------------------------------- 1 | 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 -------------------------------------------------------------------------------- /docs/img/SFNNv1_architecture_detailed_v2.drawio: -------------------------------------------------------------------------------- 1 | 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 -------------------------------------------------------------------------------- /docs/img/SFNNv2_architecture.drawio: -------------------------------------------------------------------------------- 1 | 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 -------------------------------------------------------------------------------- /docs/img/SFNNv2_architecture_detailed.drawio: -------------------------------------------------------------------------------- 1 | 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 -------------------------------------------------------------------------------- /docs/img/SFNNv2_architecture_detailed_v2.drawio: -------------------------------------------------------------------------------- 1 | 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 -------------------------------------------------------------------------------- /docs/img/SFNNv3_architecture.drawio: -------------------------------------------------------------------------------- 1 | 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 -------------------------------------------------------------------------------- /docs/img/SFNNv3_architecture_detailed.drawio: -------------------------------------------------------------------------------- 1 | 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 -------------------------------------------------------------------------------- /docs/img/SFNNv3_architecture_detailed_v2.drawio: -------------------------------------------------------------------------------- 1 | 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 -------------------------------------------------------------------------------- /docs/img/SFNNv4_architecture.drawio: -------------------------------------------------------------------------------- 1 | 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 -------------------------------------------------------------------------------- /docs/img/SFNNv4_architecture_detailed.drawio: -------------------------------------------------------------------------------- 1 | 7V1rc6M22/41mXfbmWQ4Hz5u0qbtdLvdp9t2n/ZLB9sk5q0Nro032f76R8IIgxAgQAewlU7bmMgCdJ90X7oPN+bD9vW7fbBb/5Ssws2Noa1eb8xvbgzDcV3wX3jhy+mC6RinC8/7aHW6pJ8vfIz+DfOLWn71GK3CQ2VgmiSbNNpVLy6TOA6XaeVasN8nL9VhT8mmetdd8BzWLnxcBpv61U/RKl2frnq2dr7+fRg9r9GddS3/yzZAg/MLh3WwSl5Kl8xvb8yHfZKkp9+2rw/hBq4dWpfT9x4b/lo82D6MU5ovHP/VtS/v3r/7bH16fEl+CO//sj/d2uZpms/B5pi/8Y3hbMCE96voM3zq9Eu+FM4/R/io909JnN4eMkK9BQO8HSD1/fnP4Ldn+P8kXYd7OAGg3w18tNOs4PmyiU+DGN1rF+4PO0D96HPI9T5omsWexSz4IxqVSY1wBRgx/5js03XynMTB5tvz1ft9coxXISSvBj4d/g7T5Tr/8P/H7Q59OU5iOPo8x7sk2YHrOhwXpumXXOqCY5qAS+t0u8n/Gr5G6X9Lv/8BZ7+z80/fvOY3yz58QR/idP/lv+eB8OMf5b+dv5Z9Qt8rxAN+2ASLcHMfLP9+zt7xIdkk+/ObwHXOn9mD30z3yd8hGnNjmFr2kw98DLbRBt7kITnuo4wl34cvxYKX5ScXqQMYuMyJkP7722fvx/DTwydPj8Pnb/9aHR9udRtpomD/HKYtI518IKRl6R65gH4XJtsQrAEYkKvLW+3OMjzn9KUvVU24DzdBxuEVFZert+diqmL2D0kEXuw8JHl6OoCHragINCa/vYXUGlLWvla9zWlp8m+1TeRXJyomRhOdVq420dv9PvhSGraDAw6El8rvYxvkB86nA7+cZkSfSmt+vpTpyT4602rUmZDhxqiGXFkCOQQGJDlrM2YK5/SAXJTi2HcnKHGur83VQoxdDCkrMF0DhKwMjQHiYUhO0yT7VbjHpjjsgmUUP4ML5gjjJ91QWd4QQ2VXlO+tQEtlsrJUpo5ZKpOPpTI8vpbKW2+ffn1r+3++N/787sdkvfrd+M+t7o0W6jFCWwjqH6W/sN01jhccoom3HEpxGMrqaJ9kaXda6ces8IhDyYusNjborUdsbNwmS2Tf65ph3djfQF8wG/r4Afwx2O6geoVDIsCs4Lk13XCLP7RbMYy1q4z7so7S8CPQzvCvL/tgV2XSipV43gSHA8mKlQyECz4Gm+g5Br8vAUMBPgN/jzabkr14enoylkuSJVk5C8d2BjPs53Cfhq80itm0qnrGRorx5YxeGAijWJeQC0NrZt4Ko/XeLvsErsK1Urx6C8Gas11dBYd1Rku9hW7U1t0n0cTJfirKp6x69A7VQ1ZzhWYjqjmcX2z4D3811u2nGpT6rsRFNoGJ7JE7AMTEfnVn4eDMSe2Hupg0uGI1qqPNhvf1IbyvK85ny/kWth11tIGcb+uSOR9Zrgvl/IvQ+ua0eB8D8xx9qE9nSuZ9Yza8X9nxdDlbJDSm3a3jx/mtDF3m/HH+nRjON/wOhh2Ku9fMBzvOJwNONcb/GMXPG4hpfyjDu9rH4yLOIZ6yVBxeou0mKHi5kUUwvwxjM017BD+ZAAR7NM7IHL19sIoAW+EgXn7bx9I8+Z8amZHeJXMwl6zArkrMZZK4y8SpR+CkX8CSBvkSI27CbufVb6d7JA/QrN4t2AAnNw7S8B6qmsNIfiECVE5dL4rDp4adalYU5hmsYoNPtQlVt/ajNPu6T6n8+mm1voiojukqDwtf6DV8NH7atqJNwRFDjmoaATIWp1Jdk49G8fJ5jiwmWYyZ5COQmgPUee+iOAz2pdVbEFb0KOJIUNTiR/iFwy6IWUw8dp7kCN8v+6YWwZm+hkbnZGCgTQIDtUUE1MN5tU+3rBGh4XLtzaHITYWwp2u3HT8dz0l8oaYnZ8tegGJ21zKOvQcM2PA436N8xLBIEqjj30Tb3SZaRulXFMvPVYWzfVUocBTnKrrT92Bl0HswEQBha5frJe4sz2qejMoNjwqIf3Giy4yL2cj02FfL7N6lMNtEacRHe+MIxTrYwV8PaQjfbhfuI+BlwMPg7NKH82fiCfRT9BqiKHfof2IRVEwOpQ/59AQAbhWE3hPxgNpZeuHiKXuiFDh9SVy+Wf768HPJj2bo29KDKRi4YRGwDRKU4rUgKbSn2+QQGxSzMScMg2GMDcaMJVh7Cfk3Wp4uQmQtf9zmKDvGeAjCOcqASAMNacO0+SIieOxGByKCD9fxBBAcZXZax/OBUHRS9Adba8dM748Ijr4e5OVhE+124eqX8N1vpaVbEJZToS6iURegcre7NxB7gbswLd+Mlb3ekYCLfPYrC33teXlxIdpinR8n32yJwIQuCgES7KFwfhtZUMtYyOiyiDwxN5RMy4lvdpSXOykvFz/sJEQM6BbBzXV4ublebRe/O/wzQ8fXqHi+XcF+eJgJn8N8sk9Ke5hP6bnmjKXdeY5fzS/OSTsyhAnzLU08do9RGpWOpVHpXVED2HjDbB/veWOGo4AdokdNk+fGLPFLBS5M1H1WgQvTdqFV4IIKXOAnCq2nqNcWuOC1OnGmcXLiHE37Gjp0KnABC1zwhJz7i+b8Rj6YyXsW7NrwsDMNXOAmq+roX4Eiw47+deln/9b8AJCbKWQvEBfToAQ8mFfmGMUCtkwWGFZgZVYs0IZu8C7OgmVQ65RZpL0hrVZEa3RURttKtyaobjbR7hDWDUkRrlM1JKUUVOGWoVotwrXrdoFgFnCEkplOIJVUxReXFCZVkvCqimhP+m2uTdYvcGtYcFg/knaKtkEp2mLycnWtyllDy5AYWKSWgaoiM1cl1eftQK8xzWMKUDxFqF1ZOsblMGCidYlVoFjtnzFLYxI20AZBmliUhiJzg143RMHcd1TuqC2VvHja7m2X3r+asW+a1Zp2FhPFXNs0UZZ/7F21EbuPoXVoVH/keMdqNyS60zZe4oGjZpH0ug0EXKl1sbEihZ9SUuuOUK2OdiFzq39DXfupDL202ghG9W/GRZIg0WSYByFo+41rX2vg/rtJLXMoi9OwsKRKEhOViLteIVLSKgFeq0zg2h5Ht6l9UnwLhccOcpcJV+0YJDmC7uR2DDOqETxMP85tz+DOVD/WkhwH60dcSETrR6SfZyAT86gdfK0SgTPy0FYZeAsL2gMxdhKhwpqnGtb87SbcAkG7fYmy4ObtEXZoBHIpI1tzMmSZRcBzcHOKd17k/8821hoUXvh7OWTvSmKeKQLWJIV+Brl/xDkSkkdqZQOd6RaYw1Iu5ruUZHYTHLvd5ab37uWjokFVNGg/EAMHh406iEE8zeaWI6vr9TOwRd1lm+ZptrBD6aZjc6Pr2LwfI3E6zbaMCtMxOs3GAjMMym65nX0EuB2L4+cpekflKfwF+443nfbx+LF4dbzEY3FdaoBwnxhxCQqgFCpDAxyxRnR0gqoYlWM/tv8NxvUmjrczihLE68YhdUNbZ87wO6TXMUeNb82iZ3f8JNcyu3PojUqUBhRtza69FOeqing3KCFxroRwKAMYJfP1LXQWYO2U4lPlV5k8aYjhyeEVVcawKJlMtCyKNlSDIPne2yucY7sKe7aPF7L9aaXDVPpBNXJUze00tGWy2QS7Q7ii8Q3wMjMWi667+d1IHZ6cgberdZRi2+OJzARSmzyJyJHjnZ/j0W9+kcR1q0jCcSZxINItsrtFYZtjt1dqS7/RQgojG4Twp9EQrfYULKtfqbIVAaG+tzQfqBOa4iNiag/Kj9ta2aG3skhOrGcszCz0oac40sOamK9FynHnlaJDfgNPhvauaMbvw83nMI2WwWityD9jGMetqjNQxytrZC4Q1MXT8GVabBGFDURk1LK22MgQd1tsYxoWGwejO7yYpjwWWqPdbzgGQfOx8YhkKmBpwM5kMrFN11CpcTB5ZhHjdH1FHXvTs6v83VQjo3q/aFYukt7n4RqoMsX1OcX3GLYzhZ6SMth4ikQpij7mdCFwMDMSya472Htx8hKFvFhWxaTNMSaNG0CDVSH0CAANKXeORRFCso+h19hg8s56vzPDWTrrBq2zbk3SWe+K0KqN74ghqSFjtgj325gExM4XPZ+7Aynfv7+ajoaX7eSr5od8GHZo80MZvpsE2ZiRF5lBH5ibNJ0FlYYxzB9VmWbjwqntlJSzLNtZ1mm9ZRY5WmSXwJTpLQ8LRutXcGaW3jKhGpegQI5x3EQsejlCy08uFEpQCav+msXG8ASHoFn6BtyO4gVLatAMC80yTRyONubf8qTpkGFB/njIeEf8Ssd4PgiaVe/7O35fyLjGQqcz0IiXsX2Q74PN049vPxt/rbdw7xsG6XEPY/az3IoBDsv44hN50zUV2tsrtHdYTlKPPTG2KbZIzb2FluGXbLnmmwqJKNedCsk8JHNcpQpSuU2VnSgoO5HENQ0Z6pRcMyo7UWRCIHr1i08ItHAlLzghkGBTJpMQaEuNWLiyhEDECN1aiTZiwZ6G/3RxCYGIACohcI5eQ09xnG9CoC0FQWecEIjIoBIC6WhuybTYV5YQSG+xaQvx2O40LLZKCLRJnbFUQuDMAgZVQuDcYwVVQqBKCES3VAmBKiFwdvGEKiFQJQSSeBOHy1WMY88Yx/4AzdQSAm2p9fauLCGQ3lmnrbdn+5N01i8iIRARQSUETtBqo1vK9+9VQuBFOPkqIVAlBKqEQJUQOMmlVwmBPURKJQReqLMsPyHQllLf+EoTAum9ZZ/WW2YeyDGOmwiB1SohcEBCYH/NMrmEQPQA89Us08ThaMPsHdr2bpyCwfoibjZmGy2vHUHrGM8HQUOLqhICZ5kQyM25YzvPKW+x4VlVYPKk0hktitQTsRHKKNmuugkb3rF8clRnvwfrTXXc+KCmihK3W6aiOvfUZW9qVNdRFC9hQ7LWaYyAbjAyZp0T1bB3go1vPP+oT/sxTZZ/P0WHNRj/8fH9+88W3MPCdTiBPIYWh+lLsv8b/LaKgud9sC0ZtoYzDsIj9Ts1gIvOwlyOyO58yH5IEgVeehnFz+/CJ8iX9vnKLzmrli79mjlc2vnCfZKmyTa/xkCYdMxwFqmYJWHyTIIwmdyEiZiChlER+no7droG5tXGMcxoXaA7aK3LZuLLRthveIRV4xZvo+ukqAJJvF90WW/3zeEk2KU4eQe/gixYxcbkQ+pU7WAjallwSXakTkOdGw0NkhlRNKSiIVZ9QB4NSdiEouGY4xnpJKUwSIqk7dsM6TQkOGaKhr1oSHSzhNKQUOhS0bAfTjY11UpKRVUkHXLkKI+GjqLhSBo6UxNL5UyyciZRJIB0kpKKqCqS9vEt5W+ASBUTFQ1H+JbSSUpszq1IOsS3nIqmNRUCxMrVnAxJFQLE2POUr3gVIMTK85yMlCp8iLEjKl9KFT7E2BElle4US1IFF411RKVDfqbChxg7ovJJqvAhxo6ofE2r4CLGjqh0kqIHUCRl5YhKV7yWgosYO6LypVTBRYwdUflSquAixo4oMTVaKEkVXDTWEZUeMW0pfIixIyqfpAofYuyIyte0Ci5i7IjKJ6mCixg7ovIVr4KLGDui0qWU0OlUkXSUIypdSm3WcFE3OagJXiddmQf4kUpa0q1NciHbyyo3Z+bveBXCqSXOL0Yk/L9P0vCQ3bcri792l345/LXl6Fe+ulLys14seAOz78WRAi/58UOc/noq+vFzHP6OV/3QbuEzg3+zap63u7zkVvplBys37cPdPjzAagIr8GmRVfeC1AODwudMOpOn8+jTncCIVSYo6bpgV0PT7zQ4QxjFz+R54cPdgf//ug5LX8umP8ZRCoesEsAO54K9BYHjJCXTXYvD7AZpkt0be/q7cvGkwazCnH6/568OCVOUvPi/Q/7c+ziAuqJS9gKuz+FuCs8+hWf46bhJI1geK1ueA+KvN1mmyaNueF9BPtjDJd6F+6dkvz3zyDaA1Wpg3d4DfIZ/jkGcghvCv8OSW+EWsGy+6g917gHWL/v2MogzltSOB3gXoDaiw4lS2ZNkD6KtMiJnd80fI4iXJ5kD245omT38XImK609J+0ARhVHya+CTQdxxjC6T4vn1faDpctp7eOvt069vbf/P98af3/2YrFe/G/+5JdT7SMGN9zW6Tr7c502lR67R1SSXsOnsXwGmXLeTvLyEup3kgbSN6HNOutXuPAPFwOXcdJt/HNvnGXM/bZz5Gho0960RamEnOXZXj9uR4x3Nx8SlVFP0/G30+MnTU1bcEhOwfnVHW5miWt3Na63tBlMH4D7P0bSvYYllVd+tExfDIzQIUcik6uwsyruRyV4/kQBvPXdF605E0Zr8FK3ro3qMnBWtz0fR4qi/7XQoTjxqoud4J49Fkq9oiWU0laJlXEhTm5iiRTVzL7dpJC8lalMqUVQ5XXKVeRM/zszrwDQpqtr4jh1k7bi0Ov5mbFX6ViI0gcHimiNJb+k3tQ51b4FSDJ4hkPMh3B924RKy9qEVahyJXk+fKrPonvgm22hnsGuGbkCYEIqxoRmlxR7ZQbEP1Mybrt09BzuekfgyvBvPIcLt8V1ax8OyPlbp2ARSPAsVH9TXmMNqntC8+a4nme0EMmM60mUQ0NuQzyYA20KrjoJ0xxYMPCodb01AOCYocrjKLpXLy6VC0SGlTelPAexGrH08LuLce69wy0u03QT9Yz/wYJ9H8JNRLdijcUbGF/n8jzSRIf07guARWIR4ncJ/rZazb6FAfrtfYIH4+HkTll3oyu1MUidJUkgKKkeL7hZsslPkNLyH0nXgganQVMAN4xVwjDJ5XkJZjpZV+anKvojOa0Md7TovtYpHp+OMyud1Os4lytsEwqNrYxHIhqJXaIYTYlADIOuONXasSYdjMvOYLQILTh1FZwj41HidIH3gIlSVN6Tgx3JUZU+G74SUEMN34/LoeER899PWBy/puW/COAv6eBfFYQD37mDhXt+WdlC6BmtLZz2YnOZ9FEfLaPanILGJyWYT7A7hisZUUqS29wV/m+2khaekEIpFFXB0W5sZAYZSJ5z3AVdCb2+fi3wJ3XC/dizcl7iC44d+zErLlrStufkd/qIdpLJRk7NRRdOjbiNl0e7fxBgpYrsmoGNspWME6Bi8wQyxcqFQJaMT0ukAOyhuEMANOt4Ug8AOYk2OTpGJV1LEuVu9Cg7rjMJ6CzW7s31ykvpEyC37qZiasqHROwxNYdRKJq2wYkSThvOQDf9hzCkszQy1LyQGJjAbOLsvTFBrGyMYJ6BpjDcVgbhzjMruS7/TTJNSMHS+gpHdtIT3D0HKrkhacN9zqLTgyaDcpYUiRXSy0jINEyJIUthjbILgZ82/80s/ns9GcoCivzMd7fyj45J0Z5ml+1qCBavfEYpMwdKHCBVn86Pk53x80y4/1lD5MaH84Hs+uTJDUfunJBTQbYuWwSar+PAhOUT5wf4iT++ru5kNknPyOMFPadK3+XdTCMLlUvQpJ7jRilxVZLUQ6PwvWXTF9vUZ+NHruyg5uHfREuaJBqtVT3SNUC4BzLqKwlphhZ6y1Kd2FHaySCi9SDpA5+cNo/6C0wdghQGpZDVfgn2bFP3EtjVIOTAOR6/Hg2NMbTp0W/K+ce145qJRTcDhFHeO2sIpATkLCLgrdz4Xxb14sEctVqmBe+sTYeFpRR5bxz6DXbxHfTPQfTZvn2BuCHdfx8k8fjxBOgPtezxxIUfzhLKDD5totwtXv4TvfmvgnuvkGmLxuevkGqcv11ytrilIprhGdy21rWryO7Q7iM1VfA/LL+J1pXofbr4bno73gTfVw9Gdodu3Wrhuw/atrxcjxYlx62fe32T1uj7sk9VxCX97TCBX/3xMdzBDUCntgkAs4lcuQ2kTOq/IDd8rI+ocawrkDzK8akSnXjVp67SgLqy0dVpgJRrsnOc2368N1bv8C5QQmhTod3ezAir5l/ZBLNPJCYjsqOzLSHtrWWB7Uj0FcXDAj/osnzAXnqzXab8F8GPderoymVFULRVxTNyp9yza3eRZ71m+PTO9R9U1fZ1sF8dD89kV9wJyZl1mCaWNhJ5iObOxDU314mjKxXGQP4tW/hxaazPWutS5y8XZhvOhOqEZ3QUkdffNX6NI9WaXwGZ4/h0KnWi5IdEPqt1SgCOEduHzZJF2rh/JOLdQfksxZhoQ4CplbxlkAFwOKzmzwUGHhkXwMl20gdDCTJct33TVT4BnpJcYmS68gLYyXRUWqZ/WzYhF2rlemS6xposibwiGje7YeaWQonEMl26B7qC1KwNUsKJYULe+oFZ9PZ1mAo7yVG2KgF0U9AqmiwDvkoQJBc6SurWVg+DLGaWVSHkWoeQF/Xv1EHMELjZFG80LXOz8r35dVQlceook28tdehSjK2ft/fpST92lAF/CIx40rTOvkxTxILHcBq3ngsqEd3ouqL7hRMpqoOeeNDyNH987hCKRQrFpX68t0kSlUUagk+97VbG3fH+I2I+QRloInL00Nh6KVjjYHZqvZvp1QIJTakbTM3ONanJmclxWXRldsj5ybCn6SGhLj+lggoQAB7yd0YhgCVdw8QMUEinLlrllY9aeQ91w0DqG3ZpNYK5Lzvav5ofADx+CFGIy2RVDI5rIUxLxKHvm0uYHUgcQce5Wg5kOx7Qx1uVgOjzJmzJjACPrvBiZglG7eF0qI0+k7ZLh4Z6HAEZ2SRAXmx4PD+skyZIHw1MX6Juiw3kExOC13q7jzS4Kl+FfSyAmcDBsK6OfW8pYjT0f8C2b6FbJ1EUA+jujWBSFS0oAIhfM57T78ykwe1UwgsAwfQtGjAYuXNlBdT6xTWRr1czfs6HnUt8Tr5npLUNyzcyFZ1v2cPejB9Gx7A+PUCjcF0p0qeEoIgKbRfTVGL7p8Wl7TfrT3L27fgX46Rzvme3jDdtuG89nU+WrXpNT7TX5kMRQu0IJScB/coOjOk2WLkyx02QWYPT1TdYMrrS6I1tLyiPb6EaSLc/Ilk3AymObto4nm36XQ279DIfvbVU3Q9XNsOfWH6834JJKysCQYr2+/cdP8dht/+UkUvFIimJd8AwV5rBuyj6KdTq/bvZSqEqlUXsHGq13YHPxDurbcxy0GJoJbMKarQbmC4NLbqm2anVmTkUFsZPr7vG+AIekKPncp7TbD/HhZK5+PsIFkFC8g1kGQKMSRavfO166VgT44jqS6ma/QvWTa0nazxbUmaldkroLGFGfj4qqCo8XExuoaTEAhk9AEP60HTBPrQZnn9GeEA0sNcWcBTDaVad+ao3qClHtrkpGXUaimFMybKr7bSzcOVzvFJDW8bxEhBSPN0nMTD6OWdu9KQRzJgjmy8m0Q/MLBmqLKDhcB6I5OVQTOjhtLRy9ooOjBLhOMHSZ+XjD+1mypOq8OCkX5/MKnvmKK0zezrM9KcSGZce+UqYK+bKiAsEnDYJ3gFdDWwgTgmKIbcpxX5ZdlxyznrbdXuM8I95lYm4mVnGZeHDhEshTa9V4gZgbRcb5FWJu9I3kad1zhbnJxdwcAbHtuim19dJ1Ym6osG0n5oZElQJz49MJoLd4OG0s3DkcSVfTeM9uG85JQhARFORGETpY2bBdPLpWBq+Avtju3kAIC/pdWu5+fVX2qqbywu3owVRQgwx/4owQZKp/Vn40Z4xL9noPxBMV2iAWbWiqBjgGbTBbfAz2aINbzz/o2a3nguEHPOTHI+bJXWfIj1tvnKLgh0Kcuj0VlE2t4IeJww9CQn7c2Td5mR38gESVohEhdRt0l08stAr5KVFBZUpODu7AdowqwKh04XoDjOYD9SjIR3yqJiOEQ2aapsB003nxbxHoJJiHG0Occvo8yonPYxTsRMeLnJec1TyFyDQ87DxTnokmTyGy4hDZAs6cYPwX+LhPINuc3SJYN+ynZBXCEf8D -------------------------------------------------------------------------------- /docs/img/SFNNv4_architecture_detailed_v2.drawio: -------------------------------------------------------------------------------- 1 | 7V3rk5u22v9rdk7amfVwv3xMtknTSdrmbdrmnPMlw9p4zYltHIM32f71r4QRBkmAAAmEV9uZxsbipuem56fncmPe7b7/fAwOm1/jVbi9MbTV9xvzpxvDsFwL/B8eeDofMEz/fODhGK3Oh/TLgY/RP2F+UMuPnqJVmFQGpnG8TaND9eAy3u/DZVo5FhyP8bfqsHW8rd71EDyExIGPy2BLHv0UrdLN+ahna5fjb8PoYYPurGv5L7sADc4PJJtgFX8rHTJf35h3xzhOz5923+/CLZw7NC/n897U/Fo82DHcpywneJvd+s+Xtv/f34z//vwu3qz+Nv7vVvfyh0uf0BuHKzAB+df4mG7ih3gfbF9fjr46xqf9KoSX1cC3y5j3cXwAB3Vw8H9hmj7l1AxOaQwObdLdNv81/B6l/4anL+z8239Kv/z0Pb9y9uUJfdmnx6fSSfDrf8q/XU7LvqHz1vE+fRPsoi08cBefjlF4BC/8WwhI8Yqcw3xaEzBwmU/I6R9de3r/2/tH69Obb/Ev4avP9qdbyzkPhLNVOjOf+p/DeBeCpwADjuE2SKPHKjsFOVc+FOOKUz/EEXgWQ0MCZGkLrfRnni+QS5NjatULpsHxIUzza1yYAXwoPdTlUMYidHZpfOvHYHvK3+jGcLZpPs8VRnK+nmL0w22SscJLMMA9fM8mHv0MPj1k/9qvdM2wbmxwqnYe+uYD+DHYAZ56ld0iAswKnlvTDbf44SG/xvb875vzY+SXxFm7yrjfNlEafjwEGZ2/Ae1VZdJCXuHYh22QJPnn5EuYLjcl9sr53AVfg230sAefl4ChAJ+B36Pt9i7exsfsCcz1em0sl/Ai6TH+EpZ+WTn3ju30ZtjH8JiG3xs5Mf/VtLQKD9l+/v3bRb8ZSIttSrrN0OqZt8JoXbnK9ilchWul/eolVOfg2z7eQy20CpJNRku9gW7b4D7cvgqWXx4y2qP5zi9RIp5Po4mT/VWUT1n16C2qh67mCs1GVXM4v9jwP/Fq7Kw3ch3yz1+P3rvw090nT9+HD68/r053t47BqO9KXGRTmAgdG6gWTd+uKkKcOc9vTihCUr+6mDS442pUR5sN7+t9eF9XnM+X8y29yrCO1pPzbX1izkeW60o5/yq0vikX7xsY7+s9ed80J+Z9Yza8X1nxtDlbBe8zu3XiOL+RocucP8y/G4fzDb+FYZnXO36L+eDH+VQCkEr/Y7R/ADxvaB/CY3IIl9ksGdrH0/0+TAmpSL5Fu21Q8HIti2B+GcZmmvYG/GUCEBzROCNz9I7BKgJshckNuu2b0nXyn2qZkd0lczCXzLRJl8ykcZeJU4/CSX+AKQ3yKUbchN3OI2+nezQP0KzeLdgCJ3cfpOErqGqSgfxCBagcUi+Oh09VFq+FOW/DpyoK8wJW8cGnmoSqXfsxmn3dZ1R+3bQaMGfBU2nAASqZpF7p6Ziu8jCAs9Nw8OF8f67qrB4PW0WPFxzqfOj+OAggu4BclGtnh4ZffDCKl1/nxOMi90Mu8hFITQJ13vtoHwbH0uzdU2b0RDlWhRJnNPkRfiA5BHseFx56nfgE3y87U4vglX6ERudsYKBNAgO1+wioh8tsn29JEKHmMPHmUORkIez52G3LX8tzUl+o7sn5shegmN02jUPvAVa2hif4HuUthvs4hjr+RbQ7bKNllP7AMP1CVTjfV4UCx7CvojtdN1Z6vQcXARht7nK9JJzleV0no3LNowLiX53ocuNiPjI99NUyu3ctzCYpjcRobxyh2AQH+DFJQ/h2h/AYAS8DbgZnhz5cvlN3oNfR9xDFwUD/s+rM8tmUTvLLUwC4VRB6a+oGtbP0wvt19kQpcPrifflm+evD7yU/mqNvyw6mYOCGRcE2aFCK14CksO5u00NsUMzGnDAMjjE2GDOWYO0l5N9oeT4IkbX8cQnWO4e+dOaZVjwE4RxlQKSGhroUiAgeu9GCiODDdTxEDEeZncbxYiAUnRb9wdfacdP7NNMkmfM/PfJyt40Oh3D1R/j+r9LU3VOmU6EuY6MuQOXuDi8g9gJXYVq+GCt7vQMBl+nZryz0xPOK4kK0xLo8Tr7YGgMTuioEaGQPRfDbTAW1DIWMrovIkrmhdFpKvthRXq5UXi6+2UmJGNAtipvriHJzPWIVf0i+ztDxNSqeb1uwHx5mImYzn+6Tsm7mM3quOWNpC8/xnaqXecMjhAnzLU08dq8m8Khz0ICH+bBtUQPYeMNsHu95Q4ajgB2qR305Gb1svF4n4dA4rCbuUYEL0vkvKnBBbhdaBS6owAVxotC4i/rcAhe8RifONM5OnKNpP0KHTgUuYIEL3ij7/mNzfi0fzOQ9C3atediZBi4Ik1W19a9AkX5b//rke//W/ACQGxmyF6iTaTACHtwrcwxiAXtKFuhXYGVWLNCEboguzoJlUOuMWaSdIa1GRGtwVEbTTDcmqG630SEJSUNShOtUDUkpBXV0y1CtFuHapF2gmAUcoeSmE0yGyaWFSZUkvKoimpN+z7/Gx1V47EILInCrX3BYN5K2irbBKNrj5OXqWpWz+pYhMbBILcN1BamS6vO2oNeY5jFHUDxFqF1ZOoblMGCidY1VoHitnzFLY1IW0AZFmniUhqJzg04aomDuKyp30JJqunja9mWX3nGz8VZb+KZZrWlncVHMxKLJZ9PMXRUqfh9Da9Go/sDxjtVsSHSnafyEG46aRdPrNhBwpdbHjRUp/JSSWndG1epoFTK3+jfMtZ/K0EujjeBU/2ZYJAkSTY55ECMtv3Hta/Vcf9epZQFlcWomllZJQlKJWHQKkZqsEuBzlQlc2+PoNrNPii+h8NhB4TLhqhXDRI6gK92KYUY1gvvpx7mtGdyZ6kciybG3fsSFZGz9iPTzDGRiHrWDn6tE4IzMiJWQF8Jjr8WVVW3Gn1RYs3Rhza+34Q4I2u23KAtu3p1gDxcgl1Nka0pDllkEPAc353jn+/zfbGGtQeGFn8she88k5pkhYG2i0M8g948ER0KKSK2soTPbBAuYyvv5TiWd3UaO3W5z0zv38lHRoCoatBuIgYPDBgliUHezheXI6jq5B3ZPumxy7maPtildt21utG2bd2MkQbvZllFhOk672VhgRuH5tThWrX0EhG2L4/spekvlKfwFu443nebx+LZ4dfyE2+L6pAHCXWLEJ1AApVAZFuCIN6KjU1TFoBz7of1vMK43cbydU5QgXjcOqRvWOnNI+uvrzJmDxjdm0fPbfprWMrtz6I1KlQYUbc2vvZTgqop4N6hR4lwp4VAGMErm95fQWYC1U4pvlY9T8qQxDk/2r6gyhEXpZGJlUbSg6gXJd15e4RzbVtizefwoy59GOsjSD6qWowi3E7Yc326DQxKuGvkCnYNtjVg8uu7md6N1eHJ63o7oKMW3xxOdCSZt8jRGjpzo/ByPffGLJK5dRVK2M6kDkW6ZulsUtjh2O6W2dBs9SmFkgxL+NBii1dbBsnpKla0oCPUrS/OBOmEpPjJO7cHp47ZWduitLJoT6xn3Zhb60FEc2WFNzNei5biLStGhv4E3hfauaMa34fYxTKNlMFgris8YxnGr6hWY45U1OheM1MXT8Ke02GMUNhgjo5a3xUaGuN1iG3JYbByMbvFi6vJYWI12t+EYBC3GxiOSqYClHisTaWKbnkOlxt7kmUWM0/Mr6tiZnm3l72SNjOr8olm5SHafR2igiozzc47vMWxHhp6SU7CxjEQpij7mdKFwMDcSTV13sPPk5CUKRbGsikmbY0yaMIAGq0LoUQAaWu4cjyKEdB9DJ9hAeme9257hLJ11g9VZt6R01tsitIjxLTEkBDJmj+F+G1JA7GLR87k7kNP798+mo+F1O/mq+aEYhu3b/HAK320C2ZiRF5lBH5ibJM+EToYxzB9VkbNxoWwrJeUsT+0s66zeMo8cLbpLYE7pLfcLRutWcGaW3jKlGtdIgRzDuIla9HKAlpcuFGqkElbdNYuN4QkORbN0DbgdxAvWpEEzPDSLnDgca8y/5U2mQ/oF+eMh4y3xKy3jxSBoFtn3d/i6kHONhVZnoBYv4/sgb4Pt+t3LR+PzZgfXvmGQno4wZj/LrejhsAwvPpE3XVOhvZ1Ce/vlJHVYE2OLYovW3HvUMvwTW675pkIiyrWnQnIPyRxWqYJWblNlJ46UnUjjmpoMdUauGZSdOGZCIHr1q08ItHAlP3JCIMWmSJMQaE8asfDMEgIRI7RrJdaIBVsO/+nqEgIRAVRC4By9ho7iON+EQHsSBJ1zQiAig0oIZKO5NaXFfmYJgewWm7UQj+3KYbFVQqBN64ylEgJnFjCoEgLnHiuoEgJVQiC6pUoIVAmBs4snVAmBKiGQxps4XK5iHDvGOHYHaGRLCLQnrbf3zBIC2Z111np7ti+ls34VCYGICCohUEKrjW45vX+vEgKvwslXCYEqIVAlBKqEQCmnXiUEdhAplRB4pc7y9AmB9iT1jZ9pQiC7t+yzesvcAzmGcRMlsFolBPZICOyuWaRLCEQPMF/NIicOxxpm77C2dxMUDNYVcbMx22h5zQhay3gxCBqaVJUQOMuEQGHOHd/rnPMWa55VBSZLlc5oMaSejBuhjJLtqouw/h3LpaM6/zVYZ6rjxgc1VZxwuWUqqgtPXfZko7qOongpC5KNzmIEdIOTMWu9EIG9U2x87f4HedmPabz8so6SDRj/8c1vvz1acA0L5+EM8hjaPky/xccv4NMqCh6Owa5k2Gr2OCiP1G3XAE46D3M5ILvzLvujSRR46WW0f3gfriFf2pcjf+SsWjr0Z+ZwaZcDr+I0jXf5MQ7CpGOGs0jFLAmTZ1KEyRQmTCjPoy48nYkv0RkHUatLghuJja4OUvRbnIZJdt820SDu0k0wiOnotidcwdHJHbgtZOnxSIHb0V/26Z9nS/r7PvwbN6XaLXxm6ANBiPz2kPux6dMhzLzqwzFMoIiu4JI+c5kh9cCg8CET+nh9GX2+ExixyiQl3RTsCjTwQoNXCIGw0q8LH24B/v1zE5ZOyy5/2kcpHLKKATtcdsEKAu/jlE53oGWzG6Rxdm/s6Rdlj6Q3q3Cn39/5q0PCFHbkX0n+3Md9sMVtCZyfZCHDs8vwDL+etmkEfc5sehLEXy+ydeQb3fB+gHyQefuH8LiOj7sLj+wCuASEm2EJfIavp2CfghvC36EfG+4Ay+azfkdyDzCb2dnLYJ+xpHZKwszAP0bJmVLZk2QPoq0yImd3zR8j2C/PMgcWkdEye/i5ErUl9PWaVhv5MfDNELP28CjlKUxXUNQqvdYJGZqXghsf54eh31QST422zFOcu3q5cGUwvLGUTHvNGdbqDjkn3WoLzzCdCjfd5l+HJk9jWzo2znw1Wc9dgXdUSa+4T1vi6MDxjuZj4lIC6gVV42lkiipk4jUCJrCnBlznOZr2I9y3VKBJK0CKVXCggSa0kAdh5d50hyA7eOu5K1pXEkVrilO0ro9ATsGK1hejaAvkAt3HaVGczrDxjqVLomip2LRStJzRaU0yRYs2oq43E0uUEqVUNqHPMGuFRLGhGyam2Jy8xnSdoiLGt9V+bh4/ONSjkQiqVol0aR8vgVIMHrCimclNE9Q4EL2WnyqzSEl6kS20M9g1QzcgTAjF2NCM0mRfUdmR9kSelmecpIYIItwRX6W1PCzvbZWWRSDDszDxATnHAmbzjObNdz5rN8fHYsZ0oMswQsKQqmkxbpqOaI9Kx+N9KNsEukVxqVxRLpVhEIvSX4Osmtc4BcMT4JKgcUbGF/n135ROyMPcORDAtukL/0pdb1pwJVHXm7GOOBaSQkvP8mguNFbXlG8VcTonUDAVwtver4BjlMnzEspytKzKT1X2x0hn6Otok7zUKB6tjrPhMTrOJcrbFMKjY0MRSLwYJ8a9rHVyMT+ZccOIm8dsTQr4jJHy043XKdIHDkJVmT8gYQS17K87w7dCSgalPVjNppjDG1MaZvHIFlw/hfss6KOoQ1rq1ZKtoHTNsPLAZqd+HSXQMprdKTi04wYm+rQ66V3B33o7aek44Eexy/R+G6MbSp2y3wdcCb05JxX5Errh/uhYuC/xDLYfujErK1uy5ruL2/xFK0hlo6SzUbpOKRJXs9JgXb+N1FCMEr4FdYytdMwIOgYvqkExRsKycmrYgdI3FLCD4oYRuEG32tlhXJNDbTdY76rnbvUqSDYZhfUGajb77CWS+lTILfurmJqyoWmrrFIYtZJJK6wY1aThPGTD/zhzCk8zw+wLjQMTmDWc3RUmwCUEB8tE4wTouecgEAvHqJYbWmimySgYuljByG5awvv7IGXPSFpw37OvtOhjSwut5shcpEUOEzKSpPDH2EaCnzV/4Zf+PJ+P5ABFvzAd7fKn45K0sMzSfa2RBavbFsqUgqX3ESrB5kfJz2X7pll+8HaFzPJjQvnB13zTygythDwmMyWhgG5btAy276E8fIiTKN/Yv8/T+0g3s0Zyzh4n+Ctd9GV+bgpBuFyKPuUENxqRq4qsFgKd/5JFV+y+PwA/erOI4sRdREuYJxqsVh3RtTW5KQ6uuorAu7bulfPyhmv6DFSRelIyxHnDxqT9OboAsKMBqXQ13961U7ZlDVIOotvK4pkOJl4el1PCDp65aFQTcATFnRu+EhBcQMBdhfP5WNyLB3sQsUo13EteCAtPK/LYWtYZ/OI9yMVA+968fYa5Idz9PHbmWWp+d92euJKteYvcJ6k2n6Fxz/PkGh71nK+Ea5yuXPNsdY1Bqan2XLnGtdSyqs7v0BYQm6v4HpZfxOtO6n24lFYI067f8LqFOLrTd/lGhOvWLN+6ejGTODEuuef9U1av68MxXp2W8NObGHL176f0ADMEldIuCMQjfuU6lDYKKC9x0bThe2VEXWBNgfxB+leNaNWrJmudFtNi1Ko5e8FKNNg+z22+Xuurd8UXKLHIEGF9sZgVUCm+tA9imVZOQGRHZV8G2lvLAsuT6i6IgwN+zHv5lGvhyXqt9nsEfiStpzslM45VS2U8Jm7VexbravKi9yzfnpneMxl2wI+beHd/Sur3roQXkDNJmaWUNhp1F8uZjW2oqxfHUi5OgPxZrPLnsFqbodaF5C4XZxvBm+oWKYVXkNTdNX+NIdWbXwKb4fkLFDrRcEOqH0TccgRHCK3C58kizVw/kHFuofyWYsw0IMBVyt5yyAC4HlZyZoOD9g2LEGW6WAOhRzNd9vSmi9wBnpFe4mS68ALaynRVWITcrZsRizRzvTJd45ouhrwhGDZ64OeVQoru93Dq7tEdtGZlgApWFBPqkhNqkfPp1BNwkKdqMwTsoqBXcLkI8C5NmFDgbGUvMZeachB8OaO0EinPI5S8oH87dlCWlxEnm5L3+wwmO//VJ1XViFPPkGR7vVOPYnSnmXufnGrZXQpwEh7xoGmteZ20iIcJy22wei60VvB0QuaMJ0lZDWq3dYzRJoen8e17h1IkclRs2teJSZJUGqcIdPJ9ryr2lu/3EfsB0sgKgfOXxtpN0QoHu33z1UyfBCQEpWbUPbPQqCZaj23p9JGF5b27+sT6yLEn0UejtvSQBxOkBDjg7YwGBEu4Ixc/QCGRU9kyt2zMmnOoazZah7BbvQnMdcnF/hF+CPzyIUghJpMdMTSqiTwnEQ+yZy5rfiBzAJHgbjWY6XBMG2NdAabDm3hRZvRgZF0UIzMwahuvT8rIkrRdMjzc8xiBkV0axMWnx8PdJo6z5MHw3AX6puhwHgEx+E6263hxiMJl+HkJxAQOhm1l9EtLGau25wO+ZBu7VTJzEYDuzigWReHSEoDoBfMFrf58BsxeFYygMEzXghGDgQt36qA6n9omsrFq5t/Z0Eupb8lrZnrLkF4z896zLbu/+9GB6Fj2h0cpFO6PSvRJw1HGCGweo69G/0WPz9pr0pdz9e76FeCndbxnNo83bLtpvJhFla96Tcraa/Iu3kPtCiUkBv/LDY7qNFk6IGOnySzA6MebrBlcaXYHtpacjmyDG0k2PCNfNgEzjy3aWp5M/i6HwvoZ9l/bqm6Gqpthx6U/Xm/ApZWUgSHFOrn8x3fx+C3/p0mkEpEUxbvgGSrMYd2UfRTrvH9d76UwlUpj9g40Vu/AFuIdkMtzHLTomwlswpqtBuYLg0NuqbZq9cqCigpiO9ft4/0RHJKi5HOX0m6/7JOzufr9BCdgguId3DIAapUomv3O8dJEEeCr60iqm90K1UvXkrSbLSCZqVmS2gsYMe+PjlUVHi8m1lPTYgCMmIAg/GlbYB6iBmeX0d4oGnjSFHMewGhbnXrZGtUVotpelYy5jERxzYlhU91vYuHW4XqrgDSOFyUitHg8KTGz6XFMYvWmEMyZIJjfzqYdml8wULuPguR5IJrSoZrQwWlq4egVHRwngOtGhi4zH69/P0ueVJ0XJ+XifJnBC18JhcmbebYjhfiw7NBXylShWFZUILjUIHgLeNW3hTAlKIbaphz3Zfl1yTHJtO3mGucZ8a4TczOxisvUjQuXQh6iVeMVYm4MGefPEHNjbyTP6p4rzG1azM0ZIbZdNydtvfQ8MTdU2LYVc0OiyoC5iekE0Fk8nCYWbh2OpKtuvGc3DRckIYgICnJjCB2sLNiuHl0rg1dAX+wOLyCEBf0uLXe/fih7VbK8cDN6IAtqkOFPghGCTPXPyo8WjHFNPd898USFNoyLNtRVAxyCNpgNPgZ/tMEl8w86duu5YvgBD/nxqHlyzzPkxyUbpyj4oRCndk8FZVMr+EFy+GGUkB939k1eZgc/IFFlaETI3AbdFRMLrUJ+SlRQmZLSwR3YilEFGJUOPN8Ao/lAPQryGT9VkxPCMWWa5ojppvPi3yLQaWQerg1xyunzZpr4PE7BTmy8KHjKeV2nEJmah51nyjPV5ClEdjxEtoAz5xD/5X49/P1RCx3t9uGff3/9tH/32dFvyUy6JFpBjDArQrKLH+FnwCfJIVxmHuSEuAFWXJK9AP9NGXy7kQQ4QJy/DddQVvHScbtotdo2AstlJIFKW0rqUPrPX4/eu/DT3SdP34cPrz+vTndF4yTWDrS32kI3uMCAhusvfJ9IhEbCge9DjNswmTqnBgsqEKebjGlyUaqIT0/NPV0xYkkq62Pi0le8xUsTc83WXAaAMFmoo81AcbJx6yKhAFHjejBmP7fWYt5Q69w6Cw+AKlrClsywNaoZHpxf6BBLRQ1JzOVgvsgaBu2Rd+oB7dVd5G2wXb97+Wh83uwy4CjbA9bWYZCejmGSndsZ3OsP9jBNaqaQJJ7SRpMzizlkxSLbZmDodc6RCWGw3FQd3w7ApSyvktWzBr/H8CpgtQLR2Dg4rs5OfV4MexsvM2cqgTRb/e8EXMQVlMZsGh6AmdrjixvtxXobHX44XyWAB4M0vAjwTV5gO7vM/VNx63W83cbfov0DPHI6bMN5T25JTVGFrhvQrUSvTYlxmk/WudEXYPC7j1/f3tivTOOMksGK8GABCf7/BbDxv6DEJF9PQcbyuwAG50Lh2EVg+XsMVz8g/3oDzB08c41LoZacMi2TbgJoAF/CbgXw0bL9lLf61OxhwCn4sPz4FUyBY12moHjn88sgLRMluarpAn61scJU726e3/0dhEj1y6uHSQJWV1Gw3T6hN1+kTwf4+nDry7hBu17nn6LkM+SX7M6AQXLGSRBjJFEeqkU0JOA/cdjiW0iTAueGQPOKJgWYZwfEdgne/n327SdPuxz6I1+I25dDf2a+bWnMq7x8f3asA6bS7Kt0agM7rufAEBfH1Km4TJ1VuA5O25TDRHV3zfDUFMIxo80uj6qR1Bdj6Gk8VlOPAglpDlyEF8EO7eOsyQUVYqmNXm1kNma42zYWNkFBFx0rk7A4yJ2GDImrioY0Gj5hlJmOhAwtlxUJm7RqNYNPAorqDDCgImlTsoAENGTpmqxo2LAZLIF1RDt5iog9iYh3wZWApCzdgxVJG0gqAxEtRcRBRLTkW/PQmi8pkvbxJhE8MD1NGfAXRdMG71KKNZCCebj6l1LQVME+nBxMeXStgoF4+ZvS0NRQOBBX91MG1WsoXIiX/ymPnCqYiKs7KoWcKpyIsz9KKdgzNk0VbDTMH5UB+zMUUMTVH5WCpgoo4uuPSqBrFWzE2R+VgKYKNuLqj0qhehVuxNkfnV5OKW2DFU0H+KMyyClLW19F0y7+qDU9TRVuNMwflSGCmtr+VBGxtz8qBU0VUMTXH5VA1yrYiLM/KgFNFWzE1R+VQvUq3IizPyqBnCrciKs/KoWc8saN2unBTHGSdmUmEEUr/mm44OsxhmnqxW8/g2nb/BqvIBu+/n8= -------------------------------------------------------------------------------- /docs/img/SFNNv5_architecture.drawio: -------------------------------------------------------------------------------- 1 | 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 -------------------------------------------------------------------------------- /docs/img/board_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/official-stockfish/nnue-pytorch/a7a65f615a2f3477159541fb973f91250789bdaf/docs/img/board_0.png -------------------------------------------------------------------------------- /docs/img/clipped_relu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/official-stockfish/nnue-pytorch/a7a65f615a2f3477159541fb973f91250789bdaf/docs/img/clipped_relu.png -------------------------------------------------------------------------------- /docs/img/crelu16.drawio: -------------------------------------------------------------------------------- 1 | 7Vpdb9owFP01PBbZcT7gcc2abtImTUPbpL0gE1xi1cGZcUvor59NHCCYSqtEiEeaF5Jjxx/n3GtfXzJAcV7eC1xkX/mcsIEH5uUAfRx43g2MkPrRyKZCkG+AhaDzCoJ7YEJfiAGBQZ/onKwaFSXnTNKiCaZ8uSSpbGBYCL5uVnvgrNlrgRfEAiYpZjb6i85lVqGjAOzxT4QusrpnCExJjuvKBlhleM7XBxC6G6BYcC6ru7yMCdPk1bxU7yWvlO4GJshS/ssLYVxssp9TkM8+3/14+fM7vX/5cAOBV7XzjNmTmbIZrtzUHDwTIami5AueEfaNr6ikfKmKZlxKng/QbV3hA6MLXSB5odBM5kw9QHWr5l7oxvJyoc1kOMMrmg4LLCURy4Qy9n2r3e2Dup2YjisDuV1JUuhWwK5Y8MedFmDoNfCYMy62o0Z4e5nSAzxRF0IKZ+RBE6cbtrk09OqZkfIAMtzeE54TKTaqSl0KRkbo2tJD87w+sBsDZYcmYzBsLHWxa3ovproxer5JW9Q3bQFIEqBbE4bfc4nrAefE9fsobpKc33E99xw3sLSd5rkXhNMcl1NS0JEltZqs1mGdUUkmBU41vFaSNeXERudU0UXEmfiDMGjw50ObP4hOEIhaIxCGrxFYEJE/SeKXoa+JDJUXxduhqpFuowhljZottXTGat6xaiUGTrMddM52OO7bUqR3mVb2GeDcPhOBV10Jp48r7US1t7npICjq3EEiaHFIl3JUIttP3kacMb9z8DaOjmgb27T5wUVZs48mijUYlm7ZmxccuaxvExdclrjexf3trcdeU1wfdL4e9zLut8Q98xkAAud0ts8AV67zqcTMuf0ZRuPh0UbngNT2aeXKpW7tKB8658bRfxHEAPeCmNEp4tyOmX3Uecz8fhRvLfQ74RKXXUvq3vojbiehX/c62+mCK9e5q9Cve6ntHMeVS32x0K97bXuYhmnfjcHYduMTqcoLS/2elGljZz76Y9YBnXuXlLnYct29tr3LwrR3dIIurtEnMzGOJxRGrSUU1OP+28Nt2cEXnOjuLw== -------------------------------------------------------------------------------- /docs/img/crelu32.drawio: -------------------------------------------------------------------------------- 1 | 7Zxbb6M4GIZ/TS4n8oHj5ZQtMyPNSqutdlbam4omboIGYoa4De2vHztATibTjNYYl7hSVfLZAfK+Pnx+TDPBUV59KpNi+Sedk2yCwLya4D8mCH1AAeR/ROSljmDg1IFFmc7rENwH7tJX0gRBE31K52R9VJFRmrG0OA7O6GpFZuwolpQl3RxXe6TZ8VWLZEGkwN0syeTov+mcLeto4IJ9/DNJF8v2yhA0JXnSVm4C62Uyp5uDEL6d4KiklNVHeRWRTIjX6lK/Lz5TuruxkqzYJW/wouJl+e0e5A9fbv95/fHf7NPrxw8QoPo8z0n21Hzk5nbZS6vBMylZyiX5mjyQ7C+6TllKV7zogTJG8wm+aSt8zNKFKGC04NElyzP+AvJD/tkLcbK8WohmMn1I1ulsWiSMkXIVp1n299a7m0d+eNdcuG4gN2tGCnEWsCsu6fedF2CKjuIRzWi5vWucbH94aUYeWXMfouJBFQDiGIgTNxrwj0Gqs/LCnWm8tROaE1a+8CrVqfNtS/ea15t9u8Hh1Hfr6PKw1TQ1k6axLnZn3/vJDxpLf8tePHp7y0bHM/7GsSJ/0QX+OkCruc7ozT3fd/1YuKvIW2yct144em9PDBVD8XYwPuzQKswFxpnrA8nc+zxHrndfJLPv63tSpFi2m0sgvNgsU0bueEUR3nDbji1NGq9nXB5SqpIQuccS+rKEvBPJGuL+NPQ7OoiXCYXm6TM/XIjDdMUwqoK2hF/qoNAohSGanmjsyBq7HclDfxIHwejHoF8kD3EcAGXJITjI/IwZhoLxzzHn84dAae7v8Jt5217t6X8oTzRjc/iX6b+vMP13TMsiQjh6c89331hp+m+et11Y5v1mN9DA7CbsWj2/X4l3jdEcgd1RCYzNE9gb/QSgjRFAKYFzwOCTQNcSd1z+amLzwL3IXv35uV1hK7M4BG/6q7n72uW1qvQRv+2t7r6LwPiXX9q21qBhfRdd9ba40q015Brn7ZXviauEYqZNuuiq98TVQrELJl3N3spAIV0x6FXQkzz+PVDQ9BcVuoWuK20FhAOTAgRkUqBIuboxqmlwPjBNNnkBbvY+PTJunx4BeYl7quH/boRqWfUpKECDawjHD4K0gT4skyB36IkNjh8D6QJ9/kX2aocFcPwsSBcsgKewYPDu204QIzZXEyyAp7BgeG+vmvIpJfTwFBYMb+41Uz6lhB6dwgITJl1kQZ8y0OdJz68a0H0t61OFXoCJ9sq4ryUHpMyfGHlOyqDCqMUw0fZm+b1u/xuUSyoc5CvniA8Dkcd/RR1+zoiLEfGmEwGptRiEHdwO3KoZOyCZGjYG5EklVA9MFtDpeIhTt4CW2/TJbTr4sOYRynKbHrlNh736U0jLbXrjNoN333bgH7G5Q3Gb4b213KY3bjO8uZbb9MVtTJh0seU2fXKb4buv5TY9cpvh7e18TEuQGsnl4Z7S8qWntJyh/58L4c6ntMwXLhhcOMtZeuQsbkfH0DyiWM7S0yK8y1v9+Z6FLMoW4uHb/urtu46FLKpyed84by1kUQjATTPXQhZ1dPSSnEr7vOtYzqJsbEbhtP26PHN6sOUs/XEWA+x9n5zFxb3hAv5y/33q27KDb6XHtz8B -------------------------------------------------------------------------------- /docs/img/cross_entropy_loss.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/official-stockfish/nnue-pytorch/a7a65f615a2f3477159541fb973f91250789bdaf/docs/img/cross_entropy_loss.png -------------------------------------------------------------------------------- /docs/img/cross_entropy_loss_contour.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/official-stockfish/nnue-pytorch/a7a65f615a2f3477159541fb973f91250789bdaf/docs/img/cross_entropy_loss_contour.png -------------------------------------------------------------------------------- /docs/img/cross_entropy_loss_grad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/official-stockfish/nnue-pytorch/a7a65f615a2f3477159541fb973f91250789bdaf/docs/img/cross_entropy_loss_grad.png -------------------------------------------------------------------------------- /docs/img/cross_entropy_loss_grad_contour.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/official-stockfish/nnue-pytorch/a7a65f615a2f3477159541fb973f91250789bdaf/docs/img/cross_entropy_loss_grad_contour.png -------------------------------------------------------------------------------- /docs/img/fc_input_density.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/official-stockfish/nnue-pytorch/a7a65f615a2f3477159541fb973f91250789bdaf/docs/img/fc_input_density.png -------------------------------------------------------------------------------- /docs/img/m256_add_dpbusd_epi32.drawio: -------------------------------------------------------------------------------- 1 | 7Zxdc6M2FIZ/jS/tAQQ2voztkLazO7OzmW1nerMjg2zUAKKyHDv59ZX4sAGRxp0IcKxyEcNB5uM8RwedF8UjsIyPDxSm4VcSoGhkGcFxBFYjywLA4X+F4SU3TIGdG7YUB7nJPBse8SsqjEZh3eMA7WoNGSERw2nd6JMkQT6r2SCl5FBvtiFR/awp3CLJ8OjDSLb+gQMW5lbXMc72XxDehuWZTaPYE8OycWHYhTAgh4oJ3I/AkhLC8rX4uESR8F3pl/x73ht7TxdGUcIu+QL668eXn99WNmbLe28V/Pbl6/T72JwWOJ5htC9uubhc9lL6IIJrFH0jO8wwSbiJ5ve7gBHeiu0IbcTmjlHydPKSKSwhTMUx4uNWBMckJv7TPp3EkIoPf0+jlwWFPm+yCFkcnb+VO8rgGwHchSgoNvIzLElEaHZlYJ4tohmmnH5+eQmh4goWGxxFlbaG4fGF22XHFb58RpShY8VUOPIBkRgx+sKblHsNULipCGswLygfzkFiGkXsh5UAsYt2sIjL7enYZ3R8paD3n0hOJZJAZslvkDXcXXNqQhLU8F1hKmn73G2I2xfCXZj3lLtiR4yDQJxmcQgxQ49pBnZ14OS5jZJ9EpxAqkBg2dOJ5dQo2M7ZVAFhzSYtJKzuSMwkEvYNgzBmRqMvzCUEPRNwjfezWum1L43stiaMkbjFrYykUqKqpbc13GF/kkLGsSQeB/c9eyJlDB+LE+ePPc4apeIoxml3NXcaE6tmr2QxmC1ydvM8hdnt9BD7l+wGSno9ZTdLP6IGX3okarcANRUAnS7Tl/D3n0a8/vX+x+vff/oPr3djRwHOtwcKUoqsMG4Pgg8NXt4el7w1BqGEweJ2smxVUpaQtoC/nLJtX9hvLeP03FRO2tQwF7vVnvtBpvaV9VzT1I+nyMWqeLrXxlO7J6trKOR5GsVeDU+gG09DKc/ptfG8QKG5PZ7KxkTg2ka+poqh76fjWalNP8jz6sZDsu528zxnCnle23gIXJBvSxo4zl4fvF9evhsAmca+gP7TNhPoKt7eZAtvkp3sbpfmrzmyMrfc2OCjqH0XxfWsQsbE+5E74QnL84PEnGCfJBucBIhOfH5Gywsgg/xD2Hf8c4MgCxEdC+8JudI78pGhO0mTbVuB63lTvnQcGpZdl3eBI2u7oCU0QFeh4VwwVP50oWG/GxoH8XKFr443FKHxMz8uoeNyp3jH4MEgGDxa7Lldj5bpwNFSyuL6PBgakqVaMQu0iFm9PhhmGgofXQpZg/PUTvjoVsganKd2wke3QtbgPLUUProTsgbnqaXw0Z2QNThPDYWPLoWswXnK05ZunmeXQtbgPN0WntNIODfAz3x1K1Zxku6ZgBxhH5X7hQZwbiJFgTx37fI5aRTt8CtcZ4cSIFOCE5bdurMYOStxrD3jgZVNBH5zWpwCYPN5Q10qhb/qtLXWuRJd8ZpLvA75eU90bpNDUwB2JQ5unxxaJg9i4c0YBRiyUx9Z07J/kD3L+tAt0jFta17HY8jdpA1PZ7KaK8swNw3AMRv9w5QBOL32Dw11E4W6pu1c2TjB1U43UaprSjydoXlqp5so1TUlntOheWqnmyjVNSWes6F5aqmbdMfTHZqnhrqJQl1T4jn0BCC3TTe5eZ7q5mE0eNrG0DxlXeXmeaqcV9PkaQ7Ms8wP+vBUWn86oJFvW+pPs1ee/8+rUcqzpf7sl6eG+pDC8a3Es6X+7JendvqQ0vpT4tlSf/bLUzt9SGn9KfFsqT/75amdPqS0/pR4tv6UT588tdSHlNUrTZ5t9We/PDXUhxTWnxLPlvqzX56yPoQT5h4v+rWtz/gGWvpPDEsm0Osb6Lms6Nw2Abvxn3A2GJjAKSTqCIB1dG8UQXMaRocI+Ob5ZyGzfZXf1gT3/wA= -------------------------------------------------------------------------------- /docs/img/m256_block_sparse_weight_matrix.drawio: -------------------------------------------------------------------------------- 1 | 7Zxfb5swEMA/TR4bGQiEPC5Js0nbpKndtL064BKrBjPHtMk+/QwYAoG0aYvrVDIPLZz/4Ph3dz6fo4ycRbz7zGC6+U5DREY2CHcjZzmy7Zlvib+5YF8KXNstBRHDYSmyDoJb/A9JIZDSDIdo26rIKSUcp21hQJMEBbwlg4zRx3a1O0rab01hhDqC2wCSrvQ3DvmmlPouOMi/IBxtqjdbQJbEsKosBdsNDOljQ+Rcj5wFo5SXd/FugUg+d9W8lO1WJ0rrgTGU8HMa4JtllICv4TrZBD//zK+yX7d/r2x3WvbzAEkmP7IcLt9Xc/CAGMdiSr7BNSI/6BZzTBNRtKac03jkzOuPBsRDCLcbFMoHSHCUVw3EIBETgg2PiXi2xG3V7SdZh9M074szel/PtVX2nuYjiXdRrmLjmAb3WTqOIcv/BRkj+zmDAaobLyihrBi6MysuUXKHCWnIV8Ul5KLLEIvBtdoAULSRMyOGiXYnJ92qUQoTQDRGnO1FFdnAl/Ar7ZePjwdV8iZStmmoUaUzUGpvVHd8ACxuJOMX8fZ7eHuE5+Dwg7iN8lvL9mnG04xvq8I1q8oqiXh/o0VHacSc8Tbvji4cK0CMwzBvPmdoi//BddFVrkUpxQkvpsKdj9xl3lfGhR4WzsLqcE9ogobB53htfk6Xn/+++GbPmys5MlNWDqyef4Lu+FsNrYH1tPGftsYQM+Gsy+EllOUj0GWhbpuw3yVs9xD2lRGuNO4ZA/XyjpKmhV68PTLKoWR+NQNvpLev19ux2yE2fU+bdKw+YkezHzGapSc/rwxe5AwfQoZXrzM9Wty7zqhTY9cbIK7ojxDavqflsdZwi4NxCrlQ6GQlHMpNERMWvuVWvrgMPIWKItHZ0q1Lm94QjK2WvOF7YHE94a/O0uqnNKkLWR9Gz/nYGO2XYgRgtQJnO6dXY+yLGlxlEF0DcTCIVT/eeaGgOqh9gbyB+iaox7s1zYTPiPUN4Vea7WUgnp4ROhrEL0NsT9qILc2OemoZxkMzdr0LY2wbxkMzduwjxrp9tdkKDc54Ai6M8cQwVhxU27pdtdkMD2/Gl8b4gycfPwRj3a56iINrw/jJ3bF2xibHpXo5PvNEWx1ik+QaDvGFMK1eZpiq2w07ml2zb7Jaypdf7YxNVkt5dnqi21ebrJbyrJZ2xiarpZ6xbl9t0lrKT5m0Mzbb4cH3Sj3fyH9XpjOzV1KeqtTO2MRYw9utZl88MzGV8hyHdsYmplKe49D9NdqZOUZSvjfSztjEzcrzWJ5uX23OkVQfFWpG7ACTjlae4pjqddUOMFsl5dth7YzNcqw8rJ7q9tVmOVYeciljLB4PP0tTlDV+28e5/g8= -------------------------------------------------------------------------------- /docs/img/m256_haddx4.drawio: -------------------------------------------------------------------------------- 1 | 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 -------------------------------------------------------------------------------- /docs/img/m256_process_chunk.drawio: -------------------------------------------------------------------------------- 1 | 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 -------------------------------------------------------------------------------- /docs/img/mse_loss.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/official-stockfish/nnue-pytorch/a7a65f615a2f3477159541fb973f91250789bdaf/docs/img/mse_loss.png -------------------------------------------------------------------------------- /docs/img/mse_loss_contour.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/official-stockfish/nnue-pytorch/a7a65f615a2f3477159541fb973f91250789bdaf/docs/img/mse_loss_contour.png -------------------------------------------------------------------------------- /docs/img/mse_loss_grad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/official-stockfish/nnue-pytorch/a7a65f615a2f3477159541fb973f91250789bdaf/docs/img/mse_loss_grad.png -------------------------------------------------------------------------------- /docs/img/mse_loss_grad_contour.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/official-stockfish/nnue-pytorch/a7a65f615a2f3477159541fb973f91250789bdaf/docs/img/mse_loss_grad_contour.png -------------------------------------------------------------------------------- /docs/img/mv.drawio: -------------------------------------------------------------------------------- 1 | 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 -------------------------------------------------------------------------------- /docs/img/mvs.drawio: -------------------------------------------------------------------------------- 1 | 7Zzdb5swEMD/mjw24iMQeFzaJXvYpGqZWmlvLriBFTBznCbZXz9jDAnY1TKJDwvIQwtnA+Z+d4Y7bM/M+/i0wSANviEfRjND808z82FmGNZyQf9mgnMuMBwnF+xw6Oci/SLYhn8gF2pcegh9uK9UJAhFJEyrQg8lCfRIRQYwRsdqtVcUVa+agh0UBFsPRKL0OfRJkEsdS7vIv8BwFxRX1jVeEoOiMhfsA+Cj45XI/Dwz7zFCJN+KT/cwynRX6CU/bv1BadkwDBNyywE/0HZxTuDzRl+vHr9vQvRzE9wZ+VneQXTgN8wbS86FBt4hJiFVyFfwAqNHtA9JiBJa9IIIQfHMXBUVPkXhLisgKKXSgMQR3dHpJr3zNDtZfNplNjJ/AfvQm6eAEIiTdRhF3xm51Svd3PIL5+ax2hOYZmfRymKM3koS2tyoyO9RhDBrtemzHy1lrWHNKBr8UJzsqvqa/ahc1CpXdHaX8HQl4lreQBRDgs+0Ci81HU68MPnCJI4XAyrMO7i2HS4D3GR35ZkvVOkGB/sfkM3BQ/6I5gU+5nrOrhHBV1K3Cb0Z9oZVZW+at7F3WmPv9gcfYI/RBZh8Snbsatp8aVEZTPxSkuFDCeFdv82OqHHOaGp1r01QQg9Y+WAfQJ+3I9t5zE2OSQytKa602RWfNiyRqytzarctsose++7GyGraeu1I+uNS3g1eU6vhtSR4TRlesy28RUcyrl6beXkLz+De+2FrdA/h0oUbwbmo4Vz0i9O2x4bztncqrY13KlsxX7aXY4TPuubu4S9Vg++MEX5Pnq/aU9zuMZoaHXxXMfjL0b2Rl69wncM3NdXg62OD35/nm7pq8IefJlcHvqEa/NFF7j3CN1WDv5jgdwZ/oRp8a4LfGXzVPpwtpwzfBb7eMnzVMnzL4cf5OeQKza7HRei3UW5tXETRnolyi1/eyvFPvVEefuSugC/f2GO3R3n4IboClBd9Ux5+LH4b5WY/rtcoW31THn7QrYAv231THn50rQDlZd+Uhx9GK0DZ6Zvy8EfEKEDZ7Zvy8Ie+9E/Z7Hu2iDPK3JfeMeW+c19uE/FyOaUtU2U5Uj/bAZy7R1UGcRW93Db2FYoyE4mR93ZI5zHA2T/vgKPzCgMPlgdfsXPZLyt5g8QLCjeuzkYQeGsai6no5fyQNrw20wQjArgGFlZDZlEf46Q7ErOYG65oGUZrliFGX5pgGvT+iAxznW0c+n7ElAf34R/wUk4MSVGYENZ0azWzHrJzHQg1MQZHF5ByBHWAdHcN4jDK1PcEsQ8S0BCY+nwfSa9sdOqvYrQ0UZF9QeiWihjdTFRkueBuqYjRyERFlrvtlooYPUxUZFm4bqmIb/sTFVnWrFMq5SoSE5Z/pLk6xiJ+S56wyPJSHWMRg9kJiyyR1DEW8WvthEWW+ekYixjg66PHIhum1DEWMZacsMjGIrSF5Ve8eTr8vPttuO+/7Sfv2V0l3p34wAe+/xGW6/y2VI3i8jW3kzwGIYHblCU8H44YpCwleUj8MukqT1AKypcg+o+HipigtCU8mlgWRcpDfNLHh2w1uOisPpS7pqgY9r8zLg1RobuXJepY2dU6f+bnvw== -------------------------------------------------------------------------------- /docs/img/quantmoid4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/official-stockfish/nnue-pytorch/a7a65f615a2f3477159541fb973f91250789bdaf/docs/img/quantmoid4.png -------------------------------------------------------------------------------- /docs/img/quantmoid4_equation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/official-stockfish/nnue-pytorch/a7a65f615a2f3477159541fb973f91250789bdaf/docs/img/quantmoid4_equation.png -------------------------------------------------------------------------------- /docs/img/sigmoid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/official-stockfish/nnue-pytorch/a7a65f615a2f3477159541fb973f91250789bdaf/docs/img/sigmoid.png -------------------------------------------------------------------------------- /docs/img/sigmoid_wdl_fit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/official-stockfish/nnue-pytorch/a7a65f615a2f3477159541fb973f91250789bdaf/docs/img/sigmoid_wdl_fit.png -------------------------------------------------------------------------------- /feature_block.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict 2 | 3 | def _get_main_factor_name(full_name): 4 | return full_name.replace('^', '') 5 | 6 | class FeatureBlock: 7 | ''' 8 | This is the base class for all the network input features. 9 | All features must inherit from this class. 10 | It abstracts a named set of features in a way that 11 | allows seamless introduction of factorizers. 12 | 13 | For example a set of HalfKP features is a subclass of this class, and 14 | so is the HalfKP^ feature set (where "^" denotes that it's factorized). 15 | 16 | There are 3 fundamental things about a feature block which it needs for construction: 17 | - name - whatever, just please use ascii. Also we'd like "^" to be reserved to 18 | denote that the block is a factorized version of some other block. 19 | - hash - a 32 bit unsigned integer, as defined in the original nodchip trainer 20 | - factors - the ordered list of named "festure subblocks". If there's more than one 21 | it's assumed that it's a factorized feature block. 22 | 23 | More about factors, because it's the fundamental building block. 24 | A block can have just one factor, like HalfKP, but sometimes it's possible to 25 | factorize some features further. Ideally we don't want to have multiple 26 | features talking about the same thing when the net is actually used for play, 27 | because it's wasteful, but it's helpful during training because it makes it 28 | easier to generalize over similar positions. This is for example utilized by HalfKP^, 29 | which defines 3 factors: HalfKP, HalfK, and P. 30 | Factors are passed to the constructor as an OrderedDict from string to the number of dimensions. 31 | The first factor is the "real" factor (or "main" factor), one that is supposed to be used for play. 32 | The following factors (if any) are the "virtual" factors, and are only used for training. 33 | Based on these factors and their dimensions FeatureBlock defines 3 values: 34 | - num_real_features - the number of unfactorized features that the resulting net will use in play 35 | - num_virtual_features - the number of additional features used for learning 36 | that will be coalesced when converting to .nnue 37 | - num_features - the total number of features defined by the factors. 38 | should num_real_features + num_virtual_features 39 | 40 | FeatureBlock provides default method implementations that abstract away the 41 | factorized/unfactorized nature of the feature block. These methods are described in 42 | their own docstrings. 43 | 44 | The only method that the superclass of FeatureBlock must define is 45 | get_active_features (def get_active_features(self, board: chess.Board)), 46 | which takes the board and returns the list of indices of the features 47 | that are active for this board. 48 | ''' 49 | 50 | def __init__(self, name, hash, factors): 51 | if not isinstance(factors, OrderedDict): 52 | raise Exception('Factors must be an collections.OrderedDict') 53 | 54 | self.name = name 55 | self.hash = hash 56 | self.factors = factors 57 | self.num_real_features = factors[_get_main_factor_name(name)] 58 | self.num_features = sum(v for n, v in factors.items()) 59 | self.num_virtual_features = self.num_features - self.num_real_features 60 | 61 | def get_main_factor_name(self): 62 | return _get_main_factor_name(self.name) 63 | 64 | ''' 65 | This method represents the default factorizer. If your feature block 66 | has multiple factors you need to override this method to return 67 | a list of factors for a given feature. 68 | ''' 69 | def get_feature_factors(self, idx): 70 | return [idx] 71 | 72 | ''' 73 | This method takes a string name of a factor and returns the offset of the 74 | first feature in this factor when consulted with the sizes of the previous factors. 75 | ''' 76 | def get_factor_base_feature(self, name): 77 | offset = 0 78 | for n, s in self.factors.items(): 79 | if n == name: 80 | return offset 81 | 82 | offset += s 83 | 84 | raise Exception('No factor named {} in {}'.format(name, self.name)) 85 | -------------------------------------------------------------------------------- /feature_set.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict 2 | from feature_block import * 3 | import torch 4 | import chess 5 | 6 | def _calculate_features_hash(features): 7 | if len(features) == 1: 8 | return features[0].hash 9 | 10 | tail_hash = calculate_features_hash(features[1:]) 11 | return features[0].hash ^ (tail_hash << 1) ^ (tail_hash >> 1) & 0xffffffff 12 | 13 | class FeatureSet: 14 | ''' 15 | A feature set is nothing more than a list of named FeatureBlocks. 16 | It itself functions similarily to a feature block, but we don't want to be 17 | explicit about it as we don't want it to be used as a building block for other 18 | feature sets. You can think of this class as a composite, but not the full extent. 19 | It is basically a concatenation of feature blocks. 20 | ''' 21 | 22 | def __init__(self, features): 23 | for feature in features: 24 | if not isinstance(feature, FeatureBlock): 25 | raise Exception('All features must subclass FeatureBlock') 26 | 27 | self.features = features 28 | self.hash = _calculate_features_hash(features) 29 | self.name = '+'.join(feature.name for feature in features) 30 | self.num_real_features = sum(feature.num_real_features for feature in features) 31 | self.num_virtual_features = sum(feature.num_virtual_features for feature in features) 32 | self.num_features = sum(feature.num_features for feature in features) 33 | 34 | ''' 35 | This method returns the feature ranges for the virtual factors of the 36 | underlying feature blocks. This is useful to know during initialization, 37 | when we want to zero initialize the virtual feature weights, but give some other 38 | values to the real feature weights. 39 | ''' 40 | def get_virtual_feature_ranges(self): 41 | ranges = [] 42 | offset = 0 43 | for feature in self.features: 44 | if feature.num_virtual_features: 45 | ranges.append((offset + feature.num_real_features, offset + feature.num_features)) 46 | offset += feature.num_features 47 | 48 | return ranges 49 | 50 | def get_real_feature_ranges(self): 51 | ranges = [] 52 | offset = 0 53 | for feature in self.features: 54 | ranges.append((offset, offset + feature.num_real_features)) 55 | offset += feature.num_features 56 | 57 | return ranges 58 | 59 | ''' 60 | This method goes over all of the feature blocks and gathers the active features. 61 | Each block has its own index space assigned so the features from two different 62 | blocks will never have the same index here. Basically the thing you would expect 63 | to happen after concatenating many feature blocks. 64 | ''' 65 | def get_active_features(self, board): 66 | w = torch.zeros(0) 67 | b = torch.zeros(0) 68 | 69 | offset = 0 70 | for feature in self.features: 71 | w_local, b_local = feature.get_active_features(board) 72 | w_local += offset 73 | b_local += offset 74 | w = torch.cat([w, w_local]) 75 | b = torch.cat([b, b_local]) 76 | offset += feature.num_features 77 | 78 | return w, b 79 | 80 | ''' 81 | This method takes a feature idx and looks for the block that owns it. 82 | If it found the block it asks it to factorize the index, otherwise 83 | it throws and Exception. The idx must refer to a real feature. 84 | ''' 85 | def get_feature_factors(self, idx): 86 | offset = 0 87 | for feature in self.features: 88 | if idx < offset + feature.num_real_features: 89 | return [offset + i for i in feature.get_feature_factors(idx - offset)] 90 | offset += feature.num_features 91 | 92 | raise Exception('No feature block to factorize {}'.format(idx)) 93 | 94 | ''' 95 | This method does what get_feature_factors does but for all 96 | valid features at the same time. It returns a list of length 97 | self.num_real_features with ith element being a list of factors 98 | of the ith feature. 99 | This method is technically redundant but it allows to perform the operation 100 | slightly faster when there's many feature blocks. It might be worth 101 | to add a similar method to the FeatureBlock itself - to make it faster 102 | for feature blocks with many factors. 103 | ''' 104 | def get_virtual_to_real_features_gather_indices(self): 105 | indices = [] 106 | real_offset = 0 107 | offset = 0 108 | for feature in self.features: 109 | for i_real in range(feature.num_real_features): 110 | i_fact = feature.get_feature_factors(i_real) 111 | indices.append([offset + i for i in i_fact]) 112 | real_offset += feature.num_real_features 113 | offset += feature.num_features 114 | return indices 115 | 116 | def get_initial_psqt_features(self): 117 | init = [] 118 | for feature in self.features: 119 | init += feature.get_initial_psqt_features() 120 | return init 121 | -------------------------------------------------------------------------------- /features.py: -------------------------------------------------------------------------------- 1 | from feature_block import * 2 | from feature_set import * 3 | 4 | import argparse 5 | 6 | ''' 7 | Each module that defines feature blocks must be imported here and 8 | added to the _feature_modules list. Each such module must define a 9 | function `get_feature_block_clss` at module scope that returns the list 10 | of feature block classes in that module. 11 | ''' 12 | import halfkp 13 | import halfka 14 | import halfka_v2 15 | import halfka_v2_hm 16 | 17 | _feature_modules = [halfkp, halfka, halfka_v2, halfka_v2_hm] 18 | 19 | _feature_blocks_by_name = dict() 20 | 21 | def _add_feature_block(feature_block_cls): 22 | feature_block = feature_block_cls() 23 | _feature_blocks_by_name[feature_block.name] = feature_block 24 | 25 | def _add_features_blocks_from_module(module): 26 | feature_block_clss = module.get_feature_block_clss() 27 | for feature_block_cls in feature_block_clss: 28 | _add_feature_block(feature_block_cls) 29 | 30 | def get_feature_block_from_name(name): 31 | return _feature_blocks_by_name[name] 32 | 33 | def get_feature_blocks_from_names(names): 34 | return [_feature_blocks_by_name[name] for name in names] 35 | 36 | def get_feature_set_from_name(name): 37 | feature_block_names = name.split('+') 38 | blocks = get_feature_blocks_from_names(feature_block_names) 39 | return FeatureSet(blocks) 40 | 41 | def get_available_feature_blocks_names(): 42 | return list(iter(_feature_blocks_by_name)) 43 | 44 | def add_argparse_args(parser): 45 | _default_feature_set_name = 'HalfKAv2_hm^' 46 | parser.add_argument("--features", dest='features', default=_default_feature_set_name, help="The feature set to use. Can be a union of feature blocks (for example P+HalfKP). \"^\" denotes a factorized block. Currently available feature blocks are: " + ', '.join(get_available_feature_blocks_names())) 47 | 48 | def _init(): 49 | for module in _feature_modules: 50 | _add_features_blocks_from_module(module) 51 | 52 | _init() -------------------------------------------------------------------------------- /halfka.py: -------------------------------------------------------------------------------- 1 | import chess 2 | import torch 3 | import feature_block 4 | from collections import OrderedDict 5 | from feature_block import * 6 | 7 | NUM_SQ = 64 8 | NUM_PT = 12 9 | NUM_PLANES = (NUM_SQ * NUM_PT + 1) 10 | 11 | def orient(is_white_pov: bool, sq: int): 12 | return (56 * (not is_white_pov)) ^ sq 13 | 14 | def halfka_idx(is_white_pov: bool, king_sq: int, sq: int, p: chess.Piece): 15 | p_idx = (p.piece_type - 1) * 2 + (p.color != is_white_pov) 16 | return 1 + orient(is_white_pov, sq) + p_idx * NUM_SQ + king_sq * NUM_PLANES 17 | 18 | def halfka_psqts(): 19 | # values copied from stockfish, in stockfish internal units 20 | piece_values = { 21 | chess.PAWN : 126, 22 | chess.KNIGHT : 781, 23 | chess.BISHOP : 825, 24 | chess.ROOK : 1276, 25 | chess.QUEEN : 2538 26 | } 27 | 28 | values = [0] * (NUM_PLANES * NUM_SQ) 29 | 30 | for ksq in range(64): 31 | for s in range(64): 32 | for pt, val in piece_values.items(): 33 | idxw = halfka_idx(True, ksq, s, chess.Piece(pt, chess.WHITE)) 34 | idxb = halfka_idx(True, ksq, s, chess.Piece(pt, chess.BLACK)) 35 | values[idxw] = val 36 | values[idxb] = -val 37 | 38 | return values 39 | 40 | class Features(FeatureBlock): 41 | def __init__(self): 42 | super(Features, self).__init__('HalfKA', 0x5f134cb8, OrderedDict([('HalfKA', NUM_PLANES * NUM_SQ)])) 43 | 44 | def get_active_features(self, board: chess.Board): 45 | def piece_features(turn): 46 | indices = torch.zeros(NUM_PLANES * NUM_SQ) 47 | for sq, p in board.piece_map().items(): 48 | indices[halfka_idx(turn, orient(turn, board.king(turn)), sq, p)] = 1.0 49 | return indices 50 | return (piece_features(chess.WHITE), piece_features(chess.BLACK)) 51 | 52 | def get_initial_psqt_features(self): 53 | return halfka_psqts() 54 | 55 | class FactorizedFeatures(FeatureBlock): 56 | def __init__(self): 57 | super(FactorizedFeatures, self).__init__('HalfKA^', 0x5f134cb8, OrderedDict([('HalfKA', NUM_PLANES * NUM_SQ), ('A', NUM_SQ * NUM_PT)])) 58 | 59 | def get_active_features(self, board: chess.Board): 60 | raise Exception('Not supported yet, you must use the c++ data loader for factorizer support during training') 61 | 62 | def get_feature_factors(self, idx): 63 | if idx >= self.num_real_features: 64 | raise Exception('Feature must be real') 65 | 66 | a_idx = idx % NUM_PLANES - 1 67 | 68 | return [idx, self.get_factor_base_feature('A') + a_idx] 69 | 70 | def get_initial_psqt_features(self): 71 | return halfka_psqts() + [0] * (NUM_SQ * NUM_PT) 72 | 73 | ''' 74 | This is used by the features module for discovery of feature blocks. 75 | ''' 76 | def get_feature_block_clss(): 77 | return [Features, FactorizedFeatures] 78 | -------------------------------------------------------------------------------- /halfka_v2.py: -------------------------------------------------------------------------------- 1 | import chess 2 | import torch 3 | import feature_block 4 | from collections import OrderedDict 5 | from feature_block import * 6 | 7 | NUM_SQ = 64 8 | NUM_PT_REAL = 11 9 | NUM_PT_VIRTUAL = 12 10 | NUM_PLANES_REAL = NUM_SQ * NUM_PT_REAL 11 | NUM_PLANES_VIRTUAL = NUM_SQ * NUM_PT_VIRTUAL 12 | NUM_INPUTS = NUM_PLANES_REAL * NUM_SQ 13 | 14 | def orient(is_white_pov: bool, sq: int): 15 | return (56 * (not is_white_pov)) ^ sq 16 | 17 | def halfka_idx(is_white_pov: bool, king_sq: int, sq: int, p: chess.Piece): 18 | p_idx = (p.piece_type - 1) * 2 + (p.color != is_white_pov) 19 | if p_idx == 11: 20 | p_idx -= 1 21 | return orient(is_white_pov, sq) + p_idx * NUM_SQ + king_sq * NUM_PLANES_REAL 22 | 23 | def halfka_psqts(): 24 | # values copied from stockfish, in stockfish internal units 25 | piece_values = { 26 | chess.PAWN : 126, 27 | chess.KNIGHT : 781, 28 | chess.BISHOP : 825, 29 | chess.ROOK : 1276, 30 | chess.QUEEN : 2538 31 | } 32 | 33 | values = [0] * (NUM_PLANES_REAL * NUM_SQ) 34 | 35 | for ksq in range(64): 36 | for s in range(64): 37 | for pt, val in piece_values.items(): 38 | idxw = halfka_idx(True, ksq, s, chess.Piece(pt, chess.WHITE)) 39 | idxb = halfka_idx(True, ksq, s, chess.Piece(pt, chess.BLACK)) 40 | values[idxw] = val 41 | values[idxb] = -val 42 | 43 | return values 44 | 45 | class Features(FeatureBlock): 46 | def __init__(self): 47 | super(Features, self).__init__('HalfKAv2', 0x5f234cb8, OrderedDict([('HalfKAv2', NUM_PLANES_REAL * NUM_SQ)])) 48 | 49 | def get_active_features(self, board: chess.Board): 50 | def piece_features(turn): 51 | indices = torch.zeros(NUM_PLANES_REAL * NUM_SQ) 52 | for sq, p in board.piece_map().items(): 53 | indices[halfka_idx(turn, orient(turn, board.king(turn)), sq, p)] = 1.0 54 | return indices 55 | return (piece_features(chess.WHITE), piece_features(chess.BLACK)) 56 | 57 | def get_initial_psqt_features(self): 58 | return halfka_psqts() 59 | 60 | class FactorizedFeatures(FeatureBlock): 61 | def __init__(self): 62 | super(FactorizedFeatures, self).__init__('HalfKAv2^', 0x5f234cb8, OrderedDict([('HalfKAv2', NUM_PLANES_REAL * NUM_SQ), ('A', NUM_PLANES_VIRTUAL)])) 63 | 64 | def get_active_features(self, board: chess.Board): 65 | raise Exception('Not supported yet, you must use the c++ data loader for factorizer support during training') 66 | 67 | def get_feature_factors(self, idx): 68 | if idx >= self.num_real_features: 69 | raise Exception('Feature must be real') 70 | 71 | a_idx = idx % NUM_PLANES_REAL 72 | k_idx = idx // NUM_PLANES_REAL 73 | 74 | if a_idx // NUM_SQ == 10 and k_idx != a_idx % NUM_SQ: 75 | a_idx += NUM_SQ 76 | 77 | return [idx, self.get_factor_base_feature('A') + a_idx] 78 | 79 | def get_initial_psqt_features(self): 80 | return halfka_psqts() + [0] * NUM_PLANES_VIRTUAL 81 | 82 | ''' 83 | This is used by the features module for discovery of feature blocks. 84 | ''' 85 | def get_feature_block_clss(): 86 | return [Features, FactorizedFeatures] 87 | -------------------------------------------------------------------------------- /halfka_v2_hm.py: -------------------------------------------------------------------------------- 1 | import chess 2 | import torch 3 | import feature_block 4 | from collections import OrderedDict 5 | from feature_block import * 6 | 7 | NUM_SQ = 64 8 | NUM_PT_REAL = 11 9 | NUM_PT_VIRTUAL = 12 10 | NUM_PLANES_REAL = NUM_SQ * NUM_PT_REAL 11 | NUM_PLANES_VIRTUAL = NUM_SQ * NUM_PT_VIRTUAL 12 | NUM_INPUTS = NUM_PLANES_REAL * NUM_SQ // 2 13 | 14 | KingBuckets = [ 15 | -1, -1, -1, -1, 31, 30, 29, 28, 16 | -1, -1, -1, -1, 27, 26, 25, 24, 17 | -1, -1, -1, -1, 23, 22, 21, 20, 18 | -1, -1, -1, -1, 19, 18, 17, 16, 19 | -1, -1, -1, -1, 15, 14, 13, 12, 20 | -1, -1, -1, -1, 11, 10, 9, 8, 21 | -1, -1, -1, -1, 7, 6, 5, 4, 22 | -1, -1, -1, -1, 3, 2, 1, 0 23 | ] 24 | 25 | def orient(is_white_pov: bool, sq: int, ksq: int): 26 | # ksq must not be oriented 27 | kfile = (ksq % 8) 28 | return (7 * (kfile < 4)) ^ (56 * (not is_white_pov)) ^ sq 29 | 30 | def halfka_idx(is_white_pov: bool, king_sq: int, sq: int, p: chess.Piece): 31 | p_idx = (p.piece_type - 1) * 2 + (p.color != is_white_pov) 32 | o_ksq = orient(is_white_pov, king_sq, king_sq) 33 | if p_idx == 11: 34 | p_idx -= 1 35 | return orient(is_white_pov, sq, king_sq) + p_idx * NUM_SQ + KingBuckets[o_ksq] * NUM_PLANES_REAL 36 | 37 | def halfka_psqts(): 38 | # values copied from stockfish, in stockfish internal units 39 | piece_values = { 40 | chess.PAWN : 126, 41 | chess.KNIGHT : 781, 42 | chess.BISHOP : 825, 43 | chess.ROOK : 1276, 44 | chess.QUEEN : 2538 45 | } 46 | 47 | values = [0] * NUM_INPUTS 48 | 49 | for ksq in range(64): 50 | for s in range(64): 51 | for pt, val in piece_values.items(): 52 | idxw = halfka_idx(True, ksq, s, chess.Piece(pt, chess.WHITE)) 53 | idxb = halfka_idx(True, ksq, s, chess.Piece(pt, chess.BLACK)) 54 | values[idxw] = val 55 | values[idxb] = -val 56 | 57 | return values 58 | 59 | class Features(FeatureBlock): 60 | def __init__(self): 61 | super(Features, self).__init__('HalfKAv2_hm', 0x7f234cb8, OrderedDict([('HalfKAv2_hm', NUM_INPUTS)])) 62 | 63 | def get_active_features(self, board: chess.Board): 64 | raise Exception('Not supported yet, you must use the c++ data loader for support during training') 65 | 66 | def get_initial_psqt_features(self): 67 | return halfka_psqts() 68 | 69 | class FactorizedFeatures(FeatureBlock): 70 | def __init__(self): 71 | super(FactorizedFeatures, self).__init__('HalfKAv2_hm^', 0x7f234cb8, OrderedDict([('HalfKAv2_hm', NUM_INPUTS), ('A', NUM_PLANES_VIRTUAL)])) 72 | 73 | def get_active_features(self, board: chess.Board): 74 | raise Exception('Not supported yet, you must use the c++ data loader for factorizer support during training') 75 | 76 | def get_feature_factors(self, idx): 77 | if idx >= self.num_real_features: 78 | raise Exception('Feature must be real') 79 | 80 | a_idx = idx % NUM_PLANES_REAL 81 | k_idx = idx // NUM_PLANES_REAL 82 | 83 | if a_idx // NUM_SQ == 10 and k_idx != KingBuckets[a_idx % NUM_SQ]: 84 | a_idx += NUM_SQ 85 | 86 | return [idx, self.get_factor_base_feature('A') + a_idx] 87 | 88 | def get_initial_psqt_features(self): 89 | return halfka_psqts() + [0] * NUM_PLANES_VIRTUAL 90 | 91 | ''' 92 | This is used by the features module for discovery of feature blocks. 93 | ''' 94 | def get_feature_block_clss(): 95 | return [Features, FactorizedFeatures] 96 | -------------------------------------------------------------------------------- /halfkp.py: -------------------------------------------------------------------------------- 1 | import chess 2 | import torch 3 | import feature_block 4 | from collections import OrderedDict 5 | from feature_block import * 6 | 7 | NUM_SQ = 64 8 | NUM_PT = 10 9 | NUM_PLANES = (NUM_SQ * NUM_PT + 1) 10 | 11 | def orient(is_white_pov: bool, sq: int): 12 | return (63 * (not is_white_pov)) ^ sq 13 | 14 | def halfkp_idx(is_white_pov: bool, king_sq: int, sq: int, p: chess.Piece): 15 | p_idx = (p.piece_type - 1) * 2 + (p.color != is_white_pov) 16 | return 1 + orient(is_white_pov, sq) + p_idx * NUM_SQ + king_sq * NUM_PLANES 17 | 18 | class Features(FeatureBlock): 19 | def __init__(self): 20 | super(Features, self).__init__('HalfKP', 0x5d69d5b8, OrderedDict([('HalfKP', NUM_PLANES * NUM_SQ)])) 21 | 22 | def get_active_features(self, board: chess.Board): 23 | def piece_features(turn): 24 | indices = torch.zeros(NUM_PLANES * NUM_SQ) 25 | for sq, p in board.piece_map().items(): 26 | if p.piece_type == chess.KING: 27 | continue 28 | indices[halfkp_idx(turn, orient(turn, board.king(turn)), sq, p)] = 1.0 29 | return indices 30 | return (piece_features(chess.WHITE), piece_features(chess.BLACK)) 31 | 32 | def get_initial_psqt_features(self): 33 | raise Exception('Not supported yet. See HalfKA') 34 | 35 | class FactorizedFeatures(FeatureBlock): 36 | def __init__(self): 37 | super(FactorizedFeatures, self).__init__('HalfKP^', 0x5d69d5b8, OrderedDict([('HalfKP', NUM_PLANES * NUM_SQ), ('HalfK', NUM_SQ), ('P', NUM_SQ * 10 )])) 38 | self.base = Features() 39 | 40 | def get_active_features(self, board: chess.Board): 41 | white, black = self.base.get_active_features(board) 42 | def piece_features(base, color): 43 | indices = torch.zeros(NUM_SQ * 11) 44 | piece_count = 0 45 | # P feature 46 | for sq, p in board.piece_map().items(): 47 | if p.piece_type == chess.KING: 48 | continue 49 | piece_count += 1 50 | p_idx = (p.piece_type - 1) * 2 + (p.color != color) 51 | indices[(p_idx + 1) * NUM_SQ + orient(color, sq)] = 1.0 52 | # HalfK feature 53 | indices[orient(color, board.king(color))] = piece_count 54 | return torch.cat((base, indices)) 55 | return (piece_features(white, chess.WHITE), piece_features(black, chess.BLACK)) 56 | 57 | def get_feature_factors(self, idx): 58 | if idx >= self.num_real_features: 59 | raise Exception('Feature must be real') 60 | 61 | k_idx = idx // NUM_PLANES 62 | p_idx = idx % NUM_PLANES - 1 63 | 64 | return [idx, self.get_factor_base_feature('HalfK') + k_idx, self.get_factor_base_feature('P') + p_idx] 65 | 66 | def get_initial_psqt_features(self): 67 | raise Exception('Not supported yet. See HalfKA^') 68 | 69 | ''' 70 | This is used by the features module for discovery of feature blocks. 71 | ''' 72 | def get_feature_block_clss(): 73 | return [Features, FactorizedFeatures] 74 | -------------------------------------------------------------------------------- /lib/nnue_training_data_stream.h: -------------------------------------------------------------------------------- 1 | #ifndef _SFEN_STREAM_H_ 2 | #define _SFEN_STREAM_H_ 3 | 4 | #include "nnue_training_data_formats.h" 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | namespace training_data { 12 | 13 | using namespace binpack; 14 | 15 | static bool ends_with(const std::string& lhs, const std::string& end) 16 | { 17 | if (end.size() > lhs.size()) return false; 18 | 19 | return std::equal(end.rbegin(), end.rend(), lhs.rbegin()); 20 | } 21 | 22 | static bool has_extension(const std::string& filename, const std::string& extension) 23 | { 24 | return ends_with(filename, "." + extension); 25 | } 26 | 27 | static std::string filename_with_extension(const std::string& filename, const std::string& ext) 28 | { 29 | if (ends_with(filename, ext)) 30 | { 31 | return filename; 32 | } 33 | else 34 | { 35 | return filename + "." + ext; 36 | } 37 | } 38 | 39 | struct BasicSfenInputStream 40 | { 41 | virtual std::optional next() = 0; 42 | virtual void fill(std::vector& vec, std::size_t n) 43 | { 44 | for (std::size_t i = 0; i < n; ++i) 45 | { 46 | auto v = this->next(); 47 | if (!v.has_value()) 48 | { 49 | break; 50 | } 51 | vec.emplace_back(*v); 52 | } 53 | } 54 | 55 | virtual bool eof() const = 0; 56 | virtual ~BasicSfenInputStream() {} 57 | }; 58 | 59 | struct BinSfenInputStream : BasicSfenInputStream 60 | { 61 | static constexpr auto openmode = std::ios::in | std::ios::binary; 62 | static inline const std::string extension = "bin"; 63 | 64 | BinSfenInputStream(std::string filename, bool cyclic, std::function skipPredicate) : 65 | m_stream(filename, openmode), 66 | m_filename(filename), 67 | m_eof(!m_stream), 68 | m_cyclic(cyclic), 69 | m_skipPredicate(std::move(skipPredicate)) 70 | { 71 | } 72 | 73 | std::optional next() override 74 | { 75 | nodchip::PackedSfenValue e; 76 | bool reopenedFileOnce = false; 77 | for(;;) 78 | { 79 | if(m_stream.read(reinterpret_cast(&e), sizeof(nodchip::PackedSfenValue))) 80 | { 81 | auto entry = packedSfenValueToTrainingDataEntry(e); 82 | if (!m_skipPredicate || !m_skipPredicate(entry)) 83 | return entry; 84 | } 85 | else 86 | { 87 | if (m_cyclic) 88 | { 89 | if (reopenedFileOnce) 90 | return std::nullopt; 91 | 92 | m_stream = std::fstream(m_filename, openmode); 93 | reopenedFileOnce = true; 94 | if (!m_stream) 95 | return std::nullopt; 96 | 97 | continue; 98 | } 99 | 100 | m_eof = true; 101 | return std::nullopt; 102 | } 103 | } 104 | } 105 | 106 | bool eof() const override 107 | { 108 | return m_eof; 109 | } 110 | 111 | ~BinSfenInputStream() override {} 112 | 113 | private: 114 | std::fstream m_stream; 115 | std::string m_filename; 116 | bool m_eof; 117 | bool m_cyclic; 118 | std::function m_skipPredicate; 119 | }; 120 | 121 | struct BinpackSfenInputStream : BasicSfenInputStream 122 | { 123 | static constexpr auto openmode = std::ios::in | std::ios::binary; 124 | static inline const std::string extension = "binpack"; 125 | 126 | BinpackSfenInputStream(std::string filename, bool cyclic, std::function skipPredicate) : 127 | m_stream(std::make_unique(filename, openmode)), 128 | m_filename(filename), 129 | m_eof(!m_stream->hasNext()), 130 | m_cyclic(cyclic), 131 | m_skipPredicate(std::move(skipPredicate)) 132 | { 133 | } 134 | 135 | std::optional next() override 136 | { 137 | bool reopenedFileOnce = false; 138 | for(;;) 139 | { 140 | if (!m_stream->hasNext()) 141 | { 142 | if (m_cyclic) 143 | { 144 | if (reopenedFileOnce) 145 | return std::nullopt; 146 | 147 | m_stream = std::make_unique(m_filename, openmode); 148 | reopenedFileOnce = true; 149 | 150 | if (!m_stream->hasNext()) 151 | return std::nullopt; 152 | 153 | continue; 154 | } 155 | 156 | m_eof = true; 157 | return std::nullopt; 158 | } 159 | 160 | auto e = m_stream->next(); 161 | if (!m_skipPredicate || !m_skipPredicate(e)) 162 | return e; 163 | } 164 | } 165 | 166 | bool eof() const override 167 | { 168 | return m_eof; 169 | } 170 | 171 | ~BinpackSfenInputStream() override {} 172 | 173 | private: 174 | std::unique_ptr m_stream; 175 | std::string m_filename; 176 | bool m_eof; 177 | bool m_cyclic; 178 | std::function m_skipPredicate; 179 | }; 180 | 181 | struct BinpackSfenInputParallelStream : BasicSfenInputStream 182 | { 183 | static constexpr auto openmode = std::ios::in | std::ios::binary; 184 | static inline const std::string extension = "binpack"; 185 | 186 | BinpackSfenInputParallelStream(int concurrency, const std::vector& filenames, bool cyclic, std::function skipPredicate) : 187 | m_stream(std::make_unique(concurrency, filenames, openmode, cyclic, skipPredicate)), 188 | m_filenames(filenames), 189 | m_concurrency(concurrency), 190 | m_eof(false), 191 | m_cyclic(cyclic), 192 | m_skipPredicate(skipPredicate) 193 | { 194 | } 195 | 196 | std::optional next() override 197 | { 198 | // filtering is done a layer deeper. 199 | auto v = m_stream->next(); 200 | if (!v.has_value()) 201 | { 202 | m_eof = true; 203 | return std::nullopt; 204 | } 205 | 206 | return v; 207 | } 208 | 209 | void fill(std::vector& v, std::size_t n) override 210 | { 211 | auto k = m_stream->fill(v, n); 212 | if (n != k) 213 | { 214 | m_eof = true; 215 | } 216 | } 217 | 218 | bool eof() const override 219 | { 220 | return m_eof; 221 | } 222 | 223 | ~BinpackSfenInputParallelStream() override {} 224 | 225 | private: 226 | std::unique_ptr m_stream; 227 | std::vector m_filenames; 228 | int m_concurrency; 229 | bool m_eof; 230 | bool m_cyclic; 231 | std::function m_skipPredicate; 232 | }; 233 | 234 | inline std::unique_ptr open_sfen_input_file(const std::string& filename, bool cyclic, std::function skipPredicate = nullptr) 235 | { 236 | if (has_extension(filename, BinSfenInputStream::extension)) 237 | return std::make_unique(filename, cyclic, std::move(skipPredicate)); 238 | else if (has_extension(filename, BinpackSfenInputStream::extension)) 239 | return std::make_unique(filename, cyclic, std::move(skipPredicate)); 240 | 241 | return nullptr; 242 | } 243 | 244 | inline std::unique_ptr open_sfen_input_file_parallel(int concurrency, const std::vector& filenames, bool cyclic, std::function skipPredicate = nullptr) 245 | { 246 | // TODO (low priority): optimize and parallelize .bin reading. 247 | if (has_extension(filenames[0], BinSfenInputStream::extension)) 248 | return std::make_unique(filenames[0], cyclic, std::move(skipPredicate)); 249 | else if (has_extension(filenames[0], BinpackSfenInputParallelStream::extension)) 250 | return std::make_unique(concurrency, filenames, cyclic, std::move(skipPredicate)); 251 | 252 | return nullptr; 253 | } 254 | } 255 | 256 | #endif 257 | -------------------------------------------------------------------------------- /lib/rng.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace rng 6 | { 7 | inline auto& get_thread_local_rng() 8 | { 9 | static thread_local std::mt19937_64 s_rng(std::random_device{}()); 10 | return s_rng; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /perf_sigmoid_fitter.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | import numpy as np 3 | from scipy.optimize import curve_fit 4 | import sys 5 | import glob 6 | import nnue_dataset 7 | import torch 8 | import sys 9 | import random 10 | 11 | def sigmoid(x, k): 12 | y = 1 / (1 + np.exp(-k*x)) 13 | return (y) 14 | 15 | def fit_data(x, y, sigma): 16 | # 1/361 is the initial guess. It's good enough to find the solution 17 | p0 = [1/361] 18 | popt, pcov = curve_fit(sigmoid, x, y, p0, sigma, method='dogbox') 19 | return popt[0] 20 | 21 | def do_plot(data, filename): 22 | # plot of the eval distribution 23 | fig, axs = plt.subplots(2) 24 | fig.tight_layout(pad=2.0) 25 | fig.suptitle(filename) 26 | x = list(data.keys()) 27 | y = [data[k][1] for k in x] 28 | x, y = zip(*list(sorted(zip(x, y), key=lambda x:x[0]))) 29 | axs[0].plot(x, y) 30 | axs[0].set_ylabel('density') 31 | axs[0].set_xlabel('eval') 32 | axs[0].set_xscale('symlog') 33 | 34 | # plot of the perf% by eval and the fitted sigmoid 35 | x = list(data.keys()) 36 | y = [data[k][0] / data[k][1] for k in x] 37 | # sigma is uncertainties, we con't care how correct it is. 38 | # The inverted counts are good enough. 39 | sigma = [1 / data[k][1] for k in x] 40 | k = fit_data(x, y, sigma) 41 | print('k: ', k) 42 | print('inv k: ', 1/k) 43 | axs[1].scatter(x, y, label='perf') 44 | y = [sigmoid(xx, k) for xx in x] 45 | axs[1].scatter(x, y, label='sigmoid(x/{})'.format(1.0/k)) 46 | axs[1].legend(loc="upper left") 47 | axs[1].set_ylabel('perf') 48 | axs[1].set_xlabel('eval') 49 | 50 | # save to a .png file 51 | plot_filename = '.'.join(filename.split('.')[:-1]) + '.png' 52 | plt.savefig(plot_filename) 53 | print('plot saved at {}'.format(plot_filename)) 54 | 55 | def gather_statistics_from_batches(batches, bucket_size): 56 | ''' 57 | This function takes an iterable of training batches and a bucket_size. 58 | It goes through all batches and collects evals and the outcomes. 59 | The evals are bucketed by bucket_size. Perf% is computed based on the 60 | evals and corresponding game outcomes. 61 | The result is a dictionary of the form { eval : (perf%, count) } 62 | ''' 63 | data = dict() 64 | i = 0 65 | for batch in batches: 66 | us, them, white_indices, white_values, black_indices, black_values, outcome, score, psqt_indices, layer_stack_indices = batch 67 | batch_size = len(us) 68 | bucket = torch.round(score / bucket_size) * bucket_size 69 | perf = outcome 70 | for b, p in zip(bucket, perf): 71 | bucket_id = int(b) 72 | pp = float(p) 73 | if bucket_id in data: 74 | t = data[bucket_id] 75 | data[bucket_id] = (t[0] + pp, t[1] + 1) 76 | else: 77 | data[bucket_id] = (pp, 1) 78 | i += batch_size 79 | print('Loaded {} positions...'.format(i)) 80 | return data 81 | 82 | def gather_statistics_from_data(filename, count, bucket_size): 83 | ''' 84 | Takes a .bin or .binpack file and produces perf% statistics 85 | The result is a dictionary of the form { eval : (perf%, count) } 86 | ''' 87 | batch_size = 8192 88 | cyclic = True 89 | smart_fen_skipping = True 90 | # we pass whatever feature set because we have to pass something 91 | # it doesn't actually matter, all we care about are the scores and outcomes 92 | # this is just the easiest way to do it 93 | dataset = nnue_dataset.SparseBatchDataset('HalfKP', filename, batch_size, cyclic, smart_fen_skipping) 94 | batches = iter(dataset) 95 | num_batches = (count + batch_size - 1) // batch_size 96 | data = gather_statistics_from_batches((next(batches) for i in range(num_batches)), bucket_size) 97 | return data 98 | 99 | def show_help(): 100 | print('Usage: python perf_sigmoid_fitter.py filename [count] [bucket_size]') 101 | print('count is the number of positions. Default: 1000000') 102 | print('bucket_size determines how the evals are bucketed. Default: 16') 103 | print('') 104 | print('This file can be used as a module') 105 | print('The function `gather_statistics_from_batches` can be used to determine') 106 | print('the sigmoid scaling factor for each batch during training') 107 | 108 | def main(): 109 | filename = sys.argv[1] 110 | count = 1000000 if len(sys.argv) < 3 else int(sys.argv[2]) 111 | bucket_size = 16 if len(sys.argv) < 4 else int(sys.argv[3]) 112 | data = gather_statistics_from_data(filename, count, bucket_size) 113 | do_plot(data, filename) 114 | 115 | if __name__ == '__main__': 116 | if len(sys.argv) <= 1: 117 | show_help() 118 | else: 119 | main() 120 | -------------------------------------------------------------------------------- /ranger.py: -------------------------------------------------------------------------------- 1 | # Ranger deep learning optimizer - RAdam + Lookahead + Gradient Centralization, combined into one optimizer. 2 | 3 | # https://github.com/lessw2020/Ranger-Deep-Learning-Optimizer 4 | # and/or 5 | # https://github.com/lessw2020/Best-Deep-Learning-Optimizers 6 | 7 | # Ranger has been used to capture 12 records on the FastAI leaderboard. 8 | 9 | # This version = 2020.9.4 10 | 11 | 12 | # Credits: 13 | # Gradient Centralization --> https://arxiv.org/abs/2004.01461v2 (a new optimization technique for DNNs), github: https://github.com/Yonghongwei/Gradient-Centralization 14 | # RAdam --> https://github.com/LiyuanLucasLiu/RAdam 15 | # Lookahead --> rewritten by lessw2020, but big thanks to Github @LonePatient and @RWightman for ideas from their code. 16 | # Lookahead paper --> MZhang,G Hinton https://arxiv.org/abs/1907.08610 17 | 18 | # summary of changes: 19 | # 9/4/20 - updated addcmul_ signature to avoid warning. Integrates latest changes from GC developer (he did the work for this), and verified on performance on private dataset. 20 | # 4/11/20 - add gradient centralization option. Set new testing benchmark for accuracy with it, toggle with use_gc flag at init. 21 | # full code integration with all updates at param level instead of group, moves slow weights into state dict (from generic weights), 22 | # supports group learning rates (thanks @SHolderbach), fixes sporadic load from saved model issues. 23 | # changes 8/31/19 - fix references to *self*.N_sma_threshold; 24 | # changed eps to 1e-5 as better default than 1e-8. 25 | 26 | import math 27 | import torch 28 | from torch.optim.optimizer import Optimizer, required 29 | 30 | # If dim is None it will be chosen automatically 31 | def centralized_gradient(x, use_gc=True, gc_conv_only=False, dim=None): 32 | '''credit - https://github.com/Yonghongwei/Gradient-Centralization ''' 33 | if use_gc: 34 | dim_threshold = 3 if gc_conv_only else 1 35 | if len(list(x.size())) > dim_threshold: 36 | x.add_(-x.mean(dim=(dim or tuple(range(1, len(list(x.size()))))), keepdim=True)) 37 | return x 38 | 39 | 40 | class Ranger(Optimizer): 41 | 42 | def __init__(self, params, lr=1e-3, # lr 43 | alpha=0.5, k=6, N_sma_threshhold=5, # Ranger options 44 | betas=(.95, 0.999), eps=1e-5, weight_decay=0, # Adam options 45 | # Gradient centralization on or off, applied to conv layers only or conv + fc layers 46 | use_gc=True, gc_conv_only=False, gc_loc=True 47 | ): 48 | 49 | # parameter checks 50 | if not 0.0 <= alpha <= 1.0: 51 | raise ValueError(f'Invalid slow update rate: {alpha}') 52 | if not 1 <= k: 53 | raise ValueError(f'Invalid lookahead steps: {k}') 54 | if not lr > 0: 55 | raise ValueError(f'Invalid Learning Rate: {lr}') 56 | if not eps > 0: 57 | raise ValueError(f'Invalid eps: {eps}') 58 | 59 | # parameter comments: 60 | # beta1 (momentum) of .95 seems to work better than .90... 61 | # N_sma_threshold of 5 seems better in testing than 4. 62 | # In both cases, worth testing on your dataset (.90 vs .95, 4 vs 5) to make sure which works best for you. 63 | 64 | # prep defaults and init torch.optim base 65 | defaults = dict(lr=lr, alpha=alpha, k=k, step_counter=0, betas=betas, 66 | N_sma_threshhold=N_sma_threshhold, eps=eps, weight_decay=weight_decay, 67 | gc_dim=None) 68 | super().__init__(params, defaults) 69 | 70 | # adjustable threshold 71 | self.N_sma_threshhold = N_sma_threshhold 72 | 73 | # look ahead params 74 | 75 | self.alpha = alpha 76 | self.k = k 77 | 78 | # radam buffer for state 79 | self.radam_buffer = [[None, None, None] for ind in range(10)] 80 | 81 | # gc on or off 82 | self.gc_loc = gc_loc 83 | self.use_gc = use_gc 84 | self.gc_conv_only = gc_conv_only 85 | # level of gradient centralization 86 | #self.gc_gradient_threshold = 3 if gc_conv_only else 1 87 | 88 | print( 89 | f"Ranger optimizer loaded. \nGradient Centralization usage = {self.use_gc}") 90 | if (self.use_gc and self.gc_conv_only == False): 91 | print(f"GC applied to both conv and fc layers") 92 | elif (self.use_gc and self.gc_conv_only == True): 93 | print(f"GC applied to conv layers only") 94 | 95 | def __setstate__(self, state): 96 | print("set state called") 97 | super(Ranger, self).__setstate__(state) 98 | 99 | def step(self, closure=None): 100 | loss = None 101 | if closure is not None: 102 | with torch.enable_grad(): 103 | loss = closure() 104 | 105 | # Evaluate averages and grad, update param tensors 106 | for group in self.param_groups: 107 | 108 | for p in group['params']: 109 | if p.grad is None: 110 | continue 111 | grad = p.grad.data.float() 112 | 113 | if grad.is_sparse: 114 | raise RuntimeError( 115 | 'Ranger optimizer does not support sparse gradients') 116 | 117 | p_data_fp32 = p.data.float() 118 | 119 | state = self.state[p] # get state dict for this param 120 | 121 | if len(state) == 0: # if first time to run...init dictionary with our desired entries 122 | # if self.first_run_check==0: 123 | # self.first_run_check=1 124 | #print("Initializing slow buffer...should not see this at load from saved model!") 125 | state['step'] = 0 126 | state['exp_avg'] = torch.zeros_like(p_data_fp32) 127 | state['exp_avg_sq'] = torch.zeros_like(p_data_fp32) 128 | 129 | # look ahead weight storage now in state dict 130 | state['slow_buffer'] = torch.empty_like(p.data) 131 | state['slow_buffer'].copy_(p.data) 132 | 133 | else: 134 | state['exp_avg'] = state['exp_avg'].type_as(p_data_fp32) 135 | state['exp_avg_sq'] = state['exp_avg_sq'].type_as( 136 | p_data_fp32) 137 | 138 | # begin computations 139 | exp_avg, exp_avg_sq = state['exp_avg'], state['exp_avg_sq'] 140 | beta1, beta2 = group['betas'] 141 | 142 | # GC operation for Conv layers and FC layers 143 | # if grad.dim() > self.gc_gradient_threshold: 144 | # grad.add_(-grad.mean(dim=tuple(range(1, grad.dim())), keepdim=True)) 145 | if self.gc_loc: 146 | grad = centralized_gradient(grad, use_gc=self.use_gc, gc_conv_only=self.gc_conv_only, dim=group['gc_dim']) 147 | 148 | state['step'] += 1 149 | 150 | # compute variance mov avg 151 | exp_avg_sq.mul_(beta2).addcmul_(grad, grad, value=1 - beta2) 152 | 153 | # compute mean moving avg 154 | exp_avg.mul_(beta1).add_(grad, alpha=1 - beta1) 155 | 156 | buffered = self.radam_buffer[int(state['step'] % 10)] 157 | 158 | if state['step'] == buffered[0]: 159 | N_sma, step_size = buffered[1], buffered[2] 160 | else: 161 | buffered[0] = state['step'] 162 | beta2_t = beta2 ** state['step'] 163 | N_sma_max = 2 / (1 - beta2) - 1 164 | N_sma = N_sma_max - 2 * \ 165 | state['step'] * beta2_t / (1 - beta2_t) 166 | buffered[1] = N_sma 167 | if N_sma > self.N_sma_threshhold: 168 | step_size = math.sqrt((1 - beta2_t) * (N_sma - 4) / (N_sma_max - 4) * ( 169 | N_sma - 2) / N_sma * N_sma_max / (N_sma_max - 2)) / (1 - beta1 ** state['step']) 170 | else: 171 | step_size = 1.0 / (1 - beta1 ** state['step']) 172 | buffered[2] = step_size 173 | 174 | # if group['weight_decay'] != 0: 175 | # p_data_fp32.add_(-group['weight_decay'] 176 | # * group['lr'], p_data_fp32) 177 | 178 | # apply lr 179 | if N_sma > self.N_sma_threshhold: 180 | denom = exp_avg_sq.sqrt().add_(group['eps']) 181 | G_grad = exp_avg / denom 182 | else: 183 | G_grad = exp_avg 184 | 185 | if group['weight_decay'] != 0: 186 | G_grad.add_(p_data_fp32, alpha=group['weight_decay']) 187 | # GC operation 188 | if self.gc_loc == False: 189 | G_grad = centralized_gradient(G_grad, use_gc=self.use_gc, gc_conv_only=self.gc_conv_only, dim=group['gc_dim']) 190 | 191 | p_data_fp32.add_(G_grad, alpha=-step_size * group['lr']) 192 | 193 | p.data.copy_(p_data_fp32) 194 | 195 | # integrated look ahead... 196 | # we do it at the param level instead of group level 197 | if state['step'] % group['k'] == 0: 198 | # get access to slow param tensor 199 | slow_p = state['slow_buffer'] 200 | # (fast weights - slow weights) * alpha 201 | slow_p.add_(p.data - slow_p, alpha=self.alpha) 202 | # copy interpolated weights to RAdam param tensor 203 | p.data.copy_(slow_p) 204 | 205 | return loss 206 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | psutil 2 | asciimatics 3 | GPUtil 4 | python-chess==0.31.4 5 | matplotlib 6 | --find-links https://download.pytorch.org/whl/torch_stable.html 7 | torch==2.0.1+cu118 8 | pytorch-lightning==1.9.5 9 | tensorboard 10 | cupy-cuda11x 11 | numba -------------------------------------------------------------------------------- /scripts/easy_train_example.bat: -------------------------------------------------------------------------------- 1 | python easy_train.py ^ 2 | --training-dataset=c:/dev/nnue-pytorch/noob_master_leaf_static_d12_85M_0.binpack ^ 3 | --training-dataset=c:/dev/nnue-pytorch/d8_100000.binpack ^ 4 | --training-dataset=c:/dev/nnue-pytorch/10m_d3_2.binpack ^ 5 | --num-workers=1 ^ 6 | --threads=1 ^ 7 | --gpus="0," ^ 8 | --runs-per-gpu=1 ^ 9 | --batch-size=1024 ^ 10 | --max_epoch=10 ^ 11 | --do-network-training=True ^ 12 | --do-network-testing=True ^ 13 | --tui=True ^ 14 | --network-save-period=1 ^ 15 | --random-fen-skipping=3 ^ 16 | --start-lambda=1.0 ^ 17 | --end-lambda=0.75 ^ 18 | --fail-on-experiment-exists=False ^ 19 | --build-engine-arch=x86-64-modern ^ 20 | --build-threads=1 ^ 21 | --epoch-size=1048500 ^ 22 | --validation-size=4096 ^ 23 | --network-testing-threads=2 ^ 24 | --network-testing-explore-factor=1.5 ^ 25 | --network-testing-book="https://github.com/official-stockfish/books/raw/master/UHO_Lichess_4852_v1.epd.zip" ^ 26 | --network-testing-nodes-per-move=1000 ^ 27 | --network-testing-hash-mb=8 ^ 28 | --network-testing-games-per-round=200 ^ 29 | --engine-base-branch=official-stockfish/Stockfish/master ^ 30 | --engine-test-branch=official-stockfish/Stockfish/master ^ 31 | --nnue-pytorch-branch=Sopel97/nnue-pytorch/easy_train ^ 32 | --workspace-path=./easy_train_data ^ 33 | --experiment-name=test ^ 34 | --resume-training=True ^ 35 | --additional-training-arg="--auto_lr_find=False" ^ 36 | --additional-training-arg="--detect_anomaly=False" ^ 37 | --features="HalfKAv2_hm%^" ^ 38 | -------------------------------------------------------------------------------- /scripts/easy_train_example.sh: -------------------------------------------------------------------------------- 1 | python easy_train.py \ 2 | --training-dataset=/home/vondele/chess/vondele/gensfen/gensfen_2021_09_02/nodes5000pv2_UHO.binpack \ 3 | --validation-dataset=/home/vondele/chess/vondele/gensfen/gensfen_2021_09_02/nodes5000pv2_UHO.binpack \ 4 | --num-workers=4 \ 5 | --threads=2 \ 6 | --gpus="0," \ 7 | --runs-per-gpu=2 \ 8 | --batch-size=16384 \ 9 | --max_epoch=10 \ 10 | --do-network-training=True \ 11 | --do-network-testing=True \ 12 | --tui=True \ 13 | --network-save-period=1 \ 14 | --random-fen-skipping=3 \ 15 | --start-lambda=1.0 \ 16 | --end-lambda=0.75 \ 17 | --fail-on-experiment-exists=False \ 18 | --build-engine-arch=x86-64-modern \ 19 | --build-threads=2 \ 20 | --epoch-size=1638400 \ 21 | --validation-size=16384 \ 22 | --network-testing-threads=24 \ 23 | --network-testing-explore-factor=1.5 \ 24 | --network-testing-book="https://github.com/official-stockfish/books/raw/master/UHO_Lichess_4852_v1.epd.zip" \ 25 | --network-testing-nodes-per-move=20000 \ 26 | --network-testing-hash-mb=8 \ 27 | --network-testing-games-per-round=200 \ 28 | --engine-base-branch=official-stockfish/Stockfish/master \ 29 | --engine-test-branch=official-stockfish/Stockfish/master \ 30 | --nnue-pytorch-branch=vondele/nnue-pytorch/easy_train \ 31 | --workspace-path=./easy_train_data \ 32 | --experiment-name=test \ 33 | --additional-training-arg="--auto_lr_find=False" \ 34 | --additional-training-arg="--detect_anomaly=False" \ 35 | --features="HalfKAv2_hm^" 36 | -------------------------------------------------------------------------------- /scripts/gensfen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Using this commit: 4 | # https://github.com/Sopel97/Stockfish.git 5 | # commit d7d4ec211f7ef35ff39fe8aea54623a468b36c7d 6 | 7 | DEPTH=5 8 | GAMES=128000000 9 | SEED=$RANDOM 10 | 11 | options=" 12 | uci 13 | setoption name PruneAtShallowDepth value false 14 | setoption name Use NNUE value pure 15 | setoption name Threads value 250 16 | setoption name Hash value 10240 17 | setoption name SyzygyPath value /dev/shm/vjoost/3-4-5-6/WDL/:/dev/shm/vjoost/3-4-5-6/DTZ/ 18 | isready 19 | gensfen set_recommended_uci_options ensure_quiet random_multi_pv 4 random_multi_pv_diff 50 random_move_count 8 random_move_maxply 20 write_minply 5 eval_limit 1000 seed $SEED depth $DEPTH loop $GAMES output_file_name d${DEPTH}_${GAMES}_${SEED}" 20 | 21 | printf "$options" | ./stockfish 22 | -------------------------------------------------------------------------------- /scripts/rename_net.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Renames file to SF net format. 4 | name=nn-$(sha256sum $1 | cut -c1-12).nnue 5 | echo ${name} 6 | mv $1 ${name} 7 | -------------------------------------------------------------------------------- /scripts/train.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | python train.py \ 4 | ../data/large_gensfen_multipvdiff_100_d9.binpack \ 5 | ../data/large_gensfen_multipvdiff_100_d9.binpack \ 6 | --gpus 1 \ 7 | --threads 2 \ 8 | --batch-size 8096 \ 9 | --progress_bar_refresh_rate 20 \ 10 | --smart-fen-skipping \ 11 | --random-fen-skipping 10 \ 12 | --features=HalfKP^ \ 13 | --lambda=1.0 \ 14 | --max_epochs=300 15 | -------------------------------------------------------------------------------- /visualize_multi_hist.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import chess 3 | import features 4 | import model as M 5 | import numpy as np 6 | import torch 7 | import matplotlib.pyplot as plt 8 | from matplotlib.gridspec import GridSpec 9 | 10 | from serialize import NNUEReader 11 | 12 | def load_model(filename, feature_set): 13 | if filename.endswith(".pt") or filename.endswith(".ckpt"): 14 | if filename.endswith(".pt"): 15 | model = torch.load(filename) 16 | else: 17 | model = M.NNUE.load_from_checkpoint( 18 | filename, feature_set=feature_set) 19 | model.eval() 20 | elif filename.endswith(".nnue"): 21 | with open(filename, 'rb') as f: 22 | reader = NNUEReader(f, feature_set) 23 | model = reader.model 24 | else: 25 | raise Exception("Invalid filetype: " + str(filename)) 26 | 27 | return model 28 | 29 | def get_bins(inputs_columns, num_bins): 30 | a = float('+inf') 31 | b = float('-inf') 32 | for inputs in inputs_columns: 33 | for inp in inputs: 34 | a = min(a, float(np.min(inp))) 35 | b = max(b, float(np.max(inp))) 36 | a -= 0.001 37 | b += 0.001 38 | return [a + (b-a) / num_bins * i for i in range(num_bins+1)] 39 | 40 | def plot_hists(tensors_columns, row_names, col_names, w=8.0, h=3.0, title=None, num_bins=256, filename='a.png'): 41 | fig, axs = plt.subplots(len(tensors_columns[0]), len(tensors_columns), sharex=True, sharey=True, squeeze=False, figsize=(w * len(tensors_columns), h * len(tensors_columns[0])), dpi=100) 42 | if title: 43 | fig.suptitle(title) 44 | bins = get_bins(tensors_columns, num_bins) 45 | for i, tensors in enumerate(tensors_columns): 46 | print('Processing column {}/{}.'.format(i+1, len(tensors_columns))) 47 | for j, tensor in enumerate(tensors): 48 | ax = axs[j, i] 49 | print(' Processing tensor {}/{}.'.format(j+1, len(tensors))) 50 | ax.hist(tensor, log=True, bins=bins) 51 | if i == 0 and row_names[j]: 52 | ax.set_ylabel(row_names[j]) 53 | if j == 0 and col_names[i]: 54 | ax.set_xlabel(col_names[i]) 55 | ax.xaxis.set_label_position('top') 56 | fig.savefig(filename) 57 | 58 | def main(): 59 | parser = argparse.ArgumentParser( 60 | description="Visualizes networks in ckpt, pt and nnue format.") 61 | parser.add_argument( 62 | "models", nargs='+', help="Source model (can be .ckpt, .pt or .nnue)") 63 | parser.add_argument( 64 | "--dont-show", action="store_true", 65 | help="Don't show the plots.") 66 | features.add_argparse_args(parser) 67 | args = parser.parse_args() 68 | 69 | supported_features = ('HalfKAv2', 'HalfKAv2^', 'HalfKAv2_hm', 'HalfKAv2_hm^') 70 | assert args.features in supported_features 71 | feature_set = features.get_feature_set_from_name(args.features) 72 | 73 | from os.path import basename 74 | labels = [] 75 | for m in args.models: 76 | label = basename(m) 77 | if label.startswith('nn-'): 78 | label = label[3:] 79 | if label.endswith('.nnue'): 80 | label = label[:-5] 81 | labels.append('\n'.join(label.split('-'))) 82 | 83 | models = [load_model(m, feature_set) for m in args.models] 84 | 85 | coalesced_ins = [M.coalesce_ft_weights(model, model.input) for model in models] 86 | input_weights = [coalesced_in[:, :M.L1].flatten().numpy() for coalesced_in in coalesced_ins] 87 | input_weights_psqt = [(coalesced_in[:, M.L1:] * 600).flatten().numpy() for coalesced_in in coalesced_ins] 88 | plot_hists([input_weights], labels, [None], w=10.0, h=3.0, num_bins=8*128, title='Distribution of feature transformer weights among different nets', filename='input_weights_hist.png') 89 | plot_hists([input_weights_psqt], labels, [None], w=10.0, h=3.0, num_bins=8*128, title='Distribution of feature transformer PSQT weights among different nets (in stockfish internal units)', filename='input_weights_psqt_hist.png') 90 | 91 | layer_stacks = [model.layer_stacks for model in models] 92 | layers_l1 = [[] for i in range(layer_stacks[0].count)] 93 | layers_l2 = [[] for i in range(layer_stacks[0].count)] 94 | layers_l3 = [[] for i in range(layer_stacks[0].count)] 95 | for ls in layer_stacks: 96 | for i, sublayers in enumerate(ls.get_coalesced_layer_stacks()): 97 | l1, l2, l3 = sublayers 98 | layers_l1[i].append(l1.weight.flatten().numpy()) 99 | layers_l2[i].append(l2.weight.flatten().numpy()) 100 | layers_l3[i].append(l3.weight.flatten().numpy()) 101 | col_names = ['Subnet {}'.format(i) for i in range(layer_stacks[0].count)] 102 | plot_hists(layers_l1, labels, col_names, w=2.0, h=2.0, num_bins=128, title='Distribution of l1 weights among different nets and buckets', filename='l1_weights_hist.png') 103 | plot_hists(layers_l2, labels, col_names, w=2.0, h=2.0, num_bins=32, title='Distribution of l2 weights among different nets and buckets', filename='l2_weights_hist.png') 104 | plot_hists(layers_l3, labels, col_names, w=2.0, h=2.0, num_bins=16, title='Distribution of output weights among different nets and buckets', filename='output_weights_hist.png') 105 | 106 | if not args.dont_show: 107 | plt.show() 108 | 109 | if __name__ == '__main__': 110 | main() 111 | --------------------------------------------------------------------------------