多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

腾讯广告算法大赛Rank10深度学习源码解析:特征工程与调参实战

腾讯广告算法大赛Rank10深度学习源码解析:特征工程与调参实战 简介腾讯广告算法大赛排名第十的深度部分源码与学习说明面向计算机、数学、电子信息等专业学生适合作为算法竞赛入门及进阶的实战参考。压缩包共十个文件整体仅20KB包含六个脚本、两个说明文档、一个启动脚本及一个缓存文件脚本分工明确覆盖数据加载、特征构造、NFFM模型定义、训练与预测等关键环节说明文档则梳理了代码结构与复现步骤。已有71人学习浏览。通过这份资料可以快速理解广告点击率预估场景下深度模型的工程实现学习特征工程与调参思路并借助带注释的源码和运行脚本动手复现排名第十的完整方案代码体量小、依赖少便于在此基础上自行扩展或改造对希望缩短试错成本、从零参考成熟竞赛代码的学习者尤为友好。1. 腾讯广告算法大赛Rank10深度部分源码这份资源能帮你少走多少弯路如果你正在备战广告算法类竞赛一定翻过不少榜上前十名的方案。我拿到这份腾讯广告算法大赛Rank10深度部分源码学习说明时第一反应是“代码没我想象中复杂”第二反应才是“原来把它跑通也没想象中简单”。这份资源不是单独的模型文件而是把深度学习部分的特征拼接、模型定义、训练验证和调参笔记整合在一起的压缩包。作者把“怎么读代码”和“怎么复现”写成了说明文档而不是让你自己去猜每个文件的作用。它适合两类人一是刚接触广告点击率或转化率预测的竞赛新手想找一份能完整跑通的深度模型代码作为脚手架二是已经能跑通baseline但卡在“怎么再往上提一两个千分点”的进阶玩家。下文从目录拆解开始逐步把这套方案的建模逻辑、关键参数和踩坑记录讲透。2. 先看懂Rank10方案的骨架任务设定、特征工程与模型选型比赛方案能不能复用不取决于模型有多新而取决于你对“输入和输出”的理解是否扎实。腾讯广告算法大赛的题目每年会换但深度学习部分的处理套路是稳定的。这一章先把任务设定、特征组织和模型选型的逻辑讲清楚后面看源码时才不会迷路。2.1 比赛任务与数据常见设定从广告日志到转化率腾讯广告算法大赛常见的任务可以归为两类点击率预估和转化率预估。无论是哪一类训练样本都来自广告请求日志每条样本对应一次曝光标签是被点击或是否转化。离线评估指标主要是AUC和LogLoss有些赛季还会带上GAUC或者业务约束的重排序指标。Rank10方案里深度学习部分的目标就是在这个二分类框架下把AUC往上推。处理这类数据时要注意原始日志往往很大几十GB是常态。你需要先做采样和清洗把无意义的广告位、异常设备、空特征过滤掉。常见做法是按时间前几天的数据作训练后一天的数据作验证避免随机切分带来的未来信息泄漏。这份源码的说明里也会强调如果验证集是随机切分的你会高估模型在真实投放场景下的效果因为广告数据天然存在时间漂移。数据字段大致分成三类用户侧的画像年龄、性别、兴趣标签、广告侧的信息广告位、创意类型、行业类目以及上下文侧时间、网络环境、设备型号。稀疏特征数量通常有几十到上百个每个特征的对齐方式会直接影响embedding层的效果。所以不要一上来就堆模型先把字段整明白把数值型和类别型分开这一步做不好后面全白搭。2.2 特征工程怎么组织稠密特征、稀疏特征与时序特征的拼装在深度模型里特征不会像传统LR那样做手动交叉而是靠embedding层自动学习。你需要给每个稀疏特征一个独立的embedding表比如广告id有十万个取值那你就可以设定embedding维度为16或32每个广告id映射成一个向量。稠密特征则直接标准化后拼到最终特征向量里。我一般会先在notebook里做一个小实验把稀疏特征的数量、取值个数、缺失比例列成一张表判断哪些字段能进模型。取值个数少于100的特征可以直接当作稠密特征做one-hot后降维取值个数在百万级以上的比如设备id就不适合直接进embedding表通常要截断高频或做hash映射。时序特征是这个场景里比较特殊的部分比如用户最近点击过的广告序列。如果模型结构是DIN这类带注意力机制的模型序列特征会通过attention分配权重。在纯DeepFM结构里序列特征通常被压缩成统计量比如近7天点击次数、点击类目分布等。Rank10方案的深度部分一般会把这两条路都做出来方便在不同数据规模下取舍。下面给一段特征列定义的参考代码帮助理解“稀疏特征如何处理成模型输入”from collections import namedtuple SparseFeature namedtuple(SparseFeature, [name, vocab_size, embed_dim]) DenseFeature namedtuple(DenseFeature, [name, normalize]) # 稀疏特征特征名、字典大小、embedding维度 sparse_features [ SparseFeature(ad_id, 500000, 16), SparseFeature(user_id, 1000000, 16), SparseFeature(creative_type, 30, 8), ] # 稠密特征一般做标准化 dense_features [ DenseFeature(click_count_7d, True), DenseFeature(device_price, True), ]这段代码用namedtuple定义了两个特征类型。SparseFeature里的vocab_size不是特征实际取值数而是你做了截断之后留下的词典大小。embed_dim通常取8到32太小会损失区分度太大容易在数据量不足时过拟合。DenseFeature里的normalize表示做标准缩放避免数值特征之间量级差异把梯度带偏。2.3 模型选型为什么用深度学习DeepFM/WideDeep/DIN的取舍很多选手拿到这个源码包后会问为什么不是Bert不是图神经网络而是DeepFM或者DIN这类模型原因在于广告日志的特征空间以稀疏离散特征为主DNN没法直接吃原始数据而DeepFM这类模型在处理大规模稀疏特征上有天然优势。WideDeep是把线性部分和深度部分并联线性部分记忆高频组合深度部分泛化未见过组合。DeepFM则是把FM的一阶和二阶交叉直接融入深度网络省去了手工特征工程。DIN则是重点利用用户行为序列用局部激活单元让模型关注和当前广告相关的历史行为。三者的关系不是替代而是按特征丰富度递进。Rank10方案里大多会选择一个主模型加一个辅助模型用后融合的方式提分。选型时我一般看两个指标一是样本量百万级以内用DeepFM足够二是行为序列是否丰富如果每个用户平均行为序列超过20条DIN就值得尝试。深度学习部分源码的价值就在于它是把这三类模型都做成了可切换的配置而不是写死在某个模型里。你只要改一下config文件里的model_type就能从DeepFM切到DIN免去重新写数据加载的麻烦。下面这个表是我在复现方案时常用的模型选型对照也是这份资源里“学习说明”会重点讲的部分模型特征依赖适用数据量离线AUC上的常见观察WideDeep需要用户/广告/上下文特征百万级起步快很快到瓶颈DeepFM稀疏特征为主含二阶交叉千万级稳定是竞赛中默认首选DIN需要行为序列特征行为序列丰富时更强序列特征清洗不干净会拖后腿表格里说的AUC观察是经验层面的不是源码内置结果。实际复现时建议先用DeepFM跑通全流程再切换到DIN。因为DIN对序列的构建方式更敏感特征工程没对齐前直接上注意力机制反而会放大噪声。3. 深度部分的复现路径从源码目录到训练脚本这一章是动手的核心。你要做的不是逐行读代码而是把训练链路完整拉通一遍。我会按“目录结构 → 配置参数 → 数据加载”的顺序讲下来每一步都对应到你解压后看到的文件上。3.1 源码目录结构逐层拆解哪个文件是入口哪个是工具拿到zip后先解压不要急着跑train.py。我先列一下常见Rank10方案目录长什么样这份资源的学习说明里也是按照这个顺序带你读的tencent_ad_rank10/ ├── config/ │ ├── base.py │ └── deepfm.yaml ├── data/ │ ├── feature_dict.py │ ├── dataset.py │ └── process_raw.py ├── models/ │ ├── deepfm.py │ ├── din.py │ └── layers.py ├── train.py ├── evaluate.py └── README.md简单说一下每个文件的作用。train.py是整个仓库的入口负责读配置、构建数据加载器、初始化模型、跑训练循环。config/deepfm.yaml里是模型名、学习率、batch size、epoch等参数。data/feature_dict.py把训练集的稀疏特征值映射成连续索引这是最容易踩坑的地方下面会专门讲。data/dataset.py负责把原始DataFrame转成PyTorch的Dataset。models/deepfm.py定义模型结构layers.py放一些公共网络层比如FM交叉层和多头注意力。我习惯先读config再读models最后读data。因为模型是相对固定的看懂结构之后难点就落在数据管道上。多数同学复现失败不是模型写错而是特征生成和模型输入的字段对不上。所以这份资源的说明里很强调训练和预测必须走同一个process_raw.py不要在预测时手工改字段。3.2 训练配置参数全说明embedding维度、batch size、学习率与早停下面给一个deepfm.yaml的简化示例也是这套源码里比较典型的配置。注意这不是官方参数而是我在复现类似方案时常用的起点值model: DeepFM output_dir: ./checkpoints # 数据相关 train_file: ./data/train.csv valid_file: ./data/valid.csv feature_dict: ./data/feature_dict.pkl # 模型结构 sparse_features: [ad_id, user_id, creative_type] dense_features: [click_count_7d, device_price] embedding_dim: 16 hidden_units: [512, 256, 128] dropout: 0.2 # 训练相关 batch_size: 4096 epochs: 20 learning_rate: 0.0003 lr_decay: 0.1 early_stop_rounds: 3 eval_metric: AUC大部分参数是一眼能懂的我只说几个容易出问题的。embedding_dim取16是大多数千万级样本场景的起步值不要一上来就取64训练慢而且不一定涨点。hidden_units列表表示三层全连接每层512、256、128这个容量对深度学习部分来说是足够的再加大就得靠更多数据支撑。batch_size取4096是因为广告场景样本量大小batch导致单个step梯度噪声大AUC波动明显。learning_rate取0.0003配合AdamW通常能稳定收敛如果用SGD这个值就不够看。early_stop_rounds设3意味着连续3轮验证AUC不涨就停避免把训练时间浪费在过拟合区间。实际运行时会发现这份资源的说明里建议把batch_size和learning_rate联动调整。batch_size翻倍学习率一般也调大一点但不要等比例放大常见做法是乘以sqrt(2)左右。如果你的显卡显存不够优先减小batch_size而不是降低模型hidden_units因为调小hidden层容量会造成模型欠拟合不像batch_size那样只影响训练稳定性。3.3 数据加载与特征拼接代码块把原始特征转成模型输入这是整个复现路径里最关键的一块。直接上代码PyTorch风格import torch from torch.utils.data import Dataset class AdDataset(Dataset): 广告日志样本封装稀疏特征转索引稠密特征做归一化 def __init__(self, df, feature_dict, sparse_cols, dense_cols): self.labels torch.FloatTensor(df[label].values) # 稀疏特征把原始取值映射成整数索引 self.sparse {} for col in sparse_cols: mapping feature_dict[col] raw df[col].map(lambda x: mapping.get(x, mapping[UNK])).values self.sparse[col] torch.LongTensor(raw) # 稠密特征这里直接用原始值外部做标准化 self.dense torch.FloatTensor(df[dense_cols].values) def __len__(self): return len(self.labels) def __getitem__(self, idx): return self.sparse, self.dense[idx], self.labels[idx]这段代码做了三件事。第一把稀疏特征列通过feature_dict映射成整数索引map里如果遇到词典外取值统一映射到“ ”占位符防止预测时因新ID出现导致报错。第二把稠密特征直接放进FloatTensor注意标准化一般在特征工程阶段完成不要在Dataset里每次重复算均值方差。第三label转成FloatTensor满足二分类交叉熵的输入要求。这里有一个隐藏的踩坑点__getitem__返回的self.sparse是一个字典里面每个value是向量。如果你的模型前向传播直接传这个字典需要在模型内部按feature名去查embedding表。我一般习惯返回一个索引矩阵而不是字典形式是[batch_size, num_sparse_features]这样DeepFM可以直接查表。上面代码为了可读性保留了字典形式你在动手改的时候需要和models/deepfm.py的输入对齐。参数方面sparse_cols和dense_cols要严格和配置文件里的列表一致。顺序变了embedding表查出来的向量就会喂错位置这种错误不会报错只会悄悄损害AUC。所以写完Dataset后我建议print一次前几个batch的shape确认维度符合预期再开训。4. 训练与验证把Rank10的调参思路变成自己的实验清单跑通代码只是第一步比赛里真正拉开差距的是训练策略。这一章我会把验证集切分、early stopping、超参调整和模型融合串成一条可执行的实验清单每一步都对应着能直接抄进自己工程里的做法。4.1 Early Stopping与验证集切分避免线上分数好看、线下翻车在广告竞赛里验证集怎么切直接决定了你调参的方向。很多人用train_test_split随机切出了高AUC一提交却暴跌。原因是广告数据里有大量的同一用户、同一广告的重复曝光随机切分会把同一个用户的样本同时放进训练集和验证集模型相当于见过部分答案。Rank10方案里通常的做法是按时间切分比如用前7天训练、第8天验证。如果你的数据没有时间戳也可以用曝光ID的哈希进行分桶但效果不如时间切分自然。Early stopping也不能只盯着training loss要盯验证集AUC。常见做法是每个epoch结束算一次AUC连续N轮不涨就保存上一个checkpoint。下面给一个非常轻量的early stopping实现class EarlyStopper: 连续patience轮验证AUC不涨就停并记录最佳状态 def __init__(self, patience3): self.patience patience self.best_auc 0.0 self.counter 0 def track(self, auc, model, save_path): if auc self.best_auc: self.best_auc auc self.counter 0 torch.save(model.state_dict(), save_path) else: self.counter 1 if self.counter self.patience: return True # 触发早停 return False这个类的逻辑很简单每次track传入验证AUC如果比历史最佳高就更新best并保存模型否则计数加一连续patience次不涨就返回True触发停止。注意保存模型的位置每轮都可能覆盖所以最后要用best_auc对应的checkpoint去预测而不是最后一个epoch的模型。这是很多人容易忽略的因为框架默认保存最后一步的权重。参数patience设多少取决于验证集波动幅度。样本量大、验证集大AUC波动小patience设2~3就够验证集只有几千条AUC乱跳建议设5~8否则会过早停在一个次优位置。4.2 学习率与embedding维度的边际效应从日志里看模型状态深度学习部分的调试大部分时间花在调学习率和embedding维度。我复现这类源码时习惯先把学习率固定住跑三个embedding维度8、16、32观察训练集AUC和验证集AUC的差距。如果训练集AUC远高于验证集说明embedding维度大了模型把ID类特征背下来了如果两边都低说明维度太小特征区分度不够。学习率的观察点要看第一个epoch的loss下降速度。如果第一个epoch结束时loss只降了一点点先调大学习率到3e-4如果loss直接nan学习率高到爆调小到1e-4。深度学习部分用AdamW居多它自带自适应学习率但不是所有参数都一视同仁embedding层和全连接层建议分开设学习率。embedding层的更新幅度通常要比全连接层小因为稀疏特征的梯度是稀疏的太激进会把词典向量搅乱。下面这张表是常见的调参观察记录表比赛的深度学习部分完全可以按这个格式记参数初始值现象调整方向learning_rate3e-4loss每个epoch波动大降到1e-4embedding_dim16验证AUC比训练低2个点以上降到8观察差距batch_size4096每个step耗时长降到2048配合梯度累积dropout0.2验证曲线明显抖动提高到0.3~0.5这个表不是源码里固定的是我在跑深度模型时的通用记录习惯。你可以把它当成实验清单而不是一劳永逸的最佳参数。Rank10方案里不少提分点就是这种小参数调出来的单独看每个变化都不到一个千分点叠加起来就很可观。4.3 多折交叉与模型融合Rank10方案里最后能提分的三个点当你单模型AUC提升不动时可以试试融合。广告算法竞赛不像纯CV比赛那样能无限堆模型更讲究“稳”。我在这个源码包上试过的三个提分点按性价比排序是五折模型平均、DeepFM与DIN融合、预测概率rank平均。五折模型平均是最容易操作的。把训练数据按时间切成5份每次用4份训练、1份验证得到5个模型对同一份测试集取概率平均。代价是训练时间变成5倍但AUC通常比单模型高0.1到0.3个百分点在榜单前列就是名次差距。DeepFM与DIN融合需要先确认行为序列特征有没有做对。如果只把序列压成统计量DIN和DeepFM的区别不大如果序列完整DIN的预测分布会和DeepFM有可见差异这时候加权融合才有意义。我一般先看两个模型在验证集上的预测相关系数超过0.95就不用融合了说明信息重叠太高。rank平均是另一个小技巧先把每个模型的预测概率排序转成排名序号再平均序号。这种做法能减少概率校准差异带来的影响比如一个模型喜欢输出0.1~0.3另一个模型输出0.7~0.9直接平均概率会让第二个模型主导rank平均可以避免这种量纲问题。5. 避坑与常见问题从特征错位到显存爆炸的五条踩坑记录源码包毕竟不是官方文档跑通到提交之间全是坑。这一章我把复现时最容易踩的五条记录写出来每一条都是先看到现象、再定位原因、最后给出可复制的解决方式。5.1 特征词表不一致训练AUC高预测结果却是一堆默认值现象训练集上跑出来AUC很高但把预测脚本跑在测试集上输出的概率全部集中在某个固定值附近或者效果和随机差不多。原因训练和预测用了两套特征词表。比如训练时对ad_id做了频次截断只保留出现超过10次的ID其余映射为UNK预测时直接用了原始ad_id没有走同一个process_raw.py导致模型看到大量陌生IDembedding表只能查到UNK向量信息量直接丢失。解决强制把特征词表导出成pkl文件训练和预测都从同一个词表加载。代码层面在train.py和evaluate.py里都引用同一个feature_dict.pkl并且对待未知ID的处理方式写死mapping.get(x, mapping[UNK])。从那以后我再也没有手工在预测脚本里写临时映射。5.2 显存OOMbatch_size 4096在16G卡上跑不动现象训练脚本一启动几秒钟后就报torch.cuda.OutOfMemoryError有时连数据加载都没走完就被卡死。原因batch_size设得过大加上每个稀疏特征都要展开成embedding向量中间激活值非常占显存。广告场景ID特征多每个样本的embedding查找结果是一张[batch_size, num_sparse_features, embedding_dim]的中间张量4096个样本乘几十个特征显存瞬间被吃满。解决先把batch_size降到1024再把embedding_dim从16降到8确认显存占用稳定。如果还想保持大批次在训练循环里加梯度累积凑够等效batch size再更新参数。不要直接砍模型hidden_units那样会伤模型容量。我一般会在train.py里加一个--batch_size参数先拿小batch跑通一个epoch观察峰值显存再逐步调大。5.3 loss变成NaN学习率没做warmupembedding向量直接炸掉现象训练到第二个epoch时loss变NaN打印embedding层的梯度发现大部分是NaN或者极大值。原因深度学习部分的embedding层在训练初期梯度非常不稳定尤其当某个稀疏ID出现次数很少对应向量会被大梯度直接推出正常范围。加上一开始就用3e-4的学习率AdamW还没来得及适应稀疏特征的参数就已经被更新到错误区间。解决给学习率加一个warmup步数前1000步从0线性升到目标学习率。另外给embedding层单独设一个更低的学习率比如全连接层用3e-4embedding层用1e-4。这两个改动在一起NaN基本不会再出现。我后来每次新建训练脚本都会先把warmup写进去而不是等loss炸了再补救。5.4 验证集AUC和线上结果差很多验证集随机切分太乐观现象本地验证AUC 0.78跑到线上只有0.75榜单名次远低于预期。很多人第一反应是换模型但怎么换都回不去那个本地分数。原因验证集是随机切分的广告日志里同一个用户在训练集和验证集同时出现模型相当于见过用户历史行为的部分答案。线上数据是时间上完全未来的用户和广告分布都有漂移随机切分给出的AUC天然偏高。解决改成按时间切分用前7天训练、第8天验证。如果只有一份样本没有时间戳就用曝光记录的序号按前80%后20%切至少能保留时间顺序。Rank10方案的说明里特别强调过这一点等你想通验证集问题再用同样的模型去提交排名能涨不少。5.5 复现结果和源码标注不一致随机种子、样本顺序和GPU差异现象用同一份源码和同一份数据跑出来的AUC总是比资源里学习说明写的低0.5到1个百分点而且每次复现波动大。原因深度模型对随机种子和样本顺序敏感。另外广告日志里类别特征的embedding初始化是随机的不同服务器上浮点运算顺序也不完全相同PyTorch在不同GPU上即使计算逻辑一致结果也会有微小差异。解决把随机种子固定下来包括Python、NumPy、PyTorch和CUDA的seed同时固定DataLoader的shuffle顺序。不要只看最后一次复现的分数同一组参数跑三次取中间值才适合和源码标注分数作比较。从那以后我每次训练脚本都会在开头固定seed并且把每个run的AUC记到日志里方便比较参数变化。6. 从这份源码到自己的比赛方案三个可迁移的进阶技巧看完源码、跑通训练之后真正留下来的是可以迁移到下一场比赛的能力。我在这份Rank10方案上最后做了三件事你也可以照做。第一件事是特征一致性检查。比赛最隐蔽的错误都发生在“训练和预测的特征对不上”。我会在提交前写一个对比脚本打印训练集和预测集的特征列差集train_cols set(train_df.columns) pred_cols set(pred_df.columns) diff train_cols.symmetric_difference(pred_cols) if diff: print(特征列不一致, diff) else: print(feature columns ok)这个脚本能在三十秒内暴露字段名拼写不一致、类型转换遗漏等问题。很多线上翻车不是模型问题而是这类低级错误。第二件事是验证集切分记录。我会把切分方式、AUC、LogLoss和模型结构写在同一行实验日志里。以后再调参数时可以直接对比同一验证集切分下的变化不会因为验证集变了而误判参数效果。第三件事是固定随机种子并保留完整checkpoint。固定种子保证复现性完整checkpoint则让你能在早停后回到最佳模型。我在复现这份源码时把这三个习惯固化成了自己的默认流程从那以后每次拿到新比赛源码都先跑通、再改参、再融合几乎没有再因为低级失误重跑整轮训练。这份腾讯广告算法大赛Rank10深度部分源码学习说明.zip非常适合当作第一份“完整深度学习比赛代码”去拆解。解压后先读README.md再按第2、3、4章的顺序复现一遍最后参考第5章的踩坑记录排查自己的工程习惯。希望帮到你。本文还有配套的精品资源点击获取
返回列表