☰
移动推荐算法竞赛实战:从数据切分到模型融合的完整链路
2026/10/22 19:15:51 网站建设 项目流程

简介:阿里移动推荐算法竞赛.zip 是一套面向人工智能、计算机科学与技术等专业学生的算法竞赛实战源码,适合用作毕业设计、课程作业或推荐系统入门练习。资源围绕阿里移动推荐场景,提供从样本处理到模型训练与预测的完整流程,帮助读者理解推荐算法在真实赛题中的落地方式。压缩包共118个文件,约636KB,以csv数据文件、py脚本、cs与cpp源码为主,辅以h头文件、bat批处理脚本及配置文件,覆盖数据处理、神经网络实现与工程构建等环节。已有226人学习关注。下载后可先查阅README.md了解目录结构,源码均经测试验证可正常运行,读者能获得赛题方案参考、样本处理脚本、模型代码与排错思路,便于在此基础上复现实验、修改参数并完成自己的推荐算法项目。

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

移动推荐算法竞赛这个题目,很多人第一眼会把它当成普通的推荐系统练手项目,觉得无非是协同过滤加个排序。但真正跑过一遍的人会告诉你,它和电商推荐、内容推荐最大的区别在于:行为数据极度稀疏、时间窗口极短、且用户-商品交互几乎全是隐式反馈。你拿到的往往是一份按天切分的用户行为日志,字段包括用户标识、商品标识、行为类型(点击、收藏、加购、购买)、时间戳和地理坐标。没有评分,没有显式偏好,只有“做了还是没做”。

这个赛题适合两类人:一是想从零搭一套推荐 pipeline 的工程师,二是已经在做召回排序但没系统处理过时间衰减和稀疏性的人。它解决的核心问题是——在只有几天行为数据的前提下,预测用户在未来某天会不会对某个商品产生购买行为。难点不在模型多深,而在特征工程和负采样策略。下面我从数据切分、特征构造、模型选型到排错,把这条链路拆开讲。

2. 数据切分与负采样:别让时间穿越毁掉你的离线评估

2.1 为什么随机切分是第一个坑

很多新手拿到日志后直接train_test_split(random_state=42),然后得到一个 AUC 0.95 的漂亮结果,上线就崩。原因很简单:推荐系统的评估必须按时间切分。用户在第 5 天的行为不能用来预测第 3 天的结果,否则模型学到的是“未来信息泄漏”。

常见做法是:取前 N-1 天的数据做训练,第 N 天做验证。如果数据量够,再往前推一天做测试。比如总共 5 天日志,用第 1-3 天训练,第 4 天验证调参,第 5 天测试。这样切出来的负样本才接近线上真实分布。

import pandas as pd # 假设日志字段: user_id, item_id, behavior_type, timestamp, geo df = pd.read_csv("behavior_log.csv", parse_dates=["timestamp"]) # 按天切分,不要随机 df["date"] = df["timestamp"].dt.date train = df[df["date"] < "2024-06-04"] valid = df[df["date"] == "2024-06-04"] test = df[df["date"] == "2024-06-05"] # 正样本:购买行为 train_pos = train[train["behavior_type"] == "buy"][["user_id", "item_id"]].drop_duplicates() valid_pos = valid[valid["behavior_type"] == "buy"][["user_id", "item_id"]].drop_duplicates()

这段代码的关键在drop_duplicates。同一个用户同一天可能多次购买同一商品,去重后才是真正的“用户-商品”正样本对。参数上,date的边界要根据实际日志跨度调整,一般留出至少一天做验证,否则调参没有意义。

2.2 负采样:全量负样本为什么跑不动

正样本可能只有几万条,但用户-商品笛卡尔积是千万级。全量负采样不仅内存扛不住,还会让正负比失衡到 1:1000 以上,模型直接偏向预测负类。我一般用按用户分层采样:对每个正样本用户,从该用户未交互的商品池里随机抽 3-5 个作为负样本,同时保证热门商品被抽到的概率略高,模拟真实曝光偏差。

