news 2026/9/30 5:42:24

CNN+LSTM混合模型实战:搜索广告CTR预估与排序落地

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
CNN+LSTM混合模型实战:搜索广告CTR预估与排序落地

简介:这份PDF文献聚焦深度学习在搜索广告排序中的落地应用,面向广告算法工程师、推荐系统学习者及数据研究方向的师生,帮助理解点击率(CTR)预估这一广告业务核心环节的技术演进。全文围绕卷积神经网络与LSTM的混合模型展开,先由CNN提取广告数据中的高影响力特征,再借助LSTM的时序建模能力完成预测与分类,并对比逻辑回归、决策树等传统方法在人工特征工程上的局限,同时梳理了百度FNN、谷歌Wide&Deep与PNN等业界方案。文中还完整介绍了特征提取、模型训练与测试预测的CTR预估流程,并给出LSTM层数、隐藏层节点数、学习率等参数对AUC值影响的实验对比,便于读者复现调参思路。资源包为1个PDF文件,大小约2.73MB,结构紧凑、便于检索阅读。目前已有122人学习,适合作为广告排序与深度学习交叉方向的参考文献与专业指导材料。

1. 从一份论文拆出的搜索广告排序实战:CNN+LSTM 混合模型到底怎么落地

广告排序这件事,说白了就是给每次搜索请求里的候选广告排个先后,谁排前面谁拿曝光。决定顺序的核心指标是 CTR(点击率),而 CTR 预估的准确度直接关系到平台的广告收入和用户体验。这份《基于深度学习的搜索广告排序应用》是携程工程师钟小勇写的一篇工程实践论文,核心思路不复杂:用 CNN 从广告特征里自动抽关键特征,再把结果喂给 LSTM 做时序建模和分类,最后输出一个 0 到 1 之间的点击概率。相比逻辑回归、FM 这些传统方案,它省掉了大量人工特征工程,AUC 从 LR 的 0.659 拉到了 0.772。适合谁看?正在做 CTR 预估、推荐排序、广告系统,或者想找一个能跑通的 CNN+LSTM 混合模型参考实现的同学。下面我按“论文讲了什么 → 模型怎么搭 → 参数怎么调 → 坑在哪”的顺序,把这份资源拆开讲。

2. 广告点击率预测的完整链路:从特征提取到排序输出

2.1 为什么传统模型在广告排序上越来越吃力

广告点击率预测本质上是一个二分类问题:给定一条广告曝光请求,预测用户会不会点。传统做法里,逻辑回归(LR)是最常见的 baseline,它把特征加权求和后过 sigmoid 得到概率。LR 的优点是简单、可解释、训练快,但它有个硬伤——依赖人工做特征交叉。比如“用户年龄 × 广告类目”这种组合特征,得靠工程师手动构造,特征工程的工作量极大,而且很难穷举所有有意义的交叉。

FM(因子分解机)在一定程度上缓解了这个问题,它通过隐向量内积来自动学习二阶特征交互,对高维稀疏数据比较友好。论文里的实验也印证了这一点:FM 的 AUC 是 0.696,比 LR 的 0.659 高了一截。但 FM 本质上还是浅层模型,对高阶非线性关系的表达能力有限。

普通神经网络(NN)虽然比 LR 和 FM 强,AUC 到了 0.711,但它对输入特征的 spatial 结构没有感知能力。广告数据里有很多类别特征(比如广告位 ID、用户设备类型、时间段),这些特征之间存在局部相关性,NN 的全连接层没法有效捕捉这种局部模式。这就是 CNN 介入的切入点。

2.2 CNN 和 LSTM 各自解决什么问题

CNN 在图像领域出名,但它的核心能力——卷积核在局部感受野上提取特征——同样适用于广告数据。广告特征经过 One-Hot 编码后,可以排成一个二维矩阵,卷积核在这个矩阵上滑动,能自动发现特征之间的局部组合模式。论文里用的卷积核配置是 32 个、大小 5、步长 1,卷积之后接 ReLU 激活,再做最大池化(步长 4)。这一套操作下来,原始的高维稀疏特征被压缩成了一个 256 维的稠密向量。

