新闻详情 资讯动态

全面了解最新资讯与建站知识,洞察行业趋势。

行业资讯

多目标点击率预测实战:七种用户行为建模与shared-bottom方案

发布时间:2026/9/25 8:53:04
多目标点击率预测实战:七种用户行为建模与shared-bottom方案 简介面向微信视频号场景下的用户互动行为预测这份2021年微信大数据挑战赛多目标预测方案聚焦读评论、点赞、点击头像、收藏、转发、发表评论、关注七种行为构建基于用户行为数据的点击率预测模型适合备赛学生、推荐系统学习者和算法工程师参考。压缩包共19个文件主体为7个Python脚本和4个pyc编译文件负责特征工程、模型训练与预测配套3个shell脚本用于初始化和一键运行另含2个txt环境说明、2个md文档及1个docx附赠资料包体仅84KB轻量清晰。已有86人学习浏览。通过这份代码与说明读者可快速理解多目标建模的整体流程掌握从原始行为数据到特征构造、模型训练、推理衔接的实践细节md与docx资料进一步补充赛题解读与方案思路便于在此基础上扩展特征或调优参数是参加同类点击率预测竞赛或业务建模的实用起点。1. 多目标点击率预测从一通乱调到一份能上项目的完整方案很多人拿到这类“多目标预测”资源第一反应是套个模型调参结果跑出来的分数还不如单任务。问题不在模型而在标签设计、特征窗口和验证切分从一开始就没对齐。这份 2021 微信大数据挑战赛的实战方案核心是把视频号场景下的用户互动行为拆成读评论、点赞、点击头像、收藏、转发、发表评论、关注七个目标用统一的用户行为数据做多目标点击率预测。它覆盖了特征构造、shared-bottom 多任务模型、loss 加权和按时间切分的验证流程适合正在做推荐系统、广告排序、内容平台互动预测的从业者也适合拿大数据相关实战项目练手的学习者照着完整跑一遍。2. 七种用户行为先拆标签再谈模型2.1 从曝光到七种行为标签到底在预测什么视频号信息流的典型交互链路是用户刷到一条视频系统记录这次曝光然后观察用户在后续时间窗口内是否产生互动。比赛把这层互动拆成了七种独立行为每一种都对应一个 0/1 标签。这七种行为的业务含义差异很大。点赞和收藏是轻量级的兴趣表达用户不需要付出太多成本所以正样本相对充足读评论和发表评论属于内容消费和社交参与前者是“看别人说了什么”后者是“我亲自下场说点什么”两者门槛明显不同点击头像意味着用户想进一步了解创作者本人这是从内容兴趣转向账号兴趣的信号转发和关注则是强意图行为转发需要消耗社交信用关注代表长期关系建立这两类在数据里天然稀疏。我从这批方案里读到的最关键设计就是它没有把七种行为合并成一个“综合互动”标签去训练而是保留了每个任务的独立输出。原因很简单业务方要分别知道用户会对一条视频产生哪种互动点赞概率高的人和关注概率高的人后续运营策略完全不同。如果合并成一个标签这些信息就全糊在一起了。这七种行为的稀疏度大致可以分成三档我做了个表方便对齐后续的特征和 loss 设计思路。行为业务含义数据稀疏度点赞轻量兴趣表达社交展示成本低低读评论内容消费行为成本最低低收藏深度兴趣表达用户想留存内容中发表评论社交参与需要文本输入成本中点击头像从内容兴趣转向账号兴趣中转发强社交背书消耗社交信用高关注长期关系建立意图层级最高高2.2 多目标为什么不能拆成七个独立模型七种行为在用户侧高度相关。一个对篮球内容有明显兴趣的用户看到篮球视频时点赞、收藏、转发的概率会同时上升。这种相关性意味着它们共享同一个底层的用户兴趣表示。如果拆成七个独立二分类模型每个模型都得从头学习这份兴趣表示数据利用效率很低。更现实的问题是样本量。转发和关注的正样本率通常只有千分位级别单拎出来训练模型很容易学成“永远预测负样本”。但点赞和读评论的正样本充足模型能从这两种高频行为里学到用户兴趣的底层结构这个结构对转发和关注任务同样有效。我在实践里体会最深的一点是多目标模型不是把七个模型塞进一个网络里就完事而是要设计好共享和独立的边界。共享太多低频任务会被高频任务带偏共享太少低频任务又学不动。接下来要写的特征工程和模型结构都是围绕这个平衡点展开的。2.3 数据长什么样字段、标签与样本组织方式这类比赛的常见数据组织形式是一行一个 user_id 和 feed_id 的曝光样本特征是用户侧、内容侧、上下文侧三类拼接而成标签列是七种行为的 0/1 值。我会建议拿到数据先做三件事确认每列的缺失率、确认七列标签的稀疏度、确认日期范围的跨度是否足以支撑时间切分验证。用户侧字段一般包含年龄、性别、关注数、粉丝数、历史行为统计等静态画像。内容侧字段通常包括视频的类别、时长、作者信息、发布时间以及视频自身的历史互动表现。上下文侧字段则围绕“这次曝光发生在什么场景下”展开比如所处的信息流位置、用户使用的终端、当前时段等。有一点需要提前说明这份方案里没有把原始 ID 一股脑全塞进 embedding而是先用统计特征把行为信息做了一次压缩。这样做的直接好处是训练样本的维度可控新用户和新视频出现时不会因为 ID 没出现过就直接失效。下一章详细讲这些特征具体怎么从原始日志里构造出来。3. 特征工程把行为日志拧成训练样本3.1 用户侧统计特征把行为变成历史概率多目标点击率预测任务里最有效的特征往往不是那些静态画像而是用户过去一段时间在相似内容上的表现。原因是行为预测本质上是概率估计用户过去 7 天的点赞率本身就是对“用户现在有多大概率点赞”的一个平滑估计。我一般会把用户侧统计特征分成三个层次。第一层是用户粒度的总体统计比如过去 N 天的总互动次数、发表评论数、关注数增量。第二层是用户与内容类别的交叉统计比如用户过去 N 天在体育类视频上的点赞率这个特征对当前体育视频的点赞预测有很强的指向性。第三层是用户与作者的交叉统计比如用户过去 N 天是否看过该作者的视频、看过之后的互动率是多少这个特征对点击头像和关注两个任务特别关键。构造这些特征时要特别注意一点统计区间必须与预测目标在时间上是严格隔离的。训练样本的预测目标是用户在曝光后的行为那么特征只能使用曝光时刻之前已经发生的行为数据。如果混入曝光之后的行为做统计就会把答案写进特征里造成严重的数据泄漏。3.2 窗口怎么选短期行为与长期习惯单一时间窗口很难覆盖用户兴趣的全部时间尺度。当天或 3 天内的行为反映的是瞬时兴趣比如用户刚看完一场球赛接下来几个小时点篮球视频的概率会明显上升14 天和 30 天的窗口反映的则是稳定习惯比如用户长期以来就是一个高频运动内容消费者。不同任务对窗口的敏感度不一样。点赞和读评论这类低成本行为短期窗口的特征往往更有效因为它们的触发更依赖当下的内容吸引力转发和关注这类强意图行为样本积累速度慢需要更长窗口的统计才有足够的稳定性。方案里采用了多窗口并存的方式常见配置是 1、3、7、14 天四档。短期窗口可以捕捉实时热点对用户行为的影响长期窗口则负责刻画稳定的兴趣底色模型在训练时自己学每个任务该侧重哪档窗口。3.3 行为序列特征把交互顺序也喂给模型除了统计特征行为序列也是多目标预测里常用的输入。做法是取用户最近交互过的 N 条 feed按时间排序每条记录用 feed_id 和对应行为类型组成序列截断长度一般取 50。这样模型能直接看到用户近期走过的“内容轨迹”而不只是压成几个统计值。序列特征的另一个作用是引入时间衰减。用户 10 天前看过一个视频和 10 分钟前看过一个视频对当前预测的意义差别很大。处理方式通常分成两种一种是在序列里拼接时间间隔字段让模型自己学衰减关系另一种是在构造统计特征时就按指数衰减权重计算加权均值越近的行为权重越高。我会优先采用第二种方式做统计特征因为实现简单且稳定序列模型作为增强项在 baseline 跑通之后再考虑引入。3.4 特征构造代码从行为日志到训练样本下面这段代码是方案里用户侧统计特征的 pandas 实现核心逻辑是窗口聚合和按 act_type 展开成多列特征。import pandas as pd import numpy as np def build_user_stats(behavior_log, last_days[1, 3, 7, 14]): 输入: behavior_log 为行为日志表, 至少包含 user_id, feed_id, act_type, is_click, date act_type 取值: [read_comment,like,click_avatar, collect,forward,comment,follow] 输出: 用户侧多窗口统计特征 DataFrame, 索引为 user_id feat_list [] max_date behavior_log[date].max() for win in last_days: start max_date - pd.Timedelta(dayswin - 1) sub behavior_log[behavior_log[date] start] # 按用户聚合, 计算每个行为的总次数 pivot ( sub.pivot_table( indexuser_id, columnsact_type, valuesis_click, aggfuncsum, fill_value0, ) .reset_index() ) # 把列名加上窗口后缀, 避免多窗口特征名冲突 rename_dict { col: f{col}_{win}d for col in pivot.columns if col ! user_id } pivot pivot.rename(columnsrename_dict) # 用户总互动次数与行为多样性 act_cols [c for c in pivot.columns if c.endswith(f_{win}d)] pivot[ftotal_act_{win}d] pivot[act_cols].sum(axis1) pivot[funique_act_{win}d] (pivot[act_cols] 0).sum(axis1) feat_list.append(pivot) # 按 user_id 做横向拼接, 形成宽表特征 result feat_list[0] for f in feat_list[1:]: result result.merge(f, onuser_id, howouter) return result.fillna(0)这段代码有四个设计点需要展开说明。第一用 pivot_table 直接按 act_type 展开多列一次性把七种行为的统计全部算完避免循环七次重复扫同一份数据。第二窗口后缀 _1d、_3d、_7d、_14d 是必须的因为多窗口拼接时列名如果不区分就会互相覆盖。第三total_act 和 unique_act 分别刻画互动总量和行为多样性前者反映活跃度后者反映兴趣面的宽窄这两个特征对冷启动用户的判断有直接帮助。第四外层 fillna(0) 用来兜底那些没有历史行为的用户这类用户在很多任务下会天然落在低概率区间模型需要靠其他特征来区分他们。用这份资源做学习时建议跑完 baseline 之后再试一个变体把 is_click 换成每个 act_type 的 0/1 标签分别做聚合也就是对每个目标任务单独构造统计特征。这样做会导致特征维度变成原来的七倍训练时间明显变长但某些任务的 AUC 会因此涨一截特别是关注和转发这类稀疏任务。4. 多任务模型shared-bottom 结构与七塔输出4.1 多任务模型怎么选shared-bottom 是性价比最高的基线多目标预测的建模方案在工业界已经有不少选择最常见的是 shared-bottom、MMoE、PLE 和 ESMM 这一类。ESMM 由于专为转化链路设计在这里并不完全适用MMoE 和 PLE 效果通常更好但结构复杂调参成本高。这份方案选用的 shared-bottom 是一个合理的起点底层共享用户和内容的兴趣表示上层每个任务独立出塔结构简单容易复现也方便后续替换成更复杂的门控结构。为什么共享底层对七目标任务是可行的因为七个任务共享同一个用户兴趣空间。点赞、收藏、转发、关注都依赖用户对内容的整体喜好判断只是表达强度不同。如果从第一天就强行拆成七个独立模型每个模型的数据量都很有限尤其是转发和关注这两个稀疏任务基本学不出稳定的兴趣表示。共享底层让这两个低频任务能从点赞、读评论等高频任务中借用学好的表示这是多任务建模最常见的收益来源。负迁移是 shared-bottom 的固有风险。某些任务的优化方向如果与其他任务冲突共享层反而会互相拖累。但在这个场景里七种互动行为在用户侧正相关负迁移并不严重这也是这个方案能直接跑通的底气。4.2 七塔输出与 loss 设计模型结构上底层是一个多层全连接网络输入特征向量输出一个维度为 hidden_dim 的共享表示。共享表示之上并行挂七个独立塔网络每个塔结构相同各自输出一个标量经过 sigmoid 后得到该行为的预测概率。七个预测是独立计算的不强制要求概率之和等于 1。用户完全可能既点赞又转发又关注所以每塔使用独立的 BCE loss。整体 loss 是七个 BCE 的加权和。loss sum(task_weight[i] * bce_loss(logits[i], labels[i]))task_weight 是这份方案里最值得反复调的一组超参数。初始值可以按每个任务正样本率的倒数做归一化先把低频任务的 loss 放大到和高频任务同一量级。后续再在 0.5 到 5 倍的范围内做小批量搜索。4.3 模型代码shared-bottom 七塔结构下面是方案里模型核心结构的 PyTorch 实现关键点都标了注释。import torch import torch.nn as nn import torch.nn.functional as F class SharedBottomMultiTask(nn.Module): 多任务共享底层模型 num_features: 特征维度, 与特征工程的宽表列数一致 num_tasks: 任务数, 本方案固定为 7 embed_dim: 底层共享表示的维度 def __init__(self, num_features, num_tasks7, embed_dim256): super().__init__() self.num_tasks num_tasks # 共享底层: 所有任务共用的兴趣表示层 self.bottom nn.Sequential( nn.Linear(num_features, embed_dim), nn.BatchNorm1d(embed_dim), nn.ReLU(), nn.Dropout(0.3), nn.Linear(embed_dim, embed_dim // 2), nn.ReLU(), ) # 七塔: 每塔预测一个用户行为概率 self.towers nn.ModuleList( [ nn.Sequential( nn.Linear(embed_dim // 2, 64), nn.ReLU(), nn.Dropout(0.2), nn.Linear(64, 1), ) for _ in range(num_tasks) ] ) def forward(self, x): # x: [batch_size, num_features] shared self.bottom(x) # 共享表示, 形状 [batch, 128] logits [] for tower in self.towers: logits.append(tower(shared)) # 每塔输出 [batch, 1] logits torch.cat(logits, dim1) # 拼接成 [batch, 7] return logits def predict_proba(self, x): logits self.forward(x) return torch.sigmoid(logits)forward 返回 logits 而不是 sigmoid 后的概率是有意的。torch 的 BCEWithLogitsLoss 内部会把 sigmoid 和 BCE 合并计算数值上比先算 sigmoid 再算 BCE 更稳定梯度也更平滑。predict_proba 单独用作推理时输出概率训练循环里统一走带 logits 的 loss。训练时 group 结构也很重要不同任务缺失标签时需要 mask 掉对应位置的 loss。比如数据里某条样本没有“读评论”行为发生标签可以填 -1loss 计算时把 -1 位置的样本排除否则零填充的标签会把负样本的统计向错误的方向推。criterion nn.BCEWithLogitsLoss(reductionnone) task_weights torch.tensor([1.0, 1.0, 1.0, 1.0, 1.0, 2.0, 2.0]) def multitask_loss(logits, labels, mask, task_weights): # logits: [batch, num_tasks] # labels: [batch, num_tasks], 缺失任务填 -1 # mask: [batch, num_tasks], 1 表示参与计算 loss_sum 0.0 for i in range(logits.size(1)): l criterion(logits[:, i], labels[:, i].clamp_min(0)) loss_task (l * mask[:, i]).sum() / (mask[:, i].sum() 1e-8) loss_sum task_weights[i] * loss_task return loss_sum / logits.size(1)有人认为七个任务都重要所以权重统一为 1 最公平。实际跑下来不是这样。点赞和读评论的正样本占比高每个 batch 里这两个任务贡献的 loss 值天然就大权重全设为 1 等于边缘化转发和关注。task_weights 设置的逻辑是“把稀疏任务抬高到能参与训练的程度”而不是业务上谁更重要谁权重就大。4.4 loss 权重怎么调调 loss 权重一直是多任务训练里最玄学的环节但也有一些可行的方法。先用每个任务的正样本率 r_i 算出初始权重 w_i 1 / sqrt(r_i)然后再统一缩放。比如点赞正样本率 5%转发只有 0.3%那转发的初始权重大约是点赞的 4 倍。跑一个完整 epoch 后打印每个任务单独的 loss 值如果转发任务的 loss 波动太大就进一步抬高它的权重或者给它的塔加一个更小的学习率。另一条经验是不要一开始就上 MMoE。把这份资源里 shared-bottom 的结构先跑通、把七个任务的 AUC 都打印出来确认每个任务都学到东西之后再考虑用 MMoE 替代 bottom 层。跳过验直接上复杂结构很容易陷入“线下涨、线上掉”的怪圈连问题出在模型还是数据都无法判断。5. 训练避坑不均衡样本、时间泄漏与指标陷阱5.1 低频行为全预测成近似常数第一次跑这份方案的训练脚本时现象是关注和转发这两个塔的输出全部集中在 0.005 到 0.01 之间基本没有区分度验证集上关注的 AUC 在 0.6 附近徘徊转发更低。原因出在 loss 结构上。点赞和读评论的正样本占比高每个 batch 里这两个任务贡献的梯度和数值压制了低频任务。任务权重全为 1 时转发任务的 loss 占总 loss 的比例只有个位数百分比反向传播时它对 shared-bottom 的更新信号相当于被淹没了。解决办法是给 loss 加权初始权重按 1/sqrt(正样本率) 计算。方案里提供的权重经验值是转发和关注设为其他任务的 2 到 3 倍跑完后转发任务的 AUC 能明显抬升。5.2 随机切分验证集线下线上分数分道扬镳很多人习惯拿到数据后直接 train_test_split按 8:2 比例随机切分。这种做法在行为预测任务里会带来严重的数据泄漏而且泄漏方式不容易察觉。现象是本地 AUC 看着有 0.75提交到线上直接落到 0.67怎么调都补不回来。原因在于随机切分会让同一个用户的同一天行为一部分进训练集、一部分进验证集。特征里已经包含了截止到曝光时刻的行为统计验证样本会间接看到未来信息导致线下评估过分乐观。解决办法是严格按时间切分训练集取截止日期之前的数据验证集取之后的数据。日期边界的选择要结合数据集的日期跨度预留出至少一个完整周期的数据做验证。5.3 ID 类特征不做频次截断embedding 学成了随机向量有的实现会把 user_id 和 feed_id 直接做成 embedding 输入模型。现象是训练前期 loss 下降正常但上线后遇到新用户、新视频时预测概率几乎失去区分度所有新内容都给出差不多的分数。原因是低频 ID 的样本太少embedding 在训练结束时没有学出稳定的语义而测试集里出现的新 ID 完全没有训练样本只能映射到随机初始化的向量相当于模型在朝一个随机向量做预测。解决办法是二选一要么对 ID 频次设阈值出现次数少于 10 次的 ID 统一映射到未知 ID 的 embedding要么干脆不用 ID改用用户侧统计特征内容侧统计特征代替这也是这份方案采取的策略。从复现角度讲先把 ID 走统计特征跑通再逐步引入稀疏 ID embedding会更稳。5.4 只看全局 AUC指标会骗人全局 AUC 是所有样本混在一起计算的它天然向高频用户倾斜。现象是全局 AUC 从 0.70 涨到 0.74看起来一直在提升但拆开细看粉丝数排前 20% 的用户贡献了其中绝大部分新用户和低活跃用户的 AUC 反而在掉。原因是这些头部用户行为密度高特征统计值稳定预测本来就容易冷启动用户行为稀少统计特征接近零向量模型没有足够信息做判断。解决办法是在验证阶段同时打印三组指标全局 AUC、按用户分组的 GAUC、冷启动用户子集的 AUC。三者一起看才能确定优化方向。如果 GAUC 在涨而冷启动 AUC 在下滑说明模型在记忆头部用户的行为模式对没有历史行为的用户没有泛化能力这时就得回到特征侧补冷启动特征而不是继续调模型。5.5 标签缺失值直接用 0 填充梯度方向被带偏如果训练数据里某些行为的标签因为采集原因缺失直接填 0 再算 BCE loss模型会把这些缺失样本当成明确的负样本学习导致该行为的预测概率整体被压低估。处理方式是给每个任务配一个 mask 矩阵缺失的位置在 loss 计算中被排除。方案里填的是 -1 作为缺失标记训练时通过 clamp_min(0) 和 mask 结合使用确保缺失任务不参与梯度计算。6. 验证与进阶离线评估这样切结果才接近线上6.1 验证集切分先按日期划一道硬边界我每次拿到新的行为数据集第一件事不是跑模型而是先把日期字段的分布打印出来确认数据覆盖的天数。然后按时间切分训练集截止在某一天验证集取其后完整周期的数据。import pandas as pd df pd.read_csv(train.csv, parse_dates[date]) split_date pd.Timestamp(2021-06-01) train df[df[date] split_date].copy() valid df[df[date] split_date].copy() # 校验: 两边日期范围不应有重叠 assert train[date].max() valid[date].min() # 特征构造必须只用 train 阶段信息 # 统计特征在 valid 上计算时, 只能使用 valid 样本自身 date 之前的数据这个切法的关键点是统计特征不跨边界计算。训练集里构造的所有用户统计特征都只能基于训练集本身的行为不可以用全量数据先算特征再切分那等于把验证集的信息提前放进了特征里。6.2 离线评估GAUC 才是业务视角的指标全局 AUC 衡量的是排序能力但用户行为预测更关注“同一用户内部正样本排名是否靠前”。GAUC 就是按用户分组计算 AUC再用曝光数加权平均。from sklearn.metrics import roc_auc_score import pandas as pd def gauc(y_true, y_pred, user_ids): df pd.DataFrame({y: y_true, p: y_pred, uid: user_ids}) total_w 0.0 total_auc 0.0 for uid, group in df.groupby(uid): if len(group) 2 or group[y].nunique() 2: continue try: auc roc_auc_score(group[y], group[p]) except ValueError: continue w len(group) total_auc auc * w total_w w return total_auc / total_w if total_w 0 else 0.0验证集切分和 GAUC 这两步是我拆这份资源时最想强调的收获。之前的教训来自一次线上翻车本地全局 AUC 0.73上线后效果完全不对重新排查后发现验证集切分时用了随机切分特征里混入了验证集信息。从那以后我每次做多目标行为预测都会强制走一遍“按时间切分验证加 GAUC 和冷启动子集指标双榜对照”的流程再决定要不要动模型结构。这份方案最大的价值也在这里——它不是只给你一个能跑的模型而是把数据划界、特征隔离、指标拆解这些容易被忽略但决定成败的环节都串起来了。希望帮到你。本文还有配套的精品资源点击获取

想做一个「会获客」的企业网站?

留下需求,1 小时内获取专属建站方案与透明报价。

免费咨询方案
↑