🏠 总目录📚 本教程 16 · 实战项目
📑 本页目录(点开跳转)

16 · 实战项目

三个项目,每个 1–3 天 | ⭐ 不做项目 = 没学过


🎯 一句话

看懂 ≠ 会做。这三个项目难度递进,做完第一个你就有作品集,做完第三个你能面推荐算法岗。


🧠 ADHD 专属:怎么保证你能做完项目

项目是最容易半途而废的东西。 三条规则:

  1. 先跑通最烂的版本。别一开始就想做完美。烂版本能跑 = 已经赢了 80%。
  2. 每个项目切成 5 个「一小时任务」(下面已经切好了)。每完成一个打勾。
  3. 不允许优化「还没跑通的东西」。跑通 → 打勾 → 才准优化。

🚫 必须避免:花三天调 Docker 环境。用 Colab / 本地裸 Python,能跑就行。


🥉 项目一:电影推荐(1 天)

目标:在 MovieLens 上跑通「数据 → 三个模型 → 评估对比」的完整流程。

产出:一个 Jupyter Notebook + 一张模型对比表。

任务清单

🔨 起步代码

import pandas as pd, numpy as np, zipfile, urllib.request, io
from collections import defaultdict

# ---------- T1: 数据 ----------
url = "https://files.grouplens.org/datasets/movielens/ml-latest-small.zip"
with urllib.request.urlopen(url) as r:
    z = zipfile.ZipFile(io.BytesIO(r.read()))
    df = pd.read_csv(z.open("ml-latest-small/ratings.csv"))

# 只保留正反馈(评分 >= 4 视为"喜欢")—— 转成隐式反馈问题
df = df[df.rating >= 4.0].copy()
print(f"用户 {df.userId.nunique()}, 物品 {df.movieId.nunique()}, 交互 {len(df)}")

# ---------- T2: 按时间切分(关键!) ----------
df = df.sort_values("timestamp")
split_ts = df.timestamp.quantile(0.8)
train = df[df.timestamp <= split_ts]
test  = df[df.timestamp >  split_ts]

# 测试集里只保留训练集见过的用户(冷启动单独评估)
test = test[test.userId.isin(train.userId.unique())]

train_dict = train.groupby("userId").movieId.apply(list).to_dict()
test_dict  = test.groupby("userId").movieId.apply(set).to_dict()

# ---------- T5: 评估函数(先写好,后面复用) ----------
def evaluate(recommend_fn, train_dict, test_dict, k=10):
    recalls, ndcgs, hits = [], [], []
    for user, truth in test_dict.items():
        recs = recommend_fn(user, k)
        if not recs: continue
        hit_set = set(recs) & truth
        recalls.append(len(hit_set) / len(truth))
        hits.append(1.0 if hit_set else 0.0)
        # NDCG
        dcg = sum(1/np.log2(i+2) for i, item in enumerate(recs) if item in truth)
        idcg = sum(1/np.log2(i+2) for i in range(min(len(truth), k)))
        ndcgs.append(dcg/idcg if idcg > 0 else 0)
    return {"Recall@%d" % k: np.mean(recalls),
            "NDCG@%d" % k:   np.mean(ndcgs),
            "HitRate@%d" % k: np.mean(hits)}

# ---------- T3: Baseline 1 —— 热门推荐 ----------
popular = train.movieId.value_counts().index.tolist()
def rec_popular(user, k=10):
    seen = set(train_dict.get(user, []))
    return [i for i in popular if i not in seen][:k]

print("热门推荐:", evaluate(rec_popular, train_dict, test_dict))

# ---------- T3: Baseline 2 —— ItemCF ----------
# 👉 把第 4 节的 ItemCF 类复制过来,fit(train_dict),然后:
# print("ItemCF:", evaluate(lambda u,k: [i for i,_ in itemcf.recommend(train_dict[u], k)],
#                           train_dict, test_dict))

✅ 通关标准

🎁 加分项


