From ea04c1518980587e46cb9a9ff0e316fb830c73ab Mon Sep 17 00:00:00 2001 From: cmdr-chara Date: Fri, 14 Aug 2026 13:40:49 +0200 Subject: [PATCH] Default to gated dwell-regret ranking --- README.md | 6 +- home-mixer/params/param.rs | 2 +- home-mixer/scorers/ranking_scorer.rs | 97 ++++++++++++++++++++++++++++ 3 files changed, 103 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ca947bf9..566f577f 100644 --- a/README.md +++ b/README.md @@ -321,7 +321,9 @@ Author follow author Negative not interested · mute author · block author · report · not dwelled ``` -`RankingScorer` combines them: +`RankingScorer` has two configurable objectives. The default, `gated_dwell_regret`, uses the existing value-model gate to choose between the established weighted score and a satisfaction-oriented score. The latter starts from predicted dwell time, raises posts with stronger predicted favorite, reply, repost, quote, and share actions relative to the request's candidate set, and exponentially reduces posts with predicted not-interested, block, mute, or report feedback. If the gate configuration is invalid, scoring falls back to the weighted objective. Setting `rust_home_mixer_value_model_mode` to `weighted` provides an explicit rollback; `dwell_regret_sigmoid` selects the satisfaction-oriented score without the gate. + +The weighted objective combines the action heads as: ``` Final Score = Σ (weight_i × P(action_i)) @@ -329,6 +331,8 @@ Final Score = Σ (weight_i × P(action_i)) Positive actions carry positive weights, negative actions negative ones. The weights are in [`home-mixer/params/param.rs`](home-mixer/params/param.rs); the arithmetic is in [`home-mixer/scorers/ranking_scorer.rs`](home-mixer/scorers/ranking_scorer.rs). +This change does not claim to optimize survey responses, retention, session-level outcomes, or other unavailable long-term labels. It only changes how existing per-post dwell, positive-action, and negative-feedback predictions are combined, and its positive-action normalization is relative to the candidates in the current request. + Three adjustments follow: - **Author Diversity**: each post after an author's first is multiplied by a decaying factor, down to a floor. diff --git a/home-mixer/params/param.rs b/home-mixer/params/param.rs index dc3a12e4..8d9d36d8 100644 --- a/home-mixer/params/param.rs +++ b/home-mixer/params/param.rs @@ -451,7 +451,7 @@ param!( ValueModelMode, String, "rust_home_mixer_value_model_mode", - "weighted" + "gated_dwell_regret" ); param!( DwellRegretTemperature, diff --git a/home-mixer/scorers/ranking_scorer.rs b/home-mixer/scorers/ranking_scorer.rs index 6dfcb7aa..7f5f586f 100644 --- a/home-mixer/scorers/ranking_scorer.rs +++ b/home-mixer/scorers/ranking_scorer.rs @@ -1473,6 +1473,97 @@ mod tests { assert!(scores[1] < scores[0]); } + #[test] + fn dwell_regret_high_negative_feedback_overrides_shallow_engagement() { + let shallow_high_engagement = dr_candidate( + 1, + PhoenixScores { + favorite_score: Some(0.9), + reply_score: Some(0.8), + retweet_score: Some(0.7), + quote_score: Some(0.6), + share_score: Some(0.5), + share_via_dm_score: Some(0.4), + share_via_copy_link_score: Some(0.3), + report_score: Some(0.1), + dwell_time: Some(1.0), + ..Default::default() + }, + ); + let clean_sustained = dr_candidate( + 2, + PhoenixScores { + favorite_score: Some(0.05), + dwell_time: Some(20.0), + ..Default::default() + }, + ); + + let scores = RankingScorer::compute_dwell_regret_base_scores( + &dr_weights(), + &[shallow_high_engagement, clean_sustained], + ); + + assert!( + scores[0] < 1e-20, + "report risk must dominate shallow engagement: {}", + scores[0] + ); + assert!( + scores[1] > scores[0], + "clean sustained candidate {} must outrank high-risk candidate {}", + scores[1], + scores[0] + ); + } + + #[test] + fn dwell_regret_rewards_sustained_dwell_and_positive_actions() { + let sustained_positive = dr_candidate( + 1, + PhoenixScores { + favorite_score: Some(0.3), + reply_score: Some(0.1), + dwell_time: Some(30.0), + ..Default::default() + }, + ); + let sustained_neutral = dr_candidate( + 2, + PhoenixScores { + favorite_score: Some(0.03), + reply_score: Some(0.01), + dwell_time: Some(30.0), + ..Default::default() + }, + ); + let shallow_neutral = dr_candidate( + 3, + PhoenixScores { + favorite_score: Some(0.03), + reply_score: Some(0.01), + dwell_time: Some(2.0), + ..Default::default() + }, + ); + + let scores = RankingScorer::compute_dwell_regret_base_scores( + &dr_weights(), + &[sustained_positive, sustained_neutral, shallow_neutral], + ); + + assert!( + scores[0] > scores[1], + "positive actions should lift equal dwell: {:?}", + scores + ); + assert!( + scores[1] > scores[2], + "sustained dwell should beat shallow dwell at equal actions: {:?}", + scores + ); + } + #[test] fn dwell_regret_floor_applies_to_low_dwell() { let none_dwell = dr_candidate( @@ -1676,4 +1767,10 @@ mod tests { "weighted-mode score should be small: {weighted}" ); } + + #[test] + fn default_value_model_uses_the_gated_satisfaction_path() { + let query = query_with_flags(&[]); + assert_eq!(query.params.get(ValueModelMode), GATED_DWELL_REGRET_MODE); + } }