多目标预测实战:基于MMoE的微信视频号用户行为预测模型解析

📅 2026/8/27 6:30:15
多目标预测实战:基于MMoE的微信视频号用户行为预测模型解析
简介在推荐系统和计算广告领域多任务学习是解决用户多行为预测的核心技术范式。其原理在于通过共享底层网络结构学习通用特征表示同时利用任务特定网络捕捉不同目标的独特性从而有效利用数据、提升模型泛化能力并降低服务开销。这一技术对于优化内容分发效率、提升用户粘性具有重要价值广泛应用于视频推荐、信息流排序等场景。本文以微信视频号多行为预测为具体案例深入剖析了如何运用MMoE模型架构与精细化的特征工程构建一个同时预测点赞、转发、关注等七种用户互动行为的工业级解决方案其中对序列行为建模和交叉特征构造的探讨尤为关键。1. 项目概述与背景解析最近几年数据竞赛成了很多算法工程师和在校学生验证想法、提升能力的重要途径。2021年的微信大数据挑战赛聚焦于当时正快速崛起的微信视频号场景提出了一个非常贴近实际业务的问题基于用户的历史行为数据预测其在未来一段时间内对视频内容可能产生的七种互动行为。这七种行为包括读评论、点赞、点击头像、收藏、转发、发表评论和关注。这个任务包我拿到手研究了一段时间发现它远不止是一个简单的点击率预测问题而是一个典型的多目标预测场景对模型设计和特征工程都提出了不小的挑战。简单来说这个项目的核心是给你一堆用户在视频号里的“行为痕迹”比如他过去看了哪些视频、点了哪些赞、给谁留了言然后让你猜猜面对一个新的视频他接下来最可能做什么是默默点赞还是激情转发或者干脆关注发布者这听起来像是“读心术”但实际上是一套严谨的数据建模过程。微信视频号作为一个内容分发平台精准预测用户行为对于提升内容推荐效率、优化用户体验、增加用户粘性至关重要。这个比赛提供的正是这样一个将学术理论与工业实践结合的绝佳沙盘。对于从事推荐系统、计算广告或者用户增长相关工作的朋友来说这类多目标预测问题具有很高的参考价值。它迫使你思考如何用一个模型同时处理好多个相关但又不完全相同的任务如何设计网络结构来共享信息和捕捉独特性以及如何评估一个模型在多个目标上的综合表现。接下来我就把自己在复现和优化这个方案过程中的一些核心思路、实操细节以及踩过的坑系统地梳理一遍。2. 核心问题拆解与方案设计思路2.1 从“点击率预测”到“多目标预测”的思维转变乍看标题里有“点击率预测模型”很多人可能会先入为主地认为这是一个传统的CTR预估问题。但仔细分析任务目标——预测七种不同的用户行为就明白这已经超越了单一目标的范畴。传统的CTR模型比如经典的LR、FM、DeepFM等输出的是一个0到1之间的概率值表示用户发生点击或转化的可能性。而在这里我们需要输出七个概率值分别对应七种行为发生的可能性。这带来了几个关键挑战目标相关性这七种行为并非完全独立。一个用户如果喜欢一个视频他可能先点赞然后收藏甚至转发。这些行为之间存在强烈的序列相关性和层级关系例如关注行为通常意味着更高程度的认可。模型需要能够捕捉这种多目标之间的内在关联。样本稀疏性与正负样本不平衡七种行为的频率差异很大。像“点击头像”这样的低频行为正样本可能非常少而“点赞”则相对高频。如果简单地为每个行为独立训练一个二分类模型低频行为的模型会很难学好。特征共享与特异性预测不同行为所依赖的用户兴趣和内容特征既有重叠的部分例如用户对娱乐内容的普遍偏好也有特异的部分例如触发“转发”可能更需要内容具有社交货币属性。模型设计需要平衡共享层和任务特定层。因此我们的方案设计必须从“多任务学习”的角度出发。核心思路是构建一个共享底层特征输入上层分支出多个任务特定塔层的模型架构。底层共享网络负责学习从原始特征到高阶抽象表示的通用映射捕捉用户和视频的通用兴趣。上层的每个任务塔Tower则接收这个共享表示并进一步学习针对该特定行为的决策边界。2.2 模型架构选型MMoE与PLE的权衡在多任务学习领域有几个经典的模型架构可供选择。在这个项目中我重点对比和尝试了两种MMoE和PLE。MMoE全称Multi-gate Mixture-of-Experts。它的核心思想是引入多个“专家”网络和“门控”网络。每个专家学习输入特征的不同方面而每个任务都有自己的门控网络来动态地组合这些专家的输出形成该任务的特定输入。MMoE的优势在于它通过门控机制实现了灵活的共享不同任务可以以不同的权重利用专家知识适合任务相关性较高但又不完全一致的场景。PLE全称Progressive Layered Extraction。这是腾讯在2020年提出专门解决多任务学习中“跷跷板现象”一个任务提升导致另一个任务下降的架构。它在MMoE的基础上更进一步引入了“任务特定专家”和“共享专家”的明确区分并且通过分层渐进式路由让任务先经过自己的特定专家再根据需要从共享专家中提取信息。PLE的设计更倾向于处理任务间相关性复杂、甚至存在冲突的场景。在我们的视频号行为预测场景中七种行为虽然相关但冲突可能没那么剧烈比如点赞和收藏通常是正相关的。经过实验对比我发现MMoE在这个数据集上已经能取得很不错的效果且结构相对PLE更简洁训练速度更快。因此最终方案以MMoE为主干进行构建。当然这并不是说PLE不好在任务冲突更明显的场景下PLE可能是更优解。这里的选择体现了方案设计中的一个重要原则没有最好的模型只有最适合当前数据和业务目标的模型。注意模型选型一定要基于实验。可以先快速实现一个简单的共享底层模型Shared-Bottom作为Baseline然后逐步引入MMoE、PLE等复杂结构在验证集上客观评估其提升效果避免陷入“为了复杂而复杂”的误区。2.3 特征工程蓝图设计特征决定了模型效果的上限。在这个项目中特征主要分为三大类用户侧特征、视频侧特征、上下文与交叉特征。用户侧特征静态特征用户性别、年龄、城市、设备类型等。这些是刻画用户基本属性的维度。动态统计特征这是重中之重。我们需要基于用户历史行为序列构造出丰富的统计特征。例如用户历史总点赞数、收藏数、转发数。用户对各类别如娱乐、体育、知识视频的历史行为分布点击率、点赞率等。用户近1天、7天、30天的行为活跃度行为次数/天数。用户历史关注作者数以及其关注作者发布视频的互动情况。用户行为序列的深度兴趣特征例如使用Word2Vec或Graph Embedding技术将用户交互过的视频ID序列转化为一个定长的用户兴趣向量。视频侧特征内容特征视频类别、标签、时长、清晰度、发布时间等。创作者特征创作者ID、创作者历史视频平均互动数据、创作者粉丝数等。一个头部创作者的视频通常更容易获得初始流量。实时热度特征视频发布后一段时间内的累计曝光、点击、互动数据需注意特征穿越问题只能使用历史时间窗口的数据。上下文与交叉特征上下文特征行为发生的时间小时、工作日/周末、用户当前网络环境等。交叉特征这是捕捉“用户-视频”匹配度的关键。例如用户历史对该视频类别的偏好度历史点击率与当前视频特征的交叉。用户与创作者的关联强度例如用户是否已关注该创作者用户历史与该作者视频的互动情况。基于Embedding的交叉将用户ID和视频ID的Embedding进行点积、拼接或基于注意力的交互。一个关键的实操心得对于统计类特征要特别注意数据泄露问题。例如计算“视频历史点赞率”必须严格使用当前行为发生时间点之前的数据绝不能使用包含未来信息的数据。在工程实现上这通常意味着需要基于滚动时间窗口进行复杂的离线特征计算。3. 数据预处理与特征工程实战3.1 原始数据探查与清洗拿到竞赛数据后第一步绝不是急着建模型而是花足够的时间去理解数据。数据通常包含几张表用户行为日志表、用户信息表、视频信息表等。探查重点缺失值各字段的缺失比例是多少对于用户性别、年龄等缺失可能本身就是一种模式例如新用户或不愿填写的用户可以考虑用单独一个“未知”类别填充而不是简单用众数或均值。异常值视频时长是否有为0或极长的记录用户年龄是否在合理范围内对于明显的异常记录需要根据业务逻辑判断是剔除还是修正。行为分布绘制七种行为的分布图。你会发现“点赞”行为可能占绝大多数而“发表评论”和“关注”非常稀疏。这直接影响了后续的损失函数设计可能需要加权的损失。时间跨度明确训练集、验证集和测试集的时间划分。必须保证验证集和测试集的时间在训练集之后以模拟真实的线上预测场景避免因时间相关性导致评估失真。清洗操作剔除关键字段如用户ID、视频ID、时间戳缺失的记录。对于数值异常根据业务常识设定合理范围进行截断或置为缺失。对于类别型字段的异常值归入“其他”类别。3.2 核心特征构造详解这里以一个典型的“用户-视频”交互预测场景为例展示几个核心特征的构造过程。假设我们有一张用户行为流水表df_behavior字段包括user_id,video_id,behavior_type七种行为之一,timestamp。特征1用户对视频类别的历史偏好率import pandas as pd import numpy as np # 假设有视频信息表 df_video包含 video_id 和 category # 首先将行为表与视频表关联得到每次行为的类别 df_behavior_with_cat pd.merge(df_behavior, df_video[[video_id, category]], onvideo_id, howleft) # 定义计算历史统计的函数避免数据泄露 def calculate_historical_stats(df, current_time, user_id, category, stat_days): 计算在current_time之前stat_days天内某用户对某类别视频的特定行为统计。 start_time current_time - pd.Timedelta(daysstat_days) past_data df[(df[user_id] user_id) (df[category] category) (df[timestamp] current_time) (df[timestamp] start_time)] # 例如计算点击率 (假设behavior_type1是点击) total_impressions len(past_data) clicks len(past_data[past_data[behavior_type] 1]) ctr clicks / total_impressions if total_impressions 0 else np.nan return ctr # 应用函数此处为示意实际需优化效率避免逐行循环 # 通常我们会采用滚动窗口的方式在Spark或Pandas groupby下批量计算特征2用户兴趣Embedding基于序列我们可以将用户交互过的视频ID序列视为一个“句子”使用Word2Vec的Skip-gram模型来学习每个视频ID的Embedding然后对用户序列中的所有视频Embedding取平均或加权平均得到用户的兴趣向量。# 使用gensim库的Word2Vec from gensim.models import Word2Vec # 准备用户行为序列按用户分组按时间排序收集video_id列表 user_sequences df_behavior.sort_values([user_id, timestamp]).groupby(user_id)[video_id].apply(list).tolist() # 将video_id转换为字符串因为Word2Vec期望字符串列表 user_sequences_str [[str(vid) for vid in seq] for seq in user_sequences] # 训练Word2Vec模型 model Word2Vec(sentencesuser_sequences_str, vector_size64, window5, min_count5, workers4, sg1) # sg1表示Skip-gram # 获取每个video_id的向量 video_embeddings {vid: model.wv[vid] for vid in model.wv.index_to_key} # 生成用户兴趣向量平均池化 def get_user_embedding(user_video_list): embs [video_embeddings.get(str(vid), np.zeros(64)) for vid in user_video_list] if embs: return np.mean(embs, axis0) else: return np.zeros(64)特征3用户-创作者交叉特征“用户是否已关注该视频创作者”是一个极强的信号。我们可以构造一个二值特征。更进一步可以计算“用户历史与该创作者所有视频的平均互动深度”例如平均点赞率。3.3 训练验证集划分与负样本采样这是一个点击率预测类问题样本天然包含正样本发生了某种行为和负样本曝光但未发生该行为。竞赛数据通常只提供了正样本日志因此我们需要自己构造负样本。负样本构造一种常见且有效的方法是“曝光未点击”假设。我们可以从全局视频池中为每个用户随机采样一些他没有发生过任何七种行为的视频作为负样本。采样的数量需要控制通常保持正负样本比例在1:1到1:4之间具体比例需要通过实验调整。时间划分绝对不能随机划分必须按时间划分。例如用前30天的数据做训练第31天的数据做验证第32-33天的数据做测试。这保证了模型是在用过去预测未来评估结果才可信。序列特征的对齐在划分数据集时要确保用于构造序列特征如用户历史行为序列的数据严格来自训练集时间窗验证集和测试集的特征计算只能基于它们时间点之前的训练集数据这是一个容易出错的地方。4. 多任务模型构建与实现细节4.1 基于MMoE的多任务模型搭建我们使用深度学习框架如TensorFlow或PyTorch来实现MMoE模型。以下是一个简化的PyTorch实现框架用于说明核心组件。首先定义模型输入层和嵌入层。对于类别型特征用户ID、视频ID、类别等我们将其转换为Embedding对于数值型特征统计特征直接输入。import torch import torch.nn as nn import torch.nn.functional as F class MMoE(nn.Module): def __init__(self, user_feature_dim, video_feature_dim, num_experts4, expert_dim64, tower_dim32, num_tasks7): super(MMoE, self).__init__() self.num_experts num_experts self.num_tasks num_tasks # 专家网络每个专家是一个简单的MLP self.experts nn.ModuleList([ nn.Sequential( nn.Linear(user_feature_dim video_feature_dim, expert_dim), nn.ReLU(), nn.Linear(expert_dim, expert_dim), nn.ReLU() ) for _ in range(num_experts) ]) # 门控网络每个任务有一个门控输出num_experts个权重 self.gates nn.ModuleList([ nn.Sequential( nn.Linear(user_feature_dim video_feature_dim, num_experts), nn.Softmax(dim-1) ) for _ in range(num_tasks) ]) # 任务塔网络每个任务有自己的塔接收专家混合后的输出 self.towers nn.ModuleList([ nn.Sequential( nn.Linear(expert_dim, tower_dim), nn.ReLU(), nn.Linear(tower_dim, 1) # 输出该任务的正样本概率 ) for _ in range(num_tasks) ]) def forward(self, user_features, video_features): # 拼接用户和视频特征 combined_input torch.cat([user_features, video_features], dim-1) # 计算各个专家的输出 expert_outputs [expert(combined_input) for expert in self.experts] # 列表每个元素形状 [batch_size, expert_dim] expert_outputs torch.stack(expert_outputs, dim1) # 形状变为 [batch_size, num_experts, expert_dim] # 为每个任务计算门控权重并混合专家输出 task_outputs [] for i in range(self.num_tasks): gate_weight self.gates[i](combined_input) # 形状 [batch_size, num_experts] gate_weight gate_weight.unsqueeze(-1) # 形状 [batch_size, num_experts, 1] # 加权求和gate_weight * expert_outputs 再在expert维度求和 task_input torch.sum(gate_weight * expert_outputs, dim1) # 形状 [batch_size, expert_dim] task_output self.towers[i](task_input) # 形状 [batch_size, 1] task_outputs.append(task_output.squeeze(-1)) # 形状 [batch_size] # 将7个任务的输出堆叠起来 return torch.stack(task_outputs, dim-1) # 最终输出形状 [batch_size, num_tasks]4.2 损失函数设计与优化多任务学习的损失函数通常是各任务损失的加权和。总损失 w1 * Loss_task1 w2 * Loss_task2 ... w7 * Loss_task7单任务损失对于每个二分类任务最常用的是二元交叉熵损失BCE Loss。权重选择权重的设置至关重要直接影响模型优化方向和最终效果。等权重最简单但可能使模型偏向于主导任务样本多的任务。基于样本数给样本稀疏的任务更高权重以平衡梯度。基于任务重要性根据业务目标调整。例如如果业务更关注“关注”和“转发”这种深度转化可以适当调高其权重。动态权重使用不确定性加权、GradNorm等算法让模型在训练过程中自动学习最优权重。这在研究论文中很常见但实现复杂稳定性需要调试。我个人的经验是先从等权重开始观察各个任务在验证集上的AUC或LogLoss。如果某个任务如“关注”的指标远差于其他任务可以尝试适当提高其损失权重例如1.5倍或2倍再进行训练观察。这是一个需要反复实验的过程。# 简化的损失计算示例 criterion nn.BCEWithLogitsLoss() # 内置了sigmoid和BCE def compute_loss(model_output, ground_truth_labels, weights): model_output: [batch_size, 7] 模型输出的logits ground_truth_labels: [batch_size, 7] 真实标签 (0或1) weights: list of length 7, 各任务损失权重 total_loss 0.0 for i in range(7): task_loss criterion(model_output[:, i], ground_truth_labels[:, i]) total_loss weights[i] * task_loss return total_loss4.3 模型训练技巧与超参数调优优化器与学习率Adam或AdamW优化器是默认选择。学习率可以尝试从3e-4开始配合学习率预热和余弦退火调度器有助于模型稳定收敛。Batch Size在显存允许的情况下使用较大的Batch Size如1024或2048通常能使训练更稳定梯度估计更准确。Dropout与正则化在专家网络和塔网络的线性层后加入Dropout如p0.2-0.5是防止过拟合的有效手段。对于Embedding层也可以使用Embedding Dropout。早停密切监控验证集的总损失或核心任务的评估指标如加权AUC当其在连续多个epoch如10个不再提升时停止训练。超参数搜索可以使用网格搜索或随机搜索对关键超参进行调优例如专家数量num_experts通常4或8。专家层和塔层的维度expert_dim,tower_dim。Dropout率。损失权重。学习率。实操心得在资源有限的情况下优先调损失权重和学习率它们对模型性能的影响往往比增加模型复杂度更大。可以先固定一个较小的模型快速进行多轮权重和学习的实验找到相对优的组合后再考虑扩大模型容量。5. 评估指标与线上效果模拟5.1 多任务评估指标的选择对于多目标预测不能只看单一指标需要一个综合评估体系。单任务指标计算每个任务独立的评估指标。AUC最常用的排序能力指标衡量模型将正样本排在负样本前面的能力。对每个行为单独计算AUC。LogLoss衡量预测概率的校准程度。LogLoss越低说明预测概率越准确。综合指标加权AUC根据业务重要性或样本量为每个任务的AUC分配权重然后求和。例如综合AUC 0.1*AUC_read 0.2*AUC_like ... 0.15*AUC_follow。权重的设定需要与业务方对齐。宏观/微观平均对于多个二分类任务可以计算宏平均先对每个任务算指标再平均和微平均将所有任务的预测和标签合并成一个大的二分类问题再算指标。在样本不平衡时两者差异很大。业务导向指标在竞赛或真实业务中可能会定义更复杂的指标。例如预测用户是否会进行“深度互动”收藏、转发、关注中至少一种然后计算这个聚合行为的AUC。在本次复现中我主要监控每个任务的AUC以及一个等权重的平均AUC作为模型性能的核心参考。5.2 离线评估与线上效果的一致性离线评估指标好不代表线上推荐效果一定好。我们需要尽可能模拟线上环境。时间穿越这是最大的陷阱。确保特征、样本划分都严格按照时间顺序前面已经强调过。线上服务延迟模拟离线特征计算尤其是实时特征的复杂度必须在线上可承受的延迟范围内。如果你在离线用了非常复杂的、需要全量数据扫描的特征在线推理时可能无法实时计算。AUC的局限性AUC衡量排序能力但不直接对应线上点击率或互动率的提升。有时一个模型AUC略低但因为它对头部高概率样本的预测更准反而能带来更高的线上点击率。可以补充查看校准曲线和预测概率分布。一个有用的技巧是进行离线重播评估。用训练好的模型按照时间顺序对验证集的数据进行“模拟推荐”记录下模型预测的结果然后与真实发生的用户行为对比计算诸如“Top-K预测命中率”等更贴近业务的指标。6. 实战中遇到的典型问题与解决方案在复现和优化过程中我遇到了不少坑这里记录几个典型问题及其解决思路。问题一模型对所有任务的预测概率都偏向于一个很小的值例如0.01导致AUC虽然不低但LogLoss很高。原因分析这通常是正负样本极度不平衡导致的。即使负样本采样后比例是1:4但正样本中像“关注”这样的行为本身占比可能也只有千分之一。模型会倾向于预测一个接近先验概率的低值来最小化损失。解决方案调整损失权重大幅提高低频正样本任务的损失权重迫使模型更关注这些样本。采样策略在构造负样本时对于低频任务可以适当提高其正样本在Batch中的比例例如过采样或者对负样本进行困难负样本挖掘。使用Focal LossFocal Loss通过降低易分类样本的权重让模型更聚焦于难分类的样本对类别不平衡问题有很好的缓解作用。可以尝试用Focal Loss替代标准的BCE Loss。问题二训练初期某个任务的损失突然变成NaN导致训练崩溃。原因分析可能是梯度爆炸或者某些特征存在异常值如非常大的数值经过几层网络计算后溢出。解决方案梯度裁剪在优化器步骤之前对模型参数的梯度进行裁剪限制其最大范数。特征归一化/标准化检查所有数值特征确保它们被合理地缩放如归一化到[0,1]或标准化为均值为0、方差为1。网络初始化检查模型权重初始化方法使用如He初始化或Xavier初始化。降低学习率尝试使用更小的初始学习率。问题三多任务模型出现“跷跷板”现象优化任务A导致任务B的指标下降。原因分析这是多任务学习的经典难题说明任务A和任务B在共享层存在一定的冲突或竞争。解决方案调整共享程度尝试减少共享层的维度或者增加任务特定塔层的容量让任务有更多独立参数。切换模型架构如果MMoE效果不佳可以尝试前文提到的PLE模型它专门设计了机制来缓解任务冲突。精细化损失权重调整这可能是一个信号表明你为任务B设置的损失权重相对其难度或重要性来说太低了可以尝试动态调整权重策略。问题四线上推理速度慢无法满足实时推荐要求。原因分析模型过于复杂专家和塔层太多、维度太大或特征工程中使用了复杂的实时计算。解决方案模型压缩训练完成后可以对模型进行剪枝、量化或知识蒸馏得到一个更小更快的版本。特征简化分析特征重要性剔除贡献度低的特征。将一些复杂的实时统计特征转化为可以提前预计算好的用户/视频画像特征在服务时直接查表获取。服务化优化使用高性能推理框架如TensorRT, ONNX Runtime对模型进行图优化和算子融合。7. 项目总结与延伸思考复盘整个项目从数据清洗、特征构造到模型搭建、训练调优是一个完整的机器学习Pipeline实践。微信视频号多行为预测这个场景非常具象地体现了工业界推荐系统的一个核心问题如何利用有限的用户反馈信号多种隐式行为尽可能全面地理解用户兴趣并预测其下一步动作。这个方案的核心价值在于其框架性。MMoE的多任务学习框架加上精心设计的用户、视频、交叉特征构成了一个可扩展的预测系统。当需要增加新的预测目标例如预测“分享到朋友圈”或“下载视频”时只需要在模型顶部增加一个新的任务塔并在损失函数中赋予相应权重即可大部分底层架构和特征都可以复用。在特征方面我最大的体会是序列行为信息和交叉信息的威力。单纯的用户静态属性和视频内容属性解释力有限。而用户过去看了什么、点了什么以及他与当前视频创作者的历史关系这些动态的、个性化的信息才是预测精准度的关键提升点。这也对数据管道和计算效率提出了更高要求。最后关于评估永远要记住离线指标只是参考。在真实业务中必须进行严格的A/B测试才能最终验证一个模型的价值。离线阶段的工作是尽可能提高模型上线后取得正向效果的概率。这个项目提供的正是这样一套经过竞赛验证的、扎实的离线建模方法论对于希望深入推荐系统或用户行为预测领域的同学来说是一次非常好的全流程演练。本文还有配套的精品资源点击获取