news 2026/10/22 4:37:10

阿里移动推荐算法竞赛实战:从数据切分到LightGBM排序的完整流程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
阿里移动推荐算法竞赛实战:从数据切分到LightGBM排序的完整流程

简介:阿里移动推荐算法竞赛.zip 是一份面向人工智能、计算机科学与技术等相关专业学生与算法爱好者的完整竞赛项目源码,适合用作毕业设计课题、课程作业或推荐系统入门实战参考。资源包共118个文件,约636KB,以33个csv数据文件、25个Python脚本、21个C#源码及6个C++文件为主,另含少量头文件、批处理脚本与工程配置,覆盖数据预处理、模型训练与样本扩展等环节。压缩包内提供处理训练样本、处理测试样本、扩展样本等批处理入口,并包含BP神经网络相关的C++实现与配套工程文件,便于读者理解推荐算法从数据到模型的完整链路。目前已有226人浏览学习,下载后可先查阅README.md了解目录结构与运行方式,源码均经测试验证可正常运行,遇到问题可通过私信或留言与作者沟通,仅限交流学习参考,请勿用于商业用途。

1. 移动推荐赛题到底在考什么:从一份离线数据集说起

移动端推荐和 PC 端推荐最大的差别,不在模型结构,而在数据形态。用户拿着手机,行为是碎片化的:点开、滑走、加购、收藏、下单,每一步都带着时间戳和位置信息,而且大量行为发生在极短的会话窗口里。阿里移动推荐算法竞赛给出的就是这样一个典型场景——用某电商平台移动端 4 天的用户行为日志,预测未来一天内哪些用户会对哪些商品产生购买行为。它解决的不是“推荐什么内容”,而是“在稀疏、短周期、强时序的移动行为流里,怎么把转化信号捞出来”。适合谁?适合已经会跑协同过滤、想第一次认真处理时序特征和负采样的人,也适合做特征工程练手的从业者。这个赛题的价值在于:数据规模可控,评价指标明确(F1),但坑足够多,能逼你把召回、排序、样本构造这条链路走一遍。

2. 数据切分与特征构造:把 4 天日志变成可训练的样本

2.1 先搞清楚时间窗口和标签定义

赛题数据通常包含用户行为表(user_id、item_id、behavior_type、time)和商品表、用户表。behavior_type 一般有四种:浏览、收藏、加购、购买。预测目标是:在最后一天(比如 12 月 19 日)有购买行为的用户-商品对。训练集用前 3 天(12 月 18 日之前)构造,验证集用第 4 天。

这里第一个容易翻车的地方是:很多人直接把“最后一天购买”当正样本,前面所有交互当特征,结果线下 F1 很高,线上崩掉。原因是时间穿越——你用 12 月 18 日之后的行为去预测 12 月 18 日之前的购买,特征里混入了未来信息。正确做法是:以某一天为切分点,切分点之前的行为做特征,切分点当天的购买做标签。

import pandas as pd # 假设 df 是行为日志,列:user_id, item_id, behavior_type, time # time 格式为 '2014-12-18 10:23:45' df['date'] = pd.to_datetime(df['time']).dt.date # 切分点:用 12-17 及之前做特征,12-18 的购买做标签 split_date = pd.to_datetime('2014-12-18').date() train_feat = df[df['date'] < split_date] train_label = df[(df['date'] == split_date) & (df['behavior_type'] == 4)] # 构造正样本对 pos_pairs = train_label[['user_id', 'item_id']].drop_duplicates() pos_pairs['label'] = 1

逻辑说明:split_date是人为设定的时间边界,必须保证特征窗口严格早于标签窗口。behavior_type == 4代表购买。参数上,切分点不是固定的,你可以用 12-16 做特征、12-17 做标签,多切几组做交叉验证,避免单次切分带来的偶然性。

2.2 负采样:别让模型只学会猜“不买”

正样本只有几千到几万条,负样本如果从全量用户-商品对里随机抽,比例会到 1:1000 以上,模型直接躺平预测全负。常见做法是:对每个正样本,抽 3 到 10 个负样本,负样本从“当天有交互但没购买”的 pair 里选,而不是从全量里随机选。这样负样本更接近真实分布。

