ranker: implement weight and bias
This commit is contained in:
parent
f899fba8ad
commit
9087daa205
1 changed files with 102 additions and 12 deletions
|
@ -65,6 +65,7 @@ fn recalculate_ratings(db_conn: &PgConnection) -> QueryResult<()> {
|
||||||
db::ratings::set_rating(bot_id, rating, db_conn).expect("could not update bot rating");
|
db::ratings::set_rating(bot_id, rating, db_conn).expect("could not update bot rating");
|
||||||
}
|
}
|
||||||
let elapsed = Instant::now() - start;
|
let elapsed = Instant::now() - start;
|
||||||
|
// TODO: set up proper logging infrastructure
|
||||||
println!("computed ratings in {} ms", elapsed.subsec_millis());
|
println!("computed ratings in {} ms", elapsed.subsec_millis());
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -157,11 +158,14 @@ fn estimate_ratings_from_stats(match_stats: HashMap<(i32, i32), MatchStats>) ->
|
||||||
p1_ix: player_tokenizer.tokenize(a_id),
|
p1_ix: player_tokenizer.tokenize(a_id),
|
||||||
p2_ix: player_tokenizer.tokenize(b_id),
|
p2_ix: player_tokenizer.tokenize(b_id),
|
||||||
score: stats.total_score / stats.num_matches as f64,
|
score: stats.total_score / stats.num_matches as f64,
|
||||||
|
weight: stats.num_matches as f64,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut ratings = vec![0f64; player_tokenizer.player_count()];
|
let mut ratings = vec![0f64; player_tokenizer.player_count()];
|
||||||
optimize_ratings(&mut ratings, &input_records);
|
// TODO: fetch these from config
|
||||||
|
let params = OptimizeRatingsParams::default();
|
||||||
|
optimize_ratings(&mut ratings, &input_records, ¶ms);
|
||||||
|
|
||||||
ratings
|
ratings
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
@ -182,21 +186,43 @@ struct RatingInputRecord {
|
||||||
p2_ix: usize,
|
p2_ix: usize,
|
||||||
/// score of player 1 (= 1 - score of player 2)
|
/// score of player 1 (= 1 - score of player 2)
|
||||||
score: f64,
|
score: f64,
|
||||||
|
/// weight of this record
|
||||||
|
weight: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn optimize_ratings(ratings: &mut [f64], input_records: &[RatingInputRecord]) {
|
struct OptimizeRatingsParams {
|
||||||
// TODO: group this in a params struct
|
tolerance: f64,
|
||||||
let tolerance = 10f64.powi(-6);
|
learning_rate: f64,
|
||||||
let learning_rate = 0.1;
|
max_iterations: usize,
|
||||||
let max_iterations = 10000;
|
regularization_weight: f64,
|
||||||
|
}
|
||||||
|
|
||||||
for _iteration in 0..max_iterations {
|
impl Default for OptimizeRatingsParams {
|
||||||
|
fn default() -> Self {
|
||||||
|
OptimizeRatingsParams {
|
||||||
|
tolerance: 10f64.powi(-8),
|
||||||
|
learning_rate: 0.1,
|
||||||
|
max_iterations: 10_000,
|
||||||
|
regularization_weight: 10.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn optimize_ratings(
|
||||||
|
ratings: &mut [f64],
|
||||||
|
input_records: &[RatingInputRecord],
|
||||||
|
params: &OptimizeRatingsParams,
|
||||||
|
) {
|
||||||
|
let total_weight =
|
||||||
|
params.regularization_weight + input_records.iter().map(|r| r.weight).sum::<f64>();
|
||||||
|
|
||||||
|
for _iteration in 0..params.max_iterations {
|
||||||
let mut gradients = vec![0f64; ratings.len()];
|
let mut gradients = vec![0f64; ratings.len()];
|
||||||
|
|
||||||
// calculate gradients
|
// calculate gradients
|
||||||
for record in input_records.iter() {
|
for record in input_records.iter() {
|
||||||
let predicted = sigmoid(ratings[record.p1_ix] - ratings[record.p2_ix]);
|
let predicted = sigmoid(ratings[record.p1_ix] - ratings[record.p2_ix]);
|
||||||
let gradient = predicted - record.score;
|
let gradient = record.weight * (predicted - record.score);
|
||||||
gradients[record.p1_ix] += gradient;
|
gradients[record.p1_ix] += gradient;
|
||||||
gradients[record.p2_ix] -= gradient;
|
gradients[record.p2_ix] -= gradient;
|
||||||
}
|
}
|
||||||
|
@ -204,8 +230,9 @@ fn optimize_ratings(ratings: &mut [f64], input_records: &[RatingInputRecord]) {
|
||||||
// apply update step
|
// apply update step
|
||||||
let mut converged = true;
|
let mut converged = true;
|
||||||
for (rating, gradient) in ratings.iter_mut().zip(&gradients) {
|
for (rating, gradient) in ratings.iter_mut().zip(&gradients) {
|
||||||
let update = learning_rate * gradient / input_records.len() as f64;
|
let update = params.learning_rate * (gradient + params.regularization_weight * *rating)
|
||||||
if update > tolerance {
|
/ total_weight;
|
||||||
|
if update > params.tolerance {
|
||||||
converged = false;
|
converged = false;
|
||||||
}
|
}
|
||||||
*rating -= update;
|
*rating -= update;
|
||||||
|
@ -221,16 +248,79 @@ fn optimize_ratings(ratings: &mut [f64], input_records: &[RatingInputRecord]) {
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
fn is_close(a: f64, b: f64) -> bool {
|
||||||
|
(a - b).abs() < 10f64.powi(-6)
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_optimize_ratings() {
|
fn test_optimize_ratings() {
|
||||||
let input_records = vec![RatingInputRecord {
|
let input_records = vec![RatingInputRecord {
|
||||||
p1_ix: 0,
|
p1_ix: 0,
|
||||||
p2_ix: 1,
|
p2_ix: 1,
|
||||||
score: 0.8,
|
score: 0.8,
|
||||||
|
weight: 1.0,
|
||||||
}];
|
}];
|
||||||
|
|
||||||
let mut ratings = vec![0.0; 2];
|
let mut ratings = vec![0.0; 2];
|
||||||
optimize_ratings(&mut ratings, &input_records);
|
optimize_ratings(
|
||||||
assert!(sigmoid(ratings[0] - ratings[1]) - 0.8 < 10f64.powi(-6));
|
&mut ratings,
|
||||||
|
&input_records,
|
||||||
|
&OptimizeRatingsParams {
|
||||||
|
regularization_weight: 0.0,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
assert!(is_close(sigmoid(ratings[0] - ratings[1]), 0.8));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_optimize_ratings_weight() {
|
||||||
|
let input_records = vec![
|
||||||
|
RatingInputRecord {
|
||||||
|
p1_ix: 0,
|
||||||
|
p2_ix: 1,
|
||||||
|
score: 1.0,
|
||||||
|
weight: 1.0,
|
||||||
|
},
|
||||||
|
RatingInputRecord {
|
||||||
|
p1_ix: 1,
|
||||||
|
p2_ix: 0,
|
||||||
|
score: 1.0,
|
||||||
|
weight: 3.0,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut ratings = vec![0.0; 2];
|
||||||
|
optimize_ratings(
|
||||||
|
&mut ratings,
|
||||||
|
&input_records,
|
||||||
|
&OptimizeRatingsParams {
|
||||||
|
regularization_weight: 0.0,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
assert!(is_close(sigmoid(ratings[0] - ratings[1]), 0.25));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_optimize_ratings_regularization() {
|
||||||
|
let input_records = vec![RatingInputRecord {
|
||||||
|
p1_ix: 0,
|
||||||
|
p2_ix: 1,
|
||||||
|
score: 0.8,
|
||||||
|
weight: 100.0,
|
||||||
|
}];
|
||||||
|
|
||||||
|
let mut ratings = vec![0.0; 2];
|
||||||
|
optimize_ratings(
|
||||||
|
&mut ratings,
|
||||||
|
&input_records,
|
||||||
|
&OptimizeRatingsParams {
|
||||||
|
regularization_weight: 1.0,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
let predicted = sigmoid(ratings[0] - ratings[1]);
|
||||||
|
assert!(0.5 < predicted && predicted < 0.8);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue