资讯详情 CNN+LSTM混合模型实战:搜索广告CTR预估AUC从0.659提升到0.772
📅 2026/10/30 0:26:54
简介这是一份面向广告算法工程师、推荐系统学习者与数据研究人员的专业参考文献聚焦深度学习在搜索广告排序与点击率CTR预估中的落地应用。资源为1个PDF文件压缩包约2.73MB内容源自期刊论文围绕卷积神经网络与LSTM的混合模型展开适合具备一定机器学习基础、希望深入理解CTR预估建模思路的读者研读。文中系统梳理了广告点击率预测的完整流程包括特征提取、模型训练与测试集预测并对比逻辑回归、决策树等传统方法与FNN、WideDeep、PNN等深度学习模型重点阐述CNN提取高影响力特征、LSTM捕捉用户行为时序依赖的组合设计还给出LSTM层数、隐藏层节点数与学习率等参数对AUC值影响的实验分析。目前已有122人学习可为广告排序策略优化、模型选型与实验调参提供可借鉴的思路与参考。1. 搜索广告排序的CTR预估一份把CNN和LSTM串起来的论文拆解做搜索广告的同行大概都有过这种体验竞价逻辑跑通了特征工程也堆了几百维但CTR预估的AUC就是卡在0.72上不去离线指标涨0.5个点比登天还难。这份《基于深度学习的搜索广告排序应用》就是冲着这个瓶颈去的——作者钟小勇来自携程用Kaggle上Avazu 2014年公开的100万条广告点击数据做验证把CNN和LSTM拼成一个三层混合网络最终AUC跑到0.772比单层CNN高1.38个百分点比单层LSTM高2.61个百分点。这不是纯理论推导论文里给了完整的算法伪代码、参数配置表和六种模型的横向对比适合正在做CTR预估、想从LR/FM往深度学习迁移的算法工程师和广告系统开发者。下面我按“模型怎么搭→数据怎么喂→参数怎么调→坑在哪”的顺序把这份论文里能直接抄作业的部分拆出来。2. 卷积-LSTM混合模型从One-Hot编码到Sigmoid输出的完整链路2.1 为什么是CNNLSTM而不是单独用其中一个论文的出发点很明确广告数据里既有空间特征不同特征域的交叉关系又有时间特征用户点击行为的序列依赖。CNN擅长从局部感知域里提取高影响力特征LSTM擅长捕捉时序上的长期依赖两者互补。单独用CNNAUC能到0.731但log loss偏高0.395说明分类边界不够平滑单独用LSTMAUC 0.723log loss 0.382分类精度好一些但特征提取能力弱。组合之后AUC 0.772、log loss 0.371两个指标同时改善。这个结论和工业界的实践是一致的。常见做法是先用Embedding层把高维稀疏的类别特征压到低维稠密空间再用CNN做特征交叉和筛选最后用LSTM或GRU做序列建模。论文里的做法更直接——原始特征做One-Hot编码后映射到t维空间然后直接进CNN卷积池化池化输出作为LSTM的输入序列。2.2 三层网络的具体结构和前向传播论文给出的网络结构是三层输入层→CNN层→LSTM层→输出层。算法伪代码里写得很清楚我把它翻译成可执行的PyTorch风格逻辑import torch import torch.nn as nn class CNNLSTMCTR(nn.Module): def __init__(self, vocab_size, embed_dim256, cnn_out_dim256, lstm_layers3, lstm_hidden128, dropout0.2): super().__init__() # 输入层One-Hot编码后映射到t维空间 self.embedding nn.Embedding(vocab_size, embed_dim) # CNN层卷积核32个大小5步长1 self.conv nn.Conv1d(embed_dim, 32, kernel_size5, stride1, padding2) self.relu nn.ReLU() # 最大池化移动步长为4 self.pool nn.MaxPool1d(kernel_size4, stride4) # 将池化输出映射到CNN输出向量维度256 self.fc_cnn nn.Linear(32 * (embed_dim // 4), cnn_out_dim) # LSTM层3层隐藏层节点128 self.lstm nn.LSTM(input_sizecnn_out_dim, hidden_sizelstm_hidden, num_layerslstm_layers, batch_firstTrue, dropoutdropout) # 输出层Sigmoid二分类 self.fc_out nn.Linear(lstm_hidden, 1) self.sigmoid nn.Sigmoid() def forward(self, x): # x: [batch, seq_len] 的索引序列 emb self.embedding(x) # [B, L, D] emb emb.permute(0, 2, 1) # [B, D, L] 适配Conv1d conv_out self.relu(self.conv(emb)) # 卷积提取关键特征 pool_out self.pool(conv_out) # 最大池化 pool_out pool_out.flatten(1) # 展平 cnn_feat self.fc_cnn(pool_out) # [B, 256] cnn_feat cnn_feat.unsqueeze(1) # [B, 1, 256] 作为LSTM输入 lstm_out, _ self.lstm(cnn_feat) # LSTM预测与分类 logit self.fc_out(lstm_out[:, -1, :]) return self.sigmoid(logit)逻辑说明输入层做One-Hot编码后通过Embedding映射到256维空间这一步和论文里“独立向量编码映射到t维空间”对应。CNN层用32个大小为5的卷积核做一维卷积ReLU激活后接最大池化池化窗口和步长都是4目的是压缩序列长度、保留最强特征。池化输出展平后过一个全连接层压到256维作为LSTM的输入。LSTM三层堆叠隐藏层128个节点取最后一个时间步的输出过Sigmoid得到0到1之间的点击概率。参数说明embed_dim256对应论文表2里“CNN输出向量的维度256”lstm_layers3和lstm_hidden128直接来自表2卷积核配置(32, 5, 1)也是表2的原始值。唯一需要根据自己数据调整的是vocab_size论文用的是Avazu数据集20个特征维度里9个类别特征做编码实际vocab_size取决于类别特征的基数。2.3 训练循环和损失函数的选择论文用的是交叉熵损失配RMSProp优化器学习率0.002。训练循环的骨架如下model CNNLSTMCTR(vocab_sizevocab_size) criterion nn.BCELoss() # 二分类交叉熵 optimizer torch.optim.RMSprop(model.parameters(), lr0.002) for epoch in range(30): # 论文迭代30次取最优 model.train() for batch_x, batch_y in train_loader: optimizer.zero_grad() pred model(batch_x).squeeze() loss criterion(pred, batch_y.float()) loss.backward() optimizer.step() # 每个epoch后在测试集上算AUC和log loss model.eval() with torch.no_grad(): val_pred model(val_x).squeeze() auc roc_auc_score(val_y, val_pred) logloss log_loss(val_y, val_pred) print(fEpoch {epoch}: AUC{auc:.4f}, LogLoss{logloss:.4f})逻辑说明论文的算法描述里第5步明确写了“比对分类结果与样本level计算交叉熵损失函数并运用RMSProp优化方法不停地优化权值”。这里用BCELoss对应二分类交叉熵RMSProp的lr设为0.002和表2一致。迭代30次是论文的实验设置实际训练中建议加Early Stopping因为论文自己也提到深层模型训练时间更长CNNLSTM消耗1339秒而LR只要172秒。参数说明lr0.002是论文调参后的最优值图4的学习率对比实验显示10e-4到10e-3之间AUC最高。RMSProp的alpha、eps等参数论文没给用PyTorch默认值即可。batch_size论文没提Avazu 100万条数据常见做法是设1024或2048。3. 数据预处理与特征工程Avazu数据集上的实操细节3.1 Avazu数据集的字段结构和处理流程论文用的是Avazu 2014年Kaggle比赛的公开数据集从中选了100万条做训练。每条数据20个维度10个连续性特征其中9个已脱敏 9个类别特征标签是0或1的二分类。原始数据正负样本比例和抽取后的比例接近论文特意做了这个约束来保证实验不受样本不均衡干扰。实际处理流程分三步第一步特征提取。把原始数据里的类别特征做Label Encoding或Hash Encoding连续特征做归一化或分桶离散化。论文的做法是“排重、数据清理后开始独立向量编码One-Hot Encoder”也就是先对类别特征做去重编号再One-Hot。第二步划分训练集和测试集。论文按8:1切割每训练完一轮就把测试集喂进去做预测。第三步构建输入张量。把每条样本的20个特征拼成一个固定长度的索引序列作为Embedding层的输入。import pandas as pd from sklearn.preprocessing import LabelEncoder, MinMaxScaler # 假设df是Avazu原始数据包含20个特征列1个label列 cat_cols [fC{i} for i in range(1, 15) if fC{i} in df.columns] # 类别特征 num_cols [fI{i} for i in range(1, 14) if fI{i} in df.columns] # 连续特征 # 类别特征Label Encoding 去重编号 for col in cat_cols: le LabelEncoder() df[col] le.fit_transform(df[col].astype(str)) # 连续特征归一化到[0,1] scaler MinMaxScaler() df[num_cols] scaler.fit_transform(df[num_cols]) # 连续特征分桶离散化论文提到10个连续特征中9个脱敏实际可做分桶 for col in num_cols: df[col] pd.cut(df[col], bins10, labelsFalse) # 拼接所有特征为一个序列 feature_cols cat_cols num_cols df[feat_seq] df[feature_cols].values.tolist() # 8:1划分 train_df df.sample(frac8/9, random_state42) test_df df.drop(train_df.index)逻辑说明类别特征用LabelEncoder做整数编码连续特征先归一化再分桶离散化这样所有特征都变成整数索引可以直接喂给Embedding层。论文里“独立向量编码”的本质就是One-Hot后接Embedding等价于对每个类别特征学一个低维稠密向量。参数说明bins10是连续特征分桶的常见做法论文没给具体分桶数但Avazu的连续特征大多已脱敏分桶粒度对结果影响有限。random_state42保证可复现。3.2 正负样本比例的控制论文特别强调“原始训练数据中正样本比例与抽取数据中正样本比例较为接近”这是CTR预估里容易被忽略的坑。广告点击数据天然极度不均衡正样本通常只有百分之几甚至千分之几。如果随机抽样时不控制比例训练集和测试集的正负比差异过大会导致AUC虚高或虚低。常见做法是先统计全量数据的正样本比例p然后分层抽样保证训练集和测试集的正样本比例都在p附近。如果正样本太少可以做过采样或SMOTE但论文没提这些说明Avazu 100万条数据的正样本量足够支撑训练。# 分层抽样保证正负比例一致 from sklearn.model_selection import train_test_split train_df, test_df train_test_split( df, test_size1/9, random_state42, stratifydf[label] ) print(f训练集正样本比例: {train_df[label].mean():.4f}) print(f测试集正样本比例: {test_df[label].mean():.4f})逻辑说明stratifydf[label]保证切分后训练集和测试集的正样本比例与原始数据一致。论文里“正样本比例与抽取数据中正样本比例较为接近”就是这个意思。参数说明test_size1/9对应论文的8:1切割。如果正样本比例低于1%建议先做负采样再分层否则模型会偏向预测负类。4. 参数调优与实验对比AUC从0.659到0.772的每一步4.1 四组关键参数的对比实验论文图4给了四组参数对比LSTM层数1/2/3、LSTM隐藏层节点数32/64/128/256、学习率10e-4/50e-4/100e-4/1000e-4、优化方法SGD/Adam/RMSProp等。结论是LSTM层数3层最优隐藏层节点128最优学习率0.002最优RMSProp优于Adam和SGD。我把这四组实验的关键数据整理成表参数候选值最优值AUC变化趋势LSTM层数1, 2, 33层数增加AUC先升后稳3层后不再明显提升LSTM隐藏层节点32, 64, 128, 256128128之前AUC随节点数上升256时过拟合学习率10e-4, 50e-4, 100e-4, 1000e-40.0020.002附近AUC最高过大震荡过小收敛慢优化方法SGD, Adam, RMSPropRMSPropRMSProp在AUC和log loss上均优于Adam这张表的用法如果你在自己的数据上复现先固定其他参数按这个顺序逐个调——先调LSTM层数再调隐藏层节点然后调学习率最后换优化器。论文的调参逻辑是“输入不同的参数反复迭代优化最终计算得出最优的AUC值”没有用网格搜索或贝叶斯优化就是手动试。4.2 六种模型的横向对比和选型建议论文表5给了六种模型的完整对比我把它整理成更直观的表格模型AUCLog Loss训练时间(s)适用场景LR0.6590.421172.82基线模型快速验证特征有效性FM0.6960.385217.69高维稀疏数据特征交叉明显NN0.7110.46719832常规神经网络log loss偏高CNN0.7310.395596.43特征提取为主分类精度一般LSTM0.7230.3821217.32序列依赖强特征提取弱CNNLSTM0.7720.3711339.21兼顾特征提取和序列建模选型建议如果数据量小于10万条LR或FM足够上深度学习容易过拟合如果数据量在10万到100万之间CNN或LSTM单模型可以试但要注意调参如果数据量超过100万且特征里有时序信号比如用户历史点击序列CNNLSTM的组合值得投入。论文的CNNLSTM训练时间1339秒比NN的19832秒快了一个数量级说明NN那个19832秒可能是配置有问题或者迭代次数不同实际部署时CNNLSTM的推理延迟需要单独测。4.3 实验环境配置和复现注意事项论文表4给了实验环境CentOS 7.5、NVIDIA 1080Ti、128GB内存、12TB硬盘、Python 2.7、Caffe2框架。这个配置在2019年算中端现在用PyTorch或TensorFlow复现完全没问题但有几个点要注意Python 2.7已经停止维护建议直接上Python 3.8。Caffe2已经合并进PyTorch用PyTorch 1.10的torch.nn模块实现即可。1080Ti的11GB显存跑100万条数据、batch_size 1024、模型参数量在百万级别显存占用大概2到3GB足够。如果换用更小的GPU把batch_size降到256或512。# 环境配置参考Ubuntu 20.04 CUDA 11.3 conda create -n ctr python3.8 conda activate ctr pip install torch1.10.0cu113 torchvision0.11.0cu113 -f https://download.pytorch.org/whl/torch_stable.html pip install pandas scikit-learn numpy逻辑说明PyTorch 1.10配CUDA 11.3在1080Ti上验证过可用。如果用的是30系或40系显卡需要CUDA 11.6以上。参数说明python3.8是兼容性最好的版本3.9/3.10也可以但部分旧库可能不兼容。torch1.10.0是稳定版追求新特性可以用2.x但API有变化。5. 避坑与排查复现CNNLSTM CTR模型时最容易翻车的五个点5.1 现象AUC在0.5附近震荡完全不收敛原因Embedding层的vocab_size设错了。Avazu数据集的类别特征基数很大比如C14广告ID可能有几十万个不同取值如果vocab_size设小了大量索引越界被截断模型学不到有效特征。解决先统计每个类别特征的唯一值数量vocab_size设为最大唯一值数1。或者用Hash Encoding把高基数特征哈希到固定桶数比如10万牺牲一点精度换内存和速度。# 统计每个类别特征的基数 for col in cat_cols: print(f{col}: {df[col].nunique()} unique values) # vocab_size取所有类别特征基数的最大值1 vocab_size max(df[col].nunique() for col in cat_cols) 15.2 现象训练集AUC很高但测试集AUC低10个点以上原因过拟合。论文里LSTM隐藏层节点从128加到256时AUC不升反降就是过拟合的典型表现。另外如果连续特征没做归一化量纲差异大会导致梯度更新方向偏。解决加Dropout论文没提但常见做法是0.2到0.5、加L2正则化、减小隐藏层节点数、早停。连续特征必须做MinMaxScaler或StandardScaler。5.3 现象log loss比AUC先恶化AUC还在涨但log loss已经飙升原因模型对少数样本过度自信。CTR数据正负不均衡时模型倾向于把大部分样本预测成负类AUC可能还行但log loss会爆炸。解决用Focal Loss替代BCELoss或者对正样本加权。论文用的是标准交叉熵说明Avazu数据的正负比例还在可接受范围内。如果你的数据正样本低于1%建议先做负采样再训练。5.4 现象CNN层的池化输出维度对不上LSTM的输入维度原因Conv1d的输出长度计算错误。池化窗口和步长设置不当会导致展平后的维度与全连接层的输入维度不匹配。解决用公式算清楚每一层的输出维度。Conv1d输出长度 (输入长度 2×padding - kernel_size) / stride 1。池化输出长度 (卷积输出长度 - pool_size) / pool_stride 1。论文里卷积核大小5、步长1、padding没提池化窗口4、步长4实际实现时建议先用一个batch的数据跑一遍print(x.shape)逐层确认。5.5 现象训练时间远超预期GPU利用率低原因数据加载是瓶颈。Python 2.7的DataLoader效率低或者没有用多进程加载。解决用PyTorch的DataLoader配num_workers4或8把数据预处理放到GPU上做。论文用Caffe2数据管道和PyTorch不同迁移时这部分要重写。另外把float64转成float32能省一半显存和带宽。6. 进阶技巧用AUC和Log Loss双指标做模型选择论文用AUC和log loss两个指标来评估模型这个做法比只看AUC更靠谱。AUC衡量的是排序能力log loss衡量的是概率校准程度。广告排序场景里AUC决定广告展示顺序log loss决定出价策略——如果log loss太高预估的点击率偏离真实概率出价就会失真。我一般会按这个流程做模型选择先看AUC筛掉AUC低于基线的模型再看log loss在AUC相近的模型里选log loss更低的最后看训练时间如果两个模型AUC和log loss差距在0.5%以内选训练时间短的。论文里CNNLSTM的AUC 0.772、log loss 0.371比CNN的0.731/0.395和LSTM的0.723/0.382都好但训练时间1339秒比CNN的596秒多了一倍多。如果线上推理延迟敏感CNN单模型可能是更务实的选择。还有一个技巧是画ROC曲线对比。论文提到“ROC曲线的横轴表示假正率纵轴表示真正率按阈值的取值差异可得到不同的ROC曲线”。实际调参时把不同参数组合的ROC曲线画在同一张图上能直观看出哪个模型在低假正率区间表现更好——广告排序通常更关注Top位置的准确率对应ROC曲线的左上角区域。from sklearn.metrics import roc_curve, auc import matplotlib.pyplot as plt # 假设有多个模型的预测结果 models {CNN: cnn_pred, LSTM: lstm_pred, CNNLSTM: hybrid_pred} for name, pred in models.items(): fpr, tpr, _ roc_curve(test_y, pred) roc_auc auc(fpr, tpr) plt.plot(fpr, tpr, labelf{name} (AUC{roc_auc:.3f})) plt.xlabel(False Positive Rate) plt.ylabel(True Positive Rate) plt.legend() plt.savefig(roc_comparison.png, dpi150)逻辑说明把多个模型的ROC曲线画在一起重点看假正率在0到0.2区间内谁的真正率更高。广告排序场景里假正率对应“误判为会点击但实际不点击”的比例这个比例太高会浪费广告位。参数说明roc_curve的pos_label默认是1如果标签是0/1二分类不用改。dpi150保证图片清晰度够。从那以后我每次复现CTR模型都强制走一遍“先跑LR基线→再跑单层CNN和LSTM→最后跑组合模型”的流程每一步都记录AUC和log loss不跳过基线直接上复杂模型。因为论文的数据已经证明了LR的AUC 0.659虽然低但训练只要172秒如果LR已经能满足业务需求没必要上深度学习。希望帮到你。本文还有配套的精品资源点击获取