简介:这份PDF文献聚焦深度学习在搜索广告排序中的落地应用,面向广告算法工程师、推荐系统学习者及数据分析研究者,帮助理解点击率(CTR)预估这一广告业务核心环节的技术演进。全文围绕卷积神经网络与LSTM的混合模型展开,先由CNN提取广告数据中的高影响力特征,再借助LSTM的时序建模能力完成预测与分类,并对比了逻辑回归、决策树等传统方法以及百度FNN、谷歌Wide&Deep、PNN等业界模型。文中还完整梳理了特征提取、模型训练、测试预测的CTR预估流程,并通过Kaggle Avazu开源数据集实验,讨论LSTM层数、隐藏层节点数与学习率等参数对AUC值的影响。资源包为1个PDF文件,大小约2.73MB,内容为期刊论文原文,适合作为深度学习广告排序方向的参考文献与专业指导材料。目前已有122人学习,可为广告排序策略优化与模型选型提供可复用的思路与实验参照。
1. 搜索广告排序为什么要用深度学习重做一遍
如果你在广告团队待过,一定见过这样的场景:凌晨两点,算法同学盯着大盘 CTR 曲线,发现某个品类的广告主在周末的转化率突然掉了 30%,而 LR + 人工特征交叉的排序模型完全找不到原因。这不是模型不够努力,而是它压根没能力从原始特征里自己学出「周末 + 某品类 + 某时段」这种高阶组合。搜索广告排序这件事,本质上是在一个极稀疏、高并发、强实时的场景下,对每次请求的候选广告做精准打分。传统方案靠 LR、GBDT 加大量人工特征工程,能跑通,但天花板肉眼可见。深度学习进来之后,Embedding 自动学组合、多层非线性拟合用户意图、序列建模捕捉行为演化,这些能力让排序效果有了质变空间。这篇内容面向的是正在做搜索广告排序、或者准备把现有 LR 模型升级到深度学习方案的工程师,我会把从特征设计到模型上线、从参数设置到线上踩坑的完整路径讲清楚,让你看完能直接在自己的场景里跑通一版可用的排序模型。
2. 搜索广告排序的深度学习建模:从特征到网络结构
2.1 搜索广告排序任务的特殊性在哪里
搜索广告排序和推荐系统排序看起来像,但有几个关键差异决定了模型设计不能照搬。第一,query 是强意图信号,用户输入的关键词直接表达了当下需求,这和推荐场景里用户只是被动浏览完全不同。第二,广告候选集来自竞价,同一 query 下不同广告主的出价、预算、创意质量差异巨大,排序模型必须同时考虑商业价值和用户点击概率。第三,实时性要求极高,一次搜索请求从召回、粗排到精排,留给精排模型的时间通常只有几十毫秒。
传统 LR 模型的处理方式是:人工设计特征交叉,比如「query 词 + 广告标题词」的匹配度、「用户历史点击品类 + 当前广告品类」的一致性,然后把这些交叉特征离散化后喂给模型。问题在于,搜索广告的稀疏性太强了,一个 query 可能一天只出现几次,人工枚举交叉根本覆盖不全。深度学习方案的核心优势就是 Embedding 层能把高维稀疏的 ID 特征映射到低维稠密空间,让模型自己学出「什么 query 和什么广告更搭」。
我一般会把搜索广告排序的深度学习模型拆成四个模块:特征输入层、Embedding 层、特征交叉层、输出层。特征输入层负责把原始特征分成数值型、类别型、序列型三类;Embedding 层把类别型和序列型特征转成向量;特征交叉层用 FM、Deep 网络或 Attention 做高阶组合;输出层给出 CTR 或 CVR 预估值。这个结构不是固定的,但每一层要解决什么问题必须想清楚。
2.2 特征工程:哪些特征必须进模型,哪些可以砍掉
搜索广告排序的特征可以分成四组:query 侧、广告侧、用户侧、上下文侧。query 侧包括 query 本身的分词、query 长度、query 历史点击率;广告侧包括广告标题、广告描述、广告主历史 CTR、广告出价;用户侧包括用户历史点击序列、用户画像标签;上下文侧包括时间、设备、地域。
这里有个血泪经验:不要一上来就把所有特征都塞进去。我见过太多团队把几百个特征一股脑喂给模型,结果训练慢、效果差、排查困难。正确的做法是先做特征重要性分析,保留 Top 50 到 Top 80 的特征,再逐步加。下面是一个特征处理的代码示例,用 PyTorch 实现类别特征的 Embedding 和数值特征的归一化:
import torch import torch.nn as nn class FeatureProcessor(nn.Module): def __init__(self, cat_feat_dims, num_feat_count, embed_dim=8): """ cat_feat_dims: dict, key 是特征名, value 是该特征的取值个数 num_feat_count: int, 数值特征的数量 embed_dim: int, Embedding 维度 """ super().__init__() # 为每个类别特征创建 Embedding 表 self.embeddings = nn.ModuleDict({ name: nn.Embedding(dim, embed_dim) for name, dim in cat_feat_dims.items() }) # 数值特征归一化层 self.num_bn = nn.BatchNorm1d(num_feat_count) def forward(self, cat_inputs, num_inputs): """ cat_inputs: dict, key 是特征名, value 是 (batch_size,) 的 LongTensor num_inputs: (batch_size, num_feat_count) 的 FloatTensor """ emb_list = [] for name, emb_layer in self.embeddings.items(): emb_list.append(emb_layer(cat_inputs[name])) # (B, embed_dim) num_out = self.num_bn(num_inputs) # (B, num_feat_count) # 拼接所有 Embedding 和数值特征 concat = torch.cat(emb_list + [num_out], dim=1) return concat这段代码的逻辑是:类别特征通过 Embedding 层转成低维向量,数值特征做 BatchNorm 归一化,最后全部拼接成一个稠密向量。参数方面,embed_dim一般设 8 到 16,太小表达能力不够,太大容易过拟合;cat_feat_dims里每个特征的取值个数要统计全量数据,不能只用训练集统计,否则线上会出现未知取值。
数值特征归一化用 BatchNorm 而不是 MinMax,是因为搜索广告的数值特征分布变化快,BatchNorm 对分布偏移更鲁棒。但要注意,BatchNorm 在训练和推理时的行为不同,上线前一定要确认推理模式下用的是滑动平均统计量。
2.3 网络结构选型:DeepFM、DIN 还是自定义交叉网络
搜索广告排序的深度学习网络结构,常见的有三类:DeepFM 系列、DIN 系列、自定义交叉网络。DeepFM 把 FM 的一阶和二阶交叉与 Deep 网络并联,适合特征交叉模式相对固定的场景;DIN 引入 Attention 机制,根据候选广告动态调整用户历史行为的权重,适合用户行为序列丰富的场景;自定义交叉网络比如 xDeepFM、AutoInt,用显式的高阶交叉层替代隐式学习,适合对可解释性有要求的团队。
我一般会先跑一版 DeepFM 作为 baseline,因为它的结构简单、训练稳定、调参经验成熟。如果用户行为序列特征丰富,再升级到 DIN。下面是一个简化版 DeepFM 的实现:
class DeepFM(nn.Module): def __init__(self, feat_dim, embed_dim=8, hidden_dims=[256, 128, 64]): super().__init__() # 一阶线性部分 self.linear = nn.Linear(feat_dim, 1) # 二阶 FM 部分 self.fm_embed = nn.Embedding(feat_dim, embed_dim) # Deep 部分 layers = [] input_dim = feat_dim * embed_dim for h_dim in hidden_dims: layers.append(nn.Linear(input_dim, h_dim)) layers.append(nn.ReLU()) layers.append(nn.Dropout(0.2)) input_dim = h_dim layers.append(nn.Linear(input_dim, 1)) self.deep = nn.Sequential(*layers) def forward(self, x): """ x: (batch_size, feat_dim) 的稀疏特征索引 """ # 一阶部分 linear_out = self.linear(x) # (B, 1) # FM 二阶部分 emb = self.fm_embed(x) # (B, feat_dim, embed_dim) sum_square = emb.sum(dim=1) ** 2 # (B, embed_dim) square_sum = (emb ** 2).sum(dim=1) # (B, embed_dim) fm_out = 0.5 * (sum_square - square_sum).sum(dim=1, keepdim=True) # (B, 1) # Deep 部分 deep_in = emb.view(emb.size(0), -1) # (B, feat_dim * embed_dim) deep_out = self.deep(deep_in) # (B, 1) # 输出相加后过 sigmoid out = torch.sigmoid(linear_out + fm_out + deep_out) return out这个实现里,feat_dim是所有特征的总维度,embed_dim控制 Embedding 大小,hidden_dims是 Deep 部分的隐藏层维度。关键参数是 Dropout 率,我一般设 0.2 到 0.5,搜索广告场景下 0.2 比较稳,太高会欠拟合。学习率用 Adam 的话初始设 1e-3,配合余弦退火调度。
提示:DeepFM 的 FM 部分和 Deep 部分共享 Embedding,这是它比 Wide&Deep 更省参数的原因,但也意味着 Embedding 维度不能太小,否则两边都学不好。
3. 训练与部署:从离线实验到线上服务
3.1 样本构造与负采样策略
搜索广告排序的训练样本来自曝光日志,正样本是有点击的曝光,负样本是没点击的曝光。但这里有个坑:曝光未点击的样本里,有一部分是因为广告位置靠后用户根本没看到,这部分样本如果直接当负样本,会引入位置偏差。常见做法是做位置纠偏,或者用点击率校准。
负采样方面,搜索广告场景一般不做额外负采样,因为曝光日志本身就有大量负样本。但如果正负比超过 1:100,可以考虑对负样本做下采样,同时用 Focal Loss 缓解类别不平衡。下面是一个样本构造的伪代码:
def build_samples(log_df): """ log_df: 曝光日志, 包含 query_id, ad_id, user_id, click, position 等字段 """ # 正样本: click == 1 pos = log_df[log_df['click'] == 1] # 负样本: click == 0 且 position <= 10 (只取前 10 位的未点击) neg = log_df[(log_df['click'] == 0) & (log_df['position'] <= 10)] # 合并并打乱 samples = pd.concat([pos, neg]).sample(frac=1.0).reset_index(drop=True) return samples这里position <= 10是个经验阈值,不同业务场景要调整。如果广告位只有 5 个,就取position <= 5。目的是过滤掉那些用户根本没机会看到的曝光。
3.2 离线训练与线上推理的一致性保障
离线训练用全量历史数据,线上推理用实时特征,这两者的特征处理逻辑必须完全一致,否则会出现训练-推理偏差。我见过最典型的翻车案例是:离线用 Pandas 做特征归一化,线上用 Java 重写了一遍,结果某个特征的均值算错了,线上 CTR 直接掉 20%。
解决方案是特征处理逻辑统一用一套代码,或者用特征平台统一管理。如果条件不允许,至少要做到:离线保存归一化参数(均值、方差、最大最小值),线上直接加载这些参数,不要重新计算。下面是一个保存和加载归一化参数的示例:
import json import numpy as np # 离线: 计算并保存归一化参数 def save_norm_params(num_features, path='norm_params.json'): params = {} for i, col in enumerate(num_features.columns): params[col] = { 'mean': float(num_features[col].mean()), 'std': float(num_features[col].std()) } with open(path, 'w') as f: json.dump(params, f) # 线上: 加载参数并做归一化 def load_and_norm(num_features, path='norm_params.json'): with open(path, 'r') as f: params = json.load(f) for col, p in params.items(): num_features[col] = (num_features[col] - p['mean']) / (p['std'] + 1e-8) return num_features这个做法虽然简单,但能避免大部分训练-推理偏差问题。关键点是std要加一个极小值防止除零,线上加载参数后不要再做任何统计计算。
3.3 线上服务:模型推理延迟优化
搜索广告排序对延迟极其敏感,精排模型通常要求 P99 延迟在 50ms 以内。深度学习模型如果直接用 PyTorch 推理,单次前向传播可能就要 20ms 到 30ms,再加上特征获取和网络传输,很容易超时。
优化手段有几个方向:第一,模型量化,把 FP32 转成 FP16 或 INT8,推理速度能提升 2 到 4 倍;第二,模型剪枝,去掉冗余的 Embedding 和隐藏层;第三,用 TensorRT 或 ONNX Runtime 做推理加速;第四,特征预计算,把用户侧和广告侧的静态特征提前算好缓存起来。
我一般会先做模型量化,因为改动最小、收益最明显。下面是一个 PyTorch 模型转 ONNX 再做 FP16 量化的示例:
import torch import onnx import onnxruntime as ort from onnxconverter_common import float16 # 导出 ONNX dummy_input = torch.randn(1, feat_dim) torch.onnx.export(model, dummy_input, "deepfm.onnx", opset_version=11) # FP16 量化 onnx_model = onnx.load("deepfm.onnx") onnx_model_fp16 = float16.convert_float_to_float16(onnx_model) onnx.save(onnx_model_fp16, "deepfm_fp16.onnx") # 推理 sess = ort.InferenceSession("deepfm_fp16.onnx") input_name = sess.get_inputs()[0].name output = sess.run(None, {input_name: dummy_input.numpy()})量化后要验证精度损失,一般 FP16 的 AUC 损失在 0.001 以内可以接受,如果超过 0.005 就要考虑混合精度或者只量化部分层。
注意:ONNX 导出时 opset_version 要选对,太低不支持某些算子,太高可能推理引擎不兼容。搜索广告模型一般用 11 或 13。
4. 避坑与排查:搜索广告排序深度学习的五个真实翻车现场
4.1 现象:离线 AUC 涨了,线上 CTR 反而跌了
原因:离线评估用的是全量曝光日志,线上只对精排候选集打分,两者的样本分布不同。离线 AUC 涨可能是因为模型在长尾 query 上过拟合了,而线上长尾 query 的流量占比很小。
解决:离线评估要按 query 频次分层,分别看头部、腰部、尾部 query 的 AUC。线上做 A/B 实验时,重点关注腰部 query 的 CTR 变化。如果离线涨线上跌,优先检查特征里有没有用到未来信息。
4.2 现象:模型训练 loss 震荡,收敛困难
原因:搜索广告的样本极度不平衡,正样本可能只占 1% 到 5%,加上 Embedding 层参数多,梯度更新不稳定。
解决:用 Focal Loss 替代交叉熵,或者对正样本做上采样。学习率用 warmup 策略,前 1000 步从 1e-5 线性增到 1e-3。Batch Size 不要太小,建议 1024 以上,配合梯度裁剪。
4.3 现象:线上推理超时,P99 延迟超过 100ms
原因:Embedding 表太大,单次推理要查几百个 ID,内存访问成为瓶颈。或者特征获取走了 RPC 调用,网络延迟不可控。
解决:Embedding 表做分片,按特征类型拆成多个小表并行查。特征获取尽量走本地缓存,用户侧和广告侧的静态特征提前预计算。模型量化到 FP16 或 INT8,推理引擎换成 ONNX Runtime 或 TensorRT。
4.4 现象:新广告冷启动效果差,CTR 预估偏低
原因:新广告没有历史点击数据,Embedding 是随机初始化的,模型学不到有效表示。
解决:用广告主的历史 CTR 做先验,新广告的 Embedding 初始化为同广告主其他广告的均值。或者引入内容特征,比如广告标题和描述的文本 Embedding,让模型从内容相似度推断点击率。
4.5 现象:模型上线后效果逐渐衰减
原因:用户行为和广告主策略都在变化,模型训练数据是静态的,没有及时更新。
解决:建立在线学习机制,每天用最新数据增量训练。或者做模型热更新,每隔几小时用新数据微调一次。同时监控特征分布,发现偏移及时告警。
5. 进阶技巧:用序列建模和在线学习把排序效果再推一步
搜索广告排序的深度学习方案做到上面这一步,基本能跑通一个可用的版本。但如果想进一步拉开差距,有两个方向值得投入:用户行为序列建模和在线学习。
用户行为序列建模的核心思路是,把用户历史点击的广告序列当成一个时间序列,用 Transformer 或 GRU 编码成固定长度的向量,再和候选广告做 Attention。这样模型能捕捉到用户兴趣的动态变化,而不是只依赖静态画像。具体实现上,可以用 DIN 的 Attention 机制,也可以自己搭一个轻量级的 Transformer Encoder。我一般会先用 GRU 跑一版,因为训练快、显存占用小,效果不够再上 Transformer。
在线学习的关键是解决样本延迟和模型更新频率的平衡。搜索广告的点击反馈通常有几分钟到几小时的延迟,如果等所有样本回传再训练,模型更新频率就跟不上。常见做法是用流式训练,样本回传后立即更新模型,同时用滑动窗口限制训练数据的时间范围,避免旧数据拖累模型。下面是一个简单的在线学习伪代码:
# 在线学习循环 for batch in stream_data: # batch 包含最新回传的样本 features, labels = preprocess(batch) # 前向传播 preds = model(features) loss = focal_loss(preds, labels) # 反向传播 optimizer.zero_grad() loss.backward() # 梯度裁剪防止震荡 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=5.0) optimizer.step() # 每隔 N 步保存一次模型 if step % 1000 == 0: save_model(model, f'model_step_{step}.pt')这里max_norm=5.0是梯度裁剪阈值,搜索广告场景下设 5 到 10 比较合适。学习率要比离线训练小一个数量级,用 1e-4 或 1e-5,避免新样本把模型带偏。
验证方法上,我习惯用「离线 AUC + 线上 A/B + 回放测试」三层验证。离线 AUC 看模型排序能力,线上 A/B 看真实业务指标,回放测试用历史流量模拟线上环境,检查模型在极端 case 下的表现。三层都过了再全量上线,能避开大部分翻车。
最后说个我自己的习惯:每次模型上线前,我一定会手动构造一批「边界样本」——比如 query 和广告完全不相关、用户历史点击全是某品类但当前广告是另一品类、广告出价极高但 CTR 极低——看模型打分是否符合预期。这个习惯帮我拦下过好几次严重线上事故。希望帮到你。
本文还有配套的精品资源,点击获取