
基于Python的推荐系统开发:从协同过滤到深度学习模型的实战之旅
大家好,作为一名在数据领域摸爬滚打多年的开发者,我始终觉得推荐系统是机器学习最迷人、最接地气的应用之一。今天,我想和大家分享一次完整的实战经历,聊聊如何用Python搭建一个从经典协同过滤到神经网络的推荐系统。过程中有“原来如此”的顿悟,也有不少需要留神的“坑”,我会一一道来。
一、环境搭建与数据准备:万事开头“细”
工欲善其事,必先利其器。我习惯使用Anaconda创建独立的虚拟环境,避免包版本冲突。我们这次会用到一些核心库。
# 创建并激活环境
conda create -n recsys python=3.9
conda activate recsys
# 安装核心库
pip install pandas numpy scikit-learn
pip install scipy # 稀疏矩阵运算
pip install tensorflow # 用于深度学习模型(也可用pytorch)
# 如果你觉得TensorFlow太重,可以试试轻量级的推荐专用库Surprise
pip install surprise
数据方面,我们使用经典的MovieLens 100K数据集,它包含了用户对电影的评分。实战中,第一个坑就是数据理解。别急着建模,先花时间看看数据分布。
import pandas as pd
# 加载数据
ratings = pd.read_csv('u.data', sep='t', names=['user_id', 'item_id', 'rating', 'timestamp'])
movies = pd.read_csv('u.item', sep='|', encoding='latin-1', names=['item_id', 'title'])
print(f"评分记录数: {len(ratings)}")
print(f"用户数: {ratings['user_id'].nunique()}")
print(f"电影数: {ratings['item_id'].nunique()}")
print(ratings['rating'].describe())
# 检查稀疏度:这是一个关键指标!
sparsity = 1.0 - len(ratings) / (ratings['user_id'].nunique() * ratings['item_id'].nunique())
print(f"评分矩阵稀疏度: {sparsity:.4%}")
你会发现稀疏度非常高(>95%),这意味着我们的算法必须能很好地处理“缺失”。
二、经典协同过滤实战:记忆与泛化的权衡
协同过滤(CF)是推荐系统的基石,主要分两类:基于记忆(用户/物品协同)和基于模型(矩阵分解)。
1. 基于用户的协同过滤(User-CF)
核心思想:找到和你兴趣相似的用户,把他们喜欢的东西推荐给你。计算用户相似度是关键,常用余弦相似度或皮尔逊相关系数。踩坑提示:直接计算全用户相似度矩阵(用户数×用户数)在用户量大时内存会爆炸,通常需要采样或使用稀疏矩阵技巧。
from scipy.sparse import csr_matrix
from sklearn.metrics.pairwise import cosine_similarity
import numpy as np
# 构建用户-物品评分稀疏矩阵
def create_matrix(df):
N = df['user_id'].nunique()
M = df['item_id'].nunique()
user_mapper = dict(zip(np.unique(df["user_id"]), list(range(N))))
item_mapper = dict(zip(np.unique(df["item_id"]), list(range(M))))
user_inv_mapper = {v: k for k, v in user_mapper.items()}
item_inv_mapper = {v: k for k, v in item_mapper.items()}
user_index = [user_mapper[i] for i in df['user_id']]
item_index = [item_mapper[i] for i in df['item_id']]
X = csr_matrix((df["rating"], (user_index, item_index)), shape=(N, M))
return X, user_mapper, item_mapper, user_inv_mapper, item_inv_mapper
X, user_mapper, item_mapper, user_inv_mapper, item_inv_mapper = create_matrix(ratings)
# 计算用户相似度矩阵(这里计算Top-K相似用户,避免全矩阵)
user_similarity = cosine_similarity(X, dense_output=False) # 保持稀疏
print("用户相似度矩阵形状:", user_similarity.shape)
2. 基于模型的协同过滤:矩阵分解(MF)
这是更高级的CF,通过将大矩阵分解为用户隐因子矩阵和物品隐因子矩阵的乘积来学习潜在特征。我用surprise库快速实现SVD。
from surprise import SVD, Dataset, Reader, accuracy
from surprise.model_selection import train_test_split
# 定义数据格式
reader = Reader(rating_scale=(1, 5))
data = Dataset.load_from_df(ratings[['user_id', 'item_id', 'rating']], reader)
# 划分训练集和测试集
trainset, testset = train_test_split(data, test_size=0.2, random_state=42)
# 训练SVD模型
model = SVD(n_factors=50, n_epochs=20, lr_all=0.005, reg_all=0.02)
model.fit(trainset)
# 预测并评估
predictions = model.test(testset)
print(f"RMSE: {accuracy.rmse(predictions)}")
print(f"MAE: {accuracy.mae(predictions)}")
# 为用户推荐Top-N物品
def get_top_n(predictions, user_id, n=10):
user_predictions = [pred for pred in predictions if pred.uid == user_id]
user_predictions.sort(key=lambda x: x.est, reverse=True)
top_n = user_predictions[:n]
return [(iid, est) for (_, iid, _, est, _) in top_n]
# 获取测试集中某个用户的推荐
test_user_id = str(list(ratings['user_id'])[0])
top_items = get_top_n(predictions, test_user_id, 5)
print(f"为用户 {test_user_id} 推荐的物品ID及预测评分: {top_items}")
实战经验:矩阵分解的隐因子数量(n_factors)和学习率(lr_all)需要仔细调优。因子太少欠拟合,太多过拟合且计算慢。
三、迈向深度学习:神经协同过滤(NCF)
传统方法在捕捉非线性关系上有限。神经协同过滤用神经网络代替点积,能学习更复杂的交互。这里我们用TensorFlow/Keras实现一个简化版NCF。
import tensorflow as tf
from tensorflow.keras.layers import Input, Embedding, Flatten, Concatenate, Dense, Dropout
from tensorflow.keras.models import Model
from tensorflow.keras.optimizers import Adam
# 准备数据:需要将用户和物品ID映射为连续索引
user_ids = ratings['user_id'].astype('category').cat.codes.values
item_ids = ratings['item_id'].astype('category').cat.codes.values
labels = ratings['rating'].values
n_users = ratings['user_id'].nunique()
n_items = ratings['item_id'].nunique()
# 构建NCF模型(广义矩阵分解+多层感知机部分)
def create_ncf_model(n_users, n_items, embedding_size=50, hidden_layers=[64, 32, 16]):
# 输入层
user_input = Input(shape=(1,), name='user_input')
item_input = Input(shape=(1,), name='item_input')
# 嵌入层
user_embedding = Embedding(n_users, embedding_size, name='user_embedding')(user_input)
item_embedding = Embedding(n_items, embedding_size, name='item_embedding')(item_input)
user_vec = Flatten()(user_embedding)
item_vec = Flatten()(item_embedding)
# 拼接特征
concat = Concatenate()([user_vec, item_vec])
# 多层感知机
x = concat
for units in hidden_layers:
x = Dense(units, activation='relu')(x)
x = Dropout(0.2)(x) # 防止过拟合
# 输出层(回归问题,预测评分)
output = Dense(1, activation='linear')(x)
model = Model(inputs=[user_input, item_input], outputs=output)
model.compile(optimizer=Adam(learning_rate=0.001), loss='mse', metrics=['mae'])
return model
# 创建并训练模型
model = create_ncf_model(n_users, n_items)
model.summary()
# 划分训练/验证集
from sklearn.model_selection import train_test_split
X_train, X_val, y_train, y_val = train_test_split(
[user_ids, item_ids], labels, test_size=0.2, random_state=42
)
history = model.fit(
x=[X_train[0], X_train[1]],
y=y_train,
batch_size=64,
epochs=10,
validation_data=([X_val[0], X_val[1]], y_val),
verbose=1
)
# 预测示例
sample_user = np.array([0])
sample_item = np.array([10])
pred_rating = model.predict([sample_user, sample_item])
print(f"预测评分: {pred_rating[0][0]:.2f}")
踩坑提示:深度学习模型虽然强大,但需要更多数据、更长的训练时间和更精细的调参(如嵌入维度、网络结构、Dropout率)。对于中等规模数据,经典的矩阵分解可能更快、效果相当。务必根据业务场景和数据量权衡选择。
四、评估与上线思考:不只是RMSE
模型训练完,评估至关重要。除了RMSE、MAE这类预测精度指标,在真实场景中,我们更关心排名质量。
# 使用Surprise计算排名指标(需要安装surprise)
from surprise.model_selection import cross_validate
from surprise import NormalPredictor
# 对比基准随机模型
benchmark = NormalPredictor()
results = cross_validate(benchmark, data, measures=['RMSE', 'MAE'], cv=3, verbose=True)
# 对于Top-N推荐,常用精确率@K、召回率@K、NDCG@K
# 这里展示一个简单的自定义计算思路(实际应用建议用更成熟的库如implicit)
def precision_at_k(predictions, k=10, threshold=3.5):
# 首先将预测评分转换为二元标签(喜欢/不喜欢)
user_est_true = {}
for uid, _, true_r, est, _ in predictions:
user_est_true.setdefault(uid, []).append((est, true_r))
precisions = []
for uid, user_ratings in user_est_true.items():
# 按预测评分排序
user_ratings.sort(key=lambda x: x[0], reverse=True)
# 前K个中实际喜欢的数量
n_rel = sum((true_r >= threshold) for (_, true_r) in user_ratings[:k])
precisions.append(n_rel / k)
return sum(precisions) / len(precisions)
# 注意:这里predictions需要是Surprise的预测结果格式
# prec_k = precision_at_k(predictions, k=5)
# print(f"Precision@5: {prec_k:.4f}")
最后的上线心得:实验室的RMSE降低0.01,不等于线上效果一定变好。A/B测试是黄金标准。此外,工程上要考虑实时性(在线推理延迟)、可扩展性(用户增长)和冷启动问题。对于新用户或新物品,可以结合基于内容的推荐(用物品属性、用户画像)作为补充。
这次从协同过滤到深度学习的探索就到这里。推荐系统是一个持续迭代的过程,没有银弹。希望我的这些代码和踩坑经验能为你点亮一盏灯。记住,理解业务和数据,往往比选择最复杂的模型更重要。祝你构建出出色的推荐引擎!

评论(0)