🥈 项目二:深度召回 + 排序两阶段(2–3 天)

目标:搭一个有召回有排序的完整两阶段系统。这是工业系统的最小可用形态。

产出:一个能输入 user_id、输出 top-10 的系统 + 分阶段的评估报告。

任务清单

🔑 关键实现要点

# ---------- 召回负采样:必须全库随机采,不是"曝光未点击" ----------
# 见第 8 节 in_batch_softmax_loss

# ---------- Faiss 索引 ----------
import faiss
item_vecs = model.encode_item(all_item_ids).detach().numpy().astype('float32')
faiss.normalize_L2(item_vecs)
index = faiss.IndexFlatIP(item_vecs.shape[1])   # 数据量小,暴力就行
index.add(item_vecs)

def retrieve(user_id, k=200):
    uv = model.encode_user(torch.tensor([user_id])).detach().numpy().astype('float32')
    faiss.normalize_L2(uv)
    _, ids = index.search(uv, k)
    return ids[0]

# ---------- 排序阶段的负样本:从召回结果里采 ----------
# ⭐ 这一点很重要:排序模型的负样本分布要接近它线上面对的分布(召回结果)
def build_rank_samples(user, pos_items, retrieved, n_neg=4):
    samples = [(user, i, 1) for i in pos_items]
    negs = [i for i in retrieved if i not in pos_items]
    samples += [(user, i, 0) for i in np.random.choice(negs, min(len(negs), len(pos_items)*n_neg), replace=False)]
    return samples

✅ 通关标准

🎁 加分项


🥇 项目三:端到端可运行的推荐服务(3–5 天)

目标:不只是 Notebook,是一个能被 HTTP 调用的服务,带监控和 A/B 分流。

产出:一个 GitHub 仓库,README 里有架构图和实验结果。这是能写进简历的东西。

任务清单

🔨 服务骨架

from fastapi import FastAPI
import hashlib, time, logging

app = FastAPI()
log = logging.getLogger("rec")

def assign_group(user_id: int, exp_id: str = "exp_001") -> str:
    """A/B 分流:哈希要带 exp_id,否则多个实验会用同一个分桶"""
    h = int(hashlib.md5(f"{exp_id}_{user_id}".encode()).hexdigest(), 16)
    return "treatment" if h % 100 < 50 else "control"

@app.get("/recommend")
def recommend(user_id: int, n: int = 10):
    t0 = time.time()
    group = assign_group(user_id)

    try:
        # 1. 特征
        user_feat = feature_store.get_user(user_id)

        # 2. 召回(多路)
        cands = set()
        cands |= set(vector_recall(user_feat, k=200))
        cands |= set(itemcf_recall(user_feat, k=100))
        cands |= set(hot_recall(k=50))
        cands -= set(user_feat["history"])          # 去重
        cands = list(cands)

        # 3. 排序(A/B:两组用不同模型)
        model = model_v2 if group == "treatment" else model_v1
        scored = model.predict_batch(user_id, cands)

        # 4. 重排:打散 + 截断
        result = rerank(scored, n=n)

    except Exception as e:
        log.exception("rec failed, fallback to hot")
        result = HOT_LIST[:n]                       # 🛟 兜底,绝不返回空
        group = "fallback"

    latency = (time.time() - t0) * 1000
    # 5. 埋点(生产里应该异步写 Kafka)
    log.info({"user_id": user_id, "group": group, "items": result,
              "latency_ms": round(latency, 2),
              "avg_score": float(np.mean([s for _, s in scored])) if result else None})
    return {"user_id": user_id, "items": result, "group": group,
            "latency_ms": round(latency, 2)}

@app.get("/health")
def health():
    return {"status": "ok"}

✅ 通关标准

🎁 加分项


📦 数据集选择

