Skip to content

Commit e789394

Browse files
fix: properly scaling eval and perspective (pr #65)
bench: 1583604
2 parents a9140a2 + 9cb9c06 commit e789394

File tree

6 files changed

+139
-67
lines changed

6 files changed

+139
-67
lines changed

src/bin/hce-tuner/epd_parser.rs

Lines changed: 37 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use std::{
55

66
use anyhow::{Result, anyhow};
77
use chess::{bitboard_helpers, board::Board, pieces::Piece, side::Side};
8-
use engine::psqt::GAMEPHASE_INC;
8+
use engine::{hce_values::GAME_PHASE_MAX, psqt::GAMEPHASE_INC};
99

1010
use crate::{offsets::Offsets, tuning_position::TuningPosition};
1111

@@ -77,25 +77,20 @@ fn parse_epd_line(line: &str) -> Result<TuningPosition> {
7777
}
7878
}
7979

80-
let stm = match board.side_to_move() {
81-
Side::White => 1f64,
82-
Side::Black => -1f64,
83-
Side::Both => panic!("Side to move cannot be both."),
84-
};
80+
let is_white_relative = matches!(game_result, 0.0 | 0.5 | 1.0);
8581

86-
let result = match game_result {
87-
// if we have an exact result, this indicates that we parsed a "book" file
88-
// not an epd with centipawn evaluation
89-
0.0 | 0.5 | 1.0 => game_result,
90-
// otherwise, adjust based on the side to move
91-
_ => match board.side_to_move() {
82+
let result = if is_white_relative {
83+
game_result
84+
} else {
85+
match board.side_to_move() {
9286
Side::White => game_result,
9387
Side::Black => 1.0 - game_result,
9488
Side::Both => panic!("Side to move cannot be both."),
95-
},
89+
}
9690
};
9791

98-
let tuning_pos = TuningPosition::new(w_indexes, b_indexes, phase, result, stm);
92+
let scaled_phase = phase as f64 / (GAME_PHASE_MAX as f64);
93+
let tuning_pos = TuningPosition::new(w_indexes, b_indexes, scaled_phase, result);
9994

10095
Ok(tuning_pos)
10196
}
@@ -148,7 +143,7 @@ fn get_game_result(part: &str) -> Result<f64> {
148143
#[cfg(test)]
149144
mod tests {
150145
use chess::{board::Board, side::Side};
151-
use engine::{evaluation::ByteKnightEvaluation, traits::Eval};
146+
use engine::{evaluation::ByteKnightEvaluation, hce_values::GAME_PHASE_MAX, traits::Eval};
152147

153148
use crate::{
154149
epd_parser::{get_game_result, process_epd_line},
@@ -208,20 +203,31 @@ mod tests {
208203
"r2q1rk1/ppp1npbp/4b1p1/1P3nN1/2Pp4/3P4/PB1NBPPP/R2QR1K1 b - - 0 1 [0.0]",
209204
];
210205

211-
const EXPECTED_GAME_PHASES: [usize; 10] = [7, 18, 12, 10, 10, 8, 17, 20, 5, 24];
206+
let mut expected_game_phases: [f64; 10] = [7., 18., 12., 10., 10., 8., 17., 20., 5., 24.];
207+
for phase in &mut expected_game_phases {
208+
*phase /= GAME_PHASE_MAX as f64;
209+
}
210+
212211
const EXPECTED_GAME_RESULTS: [f64; 10] = [0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0];
213212
let eval = ByteKnightEvaluation::default();
214213
let params = Parameters::create_from_engine_values();
215214

216215
let parsed_results = test_epd_lines(&epd_lines);
217216

218217
for (i, (position, board, result)) in parsed_results.iter().enumerate() {
219-
assert_eq!(position.phase, EXPECTED_GAME_PHASES[i]);
218+
assert_eq!(position.phase, expected_game_phases[i]);
220219
assert_eq!(position.game_result, EXPECTED_GAME_RESULTS[i]);
221220
assert_eq!(*result, EXPECTED_GAME_RESULTS[i]);
222221
// also verify that the evaluation matches
223222
let expected_value = eval.eval(board);
224-
let val = position.evaluate(&params);
223+
224+
// tuning position evaluation is always from white's perspective
225+
let val = match board.side_to_move() {
226+
Side::White => position.evaluate(&params),
227+
Side::Black => -position.evaluate(&params),
228+
Side::Both => panic!("Side to move cannot be both."),
229+
};
230+
225231
println!("{} // {}", expected_value, val);
226232
assert!((expected_value.0 as f64 - val).abs().round() <= 1.0)
227233
}
@@ -265,7 +271,13 @@ mod tests {
265271
assert_eq!(position.game_result, expected_game_result);
266272
assert_eq!(*result, EXPECTED_PARSED_GAME_RESULTS[i]);
267273
let expected_value = eval.eval(board);
268-
let val = position.evaluate(&params);
274+
275+
// tuning position evaluation is always from white's perspective
276+
let val = match board.side_to_move() {
277+
Side::White => position.evaluate(&params),
278+
Side::Black => -position.evaluate(&params),
279+
Side::Both => panic!("Side to move cannot be both."),
280+
};
269281
println!("{} // {}", expected_value, val);
270282
assert!((expected_value.0 as f64 - val).abs().round() <= 1.0)
271283
}
@@ -299,7 +311,12 @@ mod tests {
299311
assert_eq!(position.game_result, EXPECTED_PARSED_GAME_RESULTS[i]);
300312
assert_eq!(*result, EXPECTED_PARSED_GAME_RESULTS[i]);
301313
let expected_value = eval.eval(board);
302-
let val = position.evaluate(&params);
314+
// tuning position evaluation is always from white's perspective
315+
let val = match board.side_to_move() {
316+
Side::White => position.evaluate(&params),
317+
Side::Black => -position.evaluate(&params),
318+
Side::Both => panic!("Side to move cannot be both."),
319+
};
303320
println!("{} // {}", expected_value, val);
304321
assert!((expected_value.0 as f64 - val).abs().round() <= 1.0)
305322
}

src/bin/hce-tuner/main.rs

Lines changed: 82 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
1-
use std::process::exit;
2-
31
use chess::{
42
definitions::NumberOf,
53
pieces::{ALL_PIECES, PIECE_NAMES},
64
};
7-
use clap::Parser;
5+
use clap::{Parser, Subcommand, ValueEnum};
86
use indicatif::ParallelProgressIterator;
97
use parameters::Parameters;
108
use rayon::iter::{IndexedParallelIterator, IntoParallelIterator, ParallelIterator};
119
use textplots::{Chart, Plot, Shape};
1210
use tuner::Tuner;
1311
use tuner_score::TuningScore;
12+
use tuning_position::TuningPosition;
1413
mod epd_parser;
1514
mod math;
1615
mod offsets;
@@ -22,17 +21,42 @@ mod tuning_position;
2221
#[derive(Parser, Debug)]
2322
#[command(version, about="Texel tuner for HCE in byte-knight", long_about=None)]
2423
struct Options {
25-
#[clap(short, long, help = "Filterd, marked EPD input data.")]
26-
input_data: String,
27-
#[clap(short, long, help = "Number of epochs to run.")]
28-
epochs: Option<usize>,
29-
#[clap(
30-
long,
31-
action,
32-
default_value_t = false,
33-
help = "Plot k versus error for the given dataset"
34-
)]
35-
plot_k: bool,
24+
#[command(subcommand)]
25+
command: Command,
26+
}
27+
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug)]
28+
enum ParameterStartType {
29+
Zero,
30+
EngineValues,
31+
PieceValues,
32+
}
33+
34+
const INPUT_DATA_HELP: &str = "Filtered, marked EPD or 'book' input data.";
35+
#[derive(Subcommand, Debug)]
36+
enum Command {
37+
Tune {
38+
#[clap(short, long, help = INPUT_DATA_HELP)]
39+
input_data: String,
40+
#[clap(short, long, help = "Number of epochs to run.")]
41+
epochs: Option<usize>,
42+
#[arg(value_enum, short, long, help = "How to start the parameters", default_value_t = ParameterStartType::Zero)]
43+
param_start_type: ParameterStartType,
44+
},
45+
PlotK {
46+
#[clap(short, long, help = INPUT_DATA_HELP)]
47+
input_data: String,
48+
},
49+
ComputeError {
50+
#[clap(short, long, help = INPUT_DATA_HELP)]
51+
input_data: String,
52+
#[clap(
53+
short,
54+
long,
55+
help = "k value to compute error for (0.009)",
56+
default_value_t = 0.009
57+
)]
58+
k: f64,
59+
},
3660
}
3761