LSTM 解决的是另一个问题:时序依赖。用户的点击行为不是孤立的,它跟之前的浏览历史、搜索序列有关系。LSTM 通过输入门、遗忘门、输出门三个机制,能够选择性地记住或遗忘历史信息。论文里用了 3 层 LSTM,每层 128 个隐藏节点,最终输出一个概率值。

把两者串起来的好处是:CNN 负责“看清楚当前这条广告长什么样”,LSTM 负责“结合历史行为判断用户会不会点”。论文的实验结果很直观——CNN 单独用 AUC 是 0.731,LSTM 单独用是 0.723,合在一起到了 0.772。这不是简单的加法,而是特征提取和时序建模的互补。

2.3 数据预处理和模型训练的实操步骤

论文用的是 Avazu 在 Kaggle 上公开的 2014 年数据集,选了 100 万条做训练。每条数据 20 个维度,其中 10 个连续特征(9 个已脱敏)、9 个类别特征,标签是 0 或 1。下面是一段可复现的数据预处理和模型搭建代码,用 PyTorch 写,因为论文里用的 Caffe2 现在生态已经不太活跃了,PyTorch 是更现实的选择。

import torch import torch.nn as nn import numpy as np from sklearn.preprocessing import LabelEncoder, MinMaxScaler # 假设 df 是已经读入的 Avazu 数据,列名按实际调整 # 类别特征做 LabelEncoder,连续特征做归一化 cat_cols = ['C1', 'banner_pos', 'site_id', 'site_domain', 'device_id', 'device_model', 'C14', 'C17', 'C21'] num_cols = ['I1', 'I2', 'I3', 'I4', 'I5', 'I6', 'I7', 'I8', 'I9', 'I10'] for col in cat_cols: le = LabelEncoder() df[col] = le.fit_transform(df[col].astype(str)) scaler = MinMaxScaler() df[num_cols] = scaler.fit_transform(df[num_cols]) # 所有特征拼成一个长向量,模拟 One-Hot 后的稠密表示 feature_cols = cat_cols + num_cols X = df[feature_cols].values.astype(np.float32) y = df['click'].values.astype(np.float32) # 切分训练集和测试集,8:1 split = int(len(X) * 0.89) X_train, X_test = X[:split], X[split:] y_train, y_test = y[:split], y[split:] # 转成 Tensor,CNN 需要 [batch, channel, length] 的形状 X_train_t = torch.tensor(X_train).unsqueeze(1) # [N, 1, 19] X_test_t = torch.tensor(X_test).unsqueeze(1) y_train_t = torch.tensor(y_train).unsqueeze(1) y_test_t = torch.tensor(y_test).unsqueeze(1)

这段代码做了三件事:类别特征编码、连续特征归一化、数据切分。注意unsqueeze(1)那一步,CNN 的输入需要有一个 channel 维度,这里当成单通道信号处理。实际生产中如果特征更多,可以考虑把类别特征和连续特征分开做 embedding,但论文里的做法是统一编码后直接送卷积。

模型定义部分:

class CNNLSTM(nn.Module): def __init__(self, input_len=19, cnn_out=256, lstm_hidden=128, lstm_layers=3): super(CNNLSTM, self).__init__() # 卷积核 32 个,大小 5,步长 1 self.conv = nn.Conv1d(in_channels=1, out_channels=32, kernel_size=5, stride=1) self.relu = nn.ReLU() # 最大池化,步长 4 self.pool = nn.MaxPool1d(kernel_size=4, stride=4) # 计算池化后的长度:((19-5)/1+1 - 4)/4 + 1 = 3 self.fc = nn.Linear(32 * 3, cnn_out) self.lstm = nn.LSTM(input_size=cnn_out, hidden_size=lstm_hidden, num_layers=lstm_layers, batch_first=True) self.sigmoid = nn.Sigmoid() self.out = nn.Linear(lstm_hidden, 1) def forward(self, x): # x: [batch, 1, 19] x = self.relu(self.conv(x)) # [batch, 32, 15] x = self.pool(x) # [batch, 32, 3] x = x.view(x.size(0), -1) # [batch, 96] x = self.fc(x) # [batch, 256] x = x.unsqueeze(1) # [batch, 1, 256] 作为 LSTM 输入 x, _ = self.lstm(x) # [batch, 1, 128] x = self.out(x[:, -1, :]) # [batch, 1] return self.sigmoid(x)

