Python电商广告推荐系统源码实战:算法选型与工程落地全解析
发布时间:2026/10/1 23:04:36来源:尧图网络
简介这是一套面向电商广告推荐场景的Python源码项目适合机器学习初学者、推荐系统开发人员及数据竞赛爱好者参考。项目基于阿里天池提供的淘宝展示广告点击率预估数据集Ali_Display_Ad_Click该数据包含114万用户8天内的2600万条广告展示与点击日志围绕这些日志实现了数据解析、存储、离线召回、点击率预估等关键流程并包含基于ALS的协同过滤召回以及品牌、类目维度的评分设计。压缩包共13个文件由12个Python脚本和1个说明文档组成整体仅21KB代码精简、结构清晰便于按模块阅读和二次改造。目前已有920人学习下载。通过这套源码可了解广告推荐系统的常见模块划分与Redis缓存的使用思路学习如何将公开点击日志转换为特征与评分也能积累将离线模型、实时存储与推荐流程串联起来的项目经验对搭建小型推荐Demo或完成课程设计有直接帮助。1. 拿到“Python电商广告推荐系统源码.zip”先别急着解压做推荐系统的人每年都会从各种渠道收集源码包这个标题里的 zip 十有八九是某个课程项目、开源仓库打包或者毕业设计实锤。但我想先说句得罪人的话把 zip 解压出来能跑通和能上线投广告中间隔着十万八千里。真正用 Python 做电商广告推荐绝不是跑一个协同过滤脚本就完事——你要处理的是曝光日志、点击日志、转化日志三张表的 join是广告位和自然推荐的流量竞争是冷启动用户进来后推荐列表不能是空的。这套源码的价值在于给你一条从数据到指标的完整落地路径而不是一个能出图的 demo。这篇笔记我按自己做过电商广告推荐的经验把源码背后的算法选型、环境搭建、参数调整和踩坑点拆开讲保证你照着做能把离线实验跑得像样也知道上线前还差哪几步。2. 电商广告推荐系统到底在解决什么问题先看懂架构再碰代码2.1 电商广告推荐和“猜你喜欢”的本质区别优化目标不一样很多初学者拿到源码第一反应是看模型但我建议先看数据表和 loss 函数。电商广告推荐的优化目标不是让用户多停留而是让广告主花钱花得值、平台赚得稳。一套典型的 Python 实现里数据链路长这样用户画像表user_profile、商品表item_profile、广告计划表campaign、曝光日志、点击日志、转化日志。你做的推荐系统本质是在广告候选集上做一次排序把“最容易发生点击或转化”的商品顶到前面。这里有个关键差异自然推荐猜你喜欢的负样本是“曝光未点击”而广告推荐的负样本还要加上“竞价未获胜”和“频控排除”。很多源码包把这三类负样本混在一起做 CTR 预估离线指标虚高上线就翻车。所以先看源码里的样本构造逻辑比先看模型结构重要得多。常见的做法是点击为正样本曝光未点击为负样本但如果源码里带了竞价日志还要把“胜出未点击”单独标记不能直接丢掉。电商广告的另一个特点是广告主会设置定向条件比如地域、性别、时段。推荐系统给出的排序结果要过一轮广告投放约束budget、定向、频控这也意味着源码里通常有一个 filter 层位于排序模型之后。如果这套源码只有召回和排序没有过滤逻辑那它充其量是个“商品推荐”离“广告推荐”还差一个工程层。2.2 溯源源码里的算法选型协同过滤、矩阵分解还是序列模型Python 电商广告推荐源码里最常见的算法是三类UserCF/ItemCF、矩阵分解SVD 或 ALS、以及基于 Embedding 的双塔模型。不同源码的含金量差别很大判断标准不是模型多新而是数据利用是否完整。ItemCF 适合做“看了又看”和“相似商品”实现简单冷启动靠商品内容特征兜底。但 ItemCF 有个天生的毛病偏向热门商品长尾商品几乎永远得不到曝光。如果你源码里的召回层是纯 ItemCF那广告推荐效果会很难看因为广告主投的长尾商品根本出不来量。矩阵分解比如 ALS适合评分数据完整的场景但电商广告的曝光日志是隐式反馈只有 0/1 的点击标记直接套 SVD 会面临正样本极度稀疏的问题。常见补救方式是把隐式反馈转成置信度权重——点击过给高权重没点击给低权重然后跑加权矩阵分解。双塔模型user tower item tower是目前工程上最稳的方案user 侧特征和 item 侧特征分别过 embedding 层最后内积算相似度。训练时用 batch 内随机负采样线上用 ANN 做近邻检索。如果这套源码里用的是这个架构那值得你花时间读如果是纯协同过滤也别急着关掉——先看它有没有把商品属性特征接进相似度计算里。2.3 广告推荐的核心链路召回、粗排、精排、重排源码里到哪一步我见过很多源码包号称“推荐系统”实际只有一张协同过滤算相似度的 notebook。真正的电商广告推荐链路是四段式召回从千万级商品池里捞回几百到几千个候选常用策略有 ItemCF、热销召回、向量召回、规则召回比如同店铺、同品类。粗排双塔模型或简单 LR把几千个候选压到几百个目的是省精排的计算资源。精排用复杂模型DeepFM、DIN 等对几百个候选逐一打分输出 CTR 或 CVR 预估值。重排考虑多样性、频控、广告主预算、竞价排序对精排结果做最后调整。你在解压源码后第一件事就是打开项目 README 或目录结构看它实现了哪几层。如果只有召回精排那中间的粗排你得自己补如果连重排都有那这套源码的完整度相当高值得按下面的步骤跑起来。3. 把 Python 电商广告推荐源码跑通环境、数据、最小复现命令3.1 从 zip 到可运行的 Python 工程环境配置的四个关键点解压 zip 之前先把 Python 版本确认好。这套源码如果是基于 TensorFlow 或 PyTorch 的老版本写的大概率对 Python 版本敏感。我踩过的坑是Python 3.10 装 TensorFlow 1.x 直接报错因为contrib模块已经移除了。所以先看requirements.txt或environment.yml没有的话就看 import 语句里有没有tensorflow.contrib这种上古写法。环境配置我推荐用 conda 建一个独立环境不要在 base 环境里硬装。命令如下# 创建独立环境Python 版本按源码要求来一般 3.6~3.9 比较稳 conda create -n ads_rec python3.8 # 激活环境 conda activate ads_rec # 如果源码附带 requirements.txt直接装没有就装核心依赖 pip install -r requirements.txt # 没有 requirements.txt 时的最小依赖组合 pip install numpy pandas scikit-learn pip install tensorflow2.4 # 或 pytorch取决于源码用的框架逻辑说明conda create -n ads_rec后面指定 Python 版本是为了避开系统 Python 环境里的旧包冲突。我在本地跑项目时习惯把项目依赖全部锁在独立环境里不然pip install的版本冲突能把一个早上耗光。参数说明-n后面的名字随意起但建议和项目相关TensorFlow 版本先装一个已知稳定的版本跑通后再说升级。装依赖有个细节电商广告推荐系统的源码通常依赖faiss向量检索或annoy近似最近邻。这两个包在 Windows 上容易编译失败Linux 上装faiss-cpu比较省事。如果你的主要环境是 Windows建议用 WSL 跑这套代码省得折腾编译链。3.2 构造可复现的实验数据没有现成日志就自己造很多开源推荐系统源码会用 MovieLens 或 Amazon Review 数据演示但电商广告场景没有公开的大规模广告日志数据集。你如果只是想跑通流程可以先用公开数据集顶替如果想验证广告逻辑的完整性得按广告日志的字段结构自己构造一份小数据。我建议从两个方向准备数据第一个是拿到源码自带的数据生成脚本有些项目会带data_generator.py第二个是没有脚本时手工构造一个最小可用的 CSV。最小字段集包括import pandas as pd import numpy as np from datetime import datetime, timedelta # 构造 500 个用户、2000 个商品、14 天的曝光点击日志 np.random.seed(42) user_ids range(1, 501) item_ids range(1, 2001) rows [] for day in range(14): for _ in range(2000): # 每天 2000 条曝光 uid np.random.choice(user_ids) iid np.random.choice(item_ids) clicked 1 if np.random.rand() 0.05 else 0 # 点击率约 5% rows.append({ user_id: uid, item_id: iid, click: clicked, timestamp: datetime(2024, 1, 1) timedelta(daysday) }) df pd.DataFrame(rows) df.to_csv(ad_log.csv, indexFalse) print(df.groupby(click).size())逻辑说明这段代码生成的是最简曝光日志click列是标签timestamp列用于时间切分。参数说明np.random.seed(42)保证每次生成的数据一致否则你后面调参时换了随机种子指标变化就没法判断是模型改进还是数据变化。clicked的取值概率 0.05 是为了模拟真实广告点击率——电商广告点击率普遍在 1% 到 10% 之间太高的点击率会让负样本失去区分度模型学不到东西。注意一点真实广告日志还有campaign_id、ad_position、bid_price这些字段但最小复现集可以先不加等主链路跑通再逐步补。3.3 跑通离线实验的最小链路召回、精排、指标计算源码解压后先跑它的train.py或main.py但更推荐的做法是先把数据切成 train/valid/test 三段避免源码里默认的全量训练让你误判效果。时间切分是广告推荐的标准做法——用前 12 天训练第 13 天验证第 14 天测试因为广告推荐要处理的是时间漂移随机切分会让模型“偷看未来”。import pandas as pd from datetime import datetime df pd.read_csv(ad_log.csv, parse_dates[timestamp]) # 时间切分前 12 天训练第 13 天验证第 14 天测试 train df[df[timestamp] datetime(2024, 1, 13)] valid df[(df[timestamp] datetime(2024, 1, 13)) (df[timestamp] datetime(2024, 1, 14))] test df[df[timestamp] datetime(2024, 1, 14)] print(ftrain: {len(train)}, valid: {len(valid)}, test: {len(test)}) # 基于 ItemCF 的最小召回实现 from collections import defaultdict def build_item_similarity(train_df): 统计商品共现矩阵计算相似度 user_items defaultdict(set) for uid, iid in zip(train_df[user_id], train_df[item_id]): user_items[uid].add(iid) cooccur defaultdict(int) item_cnt defaultdict(int) for uid, items in user_items.items(): for iid in items: item_cnt[iid] 1 for jid in items: if iid ! jid: cooccur[(iid, jid)] 1 # 余弦相似度简化版 sim {} for (iid, jid), cnt in cooccur.items(): sim[(iid, jid)] cnt / (item_cnt[iid] ** 0.5 * item_cnt[jid] ** 0.5) return sim sim build_item_similarity(train) print(f相似度对数量: {len(sim)})逻辑说明build_item_similarity遍历训练集里每个用户的交互商品对统计商品共现次数再用余弦相似度公式归一。参数说明分母里的item_cnt[iid] ** 0.5是 L2 归一化的近似目的是让热门商品的相似度不被共现次数放大。这个实现只用于验证链路实际工程里这么算内存会爆后面避坑章再讲怎么处理。召回之后你需要一个评估函数。离线评估广告推荐不能只看准确率因为正样本太少准确率没有任何参考价值。标准的做法是算 RecallK 和 NDCGKdef evaluate_recall(test_df, sim, k10): 对测试集中的每个用户用训练集交互召回 TopK 商品 user_items defaultdict(set) for uid, iid in zip(train[user_id], train[item_id]): user_items[uid].add(iid) test_user_items defaultdict(set) for uid, iid in zip(test_df[user_id], test_df[item_id]): test_user_items[uid].add(iid) recalls [] for uid, test_items in test_user_items.items(): if uid not in user_items: continue # 冷启动用户直接跳过 seen user_items[uid] scored {} for iid in seen: for (sim_iid, sim_jid), s in sim.items(): if sim_iid iid and sim_jid not in seen: scored[sim_jid] max(scored.get(sim_jid, 0), s) topk sorted(scored.items(), keylambda x: x[1], reverseTrue)[:k] rec_items set([iid for iid, _ in topk]) hit len(rec_items test_items) recalls.append(hit / min(len(test_items), k)) return np.mean(recalls) recall_10 evaluate_recall(test, sim, k10) print(fRecall10: {recall_10:.4f})逻辑说明evaluate_recall的输入是测试集和相似度字典对每个用户的每个交互商品找出相似商品里得分最高的 K 个作为推荐列表再算命中率。参数说明k10是广告推荐常见的召回评测深度实际业务可能要看 Recall50 或 Recall100因为后面还有排序层召回做宽一点没关系。冷启动用户在这个实现里直接跳过但真实业务里冷启动用户恰恰是广告系统最需要照顾的——这个坑后面单独讲。4. 调参不是玄学召回、排序、评估指标的落地参数经验4.1 召回层的三个关键参数数量、相似度阈值、热门打压召回层常见的参数就那么几个但这几个参数直接影响后面排序层能吃进多少候选。我一般先调三个召回数量recall_size、相似度阈值sim_threshold、热门打压系数popular_penalty。召回数量决定了排序层的输入规模。设太小比如 20排序模型再强也救不回来设太大排序层的计算开销成倍涨。我曾经在 500 万商品池的项目上测试召回 200 和召回 500精排后的 NDCG10 几乎没变化但精排耗时翻了一倍。常见做法是先用 100 起步观察 Recall100 的覆盖情况再看精排和重排的耗时逐步缩。相似度阈值的作用是过滤低质量的候选。ItemCF 算出来的相似度如果低于 0.1基本都是噪音。但阈值太高会让长尾商品彻底没有候选。我的经验是先在验证集上扫一遍 0.05 到 0.3 的区间画一条 RecallK 随阈值变化的曲线选膝盖点。热门打压是电商广告推荐最容易漏的参数。ItemCF 天然偏向热门商品如果不打压推荐的永远是 iPhone 壳和充电宝。常见的实现是在相似度计算之后乘以一个惩罚因子商品越热门降权越狠# 热门商品打压iid 出现次数越多相似度降权越狠 popularity {iid: cnt for iid, cnt in item_cnt.items()} penalty_factor 0.8 # 打压强度0 表示完全打压1 表示不打压 def adjusted_sim(raw_sim, iid, jid, popularity, penalty_factor): 按两个商品的热门程度调整相似度 return raw_sim * (penalty_factor ** (popularity[iid] popularity[jid] - 2))逻辑说明penalty_factor小于 1 时热门商品参与计算的相似度会被指数级压低冷门商品反而更容易被推荐。参数说明penalty_factor0.8是一个比较温和的起始值如果推荐列表还是被头部商品霸占就调到 0.5 甚至 0.3。注意这个公式是简化版实际项目里常用的是log(1 popularity)做平滑再做倒数加权。4.2 排序层参数负采样比例、学习率、特征归一化精排模型比如 DeepFM的核心参数不是网络层数而是负采样比例。广告日志里正样本只有 1% 到 5%如果按全部曝光日志训练模型会严重偏向预测负样本。常见做法是负采样把负样本降到正样本的 1 到 5 倍。# 负采样正样本全保留负样本按比例抽样 train_pos train[train[click] 1] train_neg train[train[click] 0] # 负采样比例 3:1 neg_sample_size len(train_pos) * 3 train_neg_sampled train_neg.sample(neg_sample_size, random_state42) train_balanced pd.concat([train_pos, train_neg_sampled]) print(f正样本: {len(train_pos)}, 负样本(采样后): {len(train_neg_sampled)})逻辑说明sample(neg_sample_size, random_state42)从负样本里随机抽取指定数量random_state固定保证实验可复现。参数说明3:1 是我做过的最稳的起始比例比例太高的后果是模型对正样本不敏感预测出的 CTR 普遍偏低比例太低会让模型见过太多正样本线上真实场景的负样本比例会把你打回原形。学习率的设置要看 loss 曲线的形态。如果 loss 下降很慢先把学习率调大一个量级如果 loss 震荡不收敛调小。这里我的血泪经验是用带衰减的学习率比如指数衰减而不是固定学习率。固定学习率在训练后期会让模型在最优解附近震荡永远落不下去。特征归一化是排序层另一个容易被轻视的参数。广告推荐的特征里价格、销量这些数值型特征量纲差异巨大不归一化会导致 embedding 层的梯度被大数值特征主导。常见做法是做 log 变换或 min-max 归一化# 价格特征做 log1p 变换压缩量纲 train[price_log] np.log1p(train[price]) # 归一化到 [0, 1] train[price_norm] (train[price_log] - train[price_log].min()) / \ (train[price_log].max() - train[price_log].min())逻辑说明np.log1p是log(1x)对 0 值友好价格相差 100 倍的商品压缩到 5 倍以内模型学起来顺畅得多。参数说明归一化只对数值特征做类别特征如商品类目走 embedding不需要归一化。4.3 评估指标AUC 会骗人NDCG 和 GAUC 才贴近业务广告推荐系统最常见的评估指标是 AUC但我要提醒全局 AUC 在广告场景里会给你虚假的自信。因为广告数据的正样本率极低AUC 很容易到 0.85 以上但这个数字好看不代表模型判别能力强。更好的指标是 GAUCGroup AUC按用户分组计算 AUC 再加权平均。用户的个体行为差异很大有人就是爱点击有人十天不点一次——全局 AUC 被爱点击的用户主导了。GAUC 的做法是from sklearn.metrics import roc_auc_score def gauc(y_true, y_pred, user_ids): 按用户分组计算 AUC再按每个用户的样本量加权 df pd.DataFrame({y_true: y_true, y_pred: y_pred, uid: user_ids}) total_weight 0 weighted_auc 0 for uid, group in df.groupby(uid): if len(group) 2: continue # 单样本组算不了 AUC跳过 if group[y_true].nunique() 2: continue # 全正或全负的组没有区分度 auc roc_auc_score(group[y_true], group[y_pred]) weight len(group) weighted_auc auc * weight total_weight weight return weighted_auc / total_weight if total_weight 0 else 0逻辑说明groupby(uid)把每个用户的样本单独拎出来算 AUC再按样本量加权平均。参数说明跳过全正或全负的组是必须的因为这种组算出来的 AUC 没有意义。GAUC 的提升往往比全局 AUC 更难因为它把“用户偏好的差异”剥离了只看在同一用户内部模型能不能把正样本排到负样本前面。NDCGK 则要看重排后的最终排序质量它比 RecallK 更严格——不仅看推荐列表里有没有正确商品还看正确商品是否排在前面。广告推荐里第一名位置和第二名位置的点击率差异可能有 5 倍所以 NDCG 比 Recall 更能反映真实收入。5. 避坑清单初跑这套源码最常见的 5 个翻车现场5.1 中文路径加 zip 解压导致编码报错现象源码下载后放在桌面或含中文名的文件夹里运行python train.py直接报UnicodeDecodeError或者pandas.read_csv读数据时报错。原因部分 Python 库在 Windows 下对中文路径支持不完善zip 解压过程可能还把文件名的编码搞乱。更隐蔽的是数据文件本身是 GBK 编码而pd.read_csv默认用 UTF-8 读取中文注释和字段直接炸掉。解决把项目移到纯英文路径下再解压读入 CSV 时显式指定编码。我一般成功跑通的第一步永远是这条命令df pd.read_csv(ad_log.csv, encodingutf-8) # 如果报错换成 gbk 再试一次 df pd.read_csv(ad_log.csv, encodinggbk)5.2 数据太稀疏冷启动用户召回全为空现象评估脚本跑完发现测试集里大量用户的推荐列表是空的RecallK 为 0整个评估结果没法看。原因训练集只有 14 天日志很多用户在训练期只有一两次交互ItemCF 算相似度时根本没有足够共现对测试期用户又产生了新交互模型完全没见过。解决把评估逻辑拆开看——如果冷启动用户占比超过 30%说明数据量本身不够别急着调模型。常见做法是给冷启动用户加一个兜底召回策略比如热销商品、同品类热销、广告主主推商品。评估时对冷启动用户单独算指标不要和活跃用户混在一起否则你不知道模型到底是真强还是在偷懒。5.3 离线 AUC 涨了线上广告收入没变现象离线实验跑得挺开心AUC 提升 0.03上线后广告收入持平甚至下降。原因离线指标和线上收益之间的传导链太长。最常见的猫腻是离线数据的分布和线上不一致——离线测试集里的曝光是旧模型产生的你换新模型后曝光分布应该变化但离线还拿旧分布来测。这就是业界常说的“反馈循环”问题。解决上线前做 shadow 模式验证——新模型和旧模型同时跑新模型的结果只记录不干预线上展示跑 3 到 7 天后对比两个模型的离线打分差异和实际点击差异。如果 shadow 阶段新模型的点击率没有明显优势就不要全量上线。5.4 算全量相似度矩阵内存爆炸现象用 ItemCF 算商品相似度商品数 5 万时内存还能撑住商品数到 50 万时直接 OOM内存溢出。原因全量相似度矩阵是 O(N²) 的空间复杂度50 万商品要存 250 亿个相似度值Python 的 dict 存这个直接榨干内存。解决不要用 dict 存全量相似度改用faiss或annoy做近似近邻检索。ItemCF 的相似度计算也可以先按“共现次数”过滤只保留共现次数大于阈值比如 3 次的商品对再算相似度。这样能把相似度对的规模砍掉 90% 以上而且推荐质量几乎不掉。5.5 广告位和自然推荐混在一起重排逻辑失效现象源码里重排层只是简单按 CTR 预估降序排上线后发现广告收入上去了但用户体验暴跌退换率升高。原因广告推荐和自然推荐混在一个列表里如果广告只按 CTR 排序不考虑广告主预算消耗速度和用户体验就会出现一个用户翻了三屏全是广告的局面。解决重排层至少要加两个逻辑——频控同一广告对同一用户一天最多展示 N 次和广告占比控制比如前 10 个坑位里最多 3 个广告。广告主预算维度也很重要预算即将耗尽的广告要降权否则曝光给出去但点不了平台和广告主双输。这套源码如果只覆盖到精排重排逻辑你需要自己补。6. 把推荐效果真正验证出来离线复验、A/B 分流与上线前最后一道检查验证推荐系统不是看一次离线指标就结束。我的习惯是三个步骤先跑离线复验再做 A/B 分流小流量验证最后全量前做竞品对照。离线复验要用测试集之外的一段时间做时间回溯。比如你用前 12 天训练、第 13 天验证、第 14 天测试跑完别急着宣布成功——再往第 20 天的真实日志上推一次看指标会不会掉。广告推荐的时间漂移比自然推荐更严重因为广告主在换素材、改出价、调预算你今天测的 CTR 模型下周可能就因为素材热度变化失效了。如果两次时间窗口的指标波动超过 10%你调参数的功夫等于白费。A/B 分流这块Python 里最常见的实现是用 user_id 做 hash 分桶。广告场景我强烈建议按流量比例而非用户比例分桶——因为广告主看的是整体预算消耗用户分桶可能造成小桶流量太少、广告预算花不出去。一个简单可用的分桶脚本import hashlib def ab_bucket(user_id, saltrec_v1, split0.1): 按 user_id 加盐哈希分桶返回是否命中实验组 hash_val hashlib.md5(f{user_id}_{salt}.encode()).hexdigest() bucket int(hash_val[:8], 16) % 10000 return bucket split * 10000 # 上线后抽样检查分桶比例 test_users list(range(1, 10001)) exp_users [u for u in test_users if ab_bucket(u)] print(f实验组占比: {len(exp_users) / len(test_users):.3%})逻辑说明hashlib.md5对user_id salt做哈希取前 8 位十六进制转成 0 到 9999 的整数小于阈值则进实验组。参数说明saltrec_v1是实验标识每次开新实验换一个 salt避免同一个用户始终落在同一个桶里split0.1表示 10% 流量进实验组。这个方案在千万用户量级下分桶比例稳定而且同一个用户在实验期内始终落同一组不会被冲刷。A/B 的观察周期要看广告投放的转化延迟。电商广告的转化链路是曝光、点击、下单、确认收货——如果没有回传实时数据至少观察 7 天再下结论。只看一天数据的 A/B 全是玄学广告主投放节奏一天之内波动剧烈早中晚的用户意图完全不同。上线前的最后一道检查把推荐结果的 cheat 检测做了。一种最常见的模型作弊是“只推荐高 CTR 商品但用户根本不买”离线 AUC 高是因为 CTR 和 CVR 被混淆了。你要单独上线一个 CVR 预估模型做对照看 CTR 提升但 CVR 下降的坑位比例——如果超过 20%说明排序模型学的不是购买意图只是点击诱惑。最后说一句我的习惯任何源码包到我手里第一周绝不改模型结构只跑通链路、复现指标、记录数据分布。等你能自信地说出“这套代码在什么数据上表现如何、在什么情况下会崩”再去动网络结构。推荐系统的改进应该是一个一个变量换而不是一把梭全改。希望这个思路帮你在电商广告推荐这条路上少走点弯路。本文还有配套的精品资源点击获取
网站建设高端定制企业官网