【机器学习】深度学习推荐系统(二十六):X 推荐算法多模型融合机制详解

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 权重特点

  1. 动态配置:通过 Feature Switches 动态调整,无需重新部署
  2. 默认值为 0.0:新模型默认不参与融合,需要显式配置权重
  3. 范围限制 :权重范围 [-10000.0, 10000.0]
  4. 支持负权重:负反馈模型使用负权重

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 权重来源

权重可以从两个地方获取:

  1. Feature Switches :从 HomeGlobalParams 中读取(静态配置)
  2. 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 归一化的目的

不同模型的分数可能在不同的数值范围内,直接加权融合可能导致某些模型主导结果。归一化可以:

  1. 平衡不同模型的影响:确保每个模型对最终分数的贡献在合理范围内
  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 动态权重

权重可以从查询特征中动态获取,支持:

  1. 个性化权重:不同用户可以使用不同的权重
  2. A/B 测试:不同实验组使用不同的权重配置
  3. 实时调整:根据实时反馈调整权重

十、调试与监控

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 权重配置原则

  1. 平衡不同模型:确保没有单个模型过度主导
  2. 考虑业务目标:根据业务目标调整权重(如更重视转发还是点赞)
  3. A/B 测试:通过 A/B 测试找到最优权重配置
  4. 监控效果:持续监控权重调整的效果

11.2 负权重使用

  1. 设置合理阈值:负反馈阈值应该基于实际数据
  2. 避免过度过滤:负权重过大可能导致过度过滤
  3. 监控过滤率:监控负权重过滤的推文数量

11.3 模型资格

  1. 明确条件:确保模型资格条件清晰明确
  2. 处理边界情况:考虑边界情况(如视频时长为 0)
  3. 使用偏置:对于不满足条件的候选,考虑使用偏置值

十二、总结

12.1 核心要点

  1. 15 个专业化模型:每个模型预测一种特定的用户行为
  2. 动态权重配置:通过 Feature Switches 动态调整权重
  3. 加权融合:通过加权求和组合所有模型分数
  4. 负权重过滤:使用负权重过滤低质量内容
  5. 按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
}
相关推荐
huazi-J2 小时前
Datawhale Happy-LLM 课程 task 1和2:NLP基础概念
人工智能·自然语言处理·大模型·llm·datawhale
高山上有一只小老虎2 小时前
小红的矩阵染色
java·算法·矩阵
WuChao_JMUer2 小时前
YOLO26 on RDK S100P 端侧部署技术报告
人工智能·算法·yolo·rdk
Ro Jace2 小时前
传统雷达信号分选方法之SDIF:Improved algorithm for the deinterleaving of radar pulses
网络·人工智能·算法
说私域2 小时前
用户感知断裂与商业模式颠覆:AI智能名片链动2+1模式S2B2C商城小程序的破局之道
大数据·人工智能·小程序
小杨同学492 小时前
【嵌入式 C 语言实战】手动实现字符串四大核心函数(strcpy/strcat/strlen/strcmp)
后端·深度学习·算法
8K超高清2 小时前
CES 2026科技看点
网络·人工智能·科技·数码相机·计算机视觉
广州市中熠科技2 小时前
2026商业显示行业技术新动向:AI融合、空间计算与全球化布局
人工智能·智能硬件·led·空间计算·产品选择
管牛牛2 小时前
图像灰度变换
人工智能·计算机视觉