Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -332,14 +332,17 @@ 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))
```

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.
There is a common misconception to be aware of about the weights: they scale the predicted probabilities (or predicted continuous values, e.g. dwell time) — they do *not* scale the raw engagement counts, so e.g. it'd be incorrect to see that a report has 468 times higher weight than a like and conclude that e.g. "1 report cancels out 468 likes". The weights are a multiple on your own predicted probability of Liking, Reporting, etc, which is substantially driven by your own behavior.

Three adjustments follow:
Expand Down
2 changes: 1 addition & 1 deletion home-mixer/params/param.rs
Original file line number Diff line number Diff line change
Expand Up @@ -477,7 +477,7 @@ param!(
ValueModelMode,
String,
"rust_home_mixer_value_model_mode",
"weighted"
"gated_dwell_regret"
);
param!(
DwellRegretTemperature,
Expand Down
97 changes: 97 additions & 0 deletions home-mixer/scorers/ranking_scorer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1502,6 +1502,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(
Expand Down Expand Up @@ -1705,4 +1796,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);
}
}