Skip to content

推荐系统:协同过滤、深度学习与多臂老虎机

推荐系统是电商、内容平台的核心技术,从用户历史行为预测偏好。

推荐任务分类

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@KMean Average Precision考虑排序的精确率
NDCG@K归一化折损累计增益位置越前权重越高
AUCROC 曲线下面积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传统 CFPython,简单易用
LightFM混合推荐支持内容特征
Implicit隐式反馈高效 ALS
TensorFlow Recommenders (TFRS)深度学习Google 官方
DeepCTR深度学习 CTR包含 DeepFM, DIN 等
PaddleRec全流程百度,产业级
RecBole研究框架包含 70+ 算法

工业界实践

YouTube 推荐(2016)

三阶段

  1. 候选集生成(Candidate Generation):从百万级视频选几百个(双塔 + 期望观看时长)
  2. 排序(Ranking):精细打分(DNN,数百特征)
  3. 重排(Re-ranking):多样性、新鲜度、作者多样性

阿里巴巴深度兴趣网络(DIN)

  • 用户历史行为序列(数百个商品)
  • Attention 机制根据当前候选商品激活相关历史
  • 提升点击率 10%+(双十一)

入门建议

  1. 数据:MovieLens(1M/10M/20M)入门,Kaggle 有各种推荐比赛
  2. 算法:先掌握协同过滤 → 矩阵分解 → 深度学习(DeepFM)
  3. 框架:Surprise(传统),TFRS 或 DeepCTR(深度学习)
  4. 评估:不仅看 AUC,还要看多样性、新颖性
  5. 工业级:学习召回+排序+重排三阶段架构

推荐系统是数据、算法、工程的结合,建议多看工业界论文(RecSys 会议)。