这里有几个参数需要说明。kernel_size=5对应论文里的卷积核大小,out_channels=32是卷积核个数。池化层kernel_size=4, stride=4是论文里明确写的移动步长。cnn_out=256是 CNN 输出向量的维度,这个值在论文的参数对比实验里被验证为最优。LSTM 的num_layers=3和hidden_size=128也是论文表 2 里的配置。

训练循环用 RMSProp 优化器,学习率 0.002,损失函数用 BCEWithLogitsLoss 或者 BCELoss:

model = CNNLSTM() optimizer = torch.optim.RMSprop(model.parameters(), lr=0.002) criterion = nn.BCELoss() for epoch in range(30): model.train() optimizer.zero_grad() pred = model(X_train_t) loss = criterion(pred, y_train_t) loss.backward() optimizer.step() if epoch % 5 == 0: model.eval() with torch.no_grad(): test_pred = model(X_test_t) test_loss = criterion(test_pred, y_test_t) print(f"Epoch {epoch}, train_loss={loss.item():.4f}, test_loss={test_loss.item():.4f}")

论文里迭代了 30 次取最优 AUC,这个训练轮数可以作为起点。如果 loss 还在降就继续加,如果 test_loss 开始上升就是过拟合了,需要加 dropout 或者减层数。

3. 参数调优的边界:LSTM 层数、隐藏节点和学习率怎么选

3.1 论文里的参数对比实验说明了什么

论文图 4 给了三组参数对比曲线,信息量很大。第一组是 LSTM 层数从 1 到 3 的变化,AUC 在 3 层时达到最高。但注意,层数继续增加论文没有给数据,从工程经验看,超过 3 层之后收益递减很快,而且训练时间线性增长。第二组是 LSTM 隐藏层节点数从 32 到 256 的变化,128 时 AUC 最高,256 反而略有下降。这说明隐藏层不是越大越好,节点太多会过拟合,尤其在训练数据只有 100 万条的情况下。第三组是学习率从 0.01 到 0.0001 的对比,0.002 附近表现最好。学习率太大(0.01)会震荡不收敛,太小(0.0001)收敛太慢,30 轮迭代根本不够。

这些参数不是拍脑袋定的,论文作者做了相当多的反复迭代。我一般会建议先用论文的配置跑一遍 baseline,然后按“先调学习率、再调层数、最后调隐藏节点”的顺序做网格搜索。学习率对结果的影响最直接,优先确定。

3.2 优化方法的选择:RMSProp vs Adam

论文表 2 里组合模型用的是 RMSProp,学习率 0.002;表 3 里单一 CNN 模型用的是 Adam,学习率 0.001。为什么不一样?RMSProp 对非平稳目标和循环神经网络比较友好,它通过指数加权平均来调整每个参数的学习率,适合 LSTM 这种有时序依赖的结构。Adam 本质上是 RMSProp 加动量,收敛更快,但在某些 CTR 任务上容易过早收敛到局部最优。论文的实验结果也支持这个判断:组合模型用 RMSProp 达到了 0.772 的 AUC,如果换成 Adam 未必有这个效果。

实际用的时候,我建议两个都试。先跑 RMSProp,如果 loss 曲线震荡厉害就换 Adam 并把学习率降到 0.001。如果 Adam 收敛太快但 AUC 不理想,再换回 RMSProp 并适当增大学习率。

3.3 模型评估:AUC 和对数损失怎么配合看

论文用了两个指标:AUC 和对数损失(log loss)。AUC 衡量的是模型的排序能力,值越大越好,0.5 是随机猜,1.0 是完美排序。log loss 衡量的是概率预测的准确度,值越小越好。这两个指标要配合看,不能只看一个。