数据集 规模 适合 链接
MovieLens 100K/1M ⭐ 项目一,快速迭代 grouplens.org
MovieLens 32M 3200 万评分 项目二,练性能 ml-32m
Amazon Reviews 2023 5.7 亿评论,33 个类目 ⭐ 项目二/三,有丰富文本和元数据 HuggingFace
RetailRocket / Diginetica 会话推荐 Kaggle
Criteo / Avazu 4500 万 / 4000 万 CTR 预估、特征工程练习 Kaggle
Taobao UserBehavior 1 亿 序列推荐 阿里天池

💡 建议:项目一二用 MovieLens(快),项目三用 Amazon Reviews(有文本,能练冷启动和内容特征)。


🧰 可以直接用的库

用途
implicit ALS / BPR,快,工业级
RecBole 100+ 算法的统一框架,做对比实验神器 ⭐
LightFM 混合推荐(协同+内容),适合冷启动
Faiss / hnswlib 向量检索
torch-rechub / DeepCTR-Torch 深度推荐模型的现成实现
Merlin (NVIDIA) 端到端 GPU 推荐流水线
pip install implicit recbole lightfm faiss-cpu torch

⚠️ 别一上来就用 RecBole。先自己手写一遍 ItemCF 和 MF, 你需要知道框架里发生了什么,而不是调 API。框架是第二遍用的。


📝 项目 README 该写什么(简历用得上)

# 推荐系统实践

## 问题定义
数据集、规模、目标(Top-N 推荐 / CTR 预估)、评估指标

## 数据处理
- 稀疏度、长尾分布(放图)
- 切分策略:按时间切分,避免数据泄漏

## 方法
| 模型 | Recall@10 | NDCG@10 | 训练时间 |
|------|-----------|---------|---------|
| 热门推荐 | 0.052 | 0.031 | - |
| ItemCF | 0.118 | 0.076 | 12s |
| ALS | 0.145 | 0.094 | 45s |
| 双塔+排序 | 0.178 | 0.121 | 8min |

## 关键发现
- 加入热门惩罚 α=0.5 后 Coverage 从 12% → 31%,Recall 仅降 3%
- 召回负样本从"曝光未点击"改成"全库随机",Recall@200 提升 XX%

## 踩过的坑
1. 一开始随机切分数据 → Recall 虚高 3 倍,改成按时间切分后正常
2. ...

## 如何运行
...

🔑 「踩过的坑」这一节最加分。它证明你真的做过,而不是抄的教程。


🔗 这一章连到哪里

去哪为什么
ML 基础 05项目一要先造尺子:数据切分、验证集怎么留、指标为什么会骗你,源头在这里
上线之后 02项目三做完 A/B 之后必读:离线 NDCG 涨了线上却不涨,是这份教程的主线问题
Kaggle 25「先跑通最烂的版本再逐环替换」不是本教程的偏方,是竞赛圈打磨出来的标准 pipeline 打法

✅ 最终检查点

做完三个项目,你应该能回答:

  1. 为什么你的数据集要按时间切分?
  2. 召回和排序的负样本采样方式为什么不同?
  3. 你的系统在什么情况下会降级?降级到什么?
  4. 如果 Recall@200 只有 0.1,你会怎么排查?
  5. 你怎么知道模型 A 真的比模型 B 好?

👀 答案
  1. 因为随机切分会时间穿越,让模型见到未来信息,离线虚高、线上崩掉。真实场景永远是用过去预测未来。
  2. 因为两个阶段面对的候选分布不同精排面对的是召回给的几百个候选,所以用「曝光未点击」当负样本;召回面对的是全库,必须全库随机采样。⭐ 训练分布要和线上实际面对的分布一致——用曝光未点击训召回,模型只学会了精排级别的细微区分,从全库里挑不出东西。
  3. 典型降级链路:精排超时 → 退回粗排结果;召回某一路挂了 → 其余路照常;全部挂了 → 兜底热门榜 / 编辑精选。⭐ 原则是每一层都要有比它更简单、更不容易挂的下一层,最终兜底必须是不依赖模型的静态列表
  4. 从便宜到贵的顺序排查:①候选池里到底有没有正样本(可能根本没进池子)②各路召回分别的 Recall 是多少(定位是哪一路的问题)③索引/向量是不是过期了④负采样和线上分布是否一致⑤最后才怀疑模型本身
  5. 看 A/B 实验,而且要看置信区间。⚠️ 光看均值不够——提升必须超过随机波动才算数。还要确认:分流是否均匀、样本量是否达到最小可检测效应的要求、观察期是否覆盖了完整的周期(至少一周,避开新奇效应)。⭐ 离线指标涨、线上不涨是常态,以线上为准