import numpy as np # 当天有交互但未购买的用户-商品对 interact = df[df['date'] == split_date][['user_id', 'item_id']].drop_duplicates() neg_candidates = interact.merge(pos_pairs[['user_id', 'item_id']], on=['user_id', 'item_id'], how='left', indicator=True) neg_candidates = neg_candidates[neg_candidates['_merge'] == 'left_only'][['user_id', 'item_id']] # 每个正样本配 5 个负样本 neg_sample = neg_candidates.sample(n=min(len(pos_pairs) * 5, len(neg_candidates)), random_state=42) neg_sample['label'] = 0 train_data = pd.concat([pos_pairs, neg_sample], ignore_index=True)

逻辑说明:indicator=True用来标记哪些 pair 在正样本里出现过,left_only就是没购买的。n=len(pos_pairs) * 5控制负正比,5 是一个经验值,太高会让训练变慢且正样本被淹没,太低会让模型过于激进。random_state固定保证可复现。

2.3 四类行为的时间衰减特征

移动端行为的时间衰减比 PC 端更剧烈——用户上午浏览的商品,下午可能就忘了。所以特征里必须体现“最近一次交互距切分点有多久”。常见做法是对每种行为分别算:最近一次时间差、行为次数、行为在最近 1 天/3 天/7 天的计数。

# 以浏览行为为例,计算用户-商品对的特征 def build_features(df, split_date): feat = df[df['date'] < split_date].copy() feat['days_before'] = (pd.to_datetime(split_date) - pd.to_datetime(feat['date'])).dt.days # 最近一次交互时间差 last_interact = feat.groupby(['user_id', 'item_id'])['days_before'].min().reset_index() last_interact.columns = ['user_id', 'item_id', 'last_days'] # 各行为计数 behavior_count = feat.pivot_table(index=['user_id', 'item_id'], columns='behavior_type', values='time', aggfunc='count').fillna(0).reset_index() behavior_count.columns = ['user_id', 'item_id', 'view_cnt', 'fav_cnt', 'cart_cnt', 'buy_cnt'] return last_interact.merge(behavior_count, on=['user_id', 'item_id'], how='outer').fillna(0)

逻辑说明:days_before越小说明交互越近,last_days就是最近一次交互距切分点的天数。pivot_table把行为类型展开成列,fillna(0)处理没有某类行为的 pair。参数上,days_before用天数而不是小时,是因为赛题数据跨度只有几天,小时级噪声太大。

3. 召回与排序的两段式落地:从规则召回走到 LightGBM 排序

3.1 召回层:用规则先圈出候选集

全量用户-商品对是千万级,直接排序不现实。召回层的作用是把候选集压到几万条。移动推荐赛题里,常见召回策略有三类:热销商品召回、用户近期交互过的商品召回、同品类商品召回。热销召回负责覆盖新用户,近期交互召回负责捕捉短期兴趣,品类召回负责扩展。

# 热销召回:切分点前 1 天购买量 top 500 的商品 hot_items = df[(df['date'] < split_date) & (df['behavior_type'] == 4)] \ .groupby('item_id').size().sort_values(ascending=False).head(500).index.tolist() # 用户近期交互召回:切分点前 1 天有浏览/加购的用户-商品对 recent_interact = df[(df['date'] >= split_date - pd.Timedelta(days=1)) & (df['behavior_type'].isin([1, 3]))][['user_id', 'item_id']].drop_duplicates() # 合并候选集 candidates = pd.concat([ recent_interact, pd.DataFrame({'user_id': recent_interact['user_id'].unique().tolist() * len(hot_items), 'item_id': hot_items * recent_interact['user_id'].nunique()}) ]).drop_duplicates()

逻辑说明:hot_items取购买量 top 500,数量根据内存调整,500 到 2000 都常见。recent_interact用isin([1, 3])筛浏览和加购,购买行为本身已经算正样本,不放进召回。最后的笛卡尔积构造是为了让每个活跃用户都能看到热销商品,但要注意内存,用户数乘商品数可能爆炸,实际中会按用户分块处理。

3.2 排序层:LightGBM 的二分类与参数设置

召回后的候选集配上特征,送进 LightGBM 做二分类。为什么选 LightGBM 而不是深度学习?因为赛题数据量在百万级,特征维度几十到几百,树模型在稀疏特征和类别特征上更稳,训练快,调参直观。