举个例子:CNN 的 AUC 是 0.731,log loss 是 0.395;LSTM 的 AUC 是 0.723,log loss 是 0.382。CNN 排序能力更强,但 LSTM 的概率校准更好。组合模型 AUC 0.772、log loss 0.371,两个指标都最优。如果只盯 AUC,可能会选出一个概率预测偏差很大的模型,在实际竞价场景里出价会不准。

from sklearn.metrics import roc_auc_score, log_loss model.eval() with torch.no_grad(): pred_prob = model(X_test_t).numpy().flatten() auc = roc_auc_score(y_test, pred_prob) ll = log_loss(y_test, pred_prob) print(f"AUC={auc:.4f}, LogLoss={ll:.4f}")

这段评估代码可以直接用。注意pred_prob是 sigmoid 之后的概率值,不要用 logits。

4. 避坑与排查:训练 CNN+LSTM 时最容易翻车的五个地方

4.1 现象:loss 不下降,AUC 一直在 0.5 附近

原因:最常见的是输入数据没有归一化,或者类别特征编码后数值范围差异太大。CNN 的卷积核对输入尺度很敏感,如果某些特征值在 0 到 1 之间、另一些在 0 到 10000 之间,卷积核的权重更新会非常不稳定。

解决:所有连续特征必须做 MinMax 或 Standard 归一化,类别特征做 LabelEncoder 后也建议除以类别总数缩放到 0 到 1 之间。检查一遍X_train的每一列最大值和最小值,确保没有异常值。

4.2 现象:训练集 loss 很低,测试集 loss 很高

原因:过拟合。论文里的模型有 3 层 LSTM、128 个隐藏节点,参数量不小,而训练数据只有 100 万条。如果特征维度再高一些,过拟合几乎必然发生。

解决:加 Dropout,一般在 LSTM 层之间加nn.Dropout(0.3)到0.5。另外可以减小 LSTM 隐藏节点数,从 128 降到 64 试试。论文里 128 是最优,但那是在特定数据量下的结论,数据量更小的时候要往下调。

4.3 现象:CNN 卷积层报错,提示维度不匹配

原因:Conv1d 的输入要求是[batch, channel, length],很多人直接把[batch, length]喂进去,少了一个维度。或者池化层的 kernel_size 大于卷积后的长度,导致输出为负。

解决:用unsqueeze(1)补 channel 维度。池化前先算一下长度:(input_len - kernel_size) / stride + 1,确保池化 kernel 不超过这个值。论文里输入长度 19、卷积核 5、池化 4,算下来是 3,刚好够。

4.4 现象:LSTM 训练速度极慢,一个 epoch 要跑几个小时

原因:LSTM 是串行计算的,没法像 CNN 那样大规模并行。如果batch_size设得太小(比如 32),GPU 利用率会很低。另外num_layers=3的 LSTM 本身就比单层慢很多。

解决:把batch_size加到 256 或 512,只要显存够。论文里用的是 NVIDIA 1080Ti,12GB 显存,跑 100 万条数据 batch_size 可以到 512。如果还是慢,考虑把 LSTM 层数降到 2,AUC 可能掉 0.5 个点左右,但训练时间能省一半。

4.5 现象:AUC 在 0.75 左右上不去了,怎么调都没用

原因:特征工程到顶了。论文里只用了 19 个特征,Avazu 数据集本身有 20 多个字段,还有用户历史行为、广告创意内容等没被利用。CNN+LSTM 再强,输入信息不够也白搭。

解决:往特征里加交叉项,比如用户 ID 和广告类目的组合、时间段和设备类型的组合。或者引入预训练的 embedding,把广告标题、描述文本用词向量表示后拼进去。论文结尾也提到了“接下来将继续围绕特征、模型,从工程化方向做迭代”,说明作者自己也意识到特征还有挖掘空间。

5. 进阶技巧:用早停和模型集成把 AUC 再往上推一推

