143 lines
4.3 KiB
Rust
143 lines
4.3 KiB
Rust
// 衰减模型校准 — 简单线性回归拟合 decay_rate
|
|
// 每月深度整合时运行:抽样 → 按类别分组 → 回归 → 更新 decay_rate
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
use std::collections::HashMap;
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct DecaySample {
|
|
pub category: String,
|
|
pub days_old: f64,
|
|
pub current_score: f64,
|
|
pub original_score: f64,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct CalibrationResult {
|
|
pub rates: HashMap<String, f64>,
|
|
pub r_squared: HashMap<String, f64>,
|
|
pub sample_count: usize,
|
|
}
|
|
|
|
pub struct DecayCalibrator {
|
|
max_deviation: f64,
|
|
min_samples_per_category: usize,
|
|
}
|
|
|
|
impl DecayCalibrator {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
max_deviation: 0.5,
|
|
min_samples_per_category: 5,
|
|
}
|
|
}
|
|
|
|
pub fn calibrate(
|
|
&self,
|
|
samples: &[DecaySample],
|
|
old_rates: &HashMap<String, f64>,
|
|
) -> CalibrationResult {
|
|
let mut by_category: HashMap<String, Vec<&DecaySample>> = HashMap::new();
|
|
for s in samples {
|
|
by_category
|
|
.entry(s.category.clone())
|
|
.or_insert_with(Vec::new)
|
|
.push(s);
|
|
}
|
|
|
|
let mut new_rates = HashMap::new();
|
|
let mut r_squared = HashMap::new();
|
|
|
|
for (category, cat_samples) in &by_category {
|
|
if cat_samples.len() < self.min_samples_per_category {
|
|
if let Some(&old) = old_rates.get(category) {
|
|
new_rates.insert(category.clone(), old);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// 简单 OLS: log(score) = a + b * days_old
|
|
// decay_rate = -b
|
|
let mut xs = Vec::new();
|
|
let mut ys = Vec::new();
|
|
for s in cat_samples {
|
|
if s.days_old > 0.0 && s.current_score > 0.0 {
|
|
let normalized = s.current_score / s.original_score.max(0.01);
|
|
xs.push(s.days_old);
|
|
ys.push(normalized.max(0.001).ln());
|
|
}
|
|
}
|
|
|
|
let (slope, r2) = simple_ols(&xs, &ys);
|
|
|
|
let raw_rate = (-slope).max(0.0).abs();
|
|
let old = old_rates.get(category).copied().unwrap_or(raw_rate);
|
|
let rate = if old > 0.0 {
|
|
let ratio = raw_rate / old;
|
|
if ratio > 1.0 + self.max_deviation {
|
|
old * (1.0 + self.max_deviation)
|
|
} else if ratio < 1.0 - self.max_deviation {
|
|
old * (1.0 - self.max_deviation)
|
|
} else {
|
|
raw_rate
|
|
}
|
|
} else {
|
|
raw_rate
|
|
};
|
|
|
|
new_rates.insert(category.clone(), rate);
|
|
r_squared.insert(category.clone(), r2);
|
|
}
|
|
|
|
let result = CalibrationResult {
|
|
rates: new_rates,
|
|
r_squared,
|
|
sample_count: samples.len(),
|
|
};
|
|
|
|
eprintln!(
|
|
"[decay] calibrated {} categories from {} samples",
|
|
result.rates.len(),
|
|
result.sample_count,
|
|
);
|
|
result
|
|
}
|
|
}
|
|
|
|
/// 简单 OLS 线性回归: y = a + b*x
|
|
fn simple_ols(xs: &[f64], ys: &[f64]) -> (f64, f64) {
|
|
let n = xs.len() as f64;
|
|
if n < 2.0 { return (0.0, 0.0); }
|
|
|
|
let sum_x: f64 = xs.iter().sum();
|
|
let sum_y: f64 = ys.iter().sum();
|
|
let sum_xy: f64 = xs.iter().zip(ys.iter()).map(|(x, y)| x * y).sum();
|
|
let sum_xx: f64 = xs.iter().map(|x| x * x).sum();
|
|
|
|
let denom = n * sum_xx - sum_x * sum_x;
|
|
if denom.abs() < 1e-10 { return (0.0, 0.0); }
|
|
|
|
let slope = (n * sum_xy - sum_x * sum_y) / denom;
|
|
let intercept = (sum_y - slope * sum_x) / n;
|
|
|
|
// R²
|
|
let mean_y = sum_y / n;
|
|
let ss_res: f64 = xs.iter().zip(ys.iter())
|
|
.map(|(x, y)| (y - (intercept + slope * x)).powi(2))
|
|
.sum();
|
|
let ss_tot: f64 = ys.iter().map(|y| (y - mean_y).powi(2)).sum();
|
|
let r2 = if ss_tot > 0.0 { 1.0 - ss_res / ss_tot } else { 0.0 };
|
|
|
|
(slope, r2.max(0.0).min(1.0))
|
|
}
|
|
|
|
pub fn default_decay_rates() -> HashMap<String, f64> {
|
|
let mut rates = HashMap::new();
|
|
rates.insert("system_fact".into(), 0.003);
|
|
rates.insert("user_pref".into(), 0.005);
|
|
rates.insert("proj_context".into(), 0.008);
|
|
rates.insert("tool_usage".into(), 0.010);
|
|
rates.insert("code_snippet".into(), 0.012);
|
|
rates
|
|
}
|