├── .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 | 7V1bk6M2Fv41XZU82AVCwvDY3ZPe2drO1lSmdpM8bWFQt9nBxovpW379ShiwkQQIc1UbJzVtZCFs9J3vHJ2LuDHut+9/i5z95tfQw8EN0Lz3G+PLDQCWbpB/acPHsQEa6NjwHPnesUk/NXz3/8Jpo5a2vvgePhQ6xmEYxP6+2OiGux1240KbE0XhW7HbUxgUr7p3njHX8N11Ar71d9+LN+nPQtqp/Sv2nzfZlXUt/WTrZJ3ThsPG8cK3sybjlxvjPgrD+Phu+36PA3rvsvtyPO+h5NP8i0V4F8uc8PKXrn08/vPxFf7+8Bb+Hd/9B/2+QOn0vDrBS/qLb4AZkAHv1hF590zfZS2e/0p/R/yR3hzzfy/0y989hbt4cUim7pZ0sPfvp8+yMb46wdM/bl/z0cn3TIbr7gphvMERPZsA5obei9PPuHDEPY4OewIq/xV3MFr1Lz+/26BwDYA9gsj0MIziTfgc7pzgl1PrXRS+7DxM51kjR4cfOHY36cF/X7b77ORduKO9T2M8huGetOu0H47jj1T8nJc4JE2beBukn+J3P/7j7P2fdPQlSo++vKcXSw4+soNdHH38cepID/88/+x0WnKUnZfLCT0InDUO7hz3x3PyG+/DIIxOv4Te9vQ72/TMOAp/4KzPDTC05JXfU3ojS4UnbTqEL5GLKyTGWKUs5ETPOK7qaOdCTsgRh1tMfiU5MWXGhbaEx5E+iowX4cBJIFegspTGnvNx8qG/hT75Dadxs3E+mONsiPDp6UC++BlVkDdn3/DUlBBIEzKBA5FJNgbt2yUjHceTEdfRv33KckRQiaoJuyYohW5EbyQtfw/U4O6MoGfuluBuqxl3Lzoi7/G4O7s1Be5mwExmd0/f+tvEaD4H2SuOYp9YzbeB/7wjbTEFZ976SJHwLTz4sR/ST9dhHIfbcoiQ6X9KXqRLcrHbRMRTXDnZwZP/TgXnLv0+XzZxTFcFt/TmgAfX2+lLn6wLnnwiYNHSJVck4uvEDvlD2w/kr7PzIj/0Fs5+T44Mel/dDT4c1qETeQsdWMv97pmX0nOYPwfO4SAS3/SGknuA36uhWwo0AxUBYcD0+O20FslXHJvzdYimlWOvgJ2mQLGM3kkuaTpSA2rJeTnP/Xn2SR3nFRjvRIADct7xk2zBqXfMgpYkC0JNDM5mJHcbRc7HWYc9pbxDOQcaDAlCm1nXMv2B2a4/0hEjFMdv3C29Why9QqQhk5OloqS8bfwYf987yXy+Rc6+CHM5HmJQ56QU7RLgkAUz+dwPgjMQeghbHhTB0wJrwzS7YTbAMBs0BcwGBMwG+iK2lT0Tm+LEZssSWwk2+yW2jJhkiQrCdv2HITZ7JrYCsUFjasQGATdFwIQTnCBi8APXFU2QZ65N1JXmsRgxQZQK2Skyh5whYK4+ve4ZwpGgg/GUDzQllY++amtWt2MDk2MDpPNr/WthA4NhAxPZAjawBuVrfq1wzXwN2fUa4hXqoGyNdG5+9CnOjuVi8eysLURstI7kBxRnxwYCc2fQZZxKqlTVeGq/mlST1KRHw7btMo53QFmoSDjpOiof4vgL0rNOcG3s6Kp2RNX3h9XrQdaRxvTvZz1o8VQ4i59a4pelWdWKX0mQrKX4IXMY8UPV7pL6/jXix7p7BhG/7DsKUjAkI/W6JoqLP/o77ESdpAKIL5A4jRbkyNlS4yhpS6xe8RUZiskik4cY07P3OPLJnaRWUdL07XRcZ4olMcVsOclTU1NDTddEltrh7EPWSeVg60lotZmuhddPHflAVgw2Nd5qE615jN7WPLxNPeN2xi23FkQTw62u88AFk/R+D+RO0RjlbfMTBIdcDuqA96YQ+0EhI7WJu3UYY7MsoLiqiyh2aKXqmc+y3t8KUCVkF+SLa7ZRgG1X+VxslHulMbbs8du3tmUBs2Q12VoANs+M0f9N+690s9yWPZ3df9paDoMz8bauln3Bqp59Bw096jqfVnjN6hGyrqXx1SMfv4/JlWcF2YeCBIMqSNk0PxkFucqSCHpXkAzSO1KQkHEqmUaNwmPDgk3723AqCpK3f69XQUI0OQXJ8y9PvTvvlpaynrjJcw6bZLr0iqlpR29m8mpAb3oNt4npOmdoIV2zIEH0v255Uj5v42jL8Ng6ww4SYAd1w5SQCbSaFoPJErc4PxBr+7POkRLK7YyRsh+iJOT1SyCvz4C/APAGXNqrIlRXl2LeFq93h8O8yL/9qTGvLs2XmBAz6pujnnfOqIP6y4ybGfPjYp6tmBwa8xAKIH7mOsHBOnxTzWsyUO7Lg7P1A3ruVxy8Ylo5XBRlq6EbpkNR0oEhKUuwddVkS4cev6PP9a73AbNoWglyh4dd7xv8ZEzXszrnx5W7S6Ucq0ZLKijRcRAsi34sLq4orS41Y6npjIzoS1NKZzaPUoqjoaWV2mz/mow7YFT27yfjLodDWerSMfXnlvAY3aWCYO60Yc1BIjHp2PRTkrOgLSjsaHjm5xtKUQ/k3/xi/BZLc/JRp8lHOrucsnhlIko+Qv0pE5U9aeqvr+RzUMoqFYZaX4mDX60XV4j1QfftUDBUdqNpS6AVXGna0rbtGuCXwTjpdUbRF/qbr1AaAJuXb18oDcBmgomsz6J3aVDavXaJNHSnBoTy06k0SJfRjCoNhtaRNHCZv4NLg2grY1WkQf2wojKAhzbrbL4c8xAtNVAcyyTDWwMjX7TvrirI/wzLAOkN9UoqJgdaBrD1w7aWo7dxaJEt0lzxY/UOe5HjRRnYf/bFgCIywRZCtpGJFdEG0D69xjaI+J1+VJIP3TY/8/JAdivqkeWD3SztM8mHxJbXk5WPRguGUpSPqyLUEAEEOloh5xtZjGgzQcRDfHrh50mHki9JoOlQbCCSzepCnexyyy96QRHGNluyUyIPnWFY8MyqR+cDR99jMtscvA9v/jZwdgIcNw5LAtoxcjyfTBqDJy5aWQ2/46dh5OGIBWaRqjXtgbxuOolfmiyRrUQbnwLR4wQAq5sFMPkNu7Gzew7wmYdcHI8/v5wtuppRvJgTkHu6c2J8R2/loR9Q8T6UzL3+KfOmqkSrFEEWdZ1pp1exaH70WirEOwTmOWw2h7ogp2HYSVRqK+QmCXLT2dK4Wq3WG+5HC6Rzw0JnQlGc0ukoDU1n/KDAqE5D4/qbQ6SVIZH3RoUdsQzAbIelN3sum3jUOaftwpy2i5QEqlQSOfuPtusWUklJXNWzWlrrFunk6p60EFlgoyUD+AUEF+ZXL6DFj2awHqSOFJtdLbV6dfY0gj2eXVSxfalMfqsLjiaoSOxLiTN9irizzrprHREqYG4PKGYgiB7rJ3Jp9sioon0oVDA3kM7aGwaYDY7pGRz1PvxhTYjMqD8D/H3g7/fY+w0//ktQVIHuyBn3FC836MuMg45xkI6zGtuyND/r/vVD73zWLHQzgGWZCbyEZQl7six1jbMFkdHPjqD6qtIYRDUuj+qzrWpbsNoOtYbwnphAVVMwL+oazxY0RZnUKtiCnOtpNgUnaAIcZXNKpiAf+5tNwfFwMBlTUIlcmdnJeIGTEcqagmYnOzh068jLNlAsNd7sagOj5elwCFeeye8RPl3Zu6ptUlovw6RlD5VsS92v7NUsfRj4Nz591WbpxJzdl+wpsUXRoDqscRbGmVrWivSgWbCaIMjBBYUHrVWi7P5maaSg++wPJllQco+KxuKtCS9TWuPfpjcaQFPmkclpS6uoMGdSmnLMR8fnT9epFT6wapvT3e4pfWA2y64HbIZZsv/3UGCbk3yuB2wQtA26tHP2ZF+0shgwCPz9Ibm9bJVG4WYycyHptaue0MqiFM6nlwL0ZDu2e2oK8yDxlc675gZNEoeCtP6ZGtSihtSUkaKGtk64VmDLv+hcb3aMiwaBQ1jQu5BMDJZMBFsyyxSFyVWgISZ3X1SCJlfxNkARGljxUevrK2AqXeMoUYQGVnzwfJ5DxYrQCE0oZF98oiK0XNdKmA+dRAfmIrRqOajZ21ydTKC5CO1magkgdUpi4kVoYKWSM3S6i9ChYtRNdItsMKwvLTQXofVx9hBFaGAlsatYT5nHdYQ69SI0sBIV8KlgbsxFaGoYHPUPtB3YhOCrLufM4/FwMJHM4/xqSliWcxFaE8vSlrYsQU+W5VyExp3ckyloSTytaJqm4PhFaMDiYyFq2IJzEZoKJoBV/8TrgXU+HzeaTcHxcDAZU1ClSNTsZGxiCgJZU9DqxBSci9Aay55KqfXTzXaeoOxZ8g7+UQpAr74IDVhKBNemna05jSK0JipRNiU0jRR0n/0xF6Fdlo+t0q5ZF1mp+iQlXEImZYXPMKUfxzNuPnb+Red87KOLsF0+NoRFvpjzsc+Qxsejry+Xt7QMUIl8bMPkQ6zzHCqWj21kwq+EffGJ8rFzXVtvPrSuTJ/zsSXkQBRRVDIoNudj30wtFlKnJCaej21kDnUllMQcKmmgWzIbrn7Lip600JyP3cfZQ+RjG4J6TI4mekrCqSPUqedjG1kZpHLmxpyPrYTBURZcG8+E4Osd5ySc8XAwkSQcQ6lKvzkfu4nXQvZxc6kveM7HLj178vnYxoileS1NwfHzsQ1la/PmfGwlTICJleYZc2nepHAwFVNQqdK82cnYxMloyZqC3SSfzfnYjWVPpSyzOR+7yTJM2sFvdvL0gTkfu7HsKRFcm/Oxu1WJsimhaaRgzseeSD529jT3EmnFwTp8m4KSvMxA7VVJPjhbP6DnfsXBK4591ylKvZVcwCOzlgzq0iWW7x4bH/wgzxEcJ4sbZmm59VncsO3WZS2f7iARl73aDfc1u8AZFqSYZxe+A2+5r9LCd7rG95glHukzNgYp8bhID0NGV66sDPdlypV7NEXjMyyj/gy7+oyelDgQZV+UuBX9rUMBci4LlIGI9gpuU66KqQzlrY8UsN/Cgx/7If10HcZxuC1H8jFDnbxIl+Rit4c9duMU/k52kDgnsy7keBPH+0PiRn8g/7veTlv6brh78gktREuXXBE8eE7skD+0nWDjgXzDj8XaIUp14ey8KPS9RbAIQqoKwv2C9lrsk0KfB3qDHrZ49wIWOrCW+92zPPlX0ntZEY5s8U8Uxk56X+2uVIKuF4GOgEAl6BavErK2HlS4KCTEALQWb4UZ8pzDJp+7MiVbDm6ePTP52L6TudtvltvQ/fGyX26diP5xX6Lg4y5KPO08KdvJi4NURyZEQ4ydQWpB6zE6ARX3LA5LaGcMa2gAPvDCP5CU/Oa4iAgOLSxEtr7nHY0SfPD/SkO49EamWomMi+5o6IaMRQyLwynCIjQy2fnsRMaZ6bDR+GafhIwfY+asXtBvquocGyzE/o0jz9nly7D0ulrpDZcJ0jeriTQFBVC6KO4E+5sH0U78JcbALowxheT6yFuP60xG6uraUjiXE6xA0/W41LJ4mrxPXuU0KSWFVUjnMZF5eZa6bp+/eEQMmpABDVE2yzEqTe/hzcWZDfeb0HdpzS4xwsjUAo1oHSpDbMT7J2KPuWQ6Y/LmsNz72MWHn37+mfRd0LF/Tk4iZp8Gz3Iljl+tJFliUqxuVTmEesBZlhCGlppGTCu00um/ZrYRygmGgEeeOSzyeA3NTeWZiyy9tbltpxenmLZ/c2JazZ20AO00O4wpd8F62zpf4Rc3cahZ37P1uYj+J7ITzeRFz5d3sV70QOTLggDS3gFDNhcyjThVJuuIuDBraxkUgEzweaUxSJetw4FlmxHUpF12tsKH1Zv7TcZNf5k7rSBstTG45IiFdw+++3Lx5SmnjAIKVCbmL57/TvGBrgU3g229W8/o5HElTd16+SYQmZwVk016cp9ld2UOMAjSjVlH69jbdEBU/bDvyVBhI4U9zOZRikcsoazZAZHZkr3aQRTyS3A+Ie3zbCRT56uCBQaxRC6SYRmE9xJOkkGGMKaujEFMVRjEnBnkzAZBE2MQqAiDKJo1NRo9SC+PYFt6KFn/AHtpMfWOeh7aaeq1EI1mcKP17bpAPJVxwqNccoJem5ywdQhj+k6wcMMI0xQE6uJ2PG/h+pEb4EX4Egf+DneQjyAjUQ2DIxzvd8Dihl7P4h2tJMlhFNJIxQnINKb/a+hh2uP/
--------------------------------------------------------------------------------
/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 | 7V1bk6O4Ff41XbX7YBcIxOWxu2d7J5Xe1FSmkt19SmFQt8lg42D6tr8+EgYMkgBhrnLjmZoxshA2+s53js5F3Gj3u/dfI+ew/S30UHADFO/9RvtyA4CqA3BD/irex6nFNNRTw3Pke2mnc8N3/y+UNipp64vvoWOpYxyGQewfyo1uuN8jNy61OVEUvpW7PYVB+aoH5xkxDd9dJ2Bbf/e9eHtqtaBybv+K/OdtdmVVST/ZOVnntOG4dbzwrdCk/XKj3UdhGJ/e7d7vUUBuXnZfTuc9VHyaf7EI7WORE17+UpWPx388vuq/P7yFf0N3/4G/r6B2GubVCV7SX3wDjAAPeLeJ8Ltn8i5r8fxX8jvij/TmGP97IV/+7incx6tjMnW3uIN9eD9/lo3x1Qme/n77mo+Ov2cyXH9XCOMtisjZGDA35F6cf8aFIx5QdDxgUPmvqIfR6n958W6D0jUA8jAi08Mwirfhc7h3gl/OrXdR+LL3EJlnBR8df6DY3aYH/33ZHbKT9+Ge9D6P8RiGB9yukn4ojj9S8XNe4hA3beNdkH6K3v34j8L7P8noa5gefXlPL5YcfGQH+zj6+OPckRz+WfzsfFpylJ2Xywk5CJwNCu4c98dz8hvvwyCMzr+E3Pb0O9vkzDgKf6Cszw3QlOSV39OirKTicwxfIhfVCIhmpqTjRM8orutonzqS2SpcIpXFX1G4Q/hX4g4pNa6UtX4646PMeBEKnARyJSpLaew5Hycf+lvo4x91Hjcb54M6zoYIn56O+JcUqAK/KXzDc1NCIG3IRB+JTLIxSN8+Gek0noi4Tv7tU5bDgopVTdg3QUl0IwYjafF7IAd3ZwS9cDfb0erI3aueyHs67s7uVYm7KTDj2T2Qt/4uMZqLIHtFUexjq/k28J/3uC0m4MxbHwkSvoVHP/ZD8ukmjONwVw0RPP1PyQt3SS52m4h4iisnO3jy34ng3KXf58s2jsmq4JbcHPDgent17eN1wZOPBSxau/iKWHyd2MH/kfYj/t/Ze5EfeivncMBHGrmv7hYdj5vQibyVCqz1Yf/MSmkR5s+BczzyxLcSuOS2oHcRoGmwDAhNT4/fzmuRfMWxLa5DFKUaeyXstAWKpQ1OcknTiRpgR87Lee7PwidNnFdivDMBjsh5p0+yBafajQUtQRbMsNXIgu1I7jaKnI9ChwOhvGM1B2oUCeo2ta6l+gOjW3+oQkooTt+4X3q1GHrVoQINRpbKkvK29WP0/eAkE/wWOYcyzMV4iEKdk1K0i5GEF8z4cz8ICiD0ILI8nQdPC2w0w+iH2QDFbLrBYTbAYTYwFLGZ9kJschGbLUps6iyILSMmUaLS9W79xyE2eyG2ErHp2tyITQfMFMHcKT2nCcIGP3Bd3gR5xsaAA2keCAkVTjtFwDCvXvmM4UlQwWjaRzcEtY9q9m5Xd6MDg6GDkjPv5MlSFaBXeb0+KXFolH41oM0jDmNUbmfXFZ+Z22nlC+HUyheqzASpc5wey0X86dlYEBt0PUkQKE+PDTjTM+qaTya1K2vwtVetqwhq3czo7XnNx3qrLFhmnHTRlQ9x+knpWWe4tvaK1Xutmvvr9YtH2utG9R9m8WixVLiI36zFL0vBahQ/0YhaR/GDxjjiB+t9K839G8SP9g2NIn7Zd6xaCTTH0FWFF0R/9PfIiXrJG+BfIPEwrfCRsyPGUdIGzylfDeuVLIx5jBE5+4AiH99JYhUlTd/Ox02mWBKAzJaeLDW1NdRUhWepHQsf0h4tB1lPXKvNcC20eerJYWJS2FRYq83iWG3aYIse1qZecLvgllkM0o6+qXGrqixwoQqYiZ7BcnAcTyxtO9tiEzTYclAFrDsF2w8SGaltXLPjGJtV0UezKfx4uZWqZjLV7JsFUNBOPWd+mYqtlWDbV/IXHZgwFcqWPf2czrYsoGxmgy4coJPSKP3ftr+pGtW27Pns4XPcclwUxNv6vOwLm9l3VFepqrI5iJ9ZPer02nZ69cgG+2N85UVBDqEgwZAKUjQn8CIFaWbO18EVJIX0nhSkrlMKUmtQeJbSrb+tz0VBsvbv51WQujY7BcnyL0u9e++W1L2euclzjttkutSaqelGb0byakFvagO38ek6Z2guXdMggeRPJ54Uz/HITJdGmixgB3KwA/thSp0KtBoWhckKtzg7EG37086RCsrtjZGyHyIl5NVLIK8ugL8A8Jq+ts0yVM1LMW/z17vjYZ7n375qzEtD86KZ5Avq26Oedc7Ig/rLjJsF89Nini6vHBvzus6BeMF1goJN+Cab12Sk3JcHZ+cH5NyvKHhFpMy4LMpWSzfM5aKkAk1QlvT+Syw7OvTY7X8+73ofUIsmk5M8PO56X2MnY76e1SU/ruAuFXCsan1TQYWO08G67Mdi4orC6lLR1opKyYi6NoR0ZusoJeUkzb51ZVk33b8h4w5otf2HybjL8VFffHOLSYtsaYFBeN7d5iiQmHRq+inJWVBWBIckPPPzDaGoB/xvfjF2P6Yl+ajX5COVXk5ZrDLhBe/gcMpEZk+adOsr8RwU4UqFsdZX/OBX58UVpH3QQzsUNJndaMoaKCVXmrK2bbsB+FUwTnoVKPpCf/MnlAZA5+XbF0oDsKlgIu2zGFwapHavXSIN/akBrvx0kQbhMpp5SYOm9CQNmjK1NPD2PZZFGqQLK8oLeN2mnc2XY16HawWUxzLw8NbIyOdt0isL8iVcBgjvvidaMTnSMoCuH7aVHL2tQ4t0IqvJjjU47HmOF2lgf2WLAVllgi6E7CITJtYGun1+TW0Q8XYFkkc+VNu4ouWB6L7Vc5MP64rlQ2B/7NnKR6sFQyXKR1URkooABD2tkPONLCa0mbIyiHmHn2cdSr4kgeZysdGhaFZXti3Z4NFnUIaxTZfsVMhDbxjmPODq0flA0fcYzzYD7+ObvwucPQfHrcOSgHSMHM/Hs0jhiYlW1sPv9GkYeSiigVmmakV5wK86BInHLw2ayEzeLqmA9+wBQOtmDkz+idzY2T8HqOAh58fji5ezeVfTyhdzAnxP906M7sitPA4DKtaHos5xx+OL8qYa5EgYQRZxnSnnV7lofvJaKsg6BLIQyTKHYnOocnIaxp1EqbZNbpMgN9n2x/XIaTbcMwtkaMNCpUJRjNLpKQ1NpfygQKtPQ2P6G2OklcGGPZ1nuyOWBqjtsNR2D3Hjj7rktInktPWlJGCtksjZf7Jdt6BMSuKaH+zSWbcIJ1ePpYXwihuuKcCvdHBhfvVKt9jRNNqD1JNis+ulVq3Pnob6gGeXVexQKpPd6oKhCSISh87EmT+c3NlkAyu1hAqo2wPKGQi8ZwDyXJoDMipvHwoZzA3yVAva4DAWi2Nii8OqEJxaJ/64NkRm1RcQfx/4hwPy/oke/8WpqoB3+Ix7gpcb+GXBQc84SD81pzYtjWvdwH7src/axW76Ny0z+RYwLfWxTEtVYYxBqA2zJahq1lqDsMHnUX+2VW8M1hui1hjuEwPIagvmVV3TGYMGL5VaCmOQ3opdA4spOK0JkAvinE1BNvi3mILT4WA2pqAUyTKLl7HZy6iLmoLGMFs49OvJy3ZQrDTe7HoDo+Pp+hi+PINNvpiv7F3zPimdl2HCsgdF96UeVvYalj4U/FufbnZZOlFnDyV7UuxRNKoOa52GUVDLSpkeFEuvJwh80Fx50Fklim5wlkcGBk//oLIFBTepaC3eCvcylUX+XXrDETRlHpqct7TyKnNmpSlHfM58/jSdRuEDZu9J3d0e0wcWs+xqwaYZohuAjwW2JcvnasGmg96DLt2cPdk3r60GDAL/cExuL12mUbqZ1FwIeu3qJ7S2KoXx6aUA5duO7R+bQj1J3FRZ19yoWeI6J69/oYZZU0NuyghQQ+9OuE5gy7/5UnBWGQANAgcToydCJhpNJpw9mUWqwsRK0CCVvM+rQRMreRuhCg2YbNT6yiuYzsJ1JVVowGSD58scSlaFhmlCIvtC3iq0HDkC5sMw0YGlCq1eDho2N59tJtBShTazBJALlMTMq9CAKZMzdL6L0IFi1G10i2gwbDQttFShDXH2GFVowBTYVqxN5nE1cV5dFRoweRV8MpgbUKXtjSXzeHKDw6yQm/lkHgOTLbtcMo+nw8FMMo/zq0lhWX66IrQ2lqUtbFmCsSzLpQiNOXkgU9ASeFzRPE3B6YvQgMXGQuSwBRnX02IKTm0CWOK+p+l0Phs3WkzB6XAwG1NQpkjU4mRsNvCaTcFM8y1FaNWnj1GEBiyZUuvnm+08vexZ4g7+eRSAfvoiNGBJEVybd7bmJEVobVSiaEpoHhhYitBa9x6lCC17MPO8pbWLlarOUsJZmRQVvrzaR7p87PybL/nYlb7AFvnYul7miyUfu4A0Nh595bm8Z+G6knxszWBDrMscSpaPrWXCL4V9IW8+do6cZvOh/8r0JR9bQA54EUUpg2JLPvbNpLGQC5TEzPOxtczDLoWS+HShkha6JTPZmresGEsLLfnYQ5w9Rj62xqnHZGiiTRJONXFeXT62lpVBSmduLPnY8zM4ckGcbxKOxql3XJJwpsPBTJJwNKkq/T5dPnYbr4Xo8+Zy1++Sj301+dha36V545mC0+dja9LW5i352PMzAeZfmqctpXmzwsFcTEGpSvMWJ2OzgSfgZBzGFFzysVvLnkxZZp8uH7vNMkzYwZ+FApZ8bNGzh5I9KYJrSz52J5UomhKaBwaWfOzWvUfJx84e2lIhrSjYhG9zUJKXGaiDKskHZ+cH5NyvKHhFse86Zam3kgt4eNaSQV2yxPLdU+ODH+Q5gqNkcee52c1Z3HrvW5d1fLqDQFz20264r9glzrB0gnl64TvylvsyLXzna3yPWOKRP2NjihKPi/SwTulK08pwX6VcmUdTtD7D0prPsOvPGEiJA172RYVb0d85ZOaKskDoBmuv4DblqpjIUN76SAD7LTz6sR+STzdhHIe7aiSfMtTxC3dJLnZ7PCA3TuHvZAeJczLrgo+3cXw4Jm70B/zX9fbK2nfD/ZOPaSFau/iK4MFzYgf/R9oxNh7wN/xYbRysVFfO3otC31sFqyAkqiA8rEiv1SEp9HkgN+hhh/YvYKUCa33YP4uTfy29VxXhiBb/RGHspPfV7kslqGoZ6BBwVIJqsSohaxtAhfNCQhRAG/FWmiHPOW7zuatSstXgZtkzk4/dO567w3a9C90fL4f1zonIf+5LFHzcRYmnnSVlO3kxkOrJhGiJsQKkVqQeoxdQMc/isLh2xriGBmADL+wDSfEPjMuIYNBCQ2Tne97JKEFH/680MEtuZKqV8LjwjoRu8FjYsDieIyxcI5Oez15knJoOG05v9gnI+ClmTusF9aauzrHFQuzfKPKcfb4MS6+riN/w1tF4uibS4BRAqby4kz7cPPB24q8wBvZhjAgkNyfeetxkMtJU15bCuZpgOZpuwKWWxdLkffKqpkkWFA2wbhbOzMuzVlW7+GIRMWpChq7xsllOUWlyD28uzmy434a+S2p2sRGGpxYoWOsQgaEj3j9he8zF0xnjN8f1wUcuOv7088+474qM/XNyEjb7FL2QK3H6ahXJErNidavOITQAzrKEMLhWFGxaQVMl/xrZRihnGAIWeca4yGM1NDOVBRdZemtz204tTzFp/+bEpJo7aQHKeXYoU+6C9bZVXOGXN3FoWN/T9bmQ/OHZiUbyIueLu1gveiCyUBBA2DugieZC5s/GbPQOFCDJ48KsrWNQQKeCz6ZCIV20Dkev2oygIe2ytxW+Xr+532zc9Je500rC1hiDS45oeA/gu68WX5ZyqiigRGV8/mL57xwf6Ci4GUqb3Xpa7z7/i9x6+SYQmZyVk00Gcp9lt2kJMHDSjWlH69TbdOiw/mHfs6HCVgp7nM2j5IpY6qJmhw6NvtmrG0R1dgnOJqRJupHMBb4qvcQgFs9FMi6DsF7CWTLIGMbUdTOIIS2DGAuDFGwQODMG0SVhEEmzpsaiB+Hlkd47PVSsf4C9tqh6RzUP7bT1WvBG05jRhnZdQJbKGOGRLjlBbUxO2DmYMX0nWLlhhEgKAnFxO563cv3IDdAqfIkDf496yEcQkaiWwRGG93tgcU1tZvGeVpL4MApJpOIMZBLT/y30EOnxfw==
--------------------------------------------------------------------------------
/docs/img/SFNNv2_architecture_detailed.drawio:
--------------------------------------------------------------------------------
1 | 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
--------------------------------------------------------------------------------
/docs/img/SFNNv2_architecture_detailed_v2.drawio:
--------------------------------------------------------------------------------
1 | 7T1rl5s22r9mzrY9Z3y4gz9mppmmJ2k2m2Sbt/ulh7HxmIYxrsFz6a9/JZBskAQIECBmNHu2sYUsQM/9qgvz+v7pl4O/3/4Wr4PowtDWTxfmzxeGYbkW+C8ceM4HDHOZD9wdwnU+pJ8HvoT/BGhQQ6PHcB0kpYlpHEdpuC8PruLdLlilpTH/cIgfy9M2cVS+696/C6iBLys/oke/het0m496tnYefxeEd1t8Z11DV+59PBkNJFt/HT8Whsy3F+b1IY7T/NP903UQwb3D+5L/7qbi6unBDsEu5fmBt73ffH1jL//30fjfL+/j7fp34z+XuoceLn3GbxyswQagr/Eh3cZ38c6P3p5Hrw7xcbcO4LIa+Hae8yGO92BQB4N/BWn6jKDpH9MYDG3T+whdDZ7C9P/gzxc2+vZH4crPT2jl7Msz/rJLD8+FH8GvfxSvnX+WfcO/28S79Ma/DyM4cB0fD2FwAC/8MQCguKL3EG1rAiau0IYc/9G15w8fPzxY324e41+Dqz/tb5eWk0+Eu1X4Jdr6X4L4PgBPASYcgshPw4cyOvkIK+9O804//RSH4FkMDROQpS20wp+ZL4CoyTG18oKpf7gLUrTGGRnAh8JDnYcyFGGjS+1bP/jREb3RheFEKdrnEiI5fx9jfOEyyVDhDZjg7p+yjceXwae77F/7StcM68IGP9XyqTefwEX/HuDUVXaLECAreG5NN9zThTu0RpT/e5M/BlqSRO0y4j5uwzT4svczOD8C7lVG0hO9wrl3kZ8k6HPyPUhX2wJ6ITx3wVc/Cu924PMKIBTAM3A9jKLrOIoP2ROYm83GWK3gIukh/h4UrqydW8d2OiPsQ3BIg6daTERXTQdglFNCI3uJ0OjxzOJ0B41tC+zN0Krxt4RrbRHLXjIQi2RMu/UbyNHBt128g4xo7SfbDJx6Degi/zaIrvzV97sM/HjL0RIF+C1ZYHGyvxL/KXIfvYH7sDndibkxOR2JMjb83/CcLGcdiI38898H733w7fqbp++Cu7d/ro/Xl47ByfIKWGQzkAiP9eSM5tIu80ISOfM3p3ghzWJdrUwN7rhMFRPaDHBf74L7usJ8sZhv6WWEdbSOmG/rE2M+Fl4vFPNfBNc35cJ9g8B9vSPum+bEuG/MBvdLGk+TvXXCfW7LbjjMr0XoIub3M/HGwXxj2YCw3PrOskF8iMN8JgBopv8l3N0BnDe0T8Eh2QerbJcM7cvxdhekFFUkj+F95J9wuRJFCNOMQDNNuwF/GQH4BzzPyGy9g78OAVoRdINve1NYB12qREZ+q8z1ykAxbdokMzQGdpl1Nhm63WewpT7aYoxNxO08hgXosSxAs3w3PwJ27s5PgyvIapKe+ML0UTk0XxzPRVVSXk/ivMlFVWKYZ3+VGBdVHVE1cz9Osa8vOZlfO64GxJn/XJiwh0wmqWZ6OkEWHuHjbJium/XzjX7THZvA9/z1hHLLao/bOnw4e7ryodtDLxfc2Y3GWDsb6r94bz8hWucoYpHbPot8AUSZQJb6IdwF/qGwe7eMHT0yxsrOyhltfkgOJHt/J2LhvuvER/h+2S+1EK70E5RpufyC1Asmarch4D7n3c5vSQGhYph6c0hysgA2H7ts+Gt4TuYLVT25WPQCELObtlEUCvddx7I126l61kIQ4zaOIY//IbzfR+EqTH/k2P5BWbjYbYAEZ1/ZulEbuNGdtpGbTq8hBP9H2zrElk679+JQXxgaiKGJvq+dyY3hcF3MSw7DPkgLfOvv4cckDeDb7YNDCNRcGO/Mhj6dvzODrJvwKcCpHtC+KhtrYuKuCVqe4WBa+4G3YcZgnZUX3G6yJ0qBURPvijdDrw+/F+xEgbYbv7OAsEMshu3OckR5NZ4C3uhtrbE4RFqAaLxmkZ5k2vX0ps11FO73wfpz8OG/ha1TZo0MZs0qAjLlB2jcQCmjIWFTVCt7WjTTo1+R6KnnHQoLsQg5Pw4SJmMYXeOaWIQK0/BsfW85lTUwtFHTOhtteANPcmmr1Eip1EgiAq6z9EiLoUfaAvRIZqTHo9TIffL3/KI/C6OUodyULULGKYeJBrHTv3mjQZzBIIRY2sLVLSJT+UJEDNwhopZk8kdF5Lpv1AljPG/YydTq59u108txpPOP8dPHm00S9I3M16GDijVJpxGrWJPcRpmKNc0y1pR7sl9btMarNWtMI7drHE37CVpqKlxDhGu8PFhjaUugjvDsZMMbyBLWOQG84mFnGq8ZDNtVwEZZ2t0CNqfq5jEiNkxN35qfVX0hQ04lczMNTitaeMlwLxSwp0SBbpXfs0KBOgt76Kpxoq5L56xtae0nqXWT9M6Hrdvp2rKZKAr3SUALkryEnhIkhcKY0SVDuYbVtWm5wBALpNtLGE8wOTb3XJO0grI+XJUpvMwi6kuR8qvxYR0c2sCCUCW6doZoB9JG0jY4SXucaiFdK2NW1+LoU3IJXsd1B2Il5edtyMTXSQ/q8IzHnZ/KtHBbByLGqkPBKUPCQg99q+s0tk9eOJ4TxXdEDUnjfBzyqJxPxhb0EUgDU+NUuqQ7hzZCTOzG8l9cGfaw9VhU1TTZdKrd/IHQ0WKoMcD4Mp/eQOcHDCmfvpU+TonDxjg43D3Q3Ael2WDiRWnMKjtpRm0xnMyKaMLwhvmjBHNr4SBLnXUlRlHGEuzmF0U+MOHWtXhREX23GN2smKXTHSunnY63o1QKsbXTbCSYtHh6DC/P0Bamx6/7YoprZpEup9THvGXqKmxCOXZbGWftZuuCNYRaAAgtWNA2/qr8kzJasVoeUkHEygD0OCmZ03dIXNuBt7YoHx244hm3ZtaYpiU58jvjCF8HKx/SaClHetXVGJN0Zy1xxndB9BCk4crvzRWH93kTniqiUwmvx4tcxyKgO3B3HGM5pcQeIzQ3hk9YtMTGgrhZYvO6gQeW2KSbqcGKoea3FNrtphuCnVK1IFPZrB00E2kSX19Dvmtn8KjU2PpM0qlwoC08m1IgZc2kbf2iedItt80z96Tb1vuT5+ca5ZzkyeonJ0BjGYFySvxFcOFJ/Z1rp5PWm4OSbIdCWZVjO8cc28EcNEQeLatpKevYisEan5g6hQbSG+vtYoazNNYNXmPdktJYN5pyPMj5TeWppGdMcJ5mLRAmdrEP6z2fuwE5vX3/ajoNvWwjXzUlGgZhuzYlmsJ2m4A2ZmRFZq4PwkySZ0Pn1KNJMq+KnP2cZNOUlLE8tbGs81rLzmDWsjmltSzisNmmCopZWssWp7UsPpGjHzYxsrx7cXnpUqHEHxYriLMQNSYnjlEqWxwzUcqaNGlmjGOsp+EsvDn/ljcZD+mW5E+mjDfkrzTMH8aDZtHtEPvrhcO0nqk2Bir9ZWIf5J0fbd6/eYDbtQn89HiACftZYUUHa6X3rrRpLKXyek95vd0KklooxGTffMYZe4Ml+DJLoCYWW/Otm8SQE3d08zhNV9gnJajSxHFKE1lYwwYTb9V5r9LEMasB8au/+GpA8oDosasBuc9tnaAa0J40XeGVVQNiRGjmSrzpCrYcxtOLqwbEAFDVgHO0GlqS43yrAe1J3OeCqwExGFQ1IB/MX3yjTolCJvwSm7drj+3KIbFVNaBNd0OpcUeqakDWLafPwVLVgHNPFFTVgKoaEN9SVQOqasDZJROqakBVDcjCTdJdrhIcWyY4tnfQyFYNaE/abO+VVQPyG+u8zfbspZTG+ouoBsRAUNWAEkptfMvp7XtVDfgijHxVDaiqAVU1oKoGlHLrVTVgC5JS1YAv1FievhrQnqS58SutBuS3lnnPZRKfyNEPmxiJ1aoasEM1YHvOIl01IH6A+XIWOf1wvGn2DuPIxDGTwdp63GxCNlpevQetYf4wHjS8qaoaUFUDvoq83qGrAS2Oyo1xE3xxrVpZh7F1o1aH8V6zCtP+vGXyuDaThro3KtAZZy4roAsu/F1KBnTPoYD+c7BLoJjCKY7a5Zl9a+W4SKFANMOGX3dJjg7/PkI1p8rJIraALwHaHZ5nZDiC1r8p/ACpnQJA6BIdGBzusr66w9Jr6vrKtzNZ7hImyhB1BiNU9S1ZmiFp/0h8avtFq3oBGpdqKazRBlkanDbIWOe2E9Y0gb28dSuEmCNzXoSd2k6QSavE/3azxzCylrhp1owyeNqRj0Q+SUyijf4ETKLNtCxJP29CVnntjnhvOV1vR0Ztp5fPpx+I7BiGjyyZGrIlHpB64gRpCNJAYxbJMONUvEwHRr6Mke7PKRZ1ssQLaDB1s65fRiVKMREisxnrNgMe5IB24yfHarUhLV9lXph0KgTBO3jGq4HyeNA6tTjbEkJTV5iUykaGREVV9DFuHouIUPTJS9Y1yYXVsmPUihBskCjFVuV+d8+XlqmaeV4ymkvbk06qTK3xdVR/lXCclXDUeaWjPZh0nE295LgBkXZZpsK9rRantxWvOHgrL8KdSkX0RAUzSMeq084Ri8OP3IWX2ggRDR1vFkMF3eo8wkQ3BGlCjQtReh1DBFZ65uhlv6Tx6vsmTLZg/pebjx+zfLAA7kPOpg1tF6SP8eE7+LQO/buDf18QZRWON8YjtdNI4aaLSMvqEcO/zv6YAhDcLtzdfQg2kPDt88hnJA4KQ18zfqudB67iNI3vK7lS65C/TjRgPHXMLWZtWAyBZQ4lsHS72p7bV5FFDoYzeuotCIBl/fS0iKjnrDIPqh+z8lUFGKEN79+C/D/GaZBk922iadE72s4MK35iVHhGkBbHAwVpDPy6S7+i5KJd8DtpD8AkJc0H/8+088s9Enjp8z7I5P7+ECSQt6zBt9tMFkPogUnBXcat4s15dn4nMGOdkXi6PdEZEB0LDa4QAC7DXhc+3AL8+3UbFH6WLX/chSmcso4BOpyrLE8A3sUpG+5APGQ3AAokvDfx9IuiDdgZVYTD73f06hAwJwH4rwQ99wEoyqQQhPuTLDo8e4O995KkGRoD3wwxss0hE2dYyccsY0wXkZPIPvOAbtGRgjsfprTRRLSMN5pSYkj06pSMWjSrao+UaD57grfLO0KlS23hmfhHCJ0ukWnW1/IiSrvsgdLI8HFap/s0NZAln6vlfEdb1s937Lr5ZUttoFM8apGonCzucbk3Acv5CdY7qnTxxsoQwvBgpYuzePNwrJlOFwdvPXfG7ErCmM0BGbOGU9sGZszLYRgzmUZsOw2MlqSclvMdS2/HmMvzJ2TMzCoexZgF1/FokjFmXLEnfwRjuvP7anlpI9PFZc8Tp3SbRFq0Y9THBqj5TQfMmrXze8cGaoGgzkSQLhnmDWCK/h1xOF9yUedy6unFlB8qs8j2/iFTzDP3W+Y9+RFeAWRsaEZhs1/Q8QY8CUENTznJaQUYdAdST2t4WNEO9gY1kONZuDCB3uMBdjP3F853Pyvju2OmZ/UxGkZIVlOJ1FLnirWPsjsVroRiJIIVZXeHMqoMg1JLf/OzKqpxjiYeubOBTQDAYaQ5sDsbdDyxWMrOBmxMYHhVKHtb4s4G7UxtGpdqyaPRdDY8TtP5NXQ2EIaS7lw8PvPKWa1F9UZ3kskbXMUrvtyc1QaHe9vDRUiirTurs3EXTu77BpoXRayYVcwpRjaoe7YflWGB0pgZrmvCU8P7aZQepUf0aLela4aFKmScagtmQJ3UbA9bSh01tFUcRf4+CdY8SmrHwspuGqpFOds5NWIy8XcEFdWcNAIkIgzPE4UfgsXUc45GSW7KERgiBaTeKFB7TXdqxK+wkhGNTjfpwy5Rj3VIKa+SWfKetPAKmGV1D2sVcJSxrZQKK/ZYeH5NpOYWV5Ss0YBpTBn3ElS1P3B0Sx8qutXyPeaFXqdeU3reaMrk6g6Od/FmbueO5+ugrlB8GDMwtYla54TYFQ+rorkqmtvZTOE1SHSGiTBYXyzmK1gGZQhM66q9GCeTFj1I91zp5kgIbzWDaXH6TxAOwXoNTS/h0SVyq3aNlQyflm/Rngx9sZi07Uwbl904BTAYZRoxAYMdFzv0jJFZlrbAribsvCBPm+QNCrPWIhNUKkJuo+IjfUrhbOLJAvjeCEjcyPcstzXfs5ZEda30fM/kSJw5bOP722MCtaaDvw7BjrdJBKiEVIsyS5OmWUZBz1BHcLHLTWYjG0YK5/DSn8VLfw6vtOkrXWjsckm0GThXyKKp8AUkMrYMhphk7b1Lk7i4aIjhLRe4gV3NDfnyKceIiNANpWaEIvVY3xNxLiH9aoU/QMBlyF4y7NjXi0qONTvRpcshulzZRJc9veiimwHNiC8JEl1kWxolukooQifVzQhF6rFeia5xRZdGoRKFPTCLfS/OKoUQ3e3g1t3iO2j1zAAnkZ421KU31KL306kGYC9L1aY5NE1xKPAClgsB7rKICf5RsQ1ENcWOIsVgytpPthkhiIponODf7Dso0suIm03zutew2ejqkmZVI2497TJ9RVuve1Pu/ZLeatlNCih6NatsV1jLZYNlkX0rxKGbzQ2qEIpRhQgGoS6CUYsMH5+QslJqNZouuDtOo+mCy34kKcjAzy21f5oMm7OPHG+ns/UjR53aJEnJcTTSKQSllktPCNn3oEZeH7h4aqyMipYw2LUI1OSuuV3SHgkyVCuqFyz5zPYInbCcmcTLyjujT8yPcDXnyPxo1E528jgFGRkOZNfPHtkSLmeBsiiKw6llU8kytyjMtFphVhFp7YNu1SIQ8ZKz/KMMEfjlk59Cp0w2YmhMEbnM/nrJM5dR79svg2jgJo1kN1lT7Nn1bESeWCkzOiCyPhQicyBqE65PisiSdBs1PNLyGAGRXZaPS0zW+/U2jrPqsiAK7oNsLXTASwjI4IkuP/phHwar4M8VIBM4GXZT1M+dFK3K1HhSZRv7CJIKzVCA8mcQaRQuo0uUztL+RByGVWvE1OnMhe2FrxquAIuDjWc+xUmIEuZv0TkrdE5+RQObvBU1+Css+gb9NoXsE7GQb2hjjFoDslQ+cGJf6ErmVLx/AlDdbxdhnLiLcBXvkoW/XvdAGIQUw+GKSRoKjHYN4zoumN3Rawt70DlGpy4bkvdG91YBuzf6rWdbdnfzowWDoE10z2HwCGbMbzDAT5qTMkZ28xhdtrorPkveNutLOTR4yyAi5wRCN3TLyHM+B/u5Xf9zk7aqvTGcV0vVxl3WrgrX8Q5y8At0iCASaqqJe2FA6m4LWS9WUd0VVC+FFr0U9BfVQnyadgq1OrQqLn/txeXtTQyHDhvwNrcio4XiTAyZKrYWmmZejF61JTSgDp5WXEQc+4ia7Q/es/V6RtEMozmQzR0U15cL4ng/zwJD7vL8V15ZUIicYWi4TedLMUoK2v/Eq+v+28Gc+d37vN2+e/z1zV+/7e//0Df/jt/6lyad2JqE6wDr0PfxA/y8Px+LNC39GxM3+RVJ/FhyZWfO097V+3C9jmp9lEWGwASuxWAIzCaUFm/z+lOZtinmaE0y9mLZBHsYt40Am0SqQzUFVSlOtxmeIOopUQzvwUvShOglyTfDFHLINRuBZz0IpyDuWOaJgmwLp3rjKhVdCEnZZPdogSTVVnZSz2KOIdI4ctjz+hZuJb11/Qp51O6pLruguVusc35ENIGqxeQqLtZsOjqU6ahhAj0PIjOsn5eQvlMHL2HVIu/8aPP+zYOReZ72mR9qE/jp8RAk2Q9bewe7+5O4djRTDyTeTyEe1kn3kNeZ2bQDfde5iaEYDfzVtuyFa+EJleVVsowScD2GqwDNCPz3NvYP69w/htJRoniV+VoSCLP1X8ckBaoMoMZsG+6ABNyRipT2wyYK9z/mq/hw0E+DMwFfoBSXbJnb59OtN3EUxY/h7g6OHPdRMO/NLbOpOb5AVy+/YhNNDFfQfvLujb4Ak99/+fvCvnKs3DsO08fyY3m/A4r7FyTu5O+jfwimhqMBn/XTinpY/HSIWWDWFSaIf7Vxr3Ng/CTvbubv/h4GMfTzqwdJAvS10I+iZ/zmi/R5D18fBuSMC9z5PL8UJn9CyGZ3vvf3CMRwnzJnURKiwngqz1D8xvVJSuK1X7PGoRW5h4TzBtDXCrz9h+zbz552HvqMdHv7PPQ1M84Lc65QVl421sIPVG/+tCrvHtkY4ahI5GpBUATPOtj4xygVsFPtzT2i5S9fuwkRURr29hrN2ztWtu7JmVOfuwUXIYZ2cZa9yvQSVaZv1qMb/zGxxsKmYOjisdLBvAt7KCjylMkpKNb0znUlACLdck4BsRUp6uV2sDLAlJUCqGDaAqYyAJFuVq6A2AqIhgwykqMvk4JiHRTJ6hUJYMrR/knBtBamMkCRIwqnoFgHRUs6xcdmxRAVTDvZldhVMD1QebpDKaDWmJkyKEK2cvmItTOlAKryAIkyNOVht8ohJMzulAeoyiMk1gyVgvsqD5EwO1QeSlUOI7FmqRSUqjxGou1SxtFwYwNVOZB62qUyuAF5DptQUGxhl0oBVOUyEmyXTs9uHeVAEm2XSgBU5UASa5dKwX2VB0m0XSoBpSoPkli7VApKVR4k0XapNT1QlQepp10qQ3K1o1xGYu1SKYCqXEaC7dLp2a2rHEii7VIJgKocSGLtUhm4r6s8SKLtUgkoVXmQxNqlUlCqaA9SM0C4QU4Dj3Gah3hgiS/VBV8PMaxlP137BZ5s8lu8hoj49v8B
--------------------------------------------------------------------------------
/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 | 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
--------------------------------------------------------------------------------
/docs/img/SFNNv4_architecture_detailed_v2.drawio:
--------------------------------------------------------------------------------
1 | 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
--------------------------------------------------------------------------------
/docs/img/SFNNv5_architecture.drawio:
--------------------------------------------------------------------------------
1 | 7V1tk6M2tv41XZVJlV1IvH/s7qR37r2dW1OZ7GbyaQtj3GaDjQN4uju/fiVsMEgChJF4adPJzLSxkLF0zqPz8ujoTn3cvf0jcg7bX8K1F9xBZf12p/50B6FtAfQ3vvB+uqDZ+unCS+SvT5fA5cJX/2/vfFE5Xz36ay8uNUzCMEj8Q/miG+73npuUrjlRFL6Wm23CoPypB+fFoy58dZ2Avvq7v062p6uWrlyuf/b8l232yUA5v7NzssbnC/HWWYevhUvqz3fqYxSGyem33dujF+Cxy8bldN9Txbv5g0XePuG54fg3UN6f///5u/b702v4P97Dv/XfF7p66ua7ExzP3/gOGgHq8GEVod9e8G/ZlbX/HX+P5P08OMZfR/zwD5twnyzidOruUQP78HZ5L+vjsxNs/u/+e947vufqzv693WX9oO976qr8qOhy+rTivkCYbL0I343k8Q4P9WWUruzx4EXxAcms/90T0FvVN4elHqG3RuJ9fhlGyTZ8CfdO8PPl6kMUHvdrDwuNgl7Ff3qJuz2/+M9xd8hu3od73PrSx3MYHtB1gNt5SfJ+1mXnmITo0jbZBed3vTc/+Vb4/Q/c+1I/v/rp7fxh6Yv37MU+id6/XRril38U37vclr7K7suVDr8InJUXPDjuny/pd3wMgzC6fBM8yOdntvGdSRT+6WVt7qCqpD/5mBYV76yLeGwLF85q+A8v3HnomVCDMygulKV2uuO9DHaRFzipOJRQ7IxgL3k/eddfQj/VoqzfrJ934nXWRbjZxF5SRgmiD00v9wEMoo84PEaud76rriO73BEawHJHiRO9eAnVEfqlMGaXSymatUE2rSdkE4JoGTwOgWxCvsAZFpGuo6UvFI1oExoIaajOMwbjBfsM0T8M2J9wsAaAVPNsrqY4V9fQ6rh8LAStH+2XD1FgnY1VCawJYUaze8C/+rvUZC8K2XcvSnxks98H/sseXUuwcOZXn7EkfAljP/FD/O4qTJJwVy0iaPo36Q9qkn7YfarQZ7lyshcb/w0rzsP5eX7aJgn2Se7x4MAnd70HSx95JRsfKVi0dNEnImV1Egf9g6/H6F9nv478cL1wDgf0SsXj6m69OF6FTrReAGgtD/sXWkuLYv4SOHHMUt9KwcXD4r3xCJpK2gKZQLxePKHc39kWvSBFqZa9kuy0FRRLlQ5y6aUTNOgdMS/HuT8K7zRhXgnxLgDYI+ad3sncXdANBS1OFNQUThRsB3L3UeS8FxocMOTF1RioEiAIDcKrJtpn71/d3tIJpTg9sVh4tSh4hVCHFqVLZU153fqJ9/XgpBP8GjmHspjz4RAhdc4Zol0kScifRu/7QVAQwrXuWWuNJZ4WXKmGIQbZoF7hnBSRDTKQDcoCNtOegW1awGbzAhtvdEAusFH+eANQaVrH9r0Amz0DWwnYNHVswKZBhmlf8NVPritQVMjp1A4/lcg1gK7Lmsq1sTJ0UWsUYSmoEIMml/0tbTKhYX74ZaqPmAOAva1TmsG5TgFTuAXeDTgMCjiAArWbxQPVKuOBYdDQbvWK7LRXcYLxG50g0gIaA2DrgJ6jMU6Q5XrsCVpZOk5JidEgWJ4gGzKMo16dvimtplNN1wpdTBXOxTSzegU7fXS46uxlkV5XU7q2dVisPmzV2D4Lq1W2B7Xt5XiPFg2Fs/qNWv0yBlij+vGm1Dqqn270o356fXClsX2T+pHBoV7UL3vGqshAc8ocKKyc+bO/95xICE2A/QFpiGmBXjk7bBy1DWBkicw48fDtBy/y0VBisyi99OXyuskWS1OQmUtJY1NbSw0oLFMtLrxJxrQcz9owzTbDtbzVRlAgxCSE3+RzfEjCkTjHhzaqZ8GdBZd2CMks09CCCwDDHbzlmAok1r18xoaKqQBIB1WQCTEhO7VN0LUfe7MqA2k2pSCvN1QBgJyWKoA6p6l6YX+Ziq2WxFYUAYxMi5sKYc5WcHbbmrNkasNQ681TYHZrbwKj2py93C2f55bLRUG9x5is7Al9CXFTGRHtXnOVANL55AR98oy/MvAXysRfXtrZVfhrZuE96fjLt2eiNReECA8YWgOekqmntu1tbSz4S5tXt4u/JFlkcPzVMr2qQFovWIWvUwPZnoKxT87OD/C9n73gu4eJ72Wxs1qidgfwhSon+GriSb8d1396O+ztwgMk0qUmwznu1zxT6ckYryE2J2wK1hWHHaaKhoKKTKMGl+WkDeXl8m5xVRV1qQBCR8DSkGKzQYvtm1duNCDbN9hsUK1tLycFlMtHPTv0HoEW3mSFhPCyuzLmCJSfLv2QRtCUBZZD7M19usMQ9YT+zj+M3hI8x8KFxsIBEWk1LHoxYUVadXmLCSv9SK4u+/U9LiJygdG1E2/TyQE163w3JDbSnxaudP5OxSrB3INbux6R9oaO/+vokfNGRLmpMwXJ0Wskp+N6oanl1UIjfXLu1YKMdpGiLbkgAlBZacvJCPwSKuCuFD+ybbtB8KvEOG1VgOiSlgBuLblBbSATZuq15UGgXVYrVetbG+hI/AfXBnHLAFN/uLThPnx7XH1Z79+9J9dfLZRvn799W3AHa0emDKoiSBlUZWhlYJUBm4oylBSB1xbiR/kqJYjRpCTEiNygDmj20jYFqYGmLxVY7ktF3Vs9KwOrctRUlGGCjgG35POSentyDEiKu6Hk0tu6WJpBLQBUX9LFnhWKmYzYfzD3YKo6YQrUCROtBpp9+RnaRqL3o05JP4BtfBiHIcswfQD1+CC6wVG/bbS60cp/qJRwOcvDhxJ/vYrv11b8dTC8uaTptISPLxc96rzyNWya640qTeethJFtmpeeioZlMbY5UwvCNuDBScjw6CvI9ljNJWe2NwoxNIVLccc6UMaEhG285J1RCpua1YUajbBNqbLGeMspjlLYtGzD4kiELX/yWu8nCPxD7NEeBDGYxFxwkoHqJ7SSKoRnl6IKnQW01thqwTEnCnuYjBrS/XLMGRWqZmgYNTTkpgwHNAhnkXarl5g9eQEanp13L/qaoCGm5DB+9XeB00bxa1X7JXLWPhptYhIpja+f89O7YbT2IlIaSCR7Qj+tYAOf2xUEDgLGde1sVVQJYjHSbRaYqI2u2a+emzj7l8Br92mQWc3NJMDLCdBA753Ee8DjG8vw5aAJKUn7OLX4GpSLezWycLJaufyUt1HbAy9N0KRpGPMctptDwOAV9zyJdDmN8doXbfzcweyEesnhMB94t/h2Pj+not6L4K0ggCzGp9ZvBaHal0u5y9nakY/65KokqZAskdTuIC92r/O+Ep59JaIWCb12kcjRf6hCTNCcUjB0vE5o952MndcW3r3Ova1CC03Xl4TAX1I9bVOtC82ie1NtOQubXa+1DUUsdU3i3eUlVtaSyUGjwCpx6Ayc+YHZzirrWKkFVEgMDyxzfjUaUAGLSSARUenKHtMwN3RA2huXgoyzwTGQwWFW6E0tdaZnE4KuVfUY+IeDt/7Ve/4nY2Oz/oCe5RHLy53+0ywHguXg/O7QJT7zT5uEZSk//NCak3RlMl+CZcl7wFwe+pVvWQKFsgV1VU6NSGDWGoN6Q8ij/m6r3hast0OtPqInFp04mogpCFhHzvVrC1p0LmQatiAVeppNwaFNAIs/9jTcmk/njWZTcDg5GI0pOKVM1BxkbDbwmk3BbOUb+LDhegMqO4C20niz6w2MjrdrfYTyrClR68fLdh5e9yz+AL+cCoaCHSdC/FvfbnZxnYi7ZeneJJJr42ZrFpZlpQwPCjIragECvWje6tt5SeSlhOaJAensD6JcLmnXCToKjiSZNByFwX4oztZ6DyullpWlG7e2drFSwSg1nNZJXuXLd/tMjo+dP/nMx66MBbbgY5PnSMjlY+sEjc1kJJ1Hw8dWDTof/cG5vBfl+iB8bNWgU6zzHE6Mj61myj8J+2K6fOxccprNB/E702c+NoceTPXU2pmPPbJcyBWLxMj52GoWYZ/EInFzqZIWa0tmsjWXrOhrFZr52DLu7oOPrTL2Y1Iw0YaEUw2cH46PrWbbICdnbsx87PEZHLkijpeEozL2O84knOHkYCQkHHVSO/1ujo/dJmrBe5RhHvqd+dgfho+tit6a158pODwfW53s3ryZjz0+E2D8W/PUeWveqORgLKbgpLbmzUHGZgOPI8goxxSc+ditdW9KLLOb42O3ccO4A/xZKmDmY/PeLUv3JpFcm/nYnZZEXkponhiY+ditW/fCx9btWm31glX4OoZF8joDVeoi+eTs/ADf+9kLvnuJ7zplrbfSD8iPwXKxi+W7p4tPfpBzBHthcefc7GYWtya8dFnH0x14jiu+1YL7il3CDEvDMk86vj2X3J+S4zte47vHLR75GRtDbPG4ah3WiLXStDK5rzxEkazv3voOS22+w66/Q9IiDlnsi4qwor9z8MwVdQHDDVq9gvszViVYh/Krz1hgv4Sxn/ghfncVJkm4q5bkE0Md/aAm6YfdxwfPTc7i72Qv0uBk1gS93ibJIU7D6E/of3e9V5a+G+43PoKFaOmiT4RPaydx0D/4OpKNJ/SE74uVgxbVhbNfR6G/XgSLIMRLQXhY4FaLQ7rR5wkP0NPO2x/hAkBredi/8IN/LbxXbcLh3fwThYlzHldb1JIAQFnQdchYErIzh4tLQnZNwhLOSgkRAtoob6UZyk8TZQalG4WbRs9MP3ZvaO4O2+UudP88HpY7J8L/uMcoeH+I0kg7DcqnU1kpkRJkQrSUsYJILfB+DCFCRZ2OYTHtjH4NDUgnXixKrNAXTMoSQUkLKSI7f70+GSVe7P99TszigTyvSqhf/QGnblBfyLCILxkWppFJzqcQHSemw9aHN/s4dPyUMyfXBXBXt8+xhSP2Ly9aO/vcDTt/Lkun+OegdYIe2OW5MTTIQF/G1JDHOAucGlZx/gr7YB8mHh6h1QnKnleZ2jRtdTuPbjXmMhY/id6XRSPnY/pTjZy0UDRIerO+ZoGfJQB28YeWiF45GhrzCO9TohqP4d3VZIfHbei7eBsvssvQ1EIFLURYYcgk+A/IRHPRdCbol3h58D3Xi3/49Am1XeC+P6U3IUtQ0Qr0idOjVfAnRgX0Vl2MSIKcZRwxfakoyNrSTYD/NrLaKBcxZGCR0a/k0Ys2NZX8h8fj61+cBG/wTq9A5TI7hHV3hQvOwJD8CPjqw+dbxE8r0hAN8YTKxavRfc8PrxzJGfGQWCtNQNQK4N0oQx88SXQkiBepkjUQyvtaJPn3Wn1pv9EE6SUSinmC7dUqSQNClVqXgIaNLjQ6XQL6RJjQMErVYMBSgQ26nb5qTuBxR/YyeW2O7KnCD3W+SsMoSIA1GkbrJ5GfMzj3yolTVYVjebvRZAWZ4DSHLvmh6fUHh48GWDP0bJWlkFuIalrZT413O4emG6JhsJuIarTvTpPbJlqU5oq4l1ZCEItRbwb0iyB0xHGUCHJdnnNGkIseGpNFEGNGkIINoo8MQbSJIMhEGVh9wQO3n6UJh4cKRwraS4vYOwnyNFHbAAurN5XqTbZ3pdNQRinP5IgOoJHosHMQYvpOsHDDyMN0Bhwbd9brhetHbuAtwmMS+HtPALeBR6NaZlUo3BeA4ipoRnFZnuR9+Pa4+rLev3tPrr9aKN8+f/u24KEocge1BW7WqImAsa28/J0KhK6KmzE91do1ggtsmYPNSz4HGRNmLNFuqxz/0MjgFDcY28pSodOJWbeastRM6l3xGM2cHA4q2ng1oWytdNUFMOtCdSIFCtIFNWOtZR2ZfJkfUfLOQa24FXnvA/t59+KOTt41UfJOZDg0S1qGgzkBHBb4aOUdiJT2PtB9stIuytIheadUR5KlncGcc/IaMftVfEinV6EvMZrUBHl49sQMXN3BbIr+Egoq1KRhtoO81RYycpSytFS9zIhagKsEvjVthABtvaGIQl695Nr25zp01Q6EUde+IclO6CQgVDLcbGJPijbSpEU9I7TdYii7CmQLGK8xMF5eEISen9VtgiVsCZb1po9Q+0EYlOpZFXUCBiQjKXGagt60p97q2F7XeJBRPvbRuytuGfsq3amhsI/lBvWHWX3sYr4ehYD4IJVkkCFXVlC/75dqr9WDjKrWtu/MIq6dhblMoYgyhe2tNYLXqik0YskqUMhGrO410kZecLADYvEaTmAcpGkSULIjQnitnLbtCUCUBFi0yQOUrNTBDdo8JILkpnADgsizeWiu+01PkGaObIIYW7LDYzIVQ7WVj9y/VQt514hsx2Mb51qxywSXLM3YMS5Pls3RDX0Jy52I2ihH+tFGOxPaUNqZ0ET7hggme3DlO/HMOiiFyvUny/vnxasf4z3Uu2OQLBmWeXMJ/NOlVNmV9HMUB/35Ef1Z3bG2Y3N3ibv4AeCivk8AWp/m2va9OAs644Qv1kqiS1tJ6F3bNx1+IlIgBqDnp9fwE2Pb4WyKFR0Wxln1vZpiGus8XtIOGytvYnIMUdZmnVq9GQlvQiOyG1fzJpD8j5chCqesCtOjiE5VGXSS6Xa1MtjDUkS1Se8OmB5HVOP1zMcm8KI40RoZru2ZI5rVq5mkwE+OJDpZcRdm7JCptZ5JoowSDTNJVDJJlNuo0XkLltewRPuhNpGo3UT6zH3aa9s3kEQ1UyBJdNEfS5RR7+CWY1WVPuVgsaqZJjoymii3BXENmA7DE9UJgmAT75PUktbtx8IT1WaiaFECK32qwdCv/sCtEeXf+0+pZwaKOE9GMsiQS2sDT5Rq38AT1cndb33wRLNJmHmig6R+qczV0DxRfeaJVo4N5AUsXQ5gNSKGSkb+BJ2DSX1OA4GUNJfatu+FcKpDCvlmupywtZ1x/iS7Ie+JsfLpcga5bVcaXY60DJrocmT7Jrocqa/ToMvprMTZTJebbaYGm2lwulyGYbMTXna6x0KX0ydYtUlcZv06XhfX0s8u28mbq+FmyvdUxYZk4Sp8xnRjFRvDIDqSXaNsgkWbGsW95BhyCnyr3P4s8IZ5rcATUVeDPNtQtsCzjre7RYEfG8JzO3c98W3J44I4wyXN25FI01K2wHMcvjo1gefnTo0W38cm7qQdcnVZPq03g4ZdKIQRGXh23r3oa5Ie+E7606/+LnDaHKJVe0wWb6Hzes2pPvG46sDvGI1q9iBQoXQKinHWdJJNB2lnDSoMIaVYpgyB/BUfpLx/CbxCkI1McjF2Tdss55AgcTsBPqfPSbwHPNixHLGj/XpgUOI2Ube+QdO4BYgIxjAS7qwTZ6UdsAKM7gmscZ7TJHCZrjkURVyqoF7AGlddoNv0sluvp7LTbQu0oi6V8rEoC13lOxWlbY4AkPzN+hQB2dyqzxAApa55E9nT1Ja2qVx+VNaTNpkZ4mCadcZ0ITZ/7eHmz/7ecyKO4P+1H4D37C5youPpklFMP1zb8Zx44Ek8iFoBDQQKSqU2cCYnpBE6DIYRo89GDDmFpTkDKj1pLEdNWsbCmGAId3z7x+ulQ6Bd0Y8/TwtpCXeI/nid+wVpCpBCLfuECdbqPRlRX1qqURL3pQLsjiLfMhLWfHw8vyVeu4JMTmEANter6yIAMk3BqzIE/Qj0rDEWQ0HGTtFst628leIKl/csosEh77wbZOQS0wFJIjbbOX9E+850S2/1i/1PV9O+B0f1D/1/F39st+GCETkbKbVyiBAIWxAhLYjssYWi5bCT9Zo997ynoL2bGoWJcz6R1r5GqPh9nnR1BIXFsVw2JeeBlo7K7tNzNeuPyh5HJLfvqGwJkK7Y69DZJ+LlsHS03YglzZQUZCVWwoZtoHqX1nrNnobGByNNTHExVbaJSTNq6LTKjceMUv8blD0BRtwI9Bk3yrJvU3KmK93hq2geIkrxdDTbLO4DUMbmLxM8j6sdZEDwo/r2kO1BPeQ2vkYrrWJrhHgr4CoJr3WlZVsLoOq4FdH2gs3WkEpFIJ9LE7uxke0N0ov3hDY2DuF9s4eRlnF2u7Y7GAmboSN2C92Xx/yC5vREpz98ZI4YI2BeGyjpP27DfGyaVfw25NQrH2Tq69ClxbZnE6jlCE1Ppxk3ZVH4qEPyYYqnCmkQ+IfYo50OIvRCmGGcYcJ6W66WY0w5umd1kBwBhHrtzJq0NwslebPMGVXpCZxWnqv3jAErVVunLLLtctKhJHeYCTLLLSIhazRY5Wpt+85WecUk0ruQVRqibjyoVlE4po+DPCro5N2t349EJ09XqzFmLjj45LzbuHoLWgB7SR7VIQkggUZ/lNZAKG+8RRZOsriPU6Bzq5Akc89c7g/G5Wbkd2SRuSu+0ZSWoz6q+fWTf5ewGun0alSPh/J3N+nasugM2lpJ+Bf5voHWFGPNqu8ZcJ5YckV6v1adG0ro67rEu3tZSw3IWEsJ/MC6cuiMqFBxw/0e7/ddZR0rtUgLNdK8UBlbZQAr3SkPXo2GEnTjtT0UwvZQ4Wx8DGt85LrXbHwMZk4wNofNTM3h5IDgvg0nFt252uMMelwbqJ2qldli644uxcpsbbFZtRab2rQLvuH25l3xLW7vbLPVRp/qTLYMZ/2dg2erqBUYZXzXCe7PYJdgbcqvPmPR/RLG/plivgqTJNxVy/QpHIx+UJP0w+7jA7LxzorgZC9StM6aoNfbJDnEqV3xhP5313uw9JF5uPERQERLF30isiecxEH/4OsxHigHYbLvBAs3jDz0GluPT856vXD9yA28RXhMAmRELQC0lof9S4cl4cnZ+QGe0c9e8N3Dw1LWWoteDR7Tnzv+WkCdzenqnJ1Oh+VUmqkvK0/HFtlBGSJFmoCcGENv29FqN/c0Y3jmvQyN4UR6TW3gyRPVI9s2b8WrtyntUUm1EEetr53OuR5DLbdeJ7n1KuNkFFk1GWp5fvPE1UwckYkffNIgHdD51XNiZPxAZRPiyEiyxdXp2gVLrMPbKThSES7hDOVUdYPMntRAc/DX2zhukj6oHwt7ztN3jp2ddxpz1Pk+HwrXSY9XCDfCh6VT3Kuqc3wMhH8IkBmX2rTo+2ywWkzi2U8DvvGc5BiJl0I5jxw5+xhpzg4Z8sKe+LdteHzZppiQjoOfZPLuHNDUIjiFysrbhFMZJBcZWEc8PF89/MSBt0l2YZxM5OHD3c5LbaYUICV+kvd2CJx9qrdLuQNT4T/vw8TDhJbVadF+XmULc9O6fQ4cVjvc1CItlUN6hcsqzwQo5/YAo75rdtJ00QKg9pqJs9soC+DrX1FFlJszTyJg7a/uhwixi0oZsT6reARSilk/yP28tx/ffiycj/RJ8NDPWaoh6kgQlE3kuy1NztIROlzq3bWevQWUlcklg1RT2Qnd4cyfpQXMu0K0CywVJb9QtYe6l7Ji090mrVMb/5EDK6IWHyBiWz0fHGRptIoU4rhesApfB8349cMR40kQiFDrEtjgF1+cBBe1T6/g5R9/DxKh0JUnP8j2Bl2YdrZtFtFhadbjA3rRrOC8h31miehGZjW0OBVZboCaLC7TdPwm2d5oYH6pRPEWsxzRbghRk3VjLMAXoKY33xAdmYq1zKLPPSFKJhmFVfi3bXZ4ZylqM6oA3ApLU+kgT5HRN6k+NbJq0+jMKWTiOsdYXMwkHZDTSJwiV8e9n6SKdwq5Sfxafx2dfeL/fZYW7B2tUt/l1ZMbC3H2+xB/jrfZ+C52o4N3YcOJns1f42FMxS0d3EdhncfheXQU8mMsPId75PE4axyi+m07FdFGc+C5eFUITksEWml9N5nIw58UBjnch1RuI2f/MpFgZoIl6RKVwKYGBBZy8XB0Ygpf4Czsd1lOoxRgkRx4FNO3nPAltlYnGr2sNNMrjdgWuWeymq5BRzMsdamqqgZMzTCRSmiQFdC8JriBXkYhnv2LFYamb/tLuPZwi/8C
--------------------------------------------------------------------------------
/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 | 7Z1db6M4FIZ/TS4bAeYrl006tBc72k6z0kh7R4OboAKOiNMk8+vXgE2IcStGY3Ii2Fy0cMyH8XOA49cnzgQt0uNjHm4330mEk4llRMcJephYluPZ7G9hOFUGy/crwzqPo8pkng3L+BfmRoNb93GEdxcbUkISGm8vjSuSZXhFL2xhnpPD5WZvJLk86zZc45ZhuQqTtvVnHNFNZfUd42x/wvF6I85sGrwkDcXG3LDbhBE5NEzo2wQtckJotZQeFzgp2k60S7Vf8ElpXbEcZ7TLDv+QpX3K8M9HM5g/vzzG5N/HzZ1VHeUjTPb8gnll6Um0wAfOacwa5K/wFSfPZBfTmGSs6JVQStIJmosN7pN4XRRQsmXWDU0TtmKyRXbl2+Jg6XFd+Mj0NdzFq+k2pBTnWRAnyUtJbv7GFpf8xJV7zHcUb4ujGHVxTt5rEsbUurAvSELystYoKj+stKxNWQ1R4QdxsMbmQWCwD7O3W5U3dHGV+Ngw8VZ+xCTFND+xTXgp8jlx4fLCJQ5nBxLuvWn6DreF3GXX9ZHPVNkCB/sbkNHgIX9G8ww/5+1cWBP8RmWfMPWwt5xL9gh1Y+/3xd4eLfse7mRwmqYDhzPMVyWvMKf32bo8mzH1HGbDWVRbCiAko/xF7pZ7SORqQk1yGcnYDvMo3G1wxOtRrDxXTlRaLKOwbklOFyRjhw3jjL4QGvILrA55fGB78bVwT9n1/xIVC5OEHO5FUKDtjnekp73T8hELKZykNup/3vuD8BL/T7xEB1lXImsqyKpu/x7f5LNBkC2eAKBkPYms1SZrzlQh2qy397QxALK+AU7Wl8giBVlPRdbrjaw5ALKGimzTfiW8MwmvrcDrqPA6veEF7EJrxet/gte/Hl5kSHgdBV5VLGX2FkvZgJ1nrXiDQI23tHfA2w6eV+ysLATHOa+FDgcwJQdwFQ6g1E56C7lswB60Rgfw2g5w1RczsiSyXpusKuLqL+AaQle66EgDc0USV7/NVRVv9RduuQPg6gXgXG2J66zNVRVo9RdneQPgGsBzleQqpJCrVBFWfwHWMMQqw2jFz9flKolVSCFWXXXQyQaUqqBGHnyNIw/WJU5kdcLZ28iDA6hPQeHk0oYWnLaE0wbGCShKweCstUktOF0JpwuME1CEgsFpaMXpSzh9YJyjy9iohUYdOIVmIHCKsRUwnKNLwqiFRS04pVBIKO5gOAGVIyicnkacUihkQ4dCgIIRFE6uQmjBKYVCNnQoBKgTweCsxUItOKVQyIYOhQDlIbgERl04bUMKhRzoUGiEqhCXBrXglEIh0e+DwumO7t1Zi3xfppYXi9pTy90bS0Z2R/emrSXB68P3bg3+6N7LtYB4ffi39jUEd3RvcQMOvqxmQcMXT6JxwfdB4CM5fgeHP7pxo1rKvD5889bgW+OD70HBt24N/gjHpLgQc3346Nbgj24EqxZVrw/fvjX4oxvvCuDg39r8Ad4IFT4u2Lbhmz3DvzWFzxt+P7+CfEFTzuPVntopTQ9jdqPcW6auqM//lLVmfEqULWjKw++5d6OsNxFUotzxid0f5eF30btR1psfKlHumFDYH+Xh98W7U9aWNipT7pgr0R/l4Xe6u1PWluAkU3ahKQ+/d92VssYkU5myB015+N3orpQ1JivKlH1oysPPiOlGWWtKqkx5Bk15+KkvXfvL/VFG0JPm+qPUvswv9NA+KENrX+K7RX9EuZ7Zu2il+uv+xUrIua9YkxXzJDXRq31jd0FR5SIpWb3vt9M0zIt/q32enOZ5uML1zg12s/JTlLxjuhLTokqzGXzGm50uilnFpfkM8vOMq7ajyy1mU5FZIjzDcmpTc2oK1HYOS4NzfPztpE/Bdzxb+m9PPwxv/4B+3LUj85DdN7J7sGuk0k19gYE3W7OVuanlHbJLpHEUFaeZHzYxxcttSfnhwNyg5LDPotrT1FRaCBSgvqAifVPLtFo8XMXNqmOqECWPdgyd7otfAkhOtw/lTheV1nzDiok+NFFhq+efJyjLGr/xgL79Bw==
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------