推荐系统第一步:用 implicit 库做协同过滤(不用任何深度模型)

起因

老板说"加个'你可能也喜欢' 推荐"。我们的数据:
- 用户 5 万
- 商品 1 万
- 用户-商品 浏览 / 购买记录 100 万条

不是 YouTube 级,深度学习 / DSSM / DeepFM 过于豪华。
传统协同过滤(implicit feedback ALS)几行代码就有 baseline,效果
比"按热度排序" 提升 30-50% CTR。下面是真实跑通流程。

解决方案

uv add implicit scipy pandas

implicit 库是 Ben Frederickson 写的 C++ ALS 实现,比 surprise /
spotlight 快 100x。

数据格式:sparse user-item matrix

import pandas as pd
from scipy.sparse import csr_matrix

events = pd.read_csv('user_events.csv')
# user_id,item_id,event_type
# 1234,567,view
# 1234,567,view
# 1234,789,purchase

# 隐式反馈权重:购买 > 加购 > 浏览
weights = {'view': 1, 'cart': 3, 'purchase': 10}
events['w'] = events['event_type'].map(weights)

# 聚合
agg = events.groupby(['user_id', 'item_id'])['w'].sum().reset_index()
# user_id,item_id,w
# 1234,567,2
# 1234,789,10

# 编码成连续整数索引
user_ids = agg['user_id'].unique()
item_ids = agg['item_id'].unique()
user_idx = {u: i for i, u in enumerate(user_ids)}
item_idx = {it: i for i, it in enumerate(item_ids)}

agg['ui'] = agg['user_id'].map(user_idx)
agg['ii'] = agg['item_id'].map(item_idx)

# sparse matrix
matrix = csr_matrix((agg['w'].values, (agg['ui'].values, agg['ii'].values)),
                    shape=(len(user_ids), len(item_ids)))
print(matrix.shape, matrix.nnz)   # (50000, 10000) 1000000

ALS 训练

from implicit.als import AlternatingLeastSquares

model = AlternatingLeastSquares(
    factors=64,          # embedding 维度
    regularization=0.01,
    iterations=20,
    alpha=40,            # implicit feedback 信心权重
    use_gpu=False,       # GPU 可选
)
model.fit(matrix)
# 几秒钟完成

alpha 是 implicit ALS 的关键超参:
- 表示"我们对'用户买了'比'用户没买' 的信心差多少
- 论文建议 alpha=40 是好起点
- 调参可以 GridSearch on 验证集

给一个用户推荐 top-K

def recommend(user_id, k=10):
    if user_id not in user_idx:
        return []   # 冷启动
    uid = user_idx[user_id]
    item_ids_arr, scores = model.recommend(uid, matrix[uid], N=k)
    return [(item_ids[i], float(s)) for i, s in zip(item_ids_arr, scores)]

print(recommend(user_id=1234, k=5))
# [(item_id, score), ...]

model.recommend() 内部:

  1. 拿用户 embedding (64 维)
  2. 跟所有 item embedding 算 dot product
  3. 排除已交互的 item
  4. 返回 top-K

毫秒级返回。

相似商品推荐("你看了这个,也可能看那个")

def similar_items(item_id, k=10):
    if item_id not in item_idx:
        return []
    iid = item_idx[item_id]
    item_ids_arr, scores = model.similar_items(iid, N=k+1)
    # 第一个是它自己,跳过
    return [(item_ids[i], float(s)) for i, s in zip(item_ids_arr[1:], scores[1:])]

商品详情页用:"看了这个的人还看了..."。

评估:split + 看 hit@K / MAP@K

import numpy as np

def hit_at_k(model, train_matrix, test_dict, k=10):
    """test_dict: {user_idx: set(item_idx)} 是 holdout 的真 interaction"""
    hits = 0
    total = 0
    for uid, true_items in test_dict.items():
        if uid >= train_matrix.shape[0]:
            continue
        recommended, _ = model.recommend(uid, train_matrix[uid], N=k)
        if any(it in true_items for it in recommended):
            hits += 1
        total += 1
    return hits / total

hit@10 = 0.20 意味着 20% 的用户 top-10 推荐里有真实喜欢的。
随机推荐 hit@10 ≈ k / num_items ≈ 0.001。

200x 提升 = 信号强 = 模型 work。

冷启动用户

新用户没历史 → 用户 embedding 不存在 → ALS 推不了。
fallback:

def recommend_with_fallback(user_id, k=10):
    if user_id in user_idx:
        return recommend(user_id, k)
    # 冷启动:推热门
    return popular_items_in_user_segment(user_id, k)

