X 推荐算法多模型融合机制详解
目录
- [X 推荐算法多模型融合机制详解](#X 推荐算法多模型融合机制详解)
-
- 前言
- 一、多模型融合概述
-
- [1.1 为什么需要多模型?](#1.1 为什么需要多模型?)
- [1.2 融合流程概览](#1.2 融合流程概览)
- [二、15 个预测模型详解](#二、15 个预测模型详解)
-
- [2.1 模型列表](#2.1 模型列表)
- [2.2 模型分类](#2.2 模型分类)
-
- [2.2.1 正向参与模型(14个)](#2.2.1 正向参与模型(14个))
- [2.2.2 负向参与模型(1个)](#2.2.2 负向参与模型(1个))
- [2.3 模型预测分数](#2.3 模型预测分数)
- 三、权重配置系统
-
- [3.1 权重参数](#3.1 权重参数)
- [3.2 权重特点](#3.2 权重特点)
- [3.3 权重类型](#3.3 权重类型)
-
- [3.3.1 正权重](#3.3.1 正权重)
- [3.3.2 负权重](#3.3.2 负权重)
- [3.4 权重来源](#3.4 权重来源)
- 四、模型分数计算
-
- [4.1 基础分数提取](#4.1 基础分数提取)
- [4.2 模型资格检查](#4.2 模型资格检查)
-
- [4.2.1 视频质量观看模型](#4.2.1 视频质量观看模型)
- [4.2.2 停留时间模型](#4.2.2 停留时间模型)
- [4.3 分数阈值处理](#4.3 分数阈值处理)
-
- [4.3.1 视频质量观看阈值](#4.3.1 视频质量观看阈值)
- [五、按 Head 归一化](#五、按 Head 归一化)
-
- [5.1 什么是 Head?](#5.1 什么是 Head?)
- [5.2 归一化的目的](#5.2 归一化的目的)
- [5.3 归一化算法](#5.3 归一化算法)
- [5.4 归一化示例](#5.4 归一化示例)
- 六、加权聚合算法
-
- [6.1 基础聚合公式](#6.1 基础聚合公式)
- [6.2 完整聚合算法](#6.2 完整聚合算法)
- [6.3 正权重处理](#6.3 正权重处理)
- [6.4 负权重处理](#6.4 负权重处理)
-
- [6.4.1 负权重过滤](#6.4.1 负权重过滤)
- [6.4.2 负权重排序](#6.4.2 负权重排序)
- [6.5 分数归一化](#6.5 分数归一化)
- 七、完整融合流程
-
- [7.1 流程步骤](#7.1 流程步骤)
- [7.2 代码实现](#7.2 代码实现)
- 八、实际示例
-
- [8.1 示例场景](#8.1 示例场景)
- [8.2 计算过程](#8.2 计算过程)
- [8.3 负反馈示例](#8.3 负反馈示例)
- 九、高级特性
-
- [9.1 模型偏置(Bias)](#9.1 模型偏置(Bias))
- [9.2 模型去偏(Debias)](#9.2 模型去偏(Debias))
- [9.3 动态权重](#9.3 动态权重)
- 十、调试与监控
-
- [10.1 调试信息](#10.1 调试信息)
- [10.2 统计监控](#10.2 统计监控)
- 十一、最佳实践
-
- [11.1 权重配置原则](#11.1 权重配置原则)
- [11.2 负权重使用](#11.2 负权重使用)
- [11.3 模型资格](#11.3 模型资格)
- 十二、总结
-
- [12.1 核心要点](#12.1 核心要点)
- [12.2 融合公式](#12.2 融合公式)
- [12.3 关键文件](#12.3 关键文件)
- 附录:完整代码流程
-
- [A. 模型分数计算](#A. 模型分数计算)
- [B. 按Head归一化](#B. 按Head归一化)
- [C. 加权聚合](#C. 加权聚合)
前言
X(原 Twitter)的推荐系统使用了 15 个机器学习模型 来预测用户对不同推文的参与度。这些模型分别预测不同的用户行为(如点赞、回复、转发等),然后通过加权融合的方式组合成一个最终分数用于排序。本文将深入解析这个多模型融合的完整机制。
一、多模型融合概述
1.1 为什么需要多模型?
单一模型难以同时准确预测所有用户行为。X 采用多模型策略:
- 专业化:每个模型专注于预测一种特定的用户行为
- 准确性:专业化模型通常比通用模型更准确
- 灵活性:可以独立调整每个模型的重要性(权重)
1.2 融合流程概览
┌─────────────────────────────────────────────────────────┐
│ 15个预测模型 │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ... │
│ │ 点赞模型 │ │ 回复模型 │ │ 转发模型 │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ ↓ ↓ ↓ │
│ 预测分数1 预测分数2 预测分数3 │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 权重配置(Feature Switches) │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ... │
│ │ 权重1 │ │ 权重2 │ │ 权重3 │ │
│ └──────────┘ └──────────┘ └──────────┘ │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 按Head归一化(可选) │
│ 计算每个Head的最大分数,用于归一化 │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 加权聚合 │
│ finalScore = Σ(score_i × weight_i) │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 最终分数(ScoreFeature) │
│ 用于排序推文 │
└─────────────────────────────────────────────────────────┘
二、15 个预测模型详解
2.1 模型列表
X 推荐系统使用以下 15 个预测模型:
| 序号 | 模型名称 | 预测目标 | 统计名称 |
|---|---|---|---|
| 1 | PredictedFavoriteScoreFeature |
点赞概率 | fav |
| 2 | PredictedReplyScoreFeature |
回复概率 | reply |
| 3 | PredictedRetweetScoreFeature |
转发概率 | retweet |
| 4 | PredictedReplyEngagedByAuthorScoreFeature |
作者参与回复概率 | reply_engaged_by_author |
| 5 | PredictedGoodClickConvoDescFavoritedOrRepliedScoreFeature |
优质点击(V1) | click_engaged |
| 6 | PredictedGoodClickConvoDescUamGt2ScoreFeature |
优质点击(V2) | click_dwell |
| 7 | PredictedGoodProfileClickScoreFeature |
优质个人资料点击 | good_profile_click |
| 8 | PredictedVideoQualityViewScoreFeature |
视频质量观看 | vqv |
| 9 | PredictedVideoQualityViewImmersiveScoreFeature |
沉浸式视频质量观看 | vqv_immersive |
| 10 | PredictedBookmarkScoreFeature |
收藏概率 | bookmark |
| 11 | PredictedShareScoreFeature |
分享概率 | share |
| 12 | PredictedDwellScoreFeature |
停留时间 | dwell |
| 13 | PredictedVideoQualityWatchScoreFeature |
视频质量观看(完整) | video_quality_watched |
| 14 | PredictedVideoWatchTimeScoreFeature |
视频观看时长 | video_watch_time_ms |
| 15 | PredictedNegativeFeedbackV2ScoreFeature |
负反馈概率 | negative_feedback_v2 |
2.2 模型分类
2.2.1 正向参与模型(14个)
预测用户可能产生的正向行为:
- 基础交互:点赞、回复、转发
- 深度参与:作者参与回复、优质点击、个人资料点击
- 内容消费:视频观看、停留时间、收藏、分享
- 视频特定:视频质量观看、沉浸式观看、观看时长
2.2.2 负向参与模型(1个)
预测用户可能产生的负向行为:
- 负反馈 :
PredictedNegativeFeedbackV2ScoreFeature
注意 :负反馈模型使用负权重,用于过滤低质量内容。
2.3 模型预测分数
每个模型输出一个 0-1 之间的概率分数,表示用户产生该行为的可能性。
例如:
PredictedFavoriteScoreFeature = 0.85表示用户有 85% 的概率点赞PredictedNegativeFeedbackV2ScoreFeature = 0.3表示用户有 30% 的概率产生负反馈
三、权重配置系统
3.1 权重参数
每个模型都有一个对应的权重参数,定义在 HomeGlobalParams.scala 中:
scala
object ModelWeights {
object FavParam extends FSBoundedParam[Double](
name = "home_mixer_model_weight_fav",
default = 0.0,
min = -10000.0,
max = 10000.0
)
object ReplyParam extends FSBoundedParam[Double](...)
object RetweetParam extends FSBoundedParam[Double](...)
// ... 其他权重参数
}
3.2 权重特点
- 动态配置:通过 Feature Switches 动态调整,无需重新部署
- 默认值为 0.0:新模型默认不参与融合,需要显式配置权重
- 范围限制 :权重范围
[-10000.0, 10000.0] - 支持负权重:负反馈模型使用负权重
3.3 权重类型
3.3.1 正权重
用于正向参与模型,增加最终分数:
scala
// 示例配置
home_mixer_model_weight_fav = 1.5
home_mixer_model_weight_reply = 2.0
home_mixer_model_weight_retweet = 1.8
3.3.2 负权重
用于负反馈模型,降低最终分数:
scala
// 示例配置
home_mixer_model_weight_negative_feedback_v2 = -3.0
负权重的作用:
- 当负反馈概率高时,降低推文的最终分数
- 可以用于过滤低质量内容
3.4 权重来源
权重可以从两个地方获取:
- Feature Switches :从
HomeGlobalParams中读取(静态配置) - Query Features:从查询特征中读取(动态配置,支持个性化)
代码实现:
scala
val weight = query.features
.flatMap(_.get(predictedScoreFeature.weightQueryFeature))
.getOrElse(0.0)
四、模型分数计算
4.1 基础分数提取
对于每个候选推文,系统提取所有模型的预测分数:
scala
def computeModelScores(
query: PipelineQuery,
candidate: CandidateWithFeatures[TweetCandidate],
modelStatsOpt: Option[ModelStats] = None
): Seq[(Double, Double)] = {
PredictedScoreFeatures.map { predictedScoreFeature =>
// 1. 提取预测分数
val predictedScoreOpt = predictedScoreFeature.extractScore(
candidate.features,
query
)
// 2. 获取权重
val weight = query.features
.flatMap(_.get(predictedScoreFeature.weightQueryFeature))
.getOrElse(0.0)
// 3. 获取偏置(可选)
val bias = predictedScoreFeature.biasQueryFeature
.flatMap(feature => query.features.flatMap(_.get(feature)))
.getOrElse(0.0)
// 4. 计算最终分数
val score = if (predictedScoreFeature.isEligible(candidate.features, query))
predictedScoreOpt.getOrElse(0.0)
else
bias
(score, weight)
}
}
4.2 模型资格检查
某些模型只在特定条件下参与融合:
4.2.1 视频质量观看模型
scala
object PredictedVideoQualityViewScoreFeature extends PredictedScoreFeature {
override def isEligible(
features: FeatureMap,
query: PipelineQuery
): Boolean = {
val isTenSecondsLogicEnabled = query.params(EnableTenSecondsLogicForVQV)
val isVideoDurationGte10Seconds =
(features.getOrElse(VideoDurationMsFeature, None).getOrElse(0) / 1000.0) >= 10
val hasVideoFeature = features.getOrElse(HasVideoFeature, false)
hasVideoFeature && (!isTenSecondsLogicEnabled || isVideoDurationGte10Seconds)
}
}
条件:
- 推文必须有视频
- 如果启用 10 秒逻辑,视频时长必须 >= 10 秒
4.2.2 停留时间模型
scala
object PredictedDwellScoreFeature extends PredictedScoreFeature {
override def isEligible(
features: FeatureMap,
query: PipelineQuery
): Boolean = {
// 如果启用了 DwellOrVQV,且满足 VQV 条件,则不使用 Dwell
val isEligibleForVqv = ...
!(query.params(EnableDwellOrVQVParam) && isEligibleForVqv)
}
}
条件:
- 如果启用了
DwellOrVQV且满足视频质量观看条件,则使用 VQV 而不是 Dwell
4.3 分数阈值处理
某些模型支持阈值过滤:
4.3.1 视频质量观看阈值
scala
override def extractScore(features: FeatureMap, query: PipelineQuery): Some[Double] = {
val vqvScore = features.getOrElse(this, None).getOrElse(0.0)
// 如果分数低于阈值,返回 0
if (vqvScore < query.params(ScoreThresholdForVQVParam)) {
Some(0.0)
}
// 如果启用二进制方案,返回常量
else if (query.params(EnableBinarySchemeForVQVParam)) {
Some(query.params(BinarySchemeConstantForVQVParam))
}
// 否则返回原始分数
else {
Some(vqvScore)
}
}
五、按 Head 归一化
5.1 什么是 Head?
在推荐系统中,Head 指的是一个预测任务。每个模型预测一个 Head,15 个模型对应 15 个 Head。
5.2 归一化的目的
不同模型的分数可能在不同的数值范围内,直接加权融合可能导致某些模型主导结果。归一化可以:
- 平衡不同模型的影响:确保每个模型对最终分数的贡献在合理范围内
- 处理分数尺度差异:某些模型的分数可能普遍较高或较低
5.3 归一化算法
scala
def getScoresWithPerHeadMax(
scoresAndWeightsSeq: Seq[Seq[(Double, Double)]]
): Seq[Seq[(Double, Double, Double)]] = {
if (scoresAndWeightsSeq.isEmpty) Seq.empty
else {
// Step 1: 转置分数,按 Head 分组
val headsScores: Seq[Seq[Double]] = scoresAndWeightsSeq.transpose.map { headScores =>
headScores.map { case (score, _) => score }
}
// Step 2: 计算每个 Head 的最大分数
val headMaxScores: Seq[Double] = headsScores.map(_.max).toIndexedSeq
// Step 3: 为每个候选添加对应的最大分数
scoresAndWeightsSeq.map { candidateScores =>
candidateScores.zipWithIndex.map {
case ((score, weight), headIdx) =>
val headMaxScore = headMaxScores(headIdx)
(score, headMaxScore, weight)
}
}
}
}
5.4 归一化示例
假设有 3 个候选推文和 2 个模型:
原始分数:
候选1: (模型1=0.8, 模型2=0.3)
候选2: (模型1=0.6, 模型2=0.5)
候选3: (模型1=0.4, 模型2=0.7)
计算每个 Head 的最大值:
- Head1 (模型1): max(0.8, 0.6, 0.4) = 0.8
- Head2 (模型2): max(0.3, 0.5, 0.7) = 0.7
归一化后的结果:
候选1: (score=0.8, maxHead=0.8, weight=w1), (score=0.3, maxHead=0.7, weight=w2)
候选2: (score=0.6, maxHead=0.8, weight=w1), (score=0.5, maxHead=0.7, weight=w2)
候选3: (score=0.4, maxHead=0.8, weight=w1), (score=0.7, maxHead=0.7, weight=w2)
注意:归一化分数在后续的聚合步骤中使用。
六、加权聚合算法
6.1 基础聚合公式
最终分数通过加权求和计算:
finalScore = Σ(score_i × weight_i) + Epsilon
其中:
score_i:第 i 个模型的预测分数weight_i:第 i 个模型的权重Epsilon = 0.001:防止分数为 0 的小常数
6.2 完整聚合算法
scala
def aggregateWeightedScores(
query: PipelineQuery,
scoresAndWeights: Seq[(Double, Double, Double)], // (score, maxHeadScore, weight)
negativeFilterCounter: Counter
): Double = {
// 1. 计算加权分数和
val combinedScoreSum: Double = {
scoresAndWeights.foldLeft(0.0) {
case (combinedScore, (score, maxHeadScore, weight)) =>
if (weight >= 0.0 || useWeightForNeg) {
// 正权重:直接加权求和
combinedScore + score * weight
} else {
// 负权重:应用过滤逻辑
val normScore = if (maxHeadScore == 0.0) 0.0 else score / maxHeadScore
val negFilterNorm = enableNegNormalized && normScore > thresholdNegativeNormalized
val negFilterConstant = enableNegConstant && score > thresholdNegative
if (negFilterNorm || negFilterConstant) {
negativeFilterCounter.incr()
if (negSectionRanking) {
// 负权重排序:使用调整后的分数
combinedScore + weight * (1.0 min (score + 0.1))
} else {
// 负权重过滤:只添加权重值(通常是负数)
combinedScore + weight
}
} else {
combinedScore
}
}
}
}
// 2. 计算权重和
val (_, _, modelWeights) = scoresAndWeights.unzip3
val positiveModelWeightsSum = modelWeights.filter(_ > 0.0).sum
val negativeModelWeightsSum = modelWeights.filter(_ < 0).sum.abs
val modelWeightsSum = positiveModelWeightsSum + negativeModelWeightsSum
// 3. 归一化(可选)
val weightedScoresSum =
if (modelWeightsSum == 0)
combinedScoreSum.max(0.0)
else if (combinedScoreSum < 0)
(combinedScoreSum + negativeModelWeightsSum) / modelWeightsSum * Epsilon
else
combinedScoreSum + Epsilon
weightedScoresSum
}
6.3 正权重处理
对于正权重(weight >= 0),直接加权求和:
scala
combinedScore + score * weight
示例:
- 点赞分数 = 0.8,权重 = 1.5 → 贡献 = 0.8 × 1.5 = 1.2
- 回复分数 = 0.6,权重 = 2.0 → 贡献 = 0.6 × 2.0 = 1.2
- 总贡献 = 1.2 + 1.2 = 2.4
6.4 负权重处理
负权重用于过滤低质量内容,有两种处理方式:
6.4.1 负权重过滤
当负反馈分数超过阈值时,应用负权重惩罚:
scala
val normScore = if (maxHeadScore == 0.0) 0.0 else score / maxHeadScore
val negFilterNorm = enableNegNormalized && normScore > thresholdNegativeNormalized
val negFilterConstant = enableNegConstant && score > thresholdNegative
if (negFilterNorm || negFilterConstant) {
// 应用负权重惩罚
combinedScore + weight // weight 是负数
}
示例:
- 负反馈分数 = 0.7,阈值 = 0.5,权重 = -3.0
- 因为 0.7 > 0.5,应用惩罚:贡献 = -3.0
- 最终分数会降低 3.0
6.4.2 负权重排序
如果启用 EnableNegSectionRankingParam,使用调整后的分数:
scala
combinedScore + weight * (1.0 min (score + 0.1))
目的:允许负权重参与排序,而不是完全过滤。
6.5 分数归一化
在某些情况下,系统会对最终分数进行归一化:
scala
if (combinedScoreSum < 0) {
// 如果总分为负,进行归一化
(combinedScoreSum + negativeModelWeightsSum) / modelWeightsSum * Epsilon
} else {
// 否则直接添加 Epsilon
combinedScoreSum + Epsilon
}
目的:
- 防止分数为 0
- 处理负分数情况
七、完整融合流程
7.1 流程步骤
┌─────────────────────────────────────────────────────────┐
│ 步骤1: 模型预测 │
│ 15个模型分别预测用户行为概率 │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 步骤2: 提取分数和权重 │
│ computeModelScores() │
│ - 提取每个模型的预测分数 │
│ - 获取对应的权重 │
│ - 检查模型资格 │
│ 输出: Seq[(score, weight)] │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 步骤3: 按Head归一化(可选) │
│ getScoresWithPerHeadMax() │
│ - 计算每个Head的最大分数 │
│ - 为每个候选添加maxHeadScore │
│ 输出: Seq[(score, maxHeadScore, weight)] │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 步骤4: 加权聚合 │
│ aggregateWeightedScores() │
│ - 正权重:直接加权求和 │
│ - 负权重:应用过滤逻辑 │
│ - 添加 Epsilon 防止为 0 │
│ 输出: finalScore │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 步骤5: 存储最终分数 │
│ - ScoreFeature = finalScore │
│ - WeightedModelScoreFeature = finalScore │
│ - DebugStringFeature = 调试信息 │
└─────────────────────────────────────────────────────────┘
7.2 代码实现
scala
override def apply(
query: PipelineQuery,
candidates: Seq[CandidateWithFeatures[TweetCandidate]]
): Stitch[Seq[FeatureMap]] = {
// 步骤1-2: 计算所有候选的模型分数和权重
val scoresAndWeightsSeq = candidates.map(
computeModelScores(query, _, Some(modelStats))
)
// 步骤3: 按Head归一化
val transformedScoresAndWeightsSeq =
getScoresWithPerHeadMax(scoresAndWeightsSeq)
// 步骤4-5: 聚合并存储
val featureMaps = transformedScoresAndWeightsSeq.map { transformedScores =>
val finalScore = aggregateWeightedScores(
query,
transformedScores,
modelStats.negativeFilterCounter
)
FeatureMapBuilder()
.add(ScoreFeature, Some(finalScore))
.add(WeightedModelScoreFeature, Some(finalScore))
.add(DebugStringFeature, Some(updatedDebugStr))
.build()
}
Stitch.value(featureMaps)
}
八、实际示例
8.1 示例场景
假设有一个推文候选,15 个模型的预测分数如下:
| 模型 | 预测分数 | 权重 | 贡献 |
|---|---|---|---|
| 点赞 | 0.85 | 1.5 | 0.85 × 1.5 = 1.275 |
| 回复 | 0.60 | 2.0 | 0.60 × 2.0 = 1.200 |
| 转发 | 0.45 | 1.8 | 0.45 × 1.8 = 0.810 |
| 作者参与回复 | 0.30 | 1.2 | 0.30 × 1.2 = 0.360 |
| 优质点击(V1) | 0.70 | 1.0 | 0.70 × 1.0 = 0.700 |
| 优质点击(V2) | 0.65 | 1.0 | 0.65 × 1.0 = 0.650 |
| 个人资料点击 | 0.40 | 0.8 | 0.40 × 0.8 = 0.320 |
| 视频质量观看 | 0.75 | 1.5 | 0.75 × 1.5 = 1.125 |
| 沉浸式视频观看 | 0.50 | 1.2 | 0.50 × 1.2 = 0.600 |
| 收藏 | 0.25 | 0.5 | 0.25 × 0.5 = 0.125 |
| 分享 | 0.20 | 0.5 | 0.20 × 0.5 = 0.100 |
| 停留时间 | 0.55 | 1.0 | 0.55 × 1.0 = 0.550 |
| 视频质量观看(完整) | 0.80 | 1.5 | 0.80 × 1.5 = 1.200 |
| 视频观看时长 | 0.70 | 1.0 | 0.70 × 1.0 = 0.700 |
| 负反馈 | 0.10 | -2.0 | 0.10 < 阈值,不应用 |
8.2 计算过程
步骤1:计算加权和
combinedScoreSum = 1.275 + 1.200 + 0.810 + 0.360 + 0.700 + 0.650
+ 0.320 + 1.125 + 0.600 + 0.125 + 0.100 + 0.550
+ 1.200 + 0.700
= 9.715
步骤2:添加 Epsilon
finalScore = 9.715 + 0.001 = 9.716
步骤3:存储
scala
ScoreFeature = 9.716
WeightedModelScoreFeature = 9.716
8.3 负反馈示例
如果负反馈分数较高:
| 模型 | 预测分数 | 权重 | 处理 |
|---|---|---|---|
| 负反馈 | 0.70 | -3.0 | 0.70 > 阈值(0.5),应用惩罚 |
计算:
combinedScoreSum = 9.715 + (-3.0) = 6.715
finalScore = 6.715 + 0.001 = 6.716
负反馈导致最终分数降低了 3.0。
九、高级特性
9.1 模型偏置(Bias)
某些模型支持偏置(Bias),用于调整模型分数:
scala
val bias = predictedScoreFeature.biasQueryFeature
.flatMap(feature => query.features.flatMap(_.get(feature)))
.getOrElse(0.0)
val score = if (predictedScoreFeature.isEligible(...))
predictedScoreOpt.getOrElse(0.0)
else
bias // 如果不满足条件,使用偏置值
使用场景:
- 当模型不满足条件时(如没有视频),使用偏置值而不是 0
- 可以用于补偿某些特殊情况
9.2 模型去偏(Debias)
某些模型支持去偏(Debias),用于减少模型偏差:
scala
override val modelDebiasParam = Some(ModelDebiases.FavParam)
override val debiasQueryFeatureName = Some(
RecapFeatures.DEBIAS_IS_FAVORITED.getFeatureName
)
目的:
- 减少模型预测的系统性偏差
- 提高公平性
9.3 动态权重
权重可以从查询特征中动态获取,支持:
- 个性化权重:不同用户可以使用不同的权重
- A/B 测试:不同实验组使用不同的权重配置
- 实时调整:根据实时反馈调整权重
十、调试与监控
10.1 调试信息
系统会生成调试字符串,包含主要贡献者:
scala
def computeDebugMetadata(
debugStr: String,
featureNames: Seq[String],
transformedScores: Seq[(Double, Double, Double)],
finalScore: Double
): String = {
val contributions: Seq[(String, Double)] = featureNames
.zip(transformedScores)
.collect {
case (feature, (score, _, weight)) if weight >= 0 =>
(feature, score * weight)
}
val topContributors: Seq[String] = contributions.collect {
case (name, contrib) if finalScore > 0 && (contrib / finalScore) > 0.3 =>
f"$name:%.2f".format(contrib / finalScore)
}
debugStr + s" [${topContributors.mkString(", ")}]"
}
输出示例:
"推文ID:12345 [fav:0.35, reply:0.28, vqv:0.32]"
10.2 统计监控
系统会记录各种统计信息:
- 模型分数统计:每个模型的分数分布
- 权重统计:权重使用情况
- 负过滤统计:负权重过滤的次数
- 最终分数统计:最终分数的分布
十一、最佳实践
11.1 权重配置原则
- 平衡不同模型:确保没有单个模型过度主导
- 考虑业务目标:根据业务目标调整权重(如更重视转发还是点赞)
- A/B 测试:通过 A/B 测试找到最优权重配置
- 监控效果:持续监控权重调整的效果
11.2 负权重使用
- 设置合理阈值:负反馈阈值应该基于实际数据
- 避免过度过滤:负权重过大可能导致过度过滤
- 监控过滤率:监控负权重过滤的推文数量
11.3 模型资格
- 明确条件:确保模型资格条件清晰明确
- 处理边界情况:考虑边界情况(如视频时长为 0)
- 使用偏置:对于不满足条件的候选,考虑使用偏置值
十二、总结
12.1 核心要点
- 15 个专业化模型:每个模型预测一种特定的用户行为
- 动态权重配置:通过 Feature Switches 动态调整权重
- 加权融合:通过加权求和组合所有模型分数
- 负权重过滤:使用负权重过滤低质量内容
- 按Head归一化:可选的特征归一化步骤
12.2 融合公式
finalScore = Σ(score_i × weight_i) + Epsilon
其中:
- score_i: 第 i 个模型的预测分数
- weight_i: 第 i 个模型的权重
- Epsilon: 防止分数为 0 的小常数 (0.001)
12.3 关键文件
- 模型定义 :
PredictedScoreFeature.scala - 融合逻辑 :
RerankerUtil.scala - 评分器 :
WeighedModelRerankingScorer.scala - 权重配置 :
HomeGlobalParams.scala
附录:完整代码流程
A. 模型分数计算
scala
def computeModelScores(
query: PipelineQuery,
candidate: CandidateWithFeatures[TweetCandidate],
modelStatsOpt: Option[ModelStats] = None
): Seq[(Double, Double)] = {
PredictedScoreFeatures.map { predictedScoreFeature =>
val predictedScoreOpt = predictedScoreFeature.extractScore(
candidate.features,
query
)
val weight = query.features
.flatMap(_.get(predictedScoreFeature.weightQueryFeature))
.getOrElse(0.0)
val bias = predictedScoreFeature.biasQueryFeature
.flatMap(feature => query.features.flatMap(_.get(feature)))
.getOrElse(0.0)
val score = if (predictedScoreFeature.isEligible(candidate.features, query))
predictedScoreOpt.getOrElse(0.0)
else
bias
(score, weight)
}
}
B. 按Head归一化
scala
def getScoresWithPerHeadMax(
scoresAndWeightsSeq: Seq[Seq[(Double, Double)]]
): Seq[Seq[(Double, Double, Double)]] = {
if (scoresAndWeightsSeq.isEmpty) Seq.empty
else {
val headsScores: Seq[Seq[Double]] = scoresAndWeightsSeq.transpose.map { headScores =>
headScores.map { case (score, _) => score }
}
val headMaxScores: Seq[Double] = headsScores.map(_.max).toIndexedSeq
scoresAndWeightsSeq.map { candidateScores =>
candidateScores.zipWithIndex.map {
case ((score, weight), headIdx) =>
val headMaxScore = headMaxScores(headIdx)
(score, headMaxScore, weight)
}
}
}
}
C. 加权聚合
scala
def aggregateWeightedScores(
query: PipelineQuery,
scoresAndWeights: Seq[(Double, Double, Double)],
negativeFilterCounter: Counter
): Double = {
val combinedScoreSum: Double = {
scoresAndWeights.foldLeft(0.0) {
case (combinedScore, (score, maxHeadScore, weight)) =>
if (weight >= 0.0 || useWeightForNeg) {
combinedScore + score * weight
} else {
val normScore = if (maxHeadScore == 0.0) 0.0 else score / maxHeadScore
val negFilterNorm = enableNegNormalized && normScore > thresholdNegativeNormalized
val negFilterConstant = enableNegConstant && score > thresholdNegative
if (negFilterNorm || negFilterConstant) {
negativeFilterCounter.incr()
if (negSectionRanking) {
combinedScore + weight * (1.0 min (score + 0.1))
} else {
combinedScore + weight
}
} else {
combinedScore
}
}
}
}
val (_, _, modelWeights) = scoresAndWeights.unzip3
val positiveModelWeightsSum = modelWeights.filter(_ > 0.0).sum
val negativeModelWeightsSum = modelWeights.filter(_ < 0).sum.abs
val modelWeightsSum = positiveModelWeightsSum + negativeModelWeightsSum
val weightedScoresSum =
if (modelWeightsSum == 0)
combinedScoreSum.max(0.0)
else if (combinedScoreSum < 0)
(combinedScoreSum + negativeModelWeightsSum) / modelWeightsSum * Epsilon
else
combinedScoreSum + Epsilon
weightedScoresSum
}