X 推荐算法分数修正机制详解
前言
在 X(原 Twitter)的推荐系统中,多模型融合后的分数并不是最终分数。融合分数会经过多个步骤的修正,包括启发式规则、多样性调整、Phoenix 重评分等。本文将详细解析这些分数修正机制。
一、分数修正流程概览
1.1 完整流程
┌─────────────────────────────────────────────────────────┐
│ 步骤1: 多模型融合 │
│ WeighedModelRerankingScorer │
│ 输出: WeightedModelScoreFeature = Σ(score_i × weight_i) │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 步骤2: 启发式重评分 │
│ HeuristicScorer │
│ 应用多个乘法因子调整分数 │
│ 输出: ScoreFeature = WeightedModelScore × scaleFactor │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 步骤3: Phoenix 重评分(可选) │
│ PhoenixRescoringFeatureHydrator │
│ 使用 Phoenix 分数调整 │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 步骤4: 多样性重评分(可选) │
│ DiversityRescoringFeatureHydrator │
│ 基于嵌入的多样性调整(MMR算法) │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 步骤5: 类别多样性重评分(可选) │
│ CategoryDiversityRescoringFeatureHydrator │
│ 基于类别的多样性调整 │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 最终分数(ScoreFeature) │
│ 用于排序推文 │
└─────────────────────────────────────────────────────────┘
1.2 关键发现
多模型融合后的分数不是最终分数! 它还会经过:
- 启发式重评分:应用多个业务规则
- Phoenix 重评分:使用 Phoenix 模型调整
- 多样性重评分:确保内容多样性
- 类别多样性重评分:确保类别多样性
二、启发式重评分(HeuristicScorer)
2.1 概述
HeuristicScorer 是第一个修正步骤,通过乘法因子调整融合后的分数。
2.2 核心算法
scala
val scaleFactor = rescorers.map(_(query, candidate)).product
val updatedScore = scoreOpt.map { score =>
if (score < Epsilon && noNegHeuristic) score
else score * scaleFactor
}
公式:
finalScore = weightedModelScore × scaleFactor
其中:
scaleFactor = rescore1 × rescore2 × ... × rescoreN
2.3 重评分规则列表
系统应用以下 20+ 个重评分规则:
2.3.1 基础重评分规则
| 规则名称 | 说明 | 影响 |
|---|---|---|
RescoreOutOfNetwork |
外部网络内容调整 | 降低外部网络推文分数 |
RescoreReplies |
回复内容调整 | 调整回复推文的分数 |
RescoreLiveContent |
直播内容调整 | 提升直播内容的分数 |
2.3.2 归一化规则
| 规则名称 | 说明 | 影响 |
|---|---|---|
RescoreMTLNormalization |
多任务学习归一化 | 使用 MTL 归一化器调整分数 |
MTL 归一化参数:
AlphaParam: Alpha 参数(除以 100)BetaParam: Beta 参数GammaParam: Gamma 参数
2.3.3 列表式重评分规则(Listwise Rescoring)
这些规则基于整个候选列表进行调整:
| 规则名称 | 说明 | 影响 |
|---|---|---|
ContentExplorationListwiseRescoringProvider |
内容探索列表式重评分 | 调整内容探索候选的分数 |
DeepRetrievalListwiseRescoringProvider |
深度检索列表式重评分 | 调整深度检索候选的分数 |
EvergreenDeepRetrievalListwiseRescoringProvider |
常青深度检索列表式重评分 | 调整常青深度检索候选的分数 |
EvergreenDeepRetrievalCrossBorderListwiseRescoringProvider |
跨境常青深度检索列表式重评分 | 调整跨境常青深度检索候选的分数 |
AuthorBasedListwiseRescoringProvider |
基于作者列表式重评分 | 调整作者多样性 |
ImpressedAuthorDecayRescoringProvider |
已读作者衰减列表式重评分 | 降低已印象作者的分数 |
ImpressedMediaClusterBasedListwiseRescoringProvider |
已读媒体聚类列表式重评分 | 降低已印象媒体聚类的分数 |
ImpressedImageClusterBasedListwiseRescoringProvider |
已读图片聚类列表式重评分 | 降低已印象图片聚类的分数 |
CandidateSourceDiversityListwiseRescoringProvider |
候选源多样性列表式重评分 | 调整候选源多样性 |
GrokSlopScoreRescorer |
Grok Slop 分数重评分 | 基于 Grok Slop 分数调整 |
MultimodalEmbeddingRescorer |
多模态嵌入重评分 | 基于多模态嵌入调整 |
2.3.4 其他重评分规则
| 规则名称 | 说明 | 影响 |
|---|---|---|
RescoreFeedbackFatigue |
反馈疲劳重评分 | 降低频繁反馈内容的分数 |
ControlAiRescorer.allRescorers |
AI 控制重评分器 | 各种 AI 控制规则 |
2.4 重评分规则的工作原理
每个重评分规则返回一个乘法因子(通常是 0.5 到 2.0 之间):
scala
trait RescoringFactorProvider {
def apply(query: PipelineQuery, candidate: CandidateWithFeatures[TweetCandidate]): Double
}
示例:
RescoreOutOfNetwork可能返回0.8(降低外部网络内容 20%)RescoreLiveContent可能返回1.2(提升直播内容 20%)
2.5 完整代码
scala
object HeuristicScorer extends Scorer[PipelineQuery, TweetCandidate] {
override def apply(
query: PipelineQuery,
candidates: Seq[CandidateWithFeatures[TweetCandidate]]
): Stitch[Seq[FeatureMap]] = {
val rescorers = Seq(
RescoreOutOfNetwork,
RescoreReplies,
RescoreMTLNormalization(...),
RescoreListwise(ContentExplorationListwiseRescoringProvider(...)),
// ... 其他重评分规则
RescoreLiveContent
) ++ ControlAiRescorer.allRescorers
val updatedScores = candidates.map { candidate =>
val scoreOpt = candidate.features.getOrElse(ScoreFeature, None)
// 计算所有重评分规则的乘积
val scaleFactor = rescorers.map(_(query, candidate)).product
// 应用乘法因子
val updatedScore = scoreOpt.map { score =>
if (score < Epsilon && noNegHeuristic) score
else score * scaleFactor
}
FeatureMap(ScoreFeature, updatedScore)
}
Stitch.value(updatedScores)
}
}
2.6 实际示例
假设一个推文的融合分数是 10.0,应用以下重评分规则:
| 规则 | 乘法因子 |
|---|---|
| RescoreOutOfNetwork | 0.8 |
| RescoreReplies | 1.1 |
| RescoreMTLNormalization | 0.95 |
| RescoreListwise(AuthorBased) | 1.05 |
| RescoreFeedbackFatigue | 0.9 |
计算:
scaleFactor = 0.8 × 1.1 × 0.95 × 1.05 × 0.9 = 0.79
finalScore = 10.0 × 0.79 = 7.9
三、Phoenix 重评分(PhoenixRescoringFeatureHydrator)
3.1 概述
PhoenixRescoringFeatureHydrator 使用 Phoenix 模型的分数来调整最终分数。
3.2 使用条件
scala
val usePhoenixRescoring =
query.params(EnablePhoenixScorerParam) &&
query.params(EnablePhoenixRescoreParam) &&
!query.params(EnablePhoenixScoreParam)
条件:
- 启用 Phoenix Scorer
- 启用 Phoenix 重评分
- 未启用 Phoenix 分数(如果已启用,则直接使用 Phoenix 分数)
3.3 重评分算法
scala
val score = candidate.features.getOrElse(ScoreFeature, None).getOrElse(0.0)
val weightedModelScore = candidate.features.getOrElse(WeightedModelScoreFeature, None).getOrElse(0.0)
val phoenixScore = candidate.features.getOrElse(PhoenixScoreFeature, None).getOrElse(0.0)
if (score == 0.0 || weightedModelScore == 0.0) {
0.0
} else if (usePhoenixRescoring) {
phoenixScore * (score / weightedModelScore)
} else {
score
}
公式:
finalScore = phoenixScore × (currentScore / weightedModelScore)
其中:
- currentScore: 当前分数(经过启发式重评分后)
- weightedModelScore: 原始融合分数
- phoenixScore: Phoenix 模型预测分数
3.4 工作原理
Phoenix 重评分通过比例缩放的方式调整分数:
- 计算当前分数相对于原始融合分数的比例
- 使用 Phoenix 分数乘以这个比例
示例:
- 原始融合分数:
10.0 - 启发式重评分后:
8.0(比例 = 0.8) - Phoenix 分数:
12.0 - 最终分数:
12.0 × 0.8 = 9.6
3.5 使用场景
- Phoenix 模型更准确:当 Phoenix 模型的预测更准确时
- A/B 测试:用于测试 Phoenix 模型的效果
- 渐进式迁移:从旧模型逐步迁移到 Phoenix 模型
四、多样性重评分(DiversityRescoringFeatureHydrator)
4.1 概述
DiversityRescoringFeatureHydrator 使用 MMR(Maximal Marginal Relevance)算法来确保内容的多样性。
4.2 MMR 算法
MMR 算法在相关性和多样性之间取得平衡:
score = relevance + diversityWeight × minDistance
其中:
- relevance: 当前候选的相关性分数(ScoreFeature)
- diversityWeight: 多样性权重
- minDistance: 与已选择候选的最小距离
4.3 算法实现
scala
def mmr(
query: PipelineQuery,
candidates: Seq[CandidateWithFeatures[TweetCandidate]],
distanceMatrix: DenseMatrix[Double]
): Seq[Double] = {
val diversityRatio = query.params(TwhinDiversityRescoringRatioParam)
val diversityWeight = query.params(TwhinDiversityRescoringWeightParam)
val selected = scala.collection.mutable.Set[Int]()
val newScores = Array.fill(n)(0.0)
for (i <- 0 until n) {
var maxScore = Double.NegativeInfinity
var bestCandidateIndex = -1
for ((candidate, index) <- candidatesWithIndex) {
if (!selected.contains(index)) {
val relevance = candidate.features.getOrElse(ScoreFeature, None).getOrElse(0.0)
val minDistance = {
if (selected.isEmpty || selected.size < (1 - diversityRatio) * n)
None
else
selected.map(j => distanceMatrix(index, j)).reduceOption(_ min _)
}
val score = relevance + diversityWeight * minDistance.getOrElse(2.0)
if (score > maxScore) {
maxScore = score
bestCandidateIndex = index
}
}
}
if (bestCandidateIndex != -1) {
selected += bestCandidateIndex
newScores(bestCandidateIndex) = maxScore
}
}
newScores.toSeq
}
4.4 距离计算
使用嵌入向量的欧氏距离:
scala
// 1. 获取嵌入向量
val embeddings = candidates.map { candidate =>
candidate.features
.getOrElse(TransformerPostEmbeddingJointBlueFeature, EmptyDataRecord)
.getTensors
.get(PostTransformerEmbeddingsJointBlueFeature.getFeatureId)
.getFloatTensor.floats
.map(_.doubleValue)
.toSeq
}
// 2. 归一化嵌入向量
val denseEmbeddingsNormalized = embeddings.map { seq =>
val denseVector = DenseVector(seq.toArray)
val normVal = norm(denseVector)
if (normVal != 0) denseVector / normVal
else defaultDenseVector
}
// 3. 计算距离矩阵
for (i <- denseEmbeddingsNormalized.indices; j <- denseEmbeddingsNormalized.indices) {
distanceMatrix(i, j) = norm(denseEmbeddingsNormalized(i) - denseEmbeddingsNormalized(j))
}
4.5 参数配置
| 参数名称 | 说明 | 默认值 |
|---|---|---|
TwhinDiversityRescoringWeightParam |
多样性权重 | 可配置 |
TwhinDiversityRescoringRatioParam |
多样性比例 | 可配置 |
4.6 工作流程
- 计算嵌入向量:获取每个候选的嵌入向量
- 归一化嵌入:归一化嵌入向量
- 计算距离矩阵:计算所有候选之间的欧氏距离
- MMR 选择:使用 MMR 算法重新计算分数
- 更新分数 :更新
ScoreFeature
4.7 实际示例
假设有 3 个候选推文:
| 候选 | 原始分数 | 嵌入向量 |
|---|---|---|
| A | 10.0 | [0.8, 0.6, ...] |
| B | 9.0 | [0.7, 0.5, ...] |
| C | 8.0 | [0.9, 0.7, ...] |
距离矩阵:
A B C
A 0.0 0.2 0.3
B 0.2 0.0 0.4
C 0.3 0.4 0.0
MMR 选择(假设 diversityWeight = 0.5):
- 选择 A(分数最高)
- 选择 C(虽然 B 分数更高,但 C 与 A 距离更远)
- 选择 B
最终分数:
- A:
10.0 + 0.5 × 0.0 = 10.0 - C:
8.0 + 0.5 × 0.3 = 8.15 - B:
9.0 + 0.5 × 0.2 = 9.1
五、类别多样性重评分(CategoryDiversityRescoringFeatureHydrator)
5.1 概述
CategoryDiversityRescoringFeatureHydrator 基于内容类别来确保多样性。
5.2 算法原理
使用惩罚机制来降低重复类别的分数:
score = max(relevance - weight × penalty, 0.00001)
其中:
penalty = Σ log2(count(category) + 1)
- relevance: 当前候选的相关性分数
- weight: 多样性权重
- count(category): 已选择候选中该类别的数量
5.3 算法实现
scala
private def diversityPenalty(
query: PipelineQuery,
candidates: Seq[CandidateWithFeatures[TweetCandidate]],
): Seq[Double] = {
val weight = query.params(CategoryDiversityRescoringWeightParam)
val k = query.params(CategoryDiversityKParam)
val selected = scala.collection.mutable.Set[Int]()
val newScores = Array.fill(n)(0.0)
val clusterCntMap = scala.collection.mutable.Map[String, Int]()
val topKCategories = getCandidateCategory(k, candidates)
for (i <- 0 until n) {
var maxScore = Double.NegativeInfinity
var bestCandidateIndex = -1
for ((candidate, index, categories) <- candidatesWithIndexWithCategories) {
if (!selected.contains(index)) {
val relevance = candidate.features.getOrElse(ScoreFeature, None).getOrElse(0.0)
// 计算惩罚
var penalty = 0.0
categories.foreach { category =>
val cnt = clusterCntMap.getOrElse(category, 0)
penalty += math.log(cnt + 1) / math.log(2) // log2
}
val score = math.max(relevance - weight * penalty, 0.00001)
if (score > maxScore) {
maxScore = score
bestCandidateIndex = index
candidateCategories = categories
}
}
}
if (bestCandidateIndex != -1) {
selected += bestCandidateIndex
newScores(bestCandidateIndex) = maxScore
// 更新类别计数
candidateCategories.foreach { category =>
clusterCntMap.put(category, clusterCntMap.getOrElse(category, 0) + 1)
}
}
}
newScores.toSeq
}
5.4 类别提取
从 SimClusters 特征中提取 Top-K 类别:
scala
private def getCandidateCategory(
k: Int,
candidates: Seq[CandidateWithFeatures[TweetCandidate]]
): Seq[Seq[String]] = {
candidates.map { candidate =>
val topKClustersMap = candidate.features
.getOrElse(SimClustersLogFavBasedTweetFeature, new DataRecord())
.getSparseContinuousFeatures
.get(SimclustersSparseTweetEmbeddingsFeature.getFeatureId)
topKClustersMap
.asScala
.toSeq
.sortBy(-_._2) // 按分数降序
.take(k)
.map(_._1) // 提取类别名称
}
}
5.5 参数配置
| 参数名称 | 说明 | 默认值 |
|---|---|---|
CategoryDiversityRescoringWeightParam |
多样性权重 | 可配置 |
CategoryDiversityKParam |
Top-K 类别数 | 可配置 |
5.6 实际示例
假设有 3 个候选推文:
| 候选 | 原始分数 | 类别 |
|---|---|---|
| A | 10.0 | [sports, tech] |
| B | 9.0 | [sports, news] |
| C | 8.0 | [tech, science] |
选择过程(假设 weight = 0.5):
-
选择 A(分数最高)
- 分数:
10.0 - 0.5 × (log2(1) + log2(1)) = 10.0 - 0.5 × (0 + 0) = 10.0 - 更新计数:sports=1, tech=1
- 分数:
-
选择 C(虽然 B 分数更高,但类别更不同)
- B 惩罚:
0.5 × (log2(2) + log2(1)) = 0.5 × (1 + 0) = 0.5,分数 =9.0 - 0.5 = 8.5 - C 惩罚:
0.5 × (log2(2) + log2(1)) = 0.5 × (1 + 0) = 0.5,分数 =8.0 - 0.5 = 7.5 - 选择 B(分数更高)
- 更新计数:sports=2, news=1
- B 惩罚:
-
选择 C
- 惩罚:
0.5 × (log2(3) + log2(2)) = 0.5 × (1.58 + 1) = 1.29,分数 =8.0 - 1.29 = 6.71
- 惩罚:
六、分数修正流程总结
6.1 完整流程
原始融合分数 (WeightedModelScoreFeature)
↓
启发式重评分 (HeuristicScorer)
↓ ScoreFeature = WeightedModelScore × scaleFactor
Phoenix 重评分 (可选)
↓ ScoreFeature = PhoenixScore × (ScoreFeature / WeightedModelScore)
多样性重评分 (可选)
↓ ScoreFeature = MMR算法重新计算
类别多样性重评分 (可选)
↓ ScoreFeature = ScoreFeature - penalty
最终分数 (ScoreFeature)
6.2 分数修正的影响
| 修正步骤 | 影响范围 | 主要目的 |
|---|---|---|
| 启发式重评分 | 所有候选 | 应用业务规则 |
| Phoenix 重评分 | 所有候选 | 使用更准确的模型 |
| 多样性重评分 | 所有候选 | 确保内容多样性(嵌入) |
| 类别多样性重评分 | 所有候选 | 确保类别多样性 |
6.3 修正顺序的重要性
修正顺序很重要,因为:
- 启发式重评分:应用基础业务规则
- Phoenix 重评分:基于启发式重评分后的分数
- 多样性重评分:基于前面的分数,使用 MMR 算法
- 类别多样性重评分:基于前面的分数,使用惩罚机制
6.4 实际示例
假设一个推文的完整修正过程:
步骤1:多模型融合
WeightedModelScoreFeature = 10.0
步骤2:启发式重评分
scaleFactor = 0.8 × 1.1 × 0.95 = 0.836
ScoreFeature = 10.0 × 0.836 = 8.36
步骤3:Phoenix 重评分(假设启用)
PhoenixScore = 12.0
ScoreFeature = 12.0 × (8.36 / 10.0) = 12.0 × 0.836 = 10.032
步骤4:多样性重评分(假设启用)
使用 MMR 算法重新计算
ScoreFeature = 9.5(假设)
步骤5:类别多样性重评分(假设启用)
penalty = 0.3
ScoreFeature = 9.5 - 0.3 = 9.2
最终分数 :9.2
七、关键代码位置
7.1 启发式重评分
- 文件 :
home-mixer/server/src/main/scala/com/twitter/home_mixer/product/scored_tweets/scorer/HeuristicScorer.scala - 关键方法 :
apply
7.2 Phoenix 重评分
- 文件 :
home-mixer/server/src/main/scala/com/twitter/home_mixer/functional_component/feature_hydrator/PhoenixRescoringFeatureHydrator.scala - 关键方法 :
apply
7.3 多样性重评分
- 文件 :
home-mixer/server/src/main/scala/com/twitter/home_mixer/functional_component/feature_hydrator/DiversityRescoringFeatureHydrator.scala - 关键方法 :
mmr
7.4 类别多样性重评分
- 文件 :
home-mixer/server/src/main/scala/com/twitter/home_mixer/functional_component/feature_hydrator/CategoryDiversityRescoringFeatureHydrator.scala - 关键方法 :
diversityPenalty
八、总结
8.1 核心要点
- 多模型融合后的分数不是最终分数
- 启发式重评分:应用 20+ 个业务规则
- Phoenix 重评分:使用 Phoenix 模型调整
- 多样性重评分:使用 MMR 算法确保多样性
- 类别多样性重评分:使用惩罚机制确保类别多样性
8.2 修正公式总结
最终分数 = 融合分数
× 启发式因子1 × 启发式因子2 × ... × 启发式因子N
× (PhoenixScore / WeightedModelScore) [可选]
× MMR调整 [可选]
- 类别惩罚 [可选]
8.3 设计理念
- 模块化:每个修正步骤独立,易于调整
- 可配置:通过 Feature Switches 控制是否启用
- 灵活性:支持 A/B 测试和渐进式迁移
- 多样性:确保推荐结果的多样性