import numpy as np def negative_sample(pos_df, all_items, user_item_set, ratio=4): neg_rows = [] for _, row in pos_df.iterrows(): u = row["user_id"] # 该用户已交互商品集合 interacted = user_item_set.get(u, set()) candidates = list(all_items - interacted) if len(candidates) == 0: continue # 按热度加权采样,热门商品权重高 weights = np.array([item_pop[i] for i in candidates]) weights = weights / weights.sum() sampled = np.random.choice(candidates, size=min(ratio, len(candidates)), replace=False, p=weights) for i in sampled: neg_rows.append({"user_id": u, "item_id": i, "label": 0}) neg_df = pd.DataFrame(neg_rows) pos_df = pos_df.copy() pos_df["label"] = 1 return pd.concat([pos_df, neg_df], ignore_index=True)

ratio=4是经验值,正负比 1:4 在大多数稀疏场景下收敛稳定。item_pop是商品热度字典,用训练集统计。注意replace=False,同一个负样本不要重复抽。如果某个用户候选集为空(比如他已经交互过所有商品),直接跳过,不要硬造负样本。

2.3 时间衰减权重怎么加

移动场景下,用户三天前的点击和昨天的点击意义完全不同。我通常给每个行为加一个指数衰减权重:w = exp(-λ * Δt),其中 Δt 是行为时间到预测时间的天数,λ 取 0.1-0.3。λ 越大,越只看近期行为。这个权重不直接进负采样,而是用在特征聚合阶段——比如统计用户对某品类的点击次数时,用加权和代替简单计数。

import math def time_decay_weight(delta_days, lam=0.2): return math.exp(-lam * delta_days) # 示例:用户对某品类的加权点击次数 user_cate_clicks = {} for _, row in train[train["behavior_type"] == "click"].iterrows(): delta = (predict_date - row["timestamp"].date()).days w = time_decay_weight(delta, lam=0.2) key = (row["user_id"], row["cate_id"]) user_cate_clicks[key] = user_cate_clicks.get(key, 0) + w

λ 的选择要看业务节奏。如果商品更新快、用户兴趣漂移明显,λ 调到 0.3;如果品类稳定,0.1 就够。这个参数没有理论最优,只能靠验证集 AUC 来调。

3. 特征工程:把用户、商品、上下文拆成可计算的向量

3.1 用户侧特征:从行为频次到兴趣漂移

用户特征分三层:基础属性(年龄、性别,如果有)、行为统计(点击/收藏/加购/购买次数)、兴趣演化(最近一次行为距今天数、最近 3 天活跃天数)。移动端还有一个容易被忽略的维度——地理活跃度。用户经常出现的坐标网格可以编码成类别特征,反映其活动范围。

def user_features(df, predict_date): feats = {} for uid, group in df.groupby("user_id"): feats[uid] = { "click_cnt": (group["behavior_type"] == "click").sum(), "buy_cnt": (group["behavior_type"] == "buy").sum(), "cart_cnt": (group["behavior_type"] == "cart").sum(), "fav_cnt": (group["behavior_type"] == "fav").sum(), "last_active_days": (predict_date - group["timestamp"].max().date()).days, "active_days_3": group[group["timestamp"] >= predict_date - pd.Timedelta(days=3)]["date"].nunique(), "geo_grid": group["geo"].mode()[0] if not group["geo"].mode().empty else "unknown" } return feats

last_active_days和active_days_3是强特征,尤其对购买预测。geo_grid用众数填充,缺失时给 unknown。注意不要用未来数据算这些统计量,所有聚合必须限制在训练时间窗口内。

3.2 商品侧与交叉特征:热度、转化率、用户-品类偏好

商品侧核心是热度和转化率。热度用曝光或点击次数,转化率用购买/点击。交叉特征里最有效的是用户对品类的偏好分:该用户在某品类的购买次数除以该品类总购买次数,再乘以时间衰减。