论文里的训练方式是固定迭代 30 次取最优 AUC,这在工程上其实有点浪费。更实用的做法是加早停(early stopping):每跑完一个 epoch 就评估一次验证集的 AUC,如果连续 5 个 epoch 没有提升就停,同时保存验证集 AUC 最高的那个模型权重。这样既能防止过拟合,又能省训练时间。

best_auc = 0.0 patience = 5 wait = 0 for epoch in range(100): model.train() optimizer.zero_grad() pred = model(X_train_t) loss = criterion(pred, y_train_t) loss.backward() optimizer.step() model.eval() with torch.no_grad(): val_pred = model(X_test_t).numpy().flatten() val_auc = roc_auc_score(y_test, val_pred) if val_auc > best_auc: best_auc = val_auc torch.save(model.state_dict(), 'best_model.pth') wait = 0 else: wait += 1 if wait >= patience: print(f"Early stop at epoch {epoch}, best AUC={best_auc:.4f}") break

另一个提升方向是模型集成。论文里 CNN+LSTM 的 AUC 是 0.772,如果把 CNN+LSTM、纯 CNN、纯 LSTM 三个模型的预测概率做加权平均,通常还能再涨 0.5 到 1 个点。权重可以用验证集上的 AUC 来定,AUC 高的模型权重大一些。这种做法在 Kaggle 比赛里很常见,工程上也不算复杂,推理时多跑几个模型就行,延迟增加可控。

还有一个容易被忽略的点:负采样比例。论文里提到“正样本比例与抽取数据中正样本比例较为接近”,这是对的。但实际广告场景里正样本极其稀疏,点击率可能只有百分之几。如果直接拿全量数据训练,模型会倾向于预测所有样本都是负的,AUC 看起来还行但 log loss 会很难看。常见做法是保持正负样本比例在 1:1 到 1:10 之间,训练完后再做概率校准(Platt scaling 或 isotonic regression),把预测概率映射回真实分布。

我自己的习惯是:每次跑完训练,先把验证集的预测概率分布画出来,看看是不是集中在 0 附近。如果是,说明负采样比例或者损失函数权重有问题,得回头调。这个检查花不了两分钟,但能避免很多“AUC 看着不错、上线效果拉胯”的情况。从那以后我每次训练 CTR 模型都强制走一遍概率分布检查,希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/30 5:42:15

I2C多主机仲裁与时钟延展:原理详解与工程实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/30 5:42:13

图像取证第一步:从Exif元数据挖掘照片隐藏信息

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/30 5:41:24

2026年快递批量查询怎么接入?批量查单API对接方案

批量快递查询是指把大量快递单号一次性导入后,由工具或接口自动识别所属快递公司、逐条拉取物流轨迹并结构化展示的处理方式。所谓 API 对接,是指通过第三方快递查询接口的授权信息(如用户 ID 与 API Key)让本地软件或物流管理系统…

作者头像 李华
网站建设 2026/9/30 5:41:05

ECharts非平滑折线图流动特效:lineDashOffset实战

上周接了个大屏的活儿,客户指着一块实时监控面板说,这几条折线太平了,想让数据"活"起来,有种光在管子里跑的感觉。我一开始偷懒,把smooth: true打开,觉得曲线圆润一点更高级,结果流动…

作者头像 李华
网站建设 2026/9/30 5:40:40

TensorFlow不是框架而是AI交付系统:安装、SavedModel与TFLite深度解析

1. 这不是“又一个深度学习框架”:TensorFlow 的真实定位与误用起点很多人第一次听说 TensorFlow,是在某篇“2024年最值得学的AI框架”榜单里,和 PyTorch 并列排在前两位;也有人是在安装时被pip install tensorflow卡在十分钟不动…

作者头像 李华
网站建设 2026/9/30 5:40:26

Coze接入自定义模型:Ace Data Cloud对接OpenAI兼容API的完整指南

想把 Coze(扣子)里的 Bot 能力从“内置模型”扩展到自定义模型,最省事的方式不是等平台把千奇百怪的模型都接好,而是直接找到一条兼容 OpenAI Chat Completions 协议的 API 通道。Ace Data Cloud 正好提供这种接口。这篇分享就记录…

作者头像 李华