推荐系统:协同过滤、深度学习与多臂老虎机
推荐系统是电商、内容平台的核心技术,从用户历史行为预测偏好。
推荐任务分类
1. 评分预测(Rating Prediction)
预测用户对未评分物品的评分(如 Netflix Prize)。
评估:RMSE, MAE
2. 点击率预测(CTR Prediction)
预测用户点击广告/推荐物品的概率。
评估:AUC, LogLoss
3. 排序(Ranking)
生成物品列表,按相关度排序。
评估:NDCG, MAP, MRR
4. 多样性与新颖性
不仅准确,还要覆盖不同类别、长尾物品。
经典方法
1. 协同过滤(Collaborative Filtering)
核心思想:相似用户喜欢相似物品。
用户基(User-Based CF)
用户 A 喜欢物品 [1, 2, 3]
找到与 A 最相似的用户 B、C
B 喜欢物品 4,C 喜欢物品 5
→ 推荐物品 4、5 给 A相似度计算:
- 余弦相似度
- Pearson 相关系数
- Jaccard 相似度(隐式反馈)
物品基(Item-Based CF)
用户 A 喜欢物品 1
物品 1 与物品 2、3 相似(喜欢 1 的人也喜欢 2、3)
→ 推荐物品 2、3 给 A优点:可解释性强,不依赖物品内容。 缺点:冷启动问题(新用户/新物品无历史数据)。
代码示例(Surprise 库):
python
from surprise import Dataset, KNNBasic
from surprise.model_selection import cross_validate
# 加载 MovieLens 数据集
data = Dataset.load_builtin('ml-100k')
# 使用 User-Based CF
algo = KNNBasic(sim_options={'user_based': True})
# 交叉验证
cross_validate(algo, data, measures=['RMSE', 'MAE'], cv=5, verbose=True)2. 矩阵分解(Matrix Factorization)
思想:用户-物品评分矩阵分解为低维隐向量。
R (m×n) ≈ U (m×k) × V (n×k)^T其中:
U[i]:用户 i 的隐向量(兴趣偏好)V[j]:物品 j 的隐向量(属性特征)k:隐向量维度(通常 10-200)
SVD(奇异值分解):
python
import numpy as np
from scipy.sparse.linalg import svds
# R 是 m×n 评分矩阵(缺失值填 0)
U, sigma, Vt = svds(R, k=50)
pred_R = np.dot(U, np.dot(np.diag(sigma), Vt))Funk-SVD(带正则化的 SGD):
python
import torch
def matrix_factorization(R, k=50, lr=0.01, reg=0.1, epochs=100):
m, n = R.shape
U = torch.randn(m, k, requires_grad=True)
V = torch.randn(n, k, requires_grad=True)
optimizer = torch.optim.Adam([U, V], lr=lr)
for epoch in range(epochs):
optimizer.zero_grad()
# 预测评分
pred = torch.matmul(U, V.T)
# 只计算有评分的损失
mask = R > 0
loss = torch.mean((pred[mask] - R[mask])**2)
# L2 正则化
loss += reg * (torch.norm(U) + torch.norm(V))
loss.backward()
optimizer.step()
return U.detach(), V.detach()深度学习推荐
1. Wide & Deep(Google 2016)
Wide 部分:记忆(memorization),捕捉直接关联
- 输入:原始特征 + 交叉特征(如 "年龄=25 AND 性别=女")
- 输出:线性模型
Deep 部分:泛化(generalization),学习复杂模式
- 输入:嵌入层(类别特征) + 连续特征
- 输出:DNN
python
import torch.nn as nn
class WideAndDeep(nn.Module):
def __init__(self, wide_dim, deep_dims, embed_dims):
super().__init__()
# Wide 部分:线性
self.wide = nn.Linear(wide_dim, 1)
# Deep 部分:Embedding + MLP
self.embeddings = nn.ModuleList([
nn.Embedding(num_embeddings=vocab_size, embedding_dim=dim)
for vocab_size, dim in embed_dims
])
deep_input_dim = sum(dim for _, dim in embed_dims) + continuous_dim
layers = []
for dim in deep_dims:
layers.append(nn.Linear(deep_input_dim, dim))
layers.append(nn.ReLU())
deep_input_dim = dim
self.deep = nn.Sequential(*layers)
self.deep_output = nn.Linear(deep_input_dim, 1)
def forward(self, wide_input, deep_cat_inputs, deep_cont_input):
wide_out = self.wide(wide_input)
# Embedding
embeddings = [emb(deep_cat_inputs[:, i]) for i, emb in enumerate(self.embeddings)]
deep_in = torch.cat(embeddings + [deep_cont_input], dim=1)
deep_out = self.deep(deep_in)
deep_out = self.deep_output(deep_out)
return torch.sigmoid(wide_out + deep_out) # CTR 预测2. DeepFM(Huawei 2017)
Wide & Deep 的改进:
- FM(Factorization Machine) 替代 Wide 线性部分
- FM 自动学习二阶特征交叉,无需人工特征工程
python
from deepctr.models import DeepFM
from deepctr.feature_column import SparseFeat, get_feature_names
# 定义特征列
feature_columns = [
SparseFeat('user_id', vocabulary_size=10000, embedding_dim=8),
SparseFeat('item_id', vocabulary_size=5000, embedding_dim=8),
SparseFeat('category', vocabulary_size=50, embedding_dim=4),
DenseFeat('price', dimension=1),
]
model = DeepFM(linear_feature_columns=feature_columns, dnn_feature_columns=feature_columns)3. DIN / DIEN(Alibaba)
DIN (Deep Interest Network):
- 引入 Attention 机制,根据目标商品自适应激活用户历史兴趣
- 适合电商(用户兴趣多样,不同商品关注不同兴趣)
DIEN (Deep Interest Evolution Network):
- 序列建模(GRU)捕获兴趣演化
- 两层 Attention:AUGRU(Attention 更新 GRU)
序列推荐
Transformer-based
BERT4Rec:用双向 Transformer 编码用户行为序列
python
# 简化的 BERT4Rec
class TransformerBlock(nn.Module):
def __init__(self, embed_dim, num_heads, ff_dim, dropout=0.1):
super().__init__()
self.attn = nn.MultiheadAttention(embed_dim, num_heads, dropout=dropout)
self.norm1 = nn.LayerNorm(embed_dim)
self.ff = nn.Sequential(
nn.Linear(embed_dim, ff_dim),
nn.ReLU(),
nn.Linear(ff_dim, embed_dim)
)
self.norm2 = nn.LayerNorm(embed_dim)
def forward(self, x, mask=None):
attn_out, _ = self.attn(x, x, x, attn_mask=mask)
x = self.norm1(x + attn_out)
ff_out = self.ff(x)
x = self.norm2(x + ff_out)
return x
class BERT4Rec(nn.Module):
def __init__(self, num_items, embed_dim=64, num_layers=2, num_heads=4):
super().__init__()
self.item_embed = nn.Embedding(num_items, embed_dim)
self.pos_embed = nn.Embedding(200, embed_dim) # 最大序列长度
self.transformer = nn.ModuleList([
TransformerBlock(embed_dim, num_heads, embed_dim*4)
for _ in range(num_layers)
])
self.fc = nn.Linear(embed_dim, num_items)
def forward(self, item_seq):
batch, seq_len = item_seq.shape
pos = torch.arange(seq_len).unsqueeze(0).expand(batch, -1)
x = self.item_embed(item_seq) + self.pos_embed(pos)
for layer in self.transformer:
x = layer(x.transpose(0, 1)).transpose(0, 1)
# 预测下一个
logits = self.fc(x)
return logits训练目标:Masked Item Prediction(类似 BERT MLM)
多臂老虎机(Multi-Armed Bandit)
用于**探索与利用(Exploration vs Exploitation)**平衡:
- Exploit:推荐当前最优(已知偏好)
- Explore:尝试新物品,发现新偏好
算法
1. ε-Greedy
以概率 ε 随机探索,1-ε 选择最优:
python
class EpsilonGreedy:
def __init__(self, num_arms, epsilon=0.1):
self.means = np.zeros(num_arms)
self.counts = np.zeros(num_arms)
self.epsilon = epsilon
def select(self):
if np.random.random() < self.epsilon:
return np.random.randint(len(self.means))
else:
return np.argmax(self.means)
def update(self, arm, reward):
self.counts[arm] += 1
self.means[arm] += (reward - self.means[arm]) / self.counts[arm]2. UCB(Upper Confidence Bound)
python
class UCB:
def __init__(self, num_arms, c=2):
self.means = np.zeros(num_arms)
self.counts = np.zeros(num_arms)
self.total = 0
self.c = c
def select(self):
self.total += 1
ucb = self.means + self.c * np.sqrt(np.log(self.total) / (self.counts + 1e-10))
return np.argmax(ucb)
def update(self, arm, reward):
self.counts[arm] += 1
self.means[arm] += (reward - self.means[arm]) / self.counts[arm]3. Thompson Sampling
Beta-Bernoulli 模型,采样选择:
python
class ThompsonSampling:
def __init__(self, num_arms):
self.alphas = np.ones(num_arms) # 成功次数 + 1
self.betas = np.ones(num_arms) # 失败次数 + 1
def select(self):
samples = np.random.beta(self.alphas, self.betas)
return np.argmax(samples)
def update(self, arm, reward):
if reward == 1:
self.alphas[arm] += 1
else:
self.betas[arm] += 1应用:新闻推荐、广告投放、探索冷启动物品。
实战:电商推荐系统
特征工程
| 特征类型 | 示例 | 处理方式 |
|---|---|---|
| 用户人口学 | 年龄、性别、城市 | One-Hot / Embedding |
| 物品属性 | 类别、品牌、价格 | One-Hot / Normalization |
| 行为序列 | 点击、购买、收藏 | Embedding + RNN/Transformer |
| 上下文 | 时间、位置、设备 | Category / Bucket |
| 交叉特征 | 年龄×品类 | FM / DNN 自动学习 |
完整流程
python
import torch
import torch.nn as nn
from torch.utils.data import Dataset, DataLoader
class RecommendationDataset(Dataset):
def __init__(self, user_ids, item_ids, labels, user_features, item_features):
self.user_ids = user_ids
self.item_ids = item_ids
self.labels = labels
self.user_features = user_features
self.item_features = item_features
def __len__(self):
return len(self.user_ids)
def __getitem__(self, idx):
return {
'user_id': self.user_ids[idx],
'item_id': self.item_ids[idx],
'user_feat': self.user_features[idx],
'item_feat': self.item_features[idx],
'label': self.labels[idx]
}
class TwoTower(nn.Module):
"""双塔模型:用户塔 + 物品塔"""
def __init__(self, num_users, num_items, user_feat_dim, item_feat_dim, embed_dim=64):
super().__init__()
self.user_embed = nn.Embedding(num_users, embed_dim)
self.item_embed = nn.Embedding(num_items, embed_dim)
self.user_tower = nn.Sequential(
nn.Linear(embed_dim + user_feat_dim, 128),
nn.ReLU(),
nn.Linear(128, embed_dim)
)
self.item_tower = nn.Sequential(
nn.Linear(embed_dim + item_feat_dim, 128),
nn.ReLU(),
nn.Linear(128, embed_dim)
)
def forward(self, user_ids, item_ids, user_feats, item_feats):
u_emb = self.user_embed(user_ids)
i_emb = self.item_embed(item_ids)
u_concat = torch.cat([u_emb, user_feats], dim=1)
i_concat = torch.cat([i_emb, item_feats], dim=1)
user_vec = self.user_tower(u_concat)
item_vec = self.item_tower(i_concat)
# 余弦相似度
sim = torch.cosine_similarity(user_vec, item_vec, dim=1)
return torch.sigmoid(sim)
# 训练
model = TwoTower(num_users=100000, num_items=50000, user_feat_dim=10, item_feat_dim=8)
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
criterion = nn.BCELoss()
for epoch in range(10):
for batch in dataloader:
pred = model(batch['user_id'], batch['item_id'], batch['user_feat'], batch['item_feat'])
loss = criterion(pred, batch['label'].float())
loss.backward()
optimizer.step()
optimizer.zero_grad()线上服务
用户请求 → 召回(粗筛) → 排序(精排) → 重排(多样性、业务规则)召回阶段(万级候选 → 百级):
- Item-CF
- 向量召回(FAISS / Milvus)
- 双塔模型
排序阶段(百级 → 10 个):
- DeepFM / DIN / xDeepFM
- 多目标优化(点击率 + 转化率)
重排阶段:
- 多样性(MMR)
- 去重
- 业务规则(新品加权、库存)
评估指标
| 指标 | 公式 | 说明 |
|---|---|---|
| Precision@K | (推荐中正样本数) / K | 前 K 个有多少相关 |
| Recall@K | (推荐中正样本数) / 总正样本数 | 召回率 |
| MAP@K | Mean Average Precision | 考虑排序的精确率 |
| NDCG@K | 归一化折损累计增益 | 位置越前权重越高 |
| AUC | ROC 曲线下面积 | CTR 预测常用 |
| Coverage | 推荐物品数 / 总物品数 | 覆盖率,避免信息茧房 |
NDCG 计算:
DCG@K = Σ_{i=1}^K (2^{rel_i} - 1) / log2(i+1)
NDCG@K = DCG@K / IDCG@K (IDCG 是理想排序的 DCG)挑战与对策
1. 冷启动
- 新用户:热门推荐、注册信息(人口学)→ 快速兴趣捕捉
- 新物品:内容特征(文本、图像)、小流量探索(Bandit)
2. 数据稀疏
- 矩阵分解 + 内容特征(Hybrid)
- 图神经网络(GNN)利用用户-物品二部图结构
- 跨域迁移(用其他领域数据辅助)
3. 可扩展性
- 召回:向量检索(FAISS 支持十亿级)
- 排序:模型轻量化(蒸馏、量化)
- 在线:特征实时拼接(Redis)
4. 偏差问题
- 流行度偏差:热门物品过度推荐 → 去偏(逆倾向评分)
- 曝光偏差:用户只能看到历史推荐的 → 探索/利用平衡
开源工具
| 工具 | 类型 | 说明 |
|---|---|---|
| Surprise | 传统 CF | Python,简单易用 |
| LightFM | 混合推荐 | 支持内容特征 |
| Implicit | 隐式反馈 | 高效 ALS |
| TensorFlow Recommenders (TFRS) | 深度学习 | Google 官方 |
| DeepCTR | 深度学习 CTR | 包含 DeepFM, DIN 等 |
| PaddleRec | 全流程 | 百度,产业级 |
| RecBole | 研究框架 | 包含 70+ 算法 |
工业界实践
YouTube 推荐(2016)
三阶段:
- 候选集生成(Candidate Generation):从百万级视频选几百个(双塔 + 期望观看时长)
- 排序(Ranking):精细打分(DNN,数百特征)
- 重排(Re-ranking):多样性、新鲜度、作者多样性
阿里巴巴深度兴趣网络(DIN)
- 用户历史行为序列(数百个商品)
- Attention 机制根据当前候选商品激活相关历史
- 提升点击率 10%+(双十一)
入门建议
- 数据:MovieLens(1M/10M/20M)入门,Kaggle 有各种推荐比赛
- 算法:先掌握协同过滤 → 矩阵分解 → 深度学习(DeepFM)
- 框架:Surprise(传统),TFRS 或 DeepCTR(深度学习)
- 评估:不仅看 AUC,还要看多样性、新颖性
- 工业级:学习召回+排序+重排三阶段架构
推荐系统是数据、算法、工程的结合,建议多看工业界论文(RecSys 会议)。
