ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

腾讯广告算法大赛Rank10深度源码解析:从Embedding到多任务学习

腾讯广告算法大赛Rank10深度源码解析:从Embedding到多任务学习 简介面向准备腾讯广告算法大赛或对广告点击率预估、大规模稀疏特征建模感兴趣的算法学习者这份Rank10深度部分源码包提供了可复现的竞赛级方案。源码完整覆盖数据读取、预处理、特征构造、模型训练与预测链路包含数据准备脚本、稀疏数据载入脚本、特征工程脚本、深度因子分解机模型脚本以及一键运行脚本结构层次分明便于直接运行和二次修改。压缩包共10个文件以Python源码为主6个py辅以2个Markdown说明文档、1个Shell启动脚本和1个编译缓存文件整体仅20KB轻量易部署适合有Python基础、希望读懂竞赛代码的读者对照学习。已有71人学习下载可作为计算机、数学、电子信息等专业学生的竞赛项目参考资料从中学习赛事级模型调优思路、数据流水线组织方式与工程化代码习惯对于想进入广告推荐领域的开发者也是一份难得的实战范例。1. 腾讯广告算法大赛 Rank10 的深度部分这套源码到底在解什么题腾讯广告算法大赛是典型的商业场景算法竞赛数据来自真实的广告曝光日志、点击标签和转化标签混合体。Rank10 这个名次放在整场比赛里意味着特征工程和深度模型的配合已经非常成熟不是靠单模型碰运气更不是靠调一个超参数捡漏。标题里的“深度部分源码”通常指整套方案里承担深度学习建模的那部分代码——从原始样本读取、特征编码、嵌入层、深层网络到多任务输出、评估脚本的完整闭环。对于想通过竞赛源码学东西的人来说这套代码最有价值的不是网络层数有多深而是它怎么把真实业务字段喂给模型、怎么划分验证集、怎么控制训练噪声。适合两类人读正在做广告 CTR/CVR 建模的算法工程师以及想提升深度学习落地感觉的竞赛新手。下面按“看代码之前要懂的比赛逻辑 → 模型结构 → 最小复现流程 → 坑都在哪 → 怎么迁移业务”这条线讲。2. 拆源码前先拆比赛腾讯广告算法大赛的任务形态与数据接口腾讯广告算法大赛每年的具体题目不完全一样但大方向始终围绕广告系统里最核心的两个问题这条广告会不会被用户点击以及点击之后能不能带来转化。Rank10 这套深度部分源码所对应的题目大概率也是二分类或者带权重的偏好排序问题标签字段里要么是点击标记要么是转化标记。拿到代码包之后我习惯先不急着翻模型脚本而是先把题目定义弄清楚训练集有几张表主表和副表怎么 join标签列是 int 还是 float评估指标是什么。这些信息决定了你在读深度模型时该关注哪些部分。很多新手栽在同一个地方拿到开源代码就去找model.py却不知道整个 pipeline 里数据是怎么进出的最后换了自己的数据就跑不通。源码里真正的骨架不是模型而是数据和标签的流转方式。2.1 赛题形态CTR 预估与转化率预测在代码里的差异CTR 预估和转化率预测是广告算法赛最常出现的两种任务形态。CTR 预估的标签是用户是否点击正样本比例通常很低可能只有 1% 到 5%。转化率预测的标签是点击之后是否完成注册、下单等动作正样本会更稀疏训练时经常要做负采样或者样本加权。在源码里这两种任务的区别会在三个地方体现出来。第一个是 loss 函数CTR 任务常用 BCEWithLogitsLoss转化率任务经常要加权重参数。第二个是负采样逻辑CTR 的样本天然是曝光日志转化率任务会在代码里额外做“点击后再看转化”的条件过滤。第三个是评估指标CTR 看 AUC 或者 GAUC转化率会看带权重的 AUC 或者业务自定义分数。提示读源码第一件事是找到配置文件里的label字段看它来自哪张表、取值范围是什么。这能帮你快速判断赛题类型。2.2 特征字段user/ad/context 三类离散特征怎么对齐腾讯广告算法大赛的数据可以粗略分成三类特征用户特征、广告特征和上下文特征。用户特征包括年龄、性别、兴趣标签、历史行为统计广告特征包括广告 id、素材 id、行业分类、出价信息上下文特征包括投放场景、时间段、页面位置等。在深度部分源码里这些特征几乎全部走“离散化 embedding”的路线。数值型特征比如用户活跃天数、广告曝光次数通常先做分桶或者归一化再当作类别特征处理或者直接进数值层。类别型特征比如广告 id、素材 id基数很大动辄几十万甚至上百万不可能直接 one-hot所以代码里全是nn.Embedding或者EmbeddingBag。读代码时可以按字段名去对应特征分组。常见做法是源码目录下有一个feature_config.py或者column.py文件里面定义了每个字段的类型、最大 id 数量、embedding 维度。这些参数直接决定模型参数量也是你换数据集时最先需要改的地方。2.3 评估指标与提交格式从代码注释里反推打分逻辑竞赛源码的评估脚本通常写在根目录的evaluate.py或者metric文件夹里。不要跳过它。评估指标不仅决定排行榜名次还直接影响模型训练时选择哪一个 checkpoint。广告类赛题最常见的评估指标是 AUC。AUC 对正负样本比例不敏感所以训练集里负样本多一点没关系。如果赛题用了 GAUC分组 AUC那代码里一定会有“按用户分组”或“按广告分组”的逻辑这时候验证集的分层采样就非常关键。还有一个容易被忽略的点提交格式里预测的是pctr还是pctr * 10000有些赛题为了精度会把分数放大评估脚本会再除回来。读这部分代码时顺便检查一下提交文件的列名和顺序能省掉提交前最后一次翻车。3. 深度部分的模型主线特征嵌入 深层网络 多任务输出深度部分说白了就是一张稀疏特征稀疏、样本量大的表格怎么过神经网络。广告场景的数据通常是百万行起特征里大部分是 id 类离散变量。深度模型要解决的核心问题是不靠人工交叉让网络自己学会特征之间的非线性关系。Rank10 的这套源码的深度部分模型骨架基本逃不出几个经典结构底层是嵌入层把所有离散特征变成稠密向量中间是两层到四层的全连接网络或者轻量级注意力结构输出层根据任务数量决定是一个 sigmoid 还是多个 sigmoid。区别在于细节嵌入层做了多少次聚合全连接层的激活函数残差连接放在哪里dropout 加在什么位置。3.1 特征离散化和嵌入层怎么搭广告赛题里几乎不会有原始图像或文本特征以 id 和统计量为主。离散特征进入模型前要完成两个动作把字段映射成连续整数 id再查表得到 embedding。第一个动作对应源码里的vocab和index两样东西。vocab 是字段值和 id 的映射表常见实现是字典或者 hash 分桶。字段值的出现频率通常要做一个下限过滤——出现次数少于 10 次的统一归为未知 id这样能控制 vocab 大小。有些源码会把没有过滤的原始 id 全部塞进模型导致 embedding 表巨大、训练变慢这是非常常见的坏味道。第二个动作是 embedding 聚合。每个离散字段在 batch 里可能是一个 id也可能是变长序列比如用户兴趣标签列表所以常见做法是EmbeddingBag以 mean 或者 sum 做聚合把变长输入变成定长向量。聚合操作会省掉手动 padding 的麻烦训练速度也更快。代码示例里典型的实现长这样import torch.nn as nn class FeatureEmbedding(nn.Module): 把离散字段列表转成 embedding 向量。 field_ids: 每个字段的 id 序列形状为 (batch_size, field_cnt) def __init__(self, num_embeddings, embedding_dim, field_cnt): super().__init__() # num_embeddings 是所有字段 vocab 合并后的总大小 # 这里用 EmbeddingBag 而不是 Embedding padding # 因为广告赛的序列特征长度不一致EmbeddingBag 直接支持变长序列。 self.embedding nn.EmbeddingBag(num_embeddings, embedding_dim, modemean) self.field_cnt field_cnt def forward(self, x): # x: (batch_size, field_cnt) # 先展平再查表EmbeddingBag 会自动按 segment 做 mean 聚合 batch_size x.size(0) x_flat x.view(-1) offsets torch.arange(0, batch_size * self.field_cnt, self.field_cnt, devicex.device) return self.embedding(x_flat, offsets) # (batch_size, embedding_dim)这段代码的逻辑是把二维输入(batch_size, field_cnt)展平成一维后通过offsets告诉EmbeddingBag每个样本从哪个位置开始。EmbeddingBag会按段完成 mean 聚合输出每个样本一个向量。参数上需要注意num_embeddings要等于整个训练集所有字段的独立 id 总数加一个未知 id 槽位。如果设置小了训练时会出现 index out of range 的报错这是踩坑率最高的一个错误。3.2 深层网络主体与多任务输出头的取舍嵌入层出来之后是深度网络主体。Rank10 源码里最常见的是三层 MLP宽度从 512 到 1024 递减激活函数用 ReLU 或者 LeakyReLU。两层太浅表达不了广告特征里的高阶交叉五层以上在几百万样本量上容易过拟合。三层是一个比较稳的折中。多任务输出是广告赛题里的加分项。如果赛题同时给了点击和转化两种标签代码里会出现多个输出头一个头输出pctr一个头输出pcvr两个头共享底部的嵌入层和部分隐藏层。这种共享结构能让模型在转化标签稀疏时借用点击信息。需要注意的细节是共享比例——如果两个任务数据量悬殊比如点击样本是转化样本的二十倍共享层太多会导致转化任务学不动。输出头代码通常长这样class DeepModel(nn.Module): 共享嵌入和三层 MLP分别输出 pctr 和 pcvr。 def __init__(self, embedding: FeatureEmbedding, hidden_sizes: list): super().__init__() self.embedding embedding # 共享隐藏层三层 MLP self.shared_layers nn.Sequential( nn.Linear(embedding.embedding.embedding_dim, hidden_sizes[0]), nn.ReLU(), nn.Dropout(0.2), nn.Linear(hidden_sizes[0], hidden_sizes[1]), nn.ReLU(), nn.Dropout(0.2), ) # 两个任务各自的输出头 self.ctr_head nn.Linear(hidden_sizes[1], 1) self.cvr_head nn.Linear(hidden_sizes[1], 1) def forward(self, x): emb self.embedding(x) h self.shared_layers(emb) pctr self.ctr_head(h) pcvr self.cvr_head(h) return pctr, pcvr这里的取舍点有三个。第一Dropout放在 ReLU 之后而不是之前因为激活函数之后的值已经稳定dropout 不会破坏稀疏性。第二两个输出头不共享最后一层避免转化任务被点击任务带偏。第三forward 返回两个 logits 而不是已经过 sigmoid 的概率loss 计算要用BCEWithLogitsLoss数值上比先 sigmoid 再 BCE 更稳定。3.3 训练配置学习率、batch size、优化器如何定初值广告赛题的数据量决定了训练配置不可能照搬 CV 或者 NLP 的经验。学习率用太大第一轮就发散用太小训练一晚上 AUC 还是贴着 0.5。常见做法是以 1e-3 起步配合 cosine 或者 step 衰减。优化器优先选 Adam 或者 AdamW广告特征经过 embedding 层之后梯度的量级差异比 CV 大很多Adam 的自适应学习率能省掉大量调节时间。batch size 在广告赛里通常往大了设256 到 2048 都是常见区间。大 batch 的好处是 embedding 查表更规整、训练吞吐更高坏处是模型收敛会变慢而且 AUC 抖动变小后不容易判断是否需要降低学习率。经验做法是以 512 为起点观察 loss 和前五个 epoch 的 AUC再决定往上调还是往下调。还有两个细节值得单独拿出来说。一个是梯度裁剪广告赛的特征矩阵很稀疏偶发的一两个异常 id 会把梯度拉满clip_grad_norm_(model.parameters(), 5.0)可以避免这一步。另一个是学习率预热前两三个 epoch 让学习率从 1e-4 线性涨到目标值处理 embedding 层收敛慢的问题。这俩配置在源码里经常出现但新手很容易忽略。如果看到训练到第三个 epoch 时 loss 不降反升先查学习率和梯度裁剪这两项。4. 复现流从源码铺目录到跑通第一轮训练把别人的竞赛源码在自己机器上跑通这件事比想象中费时间但收益也大。很多人下载了代码包之后急着打开模型文件逐行读读了一个小时还在看 forward 函数的行数不知道整个训练流程长什么样。我的建议是先跑再读。先跑通一遍会暴露所有环境问题再回头看代码时你会带着“这段代码到底起了什么作用”的问题效率完全不一样。4.1 先按三个入口读数据加载、模型定义、训练循环一套完整的深度部分源码至少包含三个入口文件。如果你看到的目录结构里只有一个main.py那说明代码写得比较紧凑入口都在里面如果有dataset.py、model.py、train.py那就按这三个文件去拆。dataset.py负责把原始文件读进来、做特征映射、产出 batch。读的时候重点看三件事有没有缓存预处理结果、标签列怎么处理空值、返回的数据结构和 forward 函数的输入对不对得上。model.py负责网络结构读的重点是 embedding 的num_embeddings和隐藏层维度。train.py负责训练循环重点看评估频率、checkpoint 保存条件、验证集是怎么切出来的。这三段对应了三组最容易踩的问题。数据加载踩的是内存和 dtype 不匹配模型定义踩的是输出维度对不上标签训练循环踩的是验证集切分引入了未来信息。三组问题的解决方法完全不同所以读代码时不要把这三部分混在一起看。4.2 最小训练脚本把样本拼成 batch 再喂进模型跑通第一轮训练往往不需要原始全量数据。从训练集里随机抽十万行够模型跑起来就行。我习惯写一个最小可跑的训练脚本规模和全量训练一样但数据少几个数量级用来验证数据链路和模型链路是不是通的。import torch import torch.nn as nn from torch.utils.data import DataLoader, TensorDataset def run_min_train(model, fields, labels, epochs5): 最小训练流程用少量数据验证数据链路和模型链路。 fields: 形状 (num_samples, field_cnt) 的 LongTensor已经做好 id 映射 labels: 形状 (num_samples, 1) 的 FloatTensor dataset TensorDataset(fields, labels) # 广告数据字段多且稀疏drop_lastTrue 保证最后不完整的 batch 不参与更新 loader DataLoader(dataset, batch_size512, shuffleTrue, drop_lastTrue) ctr_loss nn.BCEWithLogitsLoss() optimizer torch.optim.AdamW(model.parameters(), lr1e-3) for epoch in range(epochs): total_loss 0.0 for batch_fields, batch_labels in loader: optimizer.zero_grad() pctr, _ model(batch_fields) # 单任务场景不关心 cvr 输出 loss ctr_loss(pctr, batch_labels) loss.backward() # 梯度裁剪广告特征稀疏偶发异常 id 会拉爆梯度 torch.nn.utils.clip_grad_norm_(model.parameters(), 5.0) optimizer.step() total_loss loss.item() print(fepoch {epoch:02d} loss {total_loss / len(loader):.4f})这个脚本跑通的意义是证明三件事feature_config 里的 vocab 数量足够容纳数据里的 id 最大值模型的输入维度和 DataLoader 的输出维度一致loss 在五个 epoch 内是稳定下降的。如果前两个 epoch 的 loss 从 0.69 往 0.60 走基本说明链路没问题如果 loss 直接掉到 0.1 或者变成 NaN先检查标签有没有被错误转成了 one-hot再检查labels的 dtype 是不是 FloatTensor。4.3 用验证指标判断模型状态AUC 涨到多少算正常跑通训练之后下一步就是拿到验证集上看 AUC。广告赛题的 AUC 基线不是 0.7而是要看赛题本身。有些赛题特征信息量足简单 LR 都能到 0.75深度模型跑到 0.8 以上有些赛题噪声大0.60 就算高分。所以不要拿网上别人报的 AUC 来比而要用同一个赛题的排行榜成绩做参照。验证集 AUC 的计算要注意一点数据集标签稀疏时sklearn.metrics.roc_auc_score的输入必须是分数而不是类别标签而且正负样本都要出现在验证集里。如果验证集正样本只有几十条AUC 的置信区间会很大换一个随机种子结果差三个点也正常。源码里通常会有验证脚本先看它有没有做分层抽样没有就自己补上。还有一种情况比较隐蔽验证 AUC 高得离谱比如超过 0.95。这不是好事大概率是特征泄漏。典型泄漏路径有两条一条是训练和验证集按用户 id 切分时用户的历史统计特征里包含了验证集时段的信息另一条是特征管线里用了全体数据的统计量做归一化导致验证集的信息渗透进训练阶段。5. 避坑特征泄漏、随机种子和本地重放不一致这一章的每条经验都是竞赛季的血泪教训。广告赛题和普通图像赛题最大的不同在于数据里有明显的时间结构、用户结构和稀疏标签三个因素叠加制造出了一批特别容易翻车的坑。每条我都会按照“现象 → 原因 → 解决”来写方便你对照排查。5.1 特征泄漏让验证集虚高现象、原因、解决现象验证 AUC 高达 0.96 甚至 0.98本地指标明显好于排行榜同期水平但提交后排名反而靠后。原因赛题数据的标签是在曝光之后一段时间才产生的。如果特征构造脚本拿“曝光之后的历史统计”去算用户或广告维度的均值相当于把未来信息塞进了当前样本。典型例子是把全量训练集的用户平均点击率作为特征喂给模型——训练集里已经包含了当前样本的标签信息这个特征一测一个准验证集 AUC 自然会虚高。解决特征构造严格按时间截断。用户历史统计特征只允许使用样本曝光时间点之前的数据。如果赛题没有显式给时间字段就按训练集的样本顺序和用户 id 分组做分位数切分统计量只从切分前的组里算。一个简单自检方法是把验证集的标签随机打乱再做特征如果模型 AUC 依然明显高于 0.5那特征里一定有泄漏。5.2 随机种子不同导致结果差异巨大现象、原因、解决现象同一个训练脚本只改了随机种子验证 AUC 从 0.78 掉到 0.76团队里两个人跑的结果对不上。原因广告数据集正样本稀少验证集太小会导致指标抖动。另一个原因是 embedding 初始化和负采样的随机性都会传播到最终结果而广告赛题本身特征中区分度高的就那几个字段模型对初始随机状态特别敏感。解决固定三处随机种子Python 的random、NumPy 的np.random和 PyTorch 的torch.manual_seed。验证集切分也固定下来不要每次跑都重新切。更稳妥的做法是跑出五个随机种子的结果取均值作为最终成绩提交时用均值对应的模型。这个习惯在广告赛里比在 CV 赛里更重要因为数据噪声大导致单次结果方差高。5.3 训练日志异常NaN、AUC 不动、loss 周期性弹跳现象一某个 epoch 中途 loss 变成 NaN之后所有 batch 都是 NaN。原因学习率过大导致 embedding 层的梯度爆炸或者特征里有极端数值比如数值型特征没归一化原始值超过 1e8经过隐藏层后直接溢出。解决加梯度裁剪是最快的止血手段。然后检查数值特征的分布把超过 99.9 分位数的值做截断或者取 log1p。不要在 log 之前让原始数值直接进全连接层这是广告赛的标准教训。现象二训练 loss 稳定下降但验证 AUC 始终在 0.5 附近不动。原因模型没有学到有效信息。常见原因不是模型容量不够而是特征 id 映射错了——多个字段共用了同一个 vocab 表或者 embedding 的num_embeddings设得过大绝大多数 id 落在未知槽位里。解决把 feature_config 里的字段和 vocab 一一对应检查尤其看新加的字段有没有进了旧的映射表。另一个排查办法是把模型输出和某个强特征单独做 AUC——如果广告 id 单特征 AUC 有 0.65模型整体 AUC 是 0.5问题一定出在模型结构或者训练配置里不是特征的问题。现象三loss 每轮训练先降后升呈锯齿状反复。原因batch size 太小加上学习率固定不变模型在损失面的鞍点附近来回震荡。还有可能是样本是按用户聚合进 batch 的同一个 batch 内特征相关性过强梯度方向不均衡。解决先把 batch size 提到 1024 左右再把学习率从固定值换成带衰减的调度器。如果还不行检查 DataLoader 的 shuffle 是否真的生效以及数据集是不是按用户 id 做了 sort 导致相邻样本相似度过高。6. 把竞赛思路迁到广告业务四个改动和一条验证思路竞赛代码迁移到线上业务最核心的四个改动是特征口径、样本分布、延迟约束和模型更新频率。竞赛里特征可以从一张完整宽表里随便取线上则要考虑特征服务能不能在几十毫秒内返回竞赛里正负样本比例是赛题定死的线上要结合流量收益重新做采样竞赛里模型一天跑一次评估就行线上要面对实时样本延迟和小时级更新。这些改动里最容易被低估的是特征口径迁移。竞赛的离线特征管线通常基于完整的日志回填线上则必须保证训练时用的特征和推理时拿到的是同一份。一个常见做法是把离线特征构造逻辑封装成一个统一函数离线训练和线上 serving 共用同一个函数入参和返回结构从源头避免两套口径。验证思路方面我推荐保留一个固定时段的“冻结样本集”每次模型迭代都在这个样本集上算一遍 AUC 和业务指标再用滚动时段样本做第二遍验证。这个习惯帮你区分“模型真的变好了”和“只是数据分布漂移了”。做算法这几年最大的体会是竞赛排行榜衡量的是单点指标业务线衡量的是长周期稳定性两者的差距要靠一套固定样本集和分层指标来弥合。希望帮到你。本文还有配套的精品资源点击获取
返回列表