def item_features(df): feats = {} for iid, group in df.groupby("item_id"): clicks = (group["behavior_type"] == "click").sum() buys = (group["behavior_type"] == "buy").sum() feats[iid] = { "item_click": clicks, "item_buy": buys, "item_cvr": buys / (clicks + 1), # 平滑 "item_cate": group["cate_id"].iloc[0] } return feats def cross_features(user_feats, item_feats, user_cate_buy): cross = {} for uid in user_feats: for iid in item_feats: cate = item_feats[iid]["item_cate"] cross[(uid, iid)] = { "user_cate_buy_ratio": user_cate_buy.get((uid, cate), 0), "item_cvr": item_feats[iid]["item_cvr"], "user_buy_cnt": user_feats[uid]["buy_cnt"] } return cross

item_cvr分母加 1 是拉普拉斯平滑,避免除零。user_cate_buy_ratio需要提前统计用户在各品类的购买分布。交叉特征维度会爆炸,实际训练时只对候选集(正样本+负采样)计算,不要全量笛卡尔积。

3.3 特征拼接与归一化

把上面三部分拼成一条向量,数值特征做 log1p 或 min-max 归一化,类别特征做 one-hot 或 embedding。我一般用sklearn的DictVectorizer快速拼,或者手写 numpy 数组。注意:树模型不需要归一化,但线性模型和神经网络必须做。如果后面用 LightGBM,数值特征保持原样即可,类别特征直接传 category 类型。

import lightgbm as lgb feature_cols = ["click_cnt", "buy_cnt", "cart_cnt", "fav_cnt", "last_active_days", "active_days_3", "item_click", "item_buy", "item_cvr", "user_cate_buy_ratio"] X_train = train_samples[feature_cols] y_train = train_samples["label"] X_valid = valid_samples[feature_cols] y_valid = valid_samples["label"] dtrain = lgb.Dataset(X_train, y_train, categorical_feature=["geo_grid"]) dvalid = lgb.Dataset(X_valid, y_valid, reference=dtrain) params = { "objective": "binary", "metric": "auc", "learning_rate": 0.05, "num_leaves": 63, "min_data_in_leaf": 50, "feature_fraction": 0.8, "bagging_fraction": 0.8, "bagging_freq": 5, "verbose": -1 } model = lgb.train(params, dtrain, num_boost_round=500, valid_sets=[dvalid], callbacks=[lgb.early_stopping(50)])

num_leaves=63是起点,数据量小就降到 31。min_data_in_leaf=50防止过拟合。early_stopping(50)很关键,移动推荐数据噪声大,不加早停很容易在验证集上掉点。

4. 召回与排序的衔接:别把候选集当儿戏

4.1 召回阶段:ItemCF 和热度兜底

排序模型再强,召回没覆盖到正样本也是白搭。移动推荐场景下,我一般用三路召回:ItemCF(基于物品相似度)、用户历史购买品类下的热门商品、全局热度 TopN。ItemCF 的相似度用余弦,基于用户-物品交互矩阵。

from sklearn.metrics.pairwise import cosine_similarity from scipy.sparse import csr_matrix # 构建用户-物品交互矩阵(购买行为) user_item = train[train["behavior_type"] == "buy"].groupby( ["user_id", "item_id"]).size().unstack(fill_value=0) item_sim = cosine_similarity(user_item.T) item_sim_df = pd.DataFrame(item_sim, index=user_item.columns, columns=user_item.columns) def itemcf_recall(uid, user_item_df, item_sim_df, topk=50): if uid not in user_item_df.index: return [] interacted = user_item_df.loc[uid] interacted = interacted[interacted > 0].index.tolist() scores = {} for iid in interacted: sim_items = item_sim_df[iid].sort_values(ascending=False)[1:topk] for jid, sim in sim_items.items(): scores[jid] = scores.get(jid, 0) + sim return sorted(scores, key=scores.get, reverse=True)[:topk]

topk=50是单路召回数量,三路合并后去重大概 100-150 个候选。热度兜底保证新用户也有结果。注意 ItemCF 的相似度矩阵在物品数超过 10 万时会很占内存,可以用稀疏矩阵或只保留 TopK 相似对。

4.2 排序阶段:LightGBM 二分类就够了

候选集有了,排序就是一个二分类问题。LightGBM 在这个赛题里几乎是最优解:训练快、对稀疏特征友好、不需要太多调参。上面第 3 章的参数可以直接用。唯一要注意的是样本权重:购买行为的正样本可以给更高权重,比如 2.0,让模型更关注转化。

train_samples["weight"] = train_samples["label"].apply(lambda x: 2.0 if x == 1 else 1.0) dtrain = lgb.Dataset(X_train, y_train, weight=train_samples["weight"], categorical_feature=["geo_grid"])

权重不要设太高,2.0 是经验值,超过 5.0 会导致模型过度拟合正样本,AUC 反而下降。

4.3 评估指标:AUC 之外还要看 TopN 命中率

AUC 衡量全局排序能力,但线上只看 TopN 推荐位。所以验证时除了 AUC,还要算HitRate@K:对每个用户,取排序 TopK,看是否命中该用户当天购买的商品。

def hit_rate_at_k(model, valid_samples, K=10): valid_samples["score"] = model.predict(valid_samples[feature_cols]) hits = 0 total = 0 for uid, group in valid_samples.groupby("user_id"): topk = group.nlargest(K, "score")["item_id"].tolist() true_items = group[group["label"] == 1]["item_id"].tolist() if any(i in topk for i in true_items): hits += 1 total += 1 return hits / total if total > 0 else 0

这个指标比 AUC 更贴近业务。如果 AUC 高但 HitRate@10 低,说明模型排序头部不准,需要检查特征里是否有强区分度的交叉项。

5. 避坑与排查:那些让我重跑过三次的细节

5.1 时间戳时区不一致导致特征全错

现象:训练集 AUC 正常,验证集 AUC 掉到 0.5 左右。原因:日志里的时间戳有的是 UTC,有的是本地时间,按天切分时边界错位,导致验证集混入了训练集未来的行为。解决:统一转成同一时区再切分。用pd.to_datetime(..., utc=True).dt.tz_convert("Asia/Shanghai"),然后取.dt.date。

5.2 负采样时把验证集用户漏掉了

现象:验证集 HitRate@10 为 0,但 AUC 正常。原因:负采样只对训练集用户做了,验证集用户没有候选商品,排序时直接空列表。解决:验证集和测试集的候选集要单独生成,用同样的召回逻辑,不要复用训练集的负样本。

5.3 类别特征直接传整数被树模型当数值切

现象:geo_grid 特征重要性很低,但实际地理信息应该有用。原因:LightGBM 默认把整数当数值特征,切分时按大小排序,而地理网格编号没有大小意义。解决:在categorical_feature里显式声明,或者提前做 one-hot。LightGBM 的 category 处理会自动做梯度统计,比 one-hot 更省内存。

5.4 早停轮数设太大导致过拟合

现象:验证集 AUC 在前 100 轮上升,之后缓慢下降,但 early_stopping 没触发。原因:early_stopping(50)意味着 50 轮没提升才停,但移动推荐数据噪声大,50 轮足够模型记住噪声。解决:改成early_stopping(20),同时把learning_rate降到 0.03,让收敛更平滑。

5.5 特征穿越:用未来统计量算历史特征

现象:离线 AUC 0.9,线上效果差。原因:统计商品热度时用了全量日志,包括验证集和测试集的时间段。解决:所有统计量必须按时间窗口滚动计算,训练集只能用训练时间之前的数据。写个assert检查每个特征的最大时间戳不超过切分点。

6. 进阶技巧:用贝叶斯平滑和模型融合再挤一点 AUC

6.1 转化率特征的贝叶斯平滑

商品转化率buy/click在点击少的时候波动极大,一个点击一个购买就是 100% 转化,这种特征进模型就是噪声。贝叶斯平滑用全局转化率做先验,按点击量加权:

def bayesian_smooth(buys, clicks, global_cvr, alpha=10): return (buys + alpha * global_cvr) / (clicks + alpha)

alpha=10表示当点击量达到 10 次时,先验权重降到一半。点击越多,越相信实际转化率。这个技巧在商品侧特征上通常能带来 0.005-0.01 的 AUC 提升。

6.2 多模型融合:LightGBM + XGBoost + 逻辑回归

单模型调到头之后,融合是性价比最高的提升方式。我一般用三个模型:LightGBM 处理数值和类别特征,XGBoost 做补充(不同随机种子和特征采样),逻辑回归对稀疏 one-hot 特征做线性拟合。融合方式用加权平均,权重按验证集 AUC 分配。

from sklearn.linear_model import LogisticRegression import xgboost as xgb # XGBoost xgb_model = xgb.XGBClassifier(n_estimators=300, max_depth=6, learning_rate=0.05, subsample=0.8, colsample_bytree=0.8, eval_metric="auc") xgb_model.fit(X_train, y_train) # 逻辑回归(需要 one-hot) from sklearn.preprocessing import OneHotEncoder ohe = OneHotEncoder(handle_unknown="ignore") X_train_ohe = ohe.fit_transform(X_train) lr_model = LogisticRegression(C=0.1, max_iter=1000) lr_model.fit(X_train_ohe, y_train) # 加权融合 pred_lgb = model.predict(X_valid) pred_xgb = xgb_model.predict_proba(X_valid)[:, 1] pred_lr = lr_model.predict_proba(ohe.transform(X_valid))[:, 1] final_pred = 0.5 * pred_lgb + 0.3 * pred_xgb + 0.2 * pred_lr

权重 0.5/0.3/0.2 是经验值,可以用验证集 AUC 做网格搜索。注意逻辑回归的C=0.1是强正则,移动推荐数据稀疏,正则太弱会过拟合。

6.3 线上验证:A/B 测试的最小可行方案

离线指标再好,也要线上验证。如果没有真实流量,可以做一个模拟 A/B:把测试集按用户随机分两组,一组用旧模型,一组用新模型,比较 HitRate@10 和购买转化率的差异。样本量至少覆盖 1000 个用户,否则差异不显著。

import random random.seed(42) users = valid_samples["user_id"].unique().tolist() random.shuffle(users) group_a = set(users[:len(users)//2]) group_b = set(users[len(users)//2:]) hit_a = hit_rate_at_k(model_old, valid_samples[valid_samples["user_id"].isin(group_a)]) hit_b = hit_rate_at_k(model_new, valid_samples[valid_samples["user_id"].isin(group_b)]) print(f"Group A HitRate@10: {hit_a:.4f}, Group B: {hit_b:.4f}")

如果 B 组比 A 组高 2 个百分点以上,且样本量够,就可以考虑上线。否则继续调特征。

6.4 我踩过的最大的坑:忽略地理特征的时效性

地理网格特征在移动推荐里很有用,但用户的坐标是随时间变化的。我早期直接把用户所有历史坐标的众数当特征,结果模型学到的是“用户最常待的地方”,而不是“用户购买时所在的地方”。后来改成购买行为发生时的坐标网格,AUC 直接涨了 0.015。这个细节很少有人提,但移动场景下位置和时间是强耦合的。

做这个赛题,我的习惯是每加一个特征就问自己:这个特征在预测时刻真的能拿到吗?如果答案有一丝犹豫,就砍掉。离线 AUC 是虚的,线上能用的特征才是真的。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询