import lightgbm as lgb from sklearn.model_selection import train_test_split # 假设 train_data 已经 merge 了特征 X = train_data.drop(['user_id', 'item_id', 'label'], axis=1) y = train_data['label'] X_train, X_val, y_train, y_val = train_test_split(X, y, test_size=0.2, random_state=42) params = { 'objective': 'binary', 'metric': 'auc', 'learning_rate': 0.05, 'num_leaves': 64, 'max_depth': 8, 'min_data_in_leaf': 50, 'feature_fraction': 0.8, 'bagging_fraction': 0.8, 'bagging_freq': 5, 'verbose': -1 } dtrain = lgb.Dataset(X_train, label=y_train) dval = lgb.Dataset(X_val, label=y_val, reference=dtrain) model = lgb.train(params, dtrain, num_boost_round=500, valid_sets=[dval], callbacks=[lgb.early_stopping(50), lgb.log_evaluation(100)])

逻辑说明:num_leaves=64和max_depth=8控制树复杂度,移动推荐数据噪声大,树太深容易过拟合。min_data_in_leaf=50防止叶子节点样本太少。early_stopping(50)表示验证集 AUC 50 轮不提升就停。learning_rate=0.05配合 500 轮是常见组合,想更快收敛可以调到 0.1,但容易震荡。

3.3 阈值选择:F1 不是固定 0.5

赛题评价指标是 F1,而 F1 对阈值敏感。模型输出的是概率,0.5 只是默认切分,实际要在验证集上扫一遍阈值,找 F1 最大的点。

from sklearn.metrics import f1_score y_pred_prob = model.predict(X_val) best_f1, best_thresh = 0, 0.5 for thresh in np.arange(0.1, 0.9, 0.02): y_pred = (y_pred_prob >= thresh).astype(int) f1 = f1_score(y_val, y_pred) if f1 > best_f1: best_f1, best_thresh = f1, thresh print(f"best threshold: {best_thresh:.2f}, best F1: {best_f1:.4f}")

逻辑说明:np.arange(0.1, 0.9, 0.02)是阈值扫描范围,步长 0.02 兼顾精度和速度。best_thresh要记下来,线上预测时用同一个阈值。注意:如果验证集和测试集分布差异大,阈值会漂移,所以切分点最好多试几组取平均。

4. 避坑与排查:移动推荐赛题里最容易翻车的 5 个点

4.1 现象:线下 F1 0.3,线上只有 0.05

原因:时间穿越。特征里用了标签窗口之后的行为,比如用 12-18 的浏览去预测 12-18 的购买。解决:严格按时间切分,特征窗口的右边界必须小于标签窗口的左边界。检查方法:把特征表按时间排序,看最大时间戳是否小于标签最小时间戳。

4.2 现象:模型 AUC 0.9 但 F1 只有 0.1

原因:正负样本极度不平衡,模型学会了把所有样本预测为负。解决:负采样控制比例,或者用scale_pos_weight参数给正样本加权。LightGBM 里设scale_pos_weight = 负样本数 / 正样本数,但不要超过 100,否则梯度爆炸。

4.3 现象:召回候选集里正样本覆盖率不到 30%

原因:召回策略太窄,只用了热销或只用了近期交互。解决:多路召回合并,热销、近期交互、同品类、同用户历史购买品类各出一路,最后去重。覆盖率要盯住,低于 50% 后面排序再强也白搭。

4.4 现象:LightGBM 训练到 100 轮就开始过拟合

原因:特征里有很多高基数类别特征(比如 item_id 直接当数值),树模型会记住特定 item。解决:item_id 和 user_id 不要直接进模型,改成统计特征(该 item 的点击率、该用户的购买率)。如果一定要用,做 target encoding 并加平滑。

4.5 现象:验证集 F1 波动超过 0.05

原因:单次时间切分偶然性太大,某一天的数据分布可能异常。解决:做多组切分,比如用 12-16 预测 12-17、12-17 预测 12-18,取平均 F1。如果波动还是大,检查是不是某天的促销活动导致行为突变,这种日子要么剔除要么单独建模。

5. 进阶技巧:用滑动窗口和多模型融合把 F1 再抬一截