🛑 完成了?

你现在的水平已经超过了大部分只看不做的人。

想继续拉开差距?教程还有三个高难度挑战项目,每个都对准一个用普通数据集练不到的核心矛盾:

或者直接去 👉 17-面试与求职.md


✅ 检查点

  1. 三个项目分别练的是哪几节的内容?为什么要按这个顺序做?
  2. 做项目时最容易犯的错是什么?
  3. 为什么每一步都要和 baseline 比,而不是只看最终分数?
  4. 评估推荐系统时,为什么不能只看准确率类指标?
  5. 项目 README 里最有价值的一节是什么?
👀 答案
  1. ①协同过滤 + 评估(第 3–5、12 节)②工业漏斗 + 特征工程(第 6–8 节)③冷启动 + 多样性(第 11、13 节)。按这个顺序是因为后面的项目要用到前面的产出——评估框架建好了,后面每个改动才有尺子。
  2. 一上来就搭复杂模型。正确做法是先跑通最烂的版本(哪怕是热门推荐),拿到一条能从数据跑到指标的完整链路,再逐步替换其中一环。
  3. 因为没有 baseline 的分数没有意义——0.35 的 Recall@10 是好是坏,只有和「热门推荐能到多少」对比才知道。而且逐步对比才能定位是哪个改动带来的提升。
  4. 因为准确率高的推荐可能只是「全推热门」——它讨好了指标却损害了体验。必须同时看覆盖率、多样性、新颖度(第 11 节),才知道你是不是在养信息茧房。
  5. 「踩过的坑」。分数说明你会调,坑说明你懂原理——面试时后者有说服力得多。

⚡ 走神救援

走神救援
三个递进项目:①协同过滤 + 评估框架(先把尺子造出来,后面每个改动才有意义)②工业漏斗 + 特征工程(召回→排序,体会分层的必要性)③冷启动 + 多样性(第 11、13 节的落地)。⭐ 铁律:先跑通最烂的版本(哪怕是热门推荐),拿到完整链路再逐环替换;每个改动都要和 baseline 比(0.35 的 Recall 是好是坏,只有对比才知道);别只看准确率——高准确率可能只是「全推热门」,必须同时看覆盖率/多样性/新颖度。README 里「踩过的坑」最值钱:分数说明你会调,坑说明你懂原理。三个项目的通关判据,照着自查就行:① ItemCF 的 Recall@10 要明显高于热门推荐,ALS 又要高于 ItemCF——没做到先别怀疑算法,去查 alpha(置信度)和 factors(维度);② 召回 Recall@200 要 > 0.3(做得好能到 0.5+),端到端 NDCG@10 必须明显高于「只取召回结果的前 10 个」,否则你的排序层等于白写;③ 服务 P99 < 100ms,而且把模型服务杀掉,接口还得能返回热门兜底——这条才是项目三真正在考的东西,还要能从日志里统计出 A/B 两组的指标差异。⚠️ 最容易翻车的两个地方:随机切分数据会让 Recall 虚高三倍,一律按时间切;以及召回的负样本和排序的负样本采样方式不一样(召回要从全库随机采,模拟的是「和全库比」;排序只能在召回结果里采,那才是它线上真正见到的分布)——这是面试高频题,也是自己做项目时最常搞混的一步。

打卡记录保存在你的浏览器里,首页能看到总进度