资讯动态

PyTorch协同过滤实战:MovieLens数据集推荐系统实现

发布时间:2026/9/29 2:08:37 来源:尧图企业网站定制
简介这份二十九页的PDF文档围绕推荐系统中的协同过滤算法系统讲解基于PyTorch在MovieLens数据集上的完整实现过程适合推荐系统初学者、PyTorch入门者以及希望动手实践协同过滤算法的算法工程师。内容从推荐系统与协同过滤概述、PyTorch环境搭建与核心概念到MovieLens数据集的下载、清洗与预处理再到基于物品与基于用户的协同过滤原理、矩阵分解模型搭建、损失函数与优化器选择、训练循环、评估指标均方根误差、平均绝对误差、决定系数及结果优化策略均有覆盖并附带可直接运行的示例代码思路。资源包内包含一个PDF文件体积约二点零二MB页面排版清晰文档内文字、图表齐全支持目录跳转便于读者按章节快速定位。目前已有七十人学习下载适合作为从理论到代码实现的进阶参考资料帮助读者快速建立对协同过滤落地流程的整体认知减少从零翻阅资料的摸索时间。1. 推荐系统基础PyTorch协同过滤算法在MovieLens数据集上的实现“推荐系统基础PyTorch协同过滤算法在MovieLens数据集上的实现”这份PDF我拆完后的第一感受是它把推荐系统入门最容易被卡住的三件事——数据集怎么处理、协同过滤怎么建模、训练评估怎么闭环——按顺序串了起来。无论你是要交课程设计还是准备面试想有个能讲清楚的项目沿着这份文档把代码跑通一天内就能拿到一个可用的评分预测模型。文档从MovieLens 100K的下载开始一路走到PyTorch里的Embedding矩阵分解、训练循环、RMSE/MAE评估最后还给了一组优化方向。适合那种不想只看理论、想直接动手复现的人也适合刚接触PyTorch、想拿一个小项目练手的人。如果你要自己复现这份PDF是值得对照着看的完整流程。2. 从MovieLens数据集到可训练样本文件结构、预处理与编码2.1 为什么先选MovieLens 100K规模、字段与实验意义MovieLens是明尼苏达大学GroupLens研究项目维护的公开数据集可以说推荐系统领域很多经典论文都拿它做基准。100K版本包含943个用户、1682部电影、100000条评分评分范围1到5还附带用户人口统计信息和电影分类信息。最开始做实验没必要一上来就上1M或者20M100K的规模刚好让训练循环在几分钟内跑完一轮方便快速调参。文档里对不同版本做了对比我把关键参数汇总成一张表。版本用户数电影数评分条数适合场景100K9431682100000入门、调参、课程设计1M604037061000209常规算法研究10M715671068110000054分布式与扩展性实验20M1384932727820000263大规模工业级验证我的建议是先用100K把全流程跑通确认模型和评估逻辑没问题再换1M练手。因为换规模时主要改的是num_users、num_items这两个超参数其他代码几乎不用动。2.2 文件结构与数据读取不只看u.data解压ml-100k.tar.gz之后会得到一个ml-100k文件夹。核心文件是u.data每一行是一条评分记录格式是user id、item id、rating、timestamp字段之间用制表符分隔。u.item是电影信息u.user是用户信息u.genre和u.occupation是分类映射表。注意u.data里没有表头。读取时最常翻车的点就是分隔符。用pandas读的时候sep要指定为\t而不是默认的逗号。我在项目里是这样读的import pandas as pd columns [user_id, item_id, rating, timestamp] data pd.read_csv(ml-100k/u.data, sep\t, namescolumns) print(data.info()) print(data[rating].value_counts().sort_index())这段代码把u.data读成DataFrame手动指定了四列名称。value_counts统计每个评分档位出现的次数正常情况下1到5分都有分布5分数量最多。如果你看到评分里出现0或6说明数据来源不是标准版或者读错了分隔符。timestamp字段在纯协同过滤训练里通常用不上但如果要做时间划分或验证冷启动它很有用。2.3 数据清洗与划分随机划分和时间划分要分清文档里用sklearn的train_test_split做了20%的随机划分。这种做法在分类问题里很常规但在推荐系统评估里有一个隐患同一个用户的部分评分可能落在训练集另一部分落在测试集模型在训练时已经见过这个用户的历史行为测试时等于在做“顺水推舟”的预测RMSE会显得比实际工业化场景乐观。更稳妥的做法是按时间划分。每个用户先按timestamp排序取前80%作为训练集、后20%作为测试集这样可以模拟“用过去预测未来”。代码可以这样写def temporal_split(data, ratio0.8): train_list [] test_list [] for user_id, group in data.groupby(user_id): group group.sort_values(timestamp) split_idx int(len(group) * ratio) train_list.append(group.iloc[:split_idx]) test_list.append(group.iloc[split_idx:]) return pd.concat(train_list), pd.concat(test_list) train_data, test_data temporal_split(data, ratio0.8) print(train_data.shape, test_data.shape)groupby user_id后对每个用户的评分按时间排序取前80%。这个函数不依赖随机种子结果可复现。如果你的目标是验证算法预测能力时间划分比随机划分更接近真实推荐场景如果只是课程设计跑通流程用train_test_split加random_state42也能接受但要意识到评估结果偏乐观。2.4 编码factorize可能埋下的坑矩阵分解模型需要用户ID和电影ID是连续的整数索引否则nn.Embedding没法索引。文档里用的是pandas的factorize方法看起来很简单train_data[user_id] pd.factorize(train_data[user_id])[0] train_data[item_id] pd.factorize(train_data[item_id])[0]这里有个特别容易踩的坑如果分别对训练集和测试集调factorize两次编码后的ID空间是不一致的。比如训练集里用户7被编成3测试集里同样的用户7可能被编成5Embedding索引就完全错位了评估时等于拿乱序ID去查表。正确的做法是先在全量数据上构建ID到连续索引的映射再应用到训练集和测试集。all_data pd.concat([train_data, test_data]) user_encoder {user_id: idx for idx, user_id in enumerate(all_data[user_id].unique())} item_encoder {item_id: idx for idx, item_id in enumerate(all_data[item_id].unique())} train_data[user_enc] train_data[user_id].map(user_encoder) train_data[item_enc] train_data[item_id].map(item_encoder) test_data[user_enc] test_data[user_id].map(user_encoder) test_data[item_enc] test_data[item_id].map(item_encoder)用map而不是factorize保证同一个ID在训练集和测试集里映射到同一个整数。另外测试集里如果出现全量数据中都没有的新IDmap会返回NaN这种样本要么在评估时丢弃要么给未知用户分配一个专门的Embedding槽位。后面第5章会专门讲这个问题。3. 协同过滤在PyTorch里落地从相似度计算到矩阵分解3.1 为什么传统相似度思路在PyTorch里不好直接算基于用户的协同过滤算法文档里给出了完整步骤计算用户相似度、找K个邻居、预测评分。用余弦相似度在小矩阵上跑确实没问题我自己也写过def cosine_similarity(user1, user2): dot_product np.dot(user1, user2) norm1 np.linalg.norm(user1) norm2 np.linalg.norm(user2) return dot_product / (norm1 * norm2)但真实问题是100K数据集有943个用户计算用户两两相似度需要约44万次余弦计算而且评分矩阵极度稀疏很多用户之间没有共同评过的物品相似度计算出来的结果可信度很低。这个思路到了1M数据集就不太现实因为复杂度是用户数的平方。PyTorch更适合的思路是把协同过滤转化成参数学习问题用矩阵分解学出用户Embedding和物品Embedding让它们的点积逼近真实评分。矩阵分解的基本假设是评分矩阵R可以被分解成两个低秩矩阵的乘积。一个维度是用户数乘以隐含维度另一个是隐含维度乘以物品数两个矩阵相乘后重新构造出完整的评分矩阵。这里的关键是隐含维度的选择文档里给的思路就是设置embedding_dim一般经验值是32到128100K数据用32起步就够。3.2 模型定义用nn.Embedding实现矩阵分解PyTorch里实现矩阵分解不需要手动写矩阵乘法直接用nn.Embedding层就可以。Embedding层本质上是一个可学习的查找表输入ID输出对应的向量。模型的forward就是取用户向量和物品向量做点积输出预测评分。import torch import torch.nn as nn class MatrixFactorization(nn.Module): def __init__(self, num_users, num_items, embed_dim32): super(MatrixFactorization, self).__init__() self.user_embed nn.Embedding(num_users, embed_dim) self.item_embed nn.Embedding(num_items, embed_dim) nn.init.normal_(self.user_embed.weight, std0.01) nn.init.normal_(self.item_embed.weight, std0.01) def forward(self, user_id, item_id): user_vec self.user_embed(user_id) item_vec self.item_embed(item_id) return (user_vec * item_vec).sum(dim1)注意初始化nn.Embedding默认初始化的标准差是1如果不手动改模型一开始的输出范围会很大配合MSE损失容易出现前期loss爆炸。常见的做法是把Embedding参数初始化为标准差0.01的小随机数让预测值一开始贴近0。forward里用逐元素乘法再求和等价于向量点积。3.3 损失函数与优化器选择不是所有回归都该用MSE评分预测是回归任务常见的损失函数是均方误差MSE。PyTorch里直接用nn.MSELoss()它会计算预测值和真实评分的平方差的均值。选择MSE的原因是对大误差惩罚更大这也意味着它更关注那些偏差大的评分。如果你希望模型对离群点稳健一些可以换成nn.L1Loss对应MAE损失。优化器我第一反应是Adam因为对Embedding这类稀疏参数Adam比SGD收敛更稳。学习率先从0.01开始如果loss发散就降到0.001。loss_fn nn.MSELoss() optimizer torch.optim.Adam(model.parameters(), lr0.01, weight_decay1e-5)weight_decay是L2正则项它对Embedding参数做惩罚能抑制过拟合。在100K数据上我一般给1e-5到1e-4太大会让模型欠拟合loss下不去。这里有一个细节MSE损失的值依赖于评分范围MovieLens评分是1到5平方误差范围在0到16之间所以损失在2.5左右甚至更低都是常见的不要看到2.0就觉得模型没收敛。3.4 模型参数量与隐含维度不是越大越好这里可以算一笔账。100K数据有943个用户、1682部电影embed_dim32时用户Embedding参数量是943×3230176物品Embedding参数量是1682×3253824合计约84000个参数。而数据集本身只有100000条评分参数和样本几乎一比一这是一个相对安全的范围。如果把embed_dim调到256参数量变成943×2561682×256≈67万是样本量的六倍多。六倍参数去拟合10万条评分模型很容易死记训练集换到测试集上性能立刻打回原形。所以我的习惯是先32起步训练loss降到平台期后再试着增加到64或128。在1M数据集上因为样本量到了100万embed_dim用到128甚至256问题不大但100K数据集必须克制。隐含维度还影响推荐结果的多样性。维度越低模型概括得越粗推荐列表容易集中到大众热门维度越高能捕捉越细的偏好但也更容易过拟合到个别评分上。如果你只关心RMSE100K上做到0.95到1.0之间已经不错如果你后续要分析Top-K列表还得观察推荐结果是不是单一类型扎堆。4. 训练循环与评估指标让模型真正收敛4.1 训练循环的标准写法梯度清零、模式切换、损失监控文档里把训练循环拆得很细我复现时基本是按这个模板来的。完整的训练循环分四步前向传播、计算损失、反向传播、更新参数。PyTorch里容易忘的是每一次迭代都要手动清零梯度否则梯度会累积到上一次的数值上。import torch def train_one_epoch(model, train_loader, loss_fn, optimizer, device): model.train() total_loss 0 count 0 for user_id, item_id, rating in train_loader: user_id user_id.to(device) item_id item_id.to(device) rating rating.to(device) optimizer.zero_grad() pred model(user_id, item_id) loss loss_fn(pred, rating) loss.backward() optimizer.step() total_loss loss.item() * user_id.size(0) count user_id.size(0) return total_loss / countmodel.train()在这里的作用是确保模型处于训练模式。虽然MatrixFactorization里没有BatchNorm或Dropouttrain和eval模式没有实质区别但养成这个习惯能避免以后换模型时踩坑。optimizer.zero_grad()必须在loss.backward()之前执行否则梯度会跨batch累积。loss.item()取出标量值乘以batch大小再累加最后除以样本总数得到平均损失。训练循环里还需要一个数据加载器。文档里用PyTorch的DataLoader封装了预处理后的数据我补充一点DataLoader的shuffle参数建议设为True让每个epoch内样本顺序不同否则模型会被固定的batch顺序带偏。from torch.utils.data import TensorDataset, DataLoader train_tensor TensorDataset( torch.tensor(train_data[user_enc].values, dtypetorch.long), torch.tensor(train_data[item_enc].values, dtypetorch.long), torch.tensor(train_data[rating].values, dtypetorch.float32) ) train_loader DataLoader(train_tensor, batch_size64, shuffleTrue)注意user_enc和item_enc要转成torch.long因为Embedding索引要求整数类型。rating转成float32。batch_size在100K数据上取64到256都可以太小训练慢太大每个batch统计噪声低但内存占用高。4.2 在测试集上评估RMSE、MAE和R²的计算文档里给了三个评估指标RMSE、MAE、R²。RMSE是均方根误差对大的预测偏差格外敏感因为平方放大了误差。MAE是平均绝对误差更直白地反映平均误差水平。R²是决定系数表示模型解释了评分方差的百分比越接近1越好。实际项目里最常报告RMSE因为它能跟其他论文直接对比。评估时要关掉梯度计算用torch.no_grad()包住防止不必要的计算消耗内存。以下是在测试集上计算RMSE和MAE的代码from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score torch.no_grad() def evaluate(model, test_loader, device): model.eval() all_preds [] all_ratings [] for user_id, item_id, rating in test_loader: user_id user_id.to(device) item_id item_id.to(device) pred model(user_id, item_id).cpu() all_preds.extend(pred.tolist()) all_ratings.extend(rating.tolist()) rmse mean_squared_error(all_ratings, all_preds, squaredFalse) mae mean_absolute_error(all_ratings, all_preds) r2 r2_score(all_ratings, all_preds) return rmse, mae, r2mean_squared_error的squaredFalse参数让sklearn返回RMSE而不是MSE。model.eval()和torch.no_grad()配合既切到评估模式又关闭了自动求导。如果你的运行设备是GPU记得在遍历batch时把数据移到device。4.3 设置baseline先知道随机猜的底线在哪很多人第一次跑通模型看到RMSE在1.0左右就以为效果不错其实需要先建立一个baseline。最简单的是全局平均评分baseline假设所有未知评分都预测为全体评分的平均值。在100K数据集上这个baseline的RMSE大概在1.25左右。如果模型RMSE比1.25还大说明模型基本没学到东西。global_mean train_data[rating].mean() baseline_preds [global_mean] * len(test_data) baseline_rmse mean_squared_error(test_data[rating], baseline_preds, squaredFalse)一般来说纯矩阵分解模型在100K上能做到RMSE在0.95到1.1之间这算是一个合理范围。如果超出很多先检查数据编码有没有错位再检查学习率和正则化而不是急着改网络结构。不要把baseline放在训练集上算那会高估。5. 常见问题与排查训练发散、过拟合与评估偏差这一章我把自己复现过程中实际踩过的坑整理成几条每一条都是现象、原因、解决三个角度。5.1 现象loss不降反升甚至直接变成NaN训练到第三个epochloss从2.1跳到9.8再往后直接变NaN。原因几乎可以锁定是学习率过大。Embedding初始化的标准差如果也偏大比如保持默认的1模型输出初期可能到几十甚至上百MSE反传回来的梯度会非常大参数更新一步就飞掉。解决方法是先把初始化和优化器参数固定住Embedding初始化标准差设为0.01学习率从0.001起步如果还是发散就往0.0001降。另外检查输入rating是否有NaN。如果已经加了weight_decay还是爆可以在backward之后、step之前加梯度裁剪torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0)这一行把梯度模长限制在1.0以内能防住大多数跑飞情况。但注意clip_grad_norm_只是止血根本解法还是把学习率和初始化调稳。5.2 现象测试集RMSE很低但推荐出来的物品不符合用户口味这种情况多半不是模型问题而是评估协议出了问题。如果用了随机划分同一个用户的评分同时出现在训练和测试模型在训练时已经“见”过这个用户对很多物品的偏好预测该用户其他评分时占了信息泄露的便宜。解决方法是用第2.3节的时间划分每个用户按时间前80%训练、后20%测试。这样才能模拟真实在线推荐场景里“用历史预测未来”。还要注意随机划分会让同一部电影也同时出现在训练和测试里。电影ID被Embedding学过之后测试集里对这部电影的预测自然准RMSE虚低。如果你要做的是一个类似“如何给新用户推荐”的实验甚至要把每个用户最近一次评分单独留出来做leave-one-out测试。那时候评估指标也要换成分类指标或者排序指标只看RMSE不够。5.3 现象训练集loss接近0测试集RMSE反而高这是典型的过拟合。当embed_dim设置到128甚至256而100K数据只有10万条评分时模型有足够容量去记住训练样本但对新样本泛化很差。解决方案是降低embed_dim到32或64加大weight_decay到1e-4并在训练过程中做early stopping每个epoch在验证集上计算RMSE连续5个epoch不降就停止训练。我一般会在训练时同时记录train loss和val RMSE画成两条曲线。正常情况是train loss一路下行val RMSE先降后升val RMSE开始上升的那一刻就是该停的位置。很多课程设计只给最终RMSE不画过程曲线实际上过程里才能看出模型是不是在硬背答案。5.4 现象IndexError: index out of range in self报错来自nn.Embedding索引越界。原因通常是测试集里的user_id或item_id超出了Embedding的num_users、num_items范围。常见于编码阶段没有统一映射或者全量数据编码后又有新增ID。快速排查的方法是打印两端ID的max值print(test_data[user_enc].max(), train_data[user_enc].max()) print(test_data[item_enc].max(), train_data[item_enc].max())如果test的max大于train说明编码映射没对齐。解决思路有两种如果只是离线评估丢弃那些不在训练映射里的测试样本如果要部署上线为未知用户和未知物品各预留一个固定Embedding索引例如num_users1和num_items1遇到没见过的新ID就映射到这个预留位。5.5 现象训练太慢一个epoch要跑几分钟100K数据量不该这么慢。常见原因是DataLoader没设置num_workers或者把评分矩阵转成了稠密矩阵再做全量计算。正确的做法是只对评分样本做batch训练也就是用TensorDataset只装100K条有评分的记录。100K数据在CPU上一个epoch应该在几十秒内完成如果用了GPU速度会更快。PyTorch的DataLoader可以设置num_workers2或4数据加载走子进程能明显降低CPU等待时间。如果用了GPU还要确认batch里的Tensor都在device上否则每次都要做CPU和GPU之间的拷贝反而更慢。另外batch_size如果设成1或2每步更新都做一次backward开销集中在PyTorch的计算调度上改成64或128之后速度会有质的提升。6. 进阶验证可视化Embedding并封装Top-K推荐函数模型训练完之后不要只看一个RMSE数字就收工。我习惯把Embedding拿出来可视化一下再封装一个推荐函数这样整个项目才真正有交付感。Embedding可视化用t-SNE降维。从训练好的模型里取出用户Embedding用sklearn的TSNE降到二维散点图里如果能看到明显的聚类簇说明模型不是瞎学的。from sklearn.manifold import TSNE import matplotlib.pyplot as plt user_vecs model.user_embed.weight.detach().cpu().numpy() tsne TSNE(n_components2, random_state42) user_2d tsne.fit_transform(user_vecs) plt.scatter(user_2d[:, 0], user_2d[:, 1], s1) plt.show()Top-K推荐函数也很简单给定一个用户ID把该用户对所有物品的预测评分算出来排序取前K个。注意要用torch.no_grad()包裹不然会构建计算图。torch.no_grad() def recommend(model, user_id, num_items, top_k10, devicecpu): model.eval() user_id torch.tensor([user_id], devicedevice, dtypetorch.long) item_ids torch.arange(num_items, devicedevice, dtypetorch.long) preds model(user_id.repeat(num_items), item_ids) top_items torch.topk(preds, top_k).indices.tolist() return top_itemsuser_id.repeat(num_items)把用户ID复制成物品数份一次性算完所有预测。torch.topk直接取出得分最高的物品索引。对新用户冷启动我习惯在调用这个函数前先判断用户是否在映射表里不在就返回热门物品Top-K兜底。从那以后我每次复现推荐系统项目都会强制走一遍这个流程先检查数据划分和编码再跑一个baseline来确认模型真的学到了东西最后做Embedding可视化和Top-K输出。这套习惯帮我避开了不少“看上去效果很好实际是数据泄露”的尴尬。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