Skip to content

Commit 28cca03

Browse files
authored
Merge pull request #18 from eboatwright/dev
v3.1 (Patch 3)
2 parents 8d132f2 + 8bf79b9 commit 28cca03

17 files changed

Lines changed: 819 additions & 182 deletions

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,5 @@ Cargo.lock
1313
# MSVC Windows builds of rustc generate these, which store debugging information
1414
*.pdb
1515

16-
sharpener
16+
lichess_db_eval
17+
nnue_trainer/__pycache__

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
## Features
1818
#### Parameters
1919
- fen=\<FEN STRING>: Sets up the board by a fen string (Doesn't work for UCI games) (default=STARTING_FEN)
20-
- debug=\<BOOLEAN>: Toggle debug output that gets outputed per ply (default=true)
20+
- debug_output=\<BOOLEAN>: Toggle debug output that gets outputed per ply (default=true)
2121
- opening_book=\<BOOLEAN>: Toggle built-in opening book (default=false)
2222
- time_management=\<BOOLEAN>: Toggle time management, if false the bot will use all the remaining time (default=true)
2323
- hash_size=\<INTEGER>: Sets the hash size in Megabytes, there's also a UCI option for this under the name "Hash" (default=256)

To-do list.txt

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,4 @@
1-
thoughts on NNUE:
2-
I've wanted to learn how to write neural net for a long time, so I want to implement NNUE eventually.
3-
But what I'm not going to do is just find a SF NNUE library and stick it in there because that's lame
4-
5-
Update:
6-
I've learned how to write neural networks, and trained one to evaluate Tic-Tac-Toe positions!
7-
So for either v3.2 or v3.3 I'm gonna work on writing my own version of NNUE from scratch
1+
NNUE training is currently underway!
82

93
figure out some sort of multithreading:
104
to implement pondering I think I'll have to add multithreading
@@ -28,7 +22,6 @@ calculate my own magic numbers; currently "borrowing" Sebastian Lague's ^^
2822
check out pin detection to speed up check detection
2923
try writing a struct that sorts moves incrementally
3024
I tried this a couple times, but haven't got it faster than my current solution
31-
re-implement PV table with a different approach; I don't like the 2d array
3225

3326
History reductions / pruning
3427
https://www.chessprogramming.org/Internal_Iterative_Deepening

nnue_trainer/Perfect2021.bin

84.2 KB
Binary file not shown.

nnue_trainer/config.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
GAMES_PER_MATCH = 1_500
2+
EPOCHS_PER_TRAIN = 2 # ?
3+
MINIBATCH_SIZE = 10_000
4+
LEARNING_RATE = 0.0009
5+
6+
DEPTH_PER_MOVE = 10
7+
PERC_CHANCE_FOR_RANDOM_MOVE = 2
8+
CONCURRENT_GAMES = 4
9+
MAX_MOVES = 120
10+
11+
INPUT_NODES = 768
12+
HIDDEN_NODES = 64 # 256?
13+
OUTPUT_NODES = 1
14+
15+
BUCKETS = 1 # 8

nnue_trainer/main.py

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
# The developer of the Weiawaga engine's NNUE trainer: Mimir, was very very helpful in making this!
2+
# https://github.com/Heiaha/Mimir/
3+
4+
import random
5+
6+
import chess
7+
import chess.engine
8+
import chess.polyglot
9+
import asyncio
10+
11+
import config
12+
from neural_network import NeuralNetwork
13+
14+
15+
class TrainingResults:
16+
def __init__(self):
17+
self.games = 0
18+
self.positions = 0
19+
20+
21+
class DataPoint:
22+
def __init__(self, fen, outcome):
23+
self.fen = fen
24+
self.outcome = outcome
25+
26+
27+
training_results = TrainingResults()
28+
nn = NeuralNetwork()
29+
data_points = []
30+
31+
32+
async def play_game():
33+
transport, maxwell_engine = await chess.engine.popen_uci(["./../target/release/maxwell", "debug_output=false"])
34+
board = chess.Board()
35+
36+
with chess.polyglot.open_reader("Perfect2021.bin") as reader:
37+
number_of_book_moves = random.randint(1, 10)
38+
39+
for i in range(number_of_book_moves):
40+
board.push(reader.choice(board).move)
41+
42+
fen_strings = []
43+
44+
while not board.is_game_over(claim_draw=True):
45+
if random.randint(0, 100) < config.PERC_CHANCE_FOR_RANDOM_MOVE:
46+
board.push(random.choice(list(board.legal_moves)))
47+
else:
48+
result = await maxwell_engine.play(board, chess.engine.Limit(depth=config.DEPTH_PER_MOVE))
49+
board.push(result.move)
50+
51+
fen_strings.append(board.fen())
52+
53+
if board.fullmove_number >= config.MAX_MOVES:
54+
break
55+
56+
game_outcome = 0.0
57+
58+
# For some reason when it detects a threefold-repetition or a 50 move draw, it returns None instead of a draw :P
59+
if outcome := board.outcome():
60+
if outcome.winner == chess.WHITE:
61+
game_outcome = 1.0
62+
elif outcome.winner == chess.BLACK:
63+
game_outcome = -1.0
64+
65+
for fen in fen_strings:
66+
data_points.append(DataPoint(fen, game_outcome))
67+
68+
await maxwell_engine.quit()
69+
70+
71+
async def play_games():
72+
games_completed = 0
73+
74+
pending = {asyncio.create_task(play_game()) for _ in range(config.CONCURRENT_GAMES)}
75+
76+
while len(pending) > 0:
77+
print(f"Playing self-play games... {games_completed}/{config.GAMES_PER_MATCH}", end="\r", flush=True)
78+
79+
completed, pending = await asyncio.wait(pending, return_when=asyncio.FIRST_COMPLETED)
80+
81+
for completed_task in completed:
82+
games_completed += 1
83+
84+
if games_completed + len(pending) < config.GAMES_PER_MATCH:
85+
pending.add(asyncio.create_task(play_game()))
86+
87+
88+
if __name__ == "__main__":
89+
print("### MAXWELL NNUE TRAINER ###\n")
90+
91+
training_cycle = 0
92+
# nn.save_weights() # Save the initial randomized weights so that the program has the same weights as the trainer
93+
94+
while True:
95+
data_points = []
96+
97+
print(f"Training cycle {training_cycle + 1}:")
98+
99+
asyncio.run(play_games())
100+
101+
training_results.games += config.GAMES_PER_MATCH
102+
training_results.positions += len(data_points)
103+
104+
print("\nSelf-play done!\n")
105+
print("Training network...")
106+
107+
for epoch in range(config.EPOCHS_PER_TRAIN):
108+
print(f"Epoch {epoch + 1}...")
109+
110+
random.shuffle(data_points)
111+
112+
data_point_index = 0
113+
114+
while data_point_index < len(data_points):
115+
next_index = min(data_point_index + config.MINIBATCH_SIZE, len(data_points))
116+
117+
nn.back_prop(data_point_index, next_index, data_points[data_point_index:next_index])
118+
119+
data_point_index = next_index
120+
121+
print("Done training!")
122+
# print("Calculating total error...")
123+
# print(f"Total error on data set: {nn.get_total_error(data_points)}")
124+
# print("Done!\n")
125+
126+
print(f"Total games played: {training_results.games}")
127+
print(f"Total positions trained on: {training_results.positions}\n")
128+
129+
training_cycle += 1
130+
nn.save_weights()

nnue_trainer/matrix.py

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import random
2+
3+
class Matrix:
4+
def __init__(self, rows, cols):
5+
self.rows = rows
6+
self.cols = cols
7+
8+
self.data = []
9+
for row in range(rows):
10+
new_row = []
11+
for col in range(cols):
12+
new_row.append(0.0)
13+
self.data.append(new_row)
14+
15+
def from_2d_list(input_2d_list):
16+
result = Matrix(len(input_2d_list), len(input_2d_list[0]))
17+
result.data = input_2d_list
18+
return result
19+
20+
def fill_zeros(self):
21+
for row in range(self.rows):
22+
for col in range(self.cols):
23+
self.data[row][col] = 0.0
24+
25+
def flatten(self):
26+
result = []
27+
28+
for row in range(self.rows):
29+
for col in range(self.cols):
30+
result.append(self.data[row][col])
31+
32+
return result
33+
34+
def transpose(a):
35+
result = Matrix(a.cols, a.rows)
36+
37+
for row in range(a.rows):
38+
for col in range(a.cols):
39+
result.data[col][row] = a.data[row][col]
40+
41+
return result
42+
43+
def random(rows, cols):
44+
result = Matrix(rows, cols)
45+
46+
for row in range(rows):
47+
for col in range(cols):
48+
result.data[row][col] = random.uniform(-0.8, 0.8)
49+
50+
return result
51+
52+
def add(a, b):
53+
result = Matrix(a.rows, a.cols)
54+
55+
for row in range(a.rows):
56+
for col in range(a.cols):
57+
result.data[row][col] = a.data[row][col] + b.data[row][col]
58+
59+
return result
60+
61+
def subtract(a, b):
62+
result = Matrix(a.rows, a.cols)
63+
64+
for row in range(a.rows):
65+
for col in range(a.cols):
66+
result.data[row][col] = a.data[row][col] - b.data[row][col]
67+
68+
return result
69+
70+
def multiply(a, b):
71+
result = Matrix(a.rows, a.cols)
72+
73+
for row in range(a.rows):
74+
for col in range(a.cols):
75+
result.data[row][col] = a.data[row][col] * b.data[row][col]
76+
77+
return result
78+
79+
def divide(a, b):
80+
result = Matrix(a.rows, a.cols)
81+
82+
for row in range(a.rows):
83+
for col in range(a.cols):
84+
result.data[row][col] = a.data[row][col] / b.data[row][col]
85+
86+
return result
87+
88+
def divide_by_num(mat, num):
89+
result = Matrix(mat.rows, mat.cols)
90+
91+
for row in range(mat.rows):
92+
for col in range(mat.cols):
93+
result.data[row][col] = mat.data[row][col] / num
94+
95+
return result
96+
97+
def dot(a, b):
98+
result = Matrix(a.rows, b.cols)
99+
100+
for row in range(result.rows):
101+
for col in range(result.cols):
102+
sum_of_column = 0
103+
104+
for offset in range(a.cols):
105+
sum_of_column += a.data[row][offset] * b.data[offset][col]
106+
107+
result.data[row][col] = sum_of_column
108+
109+
return result
110+
111+
def scale(m, s):
112+
result = Matrix(m.rows, m.cols)
113+
114+
for row in range(result.rows):
115+
for col in range(result.cols):
116+
result.data[row][col] = m.data[row][col] * s
117+
118+
return result
119+
120+
def pow(m, e):
121+
result = Matrix(m.rows, m.cols)
122+
123+
for row in range(result.rows):
124+
for col in range(result.cols):
125+
result.data[row][col] = m.data[row][col] ** e
126+
127+
return result
128+
129+
def map(m, fn):
130+
for row in range(m.rows):
131+
for col in range(m.cols):
132+
m.data[row][col] = fn(m.data[row][col])
133+
return m

0 commit comments

Comments
 (0)