走到这里,单模型 F1 大概能到 0.08 到 0.12 之间(赛题数据不同会有差异)。想再往上,两个方向最实在:滑动窗口构造更多训练样本,以及多模型融合。

滑动窗口的思路是:不只用“前 3 天预测第 4 天”这一组,而是构造多组。比如用 12-15 到 12-16 预测 12-17,用 12-16 到 12-17 预测 12-18。每组都生成正负样本,合并后训练。这样训练数据翻几倍,模型见过更多时间模式。代价是特征计算量线性增长,但移动推荐赛题数据量不大,值得做。

# 滑动窗口构造多组训练样本 windows = [ ('2014-12-15', '2014-12-16', '2014-12-17'), ('2014-12-16', '2014-12-17', '2014-12-18'), ] all_train = [] for feat_start, feat_end, label_date in windows: feat = df[(df['date'] >= pd.to_datetime(feat_start).date()) & (df['date'] <= pd.to_datetime(feat_end).date())] label = df[(df['date'] == pd.to_datetime(label_date).date()) & (df['behavior_type'] == 4)] # 这里复用前面的 build_features 和负采样逻辑 # 注意:每个窗口的特征计算要独立,不能跨窗口混 window_data = build_window_data(feat, label) all_train.append(window_data) final_train = pd.concat(all_train, ignore_index=True)

逻辑说明:windows列表里每个元组是(特征起始日、特征结束日、标签日)。关键点是每个窗口独立算特征,因为“最近一次交互距切分点”这个特征在不同窗口里基准日不同。build_window_data是封装好的函数,内部做特征构造和负采样。合并后final_train的样本量大概是单窗口的 2 到 3 倍。

多模型融合方面,LightGBM 和 XGBoost 各训一个,概率取平均。如果还想再进一步,加一个简单的逻辑回归做 stacking,但收益通常不如滑动窗口明显。我自己的习惯是:先把滑动窗口跑通,F1 稳定提升后再考虑融合,否则调参空间太大,容易迷失。

最后一个技巧关于阈值:滑动窗口训练出的模型,验证集要用最后一个窗口的标签日,不要用所有窗口的平均 F1 选阈值。因为线上预测的是未来一天,和最后一个窗口的分布最接近。这个细节很多人忽略,导致线下线上阈值不一致,F1 掉一截。

希望帮到你。

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

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

多智能体 AI Agent 协作实战:大语言模型任务编排与工程落地

最近在做一个偏综合信息处理的项目&#xff0c;需求大致是&#xff1a;拿到一堆半结构化资料&#xff0c;拆出关键信息、做风险判断、再给出一套可执行建议。最开始我直接押到单个 LLM 接口上&#xff0c;任务一多、上下文一长&#xff0c;问题就出来了&#xff1a;前面几个要点…

作者头像 李华
网站建设 2026/10/22 4:35:19

XGBoost股价预测实战:标签构造、特征工程与时序切分避坑指南

简介&#xff1a;这份资源是一套基于XGBoost算法预测股票未来n日价格的完整源码&#xff0c;面向具备一定Python与机器学习基础、希望将集成学习模型落地到量化选股或行情研判场景的开发者与学习者。压缩包共6个文件&#xff0c;以3个py脚本为核心&#xff0c;分别承担数据获取…

作者头像 李华
网站建设 2026/10/22 4:34:30

高精定位与高精地图:自动驾驶如何让车知道自己在哪条车道

车头明明在辅路上&#xff0c;定位却显示在主路上&#xff0c;中间隔着一道绿化带。这是我做自动驾驶定位模块这几年&#xff0c;听到的最高频吐槽。我自己第一次上车测试时就撞见了&#xff1a;在高架下方的匝道切换处&#xff0c;隔音屏把卫星信号反射得一塌糊涂&#xff0c;…

作者头像 李华
网站建设 2026/10/22 4:32:45

AI Agent会话引擎设计与工程实践:从状态机到上下文管理

1. 为什么要单独做一个会话引擎&#xff1a;从单体 Demo 到生产系统的裂痕先说说我踩过的坑。最初我做的 Agent 非常简单——一个用户消息进来&#xff0c;拼上历史记录&#xff0c;调一次大模型接口&#xff0c;拿到回复再返回给前端。这套逻辑在演示场景下跑得很顺&#xff0…

作者头像 李华