use rill_ml::feature_hasher::FeatureHasher;
use rill_ml::loss::BinaryLogLoss;
use rill_ml::metrics::{F1Score, LogLoss};
use rill_ml::models::{
FtrlClassifier, FtrlConfig, GaussianNaiveBayes, LogisticRegression, LogisticRegressionConfig,
};
use rill_ml::optim::{Optimizer, SgdConfig};
use rill_ml::{Metric, OnlineBinaryClassifier, SparseClassifier};
struct ClickEvent {
features: Vec<(&'static str, f64)>,
clicked: bool,
}
fn generate_data() -> Vec<ClickEvent> {
vec![
ClickEvent {
features: vec![
("user=alice", 1.0),
("device=mobile", 1.0),
("hour=morning", 1.0),
],
clicked: true,
},
ClickEvent {
features: vec![
("user=alice", 1.0),
("device=desktop", 1.0),
("hour=evening", 1.0),
],
clicked: true,
},
ClickEvent {
features: vec![
("user=bob", 1.0),
("device=mobile", 1.0),
("hour=afternoon", 1.0),
],
clicked: true,
},
ClickEvent {
features: vec![
("user=bob", 1.0),
("device=desktop", 1.0),
("hour=morning", 1.0),
],
clicked: false,
},
ClickEvent {
features: vec![
("user=charlie", 1.0),
("device=mobile", 1.0),
("hour=evening", 1.0),
],
clicked: false,
},
ClickEvent {
features: vec![
("user=charlie", 1.0),
("device=desktop", 1.0),
("hour=afternoon", 1.0),
],
clicked: false,
},
ClickEvent {
features: vec![
("user=alice", 1.0),
("device=mobile", 1.0),
("hour=afternoon", 1.0),
],
clicked: true,
},
ClickEvent {
features: vec![
("user=bob", 1.0),
("device=mobile", 1.0),
("hour=evening", 1.0),
],
clicked: true,
},
ClickEvent {
features: vec![
("user=charlie", 1.0),
("device=mobile", 1.0),
("hour=morning", 1.0),
],
clicked: false,
},
ClickEvent {
features: vec![
("user=alice", 1.0),
("device=desktop", 1.0),
("hour=afternoon", 1.0),
],
clicked: true,
},
ClickEvent {
features: vec![
("user=bob", 1.0),
("device=desktop", 1.0),
("hour=evening", 1.0),
],
clicked: false,
},
ClickEvent {
features: vec![
("user=charlie", 1.0),
("device=desktop", 1.0),
("hour=morning", 1.0),
],
clicked: false,
},
]
}
fn main() {
let data = generate_data();
let hasher = FeatureHasher::new(64, 42).unwrap();
let mut ftrl = FtrlClassifier::new(FtrlConfig {
alpha: 0.5,
beta: 1.0,
l1: 2.0,
l2: 0.5,
})
.unwrap();
let d = 64;
let log_reg = LogisticRegression::new(
d,
LogisticRegressionConfig {
optimizer: Optimizer::sgd(
d,
SgdConfig {
learning_rate: 0.1,
l2: 0.01,
},
)
.unwrap(),
loss: BinaryLogLoss::default(),
},
)
.unwrap();
let mut log_reg = log_reg;
let mut nb = GaussianNaiveBayes::new(d, Default::default()).unwrap();
let mut ftrl_f1 = F1Score::default();
let mut logreg_f1 = F1Score::default();
let mut nb_f1 = F1Score::default();
let mut ftrl_logloss = LogLoss::default();
let mut logreg_logloss = LogLoss::default();
let mut nb_logloss = LogLoss::default();
for event in &data {
let sparse = hasher.hash_strings(&event.features).unwrap();
let dense = hasher.transform(&sparse).unwrap();
let ftrl_proba = ftrl.predict_proba(&sparse).unwrap();
let ftrl_pred = ftrl_proba >= 0.5;
ftrl_f1.update(event.clicked, ftrl_pred).unwrap();
ftrl_logloss.update(event.clicked, ftrl_proba).unwrap();
let logreg_proba = log_reg.predict_proba(&dense).unwrap();
let logreg_pred = logreg_proba >= 0.5;
logreg_f1.update(event.clicked, logreg_pred).unwrap();
logreg_logloss.update(event.clicked, logreg_proba).unwrap();
let nb_proba = nb.predict_proba(&dense).unwrap();
let nb_pred = nb_proba >= 0.5;
nb_f1.update(event.clicked, nb_pred).unwrap();
nb_logloss.update(event.clicked, nb_proba).unwrap();
ftrl.learn(&sparse, event.clicked).unwrap();
log_reg.learn(&dense, event.clicked).unwrap();
nb.learn(&dense, event.clicked).unwrap();
}
println!("=== Click Prediction Results ({} samples) ===", data.len());
println!();
println!("{:<25} {:>10} {:>10}", "Model", "F1", "LogLoss");
println!("{:-<47}", "");
let f1_val = ftrl_f1.value().unwrap_or(0.0);
let ll_val = ftrl_logloss.value().unwrap_or(0.0);
println!("{:<25} {:>10.4} {:>10.4}", "FTRL (sparse)", f1_val, ll_val);
let f1_val = logreg_f1.value().unwrap_or(0.0);
let ll_val = logreg_logloss.value().unwrap_or(0.0);
println!(
"{:<25} {:>10.4} {:>10.4}",
"Logistic (hashed)", f1_val, ll_val
);
let f1_val = nb_f1.value().unwrap_or(0.0);
let ll_val = nb_logloss.value().unwrap_or(0.0);
println!(
"{:<25} {:>10.4} {:>10.4}",
"GaussianNB (hashed)", f1_val, ll_val
);
let weights = ftrl.weights();
let total_features = ftrl.feature_count();
let nonzero = weights.len();
println!();
println!("=== FTRL Sparsity ===");
println!("Total features seen: {}", total_features);
println!("Non-zero weights: {}", nonzero);
if total_features > 0 {
let sparsity = 1.0 - nonzero as f64 / total_features as f64;
println!("Sparsity ratio: {:.1}%", sparsity * 100.0);
}
println!();
println!("=== Non-zero FTRL Weights ===");
for (id, w) in &weights {
println!(" Feature {:<6}: {:>+.6}", id, w);
}
}