或者用 sign-up 时收集的偏好 / 人口学信息做内容-based 起步。

冷启动商品

新上架商品没 interaction → ALS 给不出 embedding。
解决:

  • 用商品 metadata(类别 / 标签 / 描述)映射到现有 embedding 空间
  • "two-tower" 模型:让 item tower 用 metadata 做 embedding

简单做法:新商品借用同类商品的 embedding 均值。

几个进阶 model

BM25 weighting

from implicit.nearest_neighbours import bm25_weight

weighted = bm25_weight(matrix, K1=100, B=0.8)
model = AlternatingLeastSquares(factors=64)
model.fit(weighted)

BM25 给罕见物品更高 weight,对长尾推荐更好。

Item-Item CF (用户少 + 商品多时)

from implicit.nearest_neighbours import CosineRecommender

model = CosineRecommender(K=10)
model.fit(matrix)

直接算 item-item 相似度,无 embedding 训练。
适合 100k+ items + 用户少场景。

BPR (Bayesian Personalized Ranking)

from implicit.bpr import BayesianPersonalizedRanking
model = BayesianPersonalizedRanking(factors=64, learning_rate=0.05)
model.fit(matrix)

learn-to-rank 风格,对"排序质量" 优化(不是 reconstruction)。
小数据集上经常更好。

部署到生产

# 训练完保存
import pickle
with open('als_model.pkl', 'wb') as f:
    pickle.dump({
        'model': model,
        'user_idx': user_idx,
        'item_idx': item_idx,
        'item_ids': item_ids,
    }, f)

# inference 服务
class RecAPI:
    def __init__(self, path):
        with open(path, 'rb') as f:
            d = pickle.load(f)
        self.model = d['model']
        self.user_idx = d['user_idx']
        self.item_ids = d['item_ids']
        # ...

    def recommend(self, user_id, k=10):
        uid = self.user_idx.get(user_id)
        if uid is None:
            return []
        item_arr, _ = self.model.recommend(uid, ...)
        return [self.item_ids[i] for i in item_arr]

FastAPI 包一下,端口暴露给业务。

每日重训

cron / Airflow:

@task
def retrain_als():
    events = load_recent_events(days=90)
    matrix = build_matrix(events)
    model = AlternatingLeastSquares(factors=64)
    model.fit(matrix)
    save_model(model, ...)
    # 通知 inference 服务 reload
    requests.post('http://rec-api/reload')

每天凌晨 1 点训。新 interaction 进入 embedding。

A/B test 验证

把 50% 用户走"推荐"接口,50% 走"按热度排"。
观察:

  • CTR(点击率)
  • conversion rate(转化率)
  • 用户 session 时长

通常协同过滤 baseline 比"按热度" CTR 提升 30-100%。

与深度模型对比

implicit ALS LightFM DeepFM YouTube DNN
数据量需求 中(万级用户) 极大
训练时间 秒-分钟 分钟 小时
冷启动支持 中(hybrid)
实施成本 极低 极高
效果上限 中等 中-高 极高

500 万以上交互 + 想榨干 5-10% CTR → 上深度模型。
否则 ALS 就够。

效果

我们小型电商上线 implicit ALS:

  • 数据:5w 用户 + 1w SKU + 100w events
  • 训练:30 秒(CPU)
  • 推理:< 5ms / user
  • A/B test:CTR +47%,conversion +18%
  • vs 之前的"按销量排序"基线,显著改善

后续上 LightFM hybrid(加 metadata)再 +12%。
深度学习准备阶段,但 ALS 已经 cover 大头 ROI。

踩过的坑

  1. matrix[uid] 而不是 matrix.getrow(uid):implicit 0.7 后 API 改了,
    recommend(user, user_items) 要传整行 sparse vector。

  2. 数据严重 popularity bias:只看热门,ALS embedding 也偏热门。
    BM25 weight 缓解。

  3. 训练 / 推理使用不同 user_idx 映射 → 错位。永远 pickle 整套
    {model, user_idx, item_idx, item_ids}。

  4. 新用户进来推不了:冷启动 fallback 一定要有,不然推荐 API
    返空。

  5. A/B test 早期看 metric 没差异:用户没注意新推荐位 / 流量太小
    = 0 statistical power。至少 1 周 + 万级用户。

精确评价 共 0 人评价
可复现性
可复现 · 0 不可复现 · 0
文风
文风流畅 · 0 文风晦涩 · 0
立场
支持 · 0 反对 · 0

登录后即可对本帖作出评价。

评论区 0 条 · 所有人可在此交流

登录后参与评论。

还没有评论,来说两句。