ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

移动推荐算法竞赛实战:从数据切分到特征工程的完整代码解析

移动推荐算法竞赛实战:从数据切分到特征工程的完整代码解析 简介本资源为阿里移动推荐算法竞赛的完整参赛代码与解析资料包面向人工智能、数据挖掘及计算机相关专业的学生、教师与科研人员尤其适合以推荐系统为课题的毕业设计、课程项目或竞赛复现场景。包内共190个文件以Python源码为核心辅以JavaScript前端脚本、C/C底层实现、JSON配置与JPG/PNG图表另有少量HTML、CSS及项目工程文件压缩包约2.58MB结构紧凑便于快速部署。资源包含完整赛题方案、算法实现细节与项目报告可帮助读者理解移动推荐场景下的特征工程、模型训练与评估流程并支持在现有代码基础上二次开发。目前已有34人学习下载适合作为推荐算法入门到进阶的实践参考。1. 从一份竞赛代码包说起移动推荐到底在解决什么问题移动推荐算法竞赛的核心场景是在用户行为稀疏、时间窗口极短的条件下预测用户对某个商品或内容的下一次交互概率。阿里移动推荐算法竞赛当年给出的数据集就是典型的「用户-商品-行为类型-时间戳」四元组结构其中行为类型分为点击、收藏、加购、购买四类。参赛者要做的是基于前若干天的行为日志预测未来某天用户会不会对指定商品产生购买行为。这个问题的难点不在于模型有多深而在于三件事第一正样本极度稀疏购买行为在所有行为中占比通常不到百分之一第二时间切分必须严格用未来数据预测过去是典型的翻车操作第三特征工程决定了天花板模型选择只决定你离天花板有多近。一份完整的参赛代码包价值就在于它把这三件事的处理流程固化下来了——从数据读取、时间滑窗切分、特征构造、样本采样到模型训练和线下验证每一步都有可复现的逻辑。适合读这份代码的人有三类一是想入门推荐系统但不知道从哪下手的新手二是打过类似竞赛但线下线上差距大的选手三是工作中要做用户行为预测的工程师。下面我会按「数据怎么切、特征怎么造、模型怎么选、坑怎么避」的顺序把这份参赛代码里最值得抄的部分拆开讲。2. 数据切分与样本构造时间滑窗为什么不能随便切2.1 移动推荐的数据结构与时序陷阱阿里移动推荐的数据格式通常是这样的user_id, item_id, behavior_type, user_geohash, item_category, time。其中 behavior_type 取 1 到 4分别对应点击、收藏、加购、购买。time 精确到小时。很多人拿到数据第一反应是按 user_id 分组做统计特征这没错但如果在切分训练集和验证集之前就做了全局统计那就等于把验证集的信息泄漏进了训练集。正确的做法是先按时间切一刀比如用 11 月 18 日到 12 月 17 日的数据做训练12 月 18 日做验证。所有统计类特征比如用户历史购买次数、商品历史点击率都只能基于训练集的时间窗口计算然后映射到验证集上。这个原则说起来简单但我在复现别人代码时见过太多次「先 concat 再 groupby」的写法线下 AUC 冲到 0.9线上一跑就崩。import pandas as pd # 读取原始行为日志假设列名为 user_id, item_id, behavior_type, time df pd.read_csv(tianchi_mobile_recommend_train_user.csv) # 关键先转时间类型再按时间切分绝不能先做全局统计 df[time] pd.to_datetime(df[time]) # 训练窗口前 30 天验证窗口最后 1 天 train_start pd.Timestamp(2014-11-18) train_end pd.Timestamp(2014-12-17) valid_start pd.Timestamp(2014-12-18) valid_end pd.Timestamp(2014-12-18 23:59:59) train_df df[(df[time] train_start) (df[time] train_end)].copy() valid_df df[(df[time] valid_start) (df[time] valid_end)].copy() # 验证集的标签当天是否发生购买 valid_label valid_df[valid_df[behavior_type] 4][[user_id, item_id]].drop_duplicates() valid_label[label] 1这段代码的逻辑说明先转时间类型是为了后续做时间窗口过滤train_df 和 valid_df 完全按时间隔离valid_label 只取购买行为作为正样本。参数说明训练窗口长度 30 天是常见选择太短会导致行为覆盖不足太长会引入过期兴趣验证窗口取 1 天是因为竞赛要求预测未来一天实际业务中可以根据预测周期调整。2.2 负样本采样与正负比控制购买行为稀疏到什么程度全量数据里购买记录大约只有点击的几十分之一。如果直接把所有「用户-商品」对拿来训练负样本会淹没正样本模型学到的几乎全是「不买」。常见做法是负样本采样但采样比例不能拍脑袋。我一般会先统计正样本数量然后按 1:3 到 1:10 的比例采负样本。比例太小模型见不到足够的负例容易过拟合比例太大训练时间暴涨且收益递减。另一个细节是负样本要从「用户有过交互但未购买」的集合里采而不是从全量商品里随机抽否则负样本太容易区分模型学不到真正有用的边界。# 正样本验证窗口内发生购买的用户-商品对 pos_samples valid_label.copy() # 负样本候选用户当天有行为但未购买的商品 valid_behavior valid_df[[user_id, item_id]].drop_duplicates() neg_candidates valid_behavior.merge(pos_samples[[user_id, item_id]], on[user_id, item_id], howleft, indicatorTrue) neg_candidates neg_candidates[neg_candidates[_merge] left_only][[user_id, item_id]] # 按 1:5 采样负样本 neg_samples neg_candidates.sample(nmin(len(pos_samples) * 5, len(neg_candidates)), random_state42) neg_samples[label] 0 # 合并训练样本 train_samples pd.concat([pos_samples, neg_samples], ignore_indexTrue)逻辑说明负样本候选来自验证窗口内有行为但未购买的商品这样构造的负样本更接近真实分布。参数说明1:5 是经验值实际可以试 1:3 和 1:10 对比线下 AUCrandom_state 固定是为了结果可复现竞赛中建议多组随机种子取平均。3. 特征工程从行为计数到时间衰减的完整构造链3.1 用户侧与商品侧的统计特征特征工程是这类竞赛的分水岭。我见过太多人把原始字段直接丢进 GBDT结果 AUC 卡在 0.6 上不去。真正有效的特征是把行为日志按 user_id 和 item_id 做聚合再交叉出组合特征。用户侧特征包括用户总点击次数、总购买次数、购买转化率、最近一次行为距今天数、活跃天数。商品侧特征包括商品总点击次数、总购买次数、商品转化率、商品所属类目的热度。这些特征的计算必须严格限定在训练窗口内然后通过 user_id 和 item_id 映射到验证集。# 用户侧统计特征只在训练窗口内计算 user_feat train_df.groupby(user_id).agg( user_click_cnt(behavior_type, lambda x: (x 1).sum()), user_buy_cnt(behavior_type, lambda x: (x 4).sum()), user_active_days(time, lambda x: x.dt.date.nunique()) ).reset_index() # 购买转化率加 1 平滑避免除零 user_feat[user_buy_rate] user_feat[user_buy_cnt] / (user_feat[user_click_cnt] 1) # 商品侧统计特征 item_feat train_df.groupby(item_id).agg( item_click_cnt(behavior_type, lambda x: (x 1).sum()), item_buy_cnt(behavior_type, lambda x: (x 4).sum()) ).reset_index() item_feat[item_buy_rate] item_feat[item_buy_cnt] / (item_feat[item_click_cnt] 1)逻辑说明groupby 聚合是特征工程的基本功lambda 里做条件计数是为了区分行为类型。参数说明加 1 平滑是标准做法避免冷门商品转化率被零除放大user_active_days 用 dt.date.nunique() 而不是 count是为了去重同一天多次行为。3.2 时间衰减特征与交叉特征时间衰减是移动推荐里最容易被忽略但收益很高的特征。用户昨天的点击和一个月前的点击对今天的购买预测价值完全不同。常见做法是给每个行为按时间距离加权权重用指数衰减weight exp(-alpha * days_diff)alpha 一般取 0.1 到 0.3。交叉特征则是把用户侧和商品侧拼在一起比如「用户购买率 × 商品购买率」「用户点击该商品的次数」「用户在该类目下的购买次数」。这些交叉特征能让模型学到「这个用户在这个类目下本来就爱买」这类模式。import numpy as np # 时间衰减权重以训练窗口最后一天为基准 base_date train_end train_df[days_diff] (base_date - train_df[time]).dt.days alpha 0.2 train_df[time_weight] np.exp(-alpha * train_df[days_diff]) # 用户-商品交叉特征用户对该商品的点击次数和加权行为分 user_item_feat train_df.groupby([user_id, item_id]).agg( ui_click_cnt(behavior_type, lambda x: (x 1).sum()), ui_weighted_score(time_weight, sum) ).reset_index() # 用户-类目交叉特征 user_cate_feat train_df.groupby([user_id, item_category]).agg( uc_buy_cnt(behavior_type, lambda x: (x 4).sum()) ).reset_index()逻辑说明时间衰减用指数函数越近的行为权重越高。参数说明alpha 越大衰减越快0.2 是中等衰减速度实际可以网格搜索ui_weighted_score 把行为频次和时间新鲜度揉在一起比单纯计数更有区分度。3.3 特征拼接与缺失值处理所有特征算完后要拼到训练样本上。拼接时用 left join保证每个样本都能拿到特征。缺失值一般填 0 或 -1具体看特征含义计数类特征填 0比率类特征填全局均值或 -1。填完之后建议做一次特征重要性排序把重要性接近零的特征删掉减少噪声。# 拼接所有特征到训练样本 train_samples train_samples.merge(user_feat, onuser_id, howleft) train_samples train_samples.merge(item_feat, onitem_id, howleft) train_samples train_samples.merge(user_item_feat, on[user_id, item_id], howleft) train_samples train_samples.merge(user_cate_feat, on[user_id, item_category], howleft) # 缺失值填充 fill_zero_cols [ui_click_cnt, ui_weighted_score, uc_buy_cnt] for col in fill_zero_cols: train_samples[col] train_samples[col].fillna(0) # 比率类特征用 -1 标记缺失 rate_cols [user_buy_rate, item_buy_rate] for col in rate_cols: train_samples[col] train_samples[col].fillna(-1)逻辑说明left join 保证样本不丢缺失值分类型处理。参数说明计数类填 0 符合业务含义比率类填 -1 是为了让模型区分「真的为零」和「没有数据」。4. 模型训练与线下验证XGBoost 参数怎么调才不玄学4.1 为什么选 GBDT 而不是深度学习这类竞赛的数据量通常在千万级行为日志特征维度几百到几千。深度学习在这个规模上不一定打得过 GBDT原因是第一GBDT 对稀疏特征和缺失值天然友好不需要复杂的嵌入层第二训练速度快一天能跑几十组参数第三特征重要性可解释方便快速迭代。我一般先用 XGBoost 或 LightGBM 打基线如果线下 AUC 已经到 0.75 以上再考虑上 FM 或 DeepFM 做融合。参赛代码里常见的是 XGBoost因为它的参数文档全、社区案例多新手不容易卡在环境配置上。import xgboost as xgb from sklearn.model_selection import train_test_split from sklearn.metrics import roc_auc_score # 特征列 feature_cols [user_click_cnt, user_buy_cnt, user_active_days, user_buy_rate, item_click_cnt, item_buy_cnt, item_buy_rate, ui_click_cnt, ui_weighted_score, uc_buy_cnt] X train_samples[feature_cols] y train_samples[label] # 再切一刀做早停验证 X_train, X_val, y_train, y_val train_test_split(X, y, test_size0.2, random_state42, stratifyy) # XGBoost 参数 params { objective: binary:logistic, eval_metric: auc, max_depth: 6, learning_rate: 0.05, subsample: 0.8, colsample_bytree: 0.8, min_child_weight: 5, scale_pos_weight: 5, seed: 42 } dtrain xgb.DMatrix(X_train, labely_train) dval xgb.DMatrix(X_val, labely_val) model xgb.train(params, dtrain, num_boost_round1000, evals[(dval, val)], early_stopping_rounds50, verbose_eval100) # 线下 AUC pred model.predict(xgb.DMatrix(X_val), iteration_range(0, model.best_iteration 1)) print(Validation AUC:, roc_auc_score(y_val, pred))逻辑说明先切训练验证做早停避免过拟合。参数说明max_depth 6 是中小规模数据的常用值太深容易过拟合learning_rate 0.05 配合 1000 轮是稳妥组合scale_pos_weight 设为负正比这里采样后约 5用来平衡正负样本early_stopping_rounds 50 表示验证集 AUC 50 轮不提升就停。4.2 线下验证的三种切法与线上差距排查线下验证不能只切一刀。我一般做三种切法随机切、按时间切、按用户切。随机切看模型拟合能力按时间切看时序泛化按用户切看新用户表现。三种切法的 AUC 差距如果超过 0.05说明特征里有泄漏或者分布偏移。线上差距大的常见原因有三个一是特征计算用了全量数据二是验证集负样本采样方式和线上不一致三是预测时特征缺失值处理逻辑和训练时不同。排查方法是把线上预测的样本拿回来逐特征对比训练集分布看哪个特征偏移最大。切分方式用途正常 AUC 范围异常信号随机切拟合能力0.80-0.90过高说明泄漏按时间切时序泛化0.70-0.80过低说明特征过期按用户切新用户0.65-0.75差距大说明用户特征过拟合5. 避坑与排查参赛代码复现时最容易翻车的 5 个点5.1 现象线下 AUC 0.9线上一跑 0.6原因特征计算时用了验证集或未来数据典型的是先 concat 全量数据再 groupby 算统计特征。解决所有统计特征严格限定在训练窗口内计算验证集只做映射不参与任何聚合。5.2 现象训练时 loss 正常下降但验证集 AUC 不涨原因负样本采样比例失衡或者正负样本在特征空间上几乎不可分。解决调整采样比例到 1:3 到 1:10 之间检查特征是否有区分度可以单独看正负样本在关键特征上的分布差异。5.3 现象模型预测结果全是同一个值原因特征缺失值填充方式导致所有样本特征相同或者 scale_pos_weight 设置过大导致模型偏向预测正类。解决检查填充逻辑确认每个样本的特征向量有差异scale_pos_weight 不要超过负正比的两倍。5.4 现象训练速度极慢一天跑不完一组参数原因特征维度太高且稀疏或者用了 pandas 的 apply 逐行计算。解决用 groupby 聚合替代 apply把类别特征做编码压缩必要时用 LightGBM 替代 XGBoost它的直方图算法在稀疏数据上快很多。5.5 现象复现别人代码时结果对不上原因随机种子没固定、数据版本不一致、或者环境依赖版本差异。解决固定所有 random_state记录数据文件的 MD5用 requirements.txt 锁死依赖版本。竞赛代码尤其要注意 pandas 和 numpy 的版本不同版本 groupby 的默认行为可能不同。提示复现任何竞赛代码前先跑通数据读取和切分确认样本量和标签分布与原作者描述一致再往下做特征和模型。6. 进阶技巧用特征重要性反推业务逻辑而不是盲目堆特征6.1 特征重要性排序的正确用法很多人把特征重要性当成「哪个特征分高就留哪个」的筛选工具这其实浪费了它最大的价值。特征重要性真正的作用是帮你验证业务假设如果你认为「用户最近购买行为」应该很重要但重要性排到 20 名开外那要么特征构造有问题要么这个假设本身不成立。我一般会做两件事第一把重要性前 20 的特征列出来逐个问自己「这个特征在业务上说得通吗」第二把重要性接近零的特征删掉后重训看 AUC 是否下降。如果删了不降说明这些特征就是噪声留着只会增加过拟合风险。from xgboost import plot_importance import matplotlib.pyplot as plt # 输出特征重要性 importance model.get_score(importance_typegain) importance sorted(importance.items(), keylambda x: x[1], reverseTrue) for feat, score in importance[:20]: print(f{feat}: {score:.2f}) # 删除低重要性特征后重训对比 low_importance_feats [f for f, s in importance if s 1.0] print(待删除特征:, low_importance_feats)逻辑说明用 gain 而不是 weight 看重要性gain 反映特征对损失的实际贡献。参数说明阈值 1.0 是经验值实际可以根据重要性分布曲线找拐点。6.2 模型融合的轻量做法单模型调到头之后可以试模型融合。最轻量的做法是加权平均训练两个不同参数的 XGBoost按验证集 AUC 分配权重。再重一点可以用 stacking但要注意 stacking 的元特征必须用交叉验证生成否则泄漏风险很高。我自己的习惯是先把单模型做到线下 AUC 0.78 以上再考虑融合。融合带来的提升通常只有 0.005 到 0.01但代码复杂度和调试成本翻倍。如果时间有限优先把特征工程做深而不是把模型做复杂。6.3 从竞赛代码到业务落地的距离竞赛代码和业务代码最大的区别是竞赛只看 AUC业务还要看覆盖率、响应时间、可解释性。把竞赛方案往业务迁移时我会先做三件事第一把离线特征改成实时特征确认延迟在可接受范围第二把模型预测分数映射到业务动作比如 Top N 推荐还是阈值过滤第三加监控跟踪特征分布和预测分布的变化。最后说一个我自己的教训早年打比赛时我花了两周调模型参数AUC 只涨了 0.003后来花三天重新构造了时间衰减特征AUC 直接涨了 0.02。从那以后我给自己定了个规矩——模型调参的时间不超过总时间的 20%剩下的全砸在数据和特征上。这个习惯到现在做业务推荐系统还在用希望帮到你。本文还有配套的精品资源点击获取
返回列表