3862
fn print_table(indent: usize, table: &[TuningScore]) {
@@ -69,8 +93,8 @@ fn print_params(params: &Parameters) {
6993

7094
fn plot_k(tuner: &Tuner) {
7195
let mut points = Vec::new();
72-
let data_point_count = 10_000;
73-
let k_min = -0.1;
96+
let data_point_count = 1_000;
97+
let k_min = 0.;
7498
let k_max = 0.1;
7599
(0..data_point_count)
76100
.into_par_iter()
@@ -84,26 +108,52 @@ fn plot_k(tuner: &Tuner) {
84108

85109
Chart::new(180, 60, k_min as f32, k_max as f32)
86110
.lineplot(&Shape::Points(points.as_slice()))
87-
.display();
111+
.nice();
88112
}
89113

90-
fn main() {
91-
let options = Options::parse();
92-
println!("Reading data from: {}", options.input_data);
93-
let positions = epd_parser::parse_epd_file(options.input_data.as_str());
114+
fn parse_data(input_data: &str) -> Vec<TuningPosition> {
115+
println!("Reading data from: {}", input_data);
116+
let positions = epd_parser::parse_epd_file(input_data);
94117
// let positions = get_positions();
95118
println!("Read {} positions", positions.len());
119+
positions
120+
}
96121

97-
let epochs = options.epochs.unwrap_or(10_000);
98-
let parameters = Parameters::create_from_engine_values();
99-
let mut tuner = tuner::Tuner::new(parameters, &positions, epochs);
100-
101-
if options.plot_k {
102-
plot_k(&tuner);
103-
exit(0);
122+
fn main() {
123+
let options = Options::parse();
124+
match options.command {
125+
Command::Tune {
126+
input_data,
127+
epochs,
128+
param_start_type,
129+
} => {
130+
let positions = parse_data(&input_data);
131+
let parameters = match param_start_type {
132+
ParameterStartType::Zero => Parameters::default(),
133+
ParameterStartType::EngineValues => Parameters::create_from_engine_values(),
134+
ParameterStartType::PieceValues => Parameters::create_from_piece_values(),
135+
};
136+
let epchs = epochs.unwrap_or(10_000);
137+
println!(
138+
"Tuning parameters from {:?} for {} epochs",
139+
param_start_type, epchs
140+
);
141+
let mut tuner = tuner::Tuner::new(parameters, &positions, epchs);
142+
let tuned_results = tuner.tune();
143+
print_params(tuned_results);
144+
}
145+
Command::PlotK { input_data } => {
146+
let positions = parse_data(&input_data);
147+
let parameters = Parameters::create_from_engine_values();
148+
let tuner = tuner::Tuner::new(parameters, &positions, 10_000);
149+
plot_k(&tuner);
150+
}
151+
Command::ComputeError { input_data, k } => {
152+
let positions = parse_data(&input_data);
153+
let parameters = Parameters::create_from_engine_values();
154+
let tuner = tuner::Tuner::new(parameters, &positions, 10_000);
155+
let error = tuner.mean_square_error(k);
156+
println!("Error for k {:.8}: {:.8}", k, error);
157+
}
104158
}
105-
106-
let tuned_result = tuner.tune();
107-
108-
print_params(tuned_result);
109159
}

src/bin/hce-tuner/parameters.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,7 @@ impl Parameters {
6969
let sigmoid_result = math::sigmoid(k * point.evaluate(self));
7070
let term =
7171
(point.game_result - sigmoid_result) * (1. - sigmoid_result) * sigmoid_result;
72-
let phase_adjustment =
73-
term * TuningScore::new(point.phase as f64, 1f64 - point.phase as f64);
72+
let phase_adjustment = term * TuningScore::new(point.phase, 1. - point.phase);
7473

7574
for idx in &point.parameter_indexes[Side::White as usize] {
7675
gradient[*idx] += phase_adjustment;

src/bin/hce-tuner/tuner.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ impl<'a> Tuner<'a> {
3232
pub(crate) fn tune(&mut self) -> &Parameters {
3333
println!("Computing optimal K value...");
3434
let computed_k: f64 = self.compute_k();
35-
println!("Optimal K value: {}", computed_k);
35+
println!("Optimal K value: {:.8}", computed_k);
3636
println!("Using {} positions", self.positions.len());
3737

3838
for epoch in 1..=self.max_epochs {
@@ -102,7 +102,7 @@ impl<'a> Tuner<'a> {
102102

103103
/// Computes the optimal K value to minimize the error of the initial parameters.
104104
/// Taken from https://github.com/jw1912/hce-tuner/
105-
fn compute_k(&self) -> f64 {
105+
pub(crate) fn compute_k(&self) -> f64 {
106106
let mut k = 0.009;
107107
let delta = 0.00001;
108108
let goal = 0.000001;

src/bin/hce-tuner/tuner_score.rs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,15 @@ impl TuningScore {
2929
Self::new(self.mg().sqrt(), self.eg().sqrt())
3030
}
3131

32-
pub fn taper(&self, phase: f64, max_phase: f64) -> f64 {
33-
let mg_phase = phase.min(max_phase);
34-
let eg_phase = max_phase - mg_phase;
35-
(self.mg() * mg_phase + self.eg() * eg_phase) / max_phase
32+
/// Taper the score based on the phase of the game.
33+
///
34+
/// # Arguments
35+
/// * `phase` - The current phase of the game. This should already be scaled to the range [0, max_phase].
36+
///
37+
/// # Returns
38+
/// The tapered score.
39+
pub fn taper(&self, phase: f64) -> f64 {
40+
self.mg() * phase + self.eg() * (1. - phase)
3641
}
3742
}
3843

src/bin/hce-tuner/tuning_position.rs

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,34 @@
11
use chess::{definitions::NumberOf, side::Side};
2-
use engine::hce_values::GAME_PHASE_MAX;
32

43
use crate::{math, parameters::Parameters, tuner_score::TuningScore};
54

65
pub(crate) struct TuningPosition {
76
pub(crate) parameter_indexes: [Vec<usize>; NumberOf::SIDES],
8-
pub(crate) phase: usize,
7+
pub(crate) phase: f64,
98
pub(crate) game_result: f64,
10-
pub(crate) side_to_move: f64,
119
}
1210

1311
impl TuningPosition {
1412
pub(crate) fn new(
1513
white_indexes: Vec<usize>,
1614
black_indexes: Vec<usize>,
17-
phase: usize,
15+
phase: f64,
1816
game_result: f64,
19-
side_to_move: f64,
2017
) -> Self {
2118
// Side::White == 0, Side::Black == 1
2219
let parameter_indexes = [white_indexes, black_indexes];
2320
Self {
2421
parameter_indexes,
2522
phase,
2623
game_result,
27-
side_to_move,
2824
}
2925
}
3026

27+
/// Evaluate the tuning position based on the given parameters from white's perspective.
28+
/// # Arguments
29+
/// * `parameters` - The parameters to evaluate.
30+
/// # Returns
31+
/// The evaluated score from white's perspective.
3132
pub(crate) fn evaluate(&self, parameters: &Parameters) -> f64 {
3233
let mut score: TuningScore = Default::default();
3334

@@ -39,7 +40,7 @@ impl TuningPosition {
3940
score -= parameters[idx];
4041
}
4142

42-
score.taper(self.phase as f64, GAME_PHASE_MAX as f64) * self.side_to_move
43+
score.taper(self.phase)
4344
}
4445

4546
pub(crate) fn error(&self, k: f64, params: &Parameters) -> f64 {

0 commit comments

Comments
 (0)