跳转至

推荐系统实战

概述

推荐系统是AI最成功的商业应用之一,本教程从零构建推荐系统,涵盖协同过滤、深度学习等方法。


推荐系统类型

# 主流推荐算法

1. 基于内容(Content-based)
   - 推荐相似物品
   - 适合冷启动

2. 协同过滤(Collaborative Filtering)
   - 用户-物品交互
   - 用户协同过滤
   - 物品协同过滤

3. 深度学习
   - 神经网络推荐
   - 序列推荐

4. 混合推荐
   - 结合多种方法

协同过滤

基于用户的协同过滤

import numpy as np
from sklearn.metrics.pairwise import cosine_similarity

class UserCF:
    def __init__(self, n_neighbors=10):
        self.n_neighbors = n_neighbors

    def fit(self, ratings_matrix):
        """ratings_matrix: 用户-物品评分矩阵"""
        self.ratings = ratings_matrix
        self.user_similarity = cosine_similarity(ratings_matrix)

    def predict(self, user_id, item_id):
        # 找到相似用户
        sim_scores = self.user_similarity[user_id]
        top_users = np.argsort(sim_scores)[-self.n_neighbors:]

        # 加权平均
        weighted_sum = 0
        sim_sum = 0
        for u in top_users:
            if u != user_id and self.ratings[u, item_id] > 0:
                weighted_sum += sim_scores[u] * self.ratings[u, item_id]
                sim_sum += abs(sim_scores[u])

        if sim_sum == 0:
            return np.mean(self.ratings[user_id][self.ratings[user_id] > 0])

        return weighted_sum / sim_sum

    def recommend(self, user_id, n_items=10):
        # 预测未评分物品
        unrated = np.where(self.ratings[user_id] == 0)[0]
        predictions = [(item, self.predict(user_id, item)) for item in unrated]
        predictions.sort(key=lambda x: x[1], reverse=True)
        return predictions[:n_items]

基于物品的协同过滤

class ItemCF:
    def __init__(self, n_neighbors=10):
        self.n_neighbors = n_neighbors

    def fit(self, ratings_matrix):
        self.ratings = ratings_matrix
        self.item_similarity = cosine_similarity(ratings_matrix.T)

    def predict(self, user_id, item_id):
        # 用户评过的物品
        rated_items = np.where(self.ratings[user_id] > 0)[0]

        weighted_sum = 0
        sim_sum = 0
        for i in rated_items:
            sim = self.item_similarity[item_id, i]
            weighted_sum += sim * self.ratings[user_id, i]
            sim_sum += abs(sim)

        if sim_sum == 0:
            return 0

        return weighted_sum / sim_sum

矩阵分解(SVD)

class MatrixFactorization:
    def __init__(self, n_factors=50, lr=0.01, reg=0.02, epochs=100):
        self.n_factors = n_factors
        self.lr = lr
        self.reg = reg
        self.epochs = epochs

    def fit(self, ratings_matrix):
        n_users, n_items = ratings_matrix.shape

        # 初始化用户和物品矩阵
        self.P = np.random.normal(0, 0.1, (n_users, self.n_factors))
        self.Q = np.random.normal(0, 0.1, (n_items, self.n_factors))

        # 记录非零位置
        self.samples = [(i, j, ratings_matrix[i, j]) 
                       for i in range(n_users) 
                       for j in range(n_items) 
                       if ratings_matrix[i, j] > 0]

        # 训练
        for _ in range(self.epochs):
            np.random.shuffle(self.samples)
            total_loss = 0

            for user, item, rating in self.samples:
                # 预测
                pred = self.P[user] @ self.Q[item]
                error = rating - pred

                # 更新
                self.P[user] += self.lr * (error * self.Q[item] - self.reg * self.P[user])
                self.Q[item] += self.lr * (error * self.P[user] - self.reg * self.Q[item])

                total_loss += error ** 2

    def predict(self, user_id, item_id):
        return self.P[user_id] @ self.Q[item_id]

    def recommend(self, user_id, n_items=10):
        scores = self.P[user_id] @ self.Q.T
        top_items = np.argsort(scores)[-n_items:][::-1]
        return [(item, scores[item]) for item in top_items]

深度学习推荐

NCF(神经协同过滤)

import torch
import torch.nn as nn

class NCF(nn.Module):
    def __init__(self, n_users, n_items, embed_dim=64, hidden_dims=[128, 64, 32]):
        super().__init__()

        # 嵌入层
        self.user_embed = nn.Embedding(n_users, embed_dim)
        self.item_embed = nn.Embedding(n_items, embed_dim)

        # MLP层
        mlp_layers = []
        input_dim = embed_dim * 2
        for dim in hidden_dims:
            mlp_layers.extend([
                nn.Linear(input_dim, dim),
                nn.ReLU(),
                nn.Dropout(0.2)
            ])
            input_dim = dim
        self.mlp = nn.Sequential(*mlp_layers)

        # 输出层
        self.output = nn.Linear(hidden_dims[-1], 1)

    def forward(self, user_ids, item_ids):
        user_embed = self.user_embed(user_ids)
        item_embed = self.item_embed(item_ids)

        # 拼接
        x = torch.cat([user_embed, item_embed], dim=1)

        # MLP
        x = self.mlp(x)

        # 输出
        return torch.sigmoid(self.output(x)).squeeze()

双塔模型

class TwoTower(nn.Module):
    def __init__(self, n_users, n_items, embed_dim=64):
        super().__init__()

        # 用户塔
        self.user_tower = nn.Sequential(
            nn.Embedding(n_users, embed_dim),
            nn.Linear(embed_dim, 128),
            nn.ReLU(),
            nn.Linear(128, 64)
        )

        # 物品塔
        self.item_tower = nn.Sequential(
            nn.Embedding(n_items, embed_dim),
            nn.Linear(embed_dim, 128),
            nn.ReLU(),
            nn.Linear(128, 64)
        )

    def forward(self, user_ids, item_ids):
        user_vec = self.user_tower(user_ids)
        item_vec = self.item_tower(item_ids)

        # 内积作为相似度
        return (user_vec * item_vec).sum(dim=1)

    def get_user_embedding(self, user_ids):
        return self.user_tower(user_ids)

    def get_item_embedding(self, item_ids):
        return self.item_tower(item_ids)

实战:电影推荐系统

数据准备

import pandas as pd
from sklearn.model_selection import train_test_split

# 加载MovieLens数据
ratings = pd.read_csv('ratings.csv')
movies = pd.read_csv('movies.csv')

# 构建用户-物品矩阵
n_users = ratings['userId'].nunique()
n_items = ratings['movieId'].nunique()

# 创建映射
user_map = {id: i for i, id in enumerate(ratings['userId'].unique())}
item_map = {id: i for i, id in enumerate(ratings['movieId'].unique())}

# 训练测试集
train_data, test_data = train_test_split(ratings, test_size=0.2, random_state=42)

训练模型

from torch.utils.data import Dataset, DataLoader

class RatingsDataset(Dataset):
    def __init__(self, users, items, ratings):
        self.users = torch.LongTensor(users)
        self.items = torch.LongTensor(items)
        self.ratings = torch.FloatTensor(ratings)

    def __len__(self):
        return len(self.ratings)

    def __getitem__(self, idx):
        return self.users[idx], self.items[idx], self.ratings[idx]

# 创建数据集
train_dataset = RatingsDataset(
    train_data['userId'].map(user_map),
    train_data['movieId'].map(item_map),
    train_data['rating']
)

train_loader = DataLoader(train_dataset, batch_size=256, shuffle=True)

# 训练
model = NCF(n_users, n_items)
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
criterion = nn.MSELoss()

for epoch in range(10):
    model.train()
    total_loss = 0
    for users, items, ratings in train_loader:
        pred = model(users, items)
        loss = criterion(pred, ratings)

        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        total_loss += loss.item()

    print(f"Epoch {epoch+1}, Loss: {total_loss/len(train_loader):.4f}")

评估指标

from sklearn.metrics import mean_squared_error, mean_absolute_error
import numpy as np

def evaluate(model, test_data, user_map, item_map):
    model.eval()
    with torch.no_grad():
        users = torch.LongTensor(test_data['userId'].map(user_map))
        items = torch.LongTensor(test_data['movieId'].map(item_map))
        true_ratings = test_data['rating'].values

        pred_ratings = model(users, items).numpy()

        rmse = np.sqrt(mean_squared_error(true_ratings, pred_ratings))
        mae = mean_absolute_error(true_ratings, pred_ratings)

    return rmse, mae

rmse, mae = evaluate(model, test_data, user_map, item_map)
print(f"RMSE: {rmse:.4f}, MAE: {mae:.4f}")

冷启动问题

# 解决方案

新用户冷启动
- 基于内容推荐
- 热门推荐
- 收集用户兴趣

新物品冷启动
- 内容特征
- 探索性推荐
- 利用元数据

下一步学习

  1. 深度学习教程 - 理解模型原理
  2. 时间序列预测 - 推荐系统中的时序建模
  3. MLOps实践 - 推荐系统部署