多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Python协同过滤实战:UserCF与ItemCF算法详解与避坑指南

Python协同过滤实战:UserCF与ItemCF算法详解与避坑指南 简介这份资源面向推荐算法入门与进阶学习者提供基于Python实现的协同过滤推荐算法代码涵盖基于物品与基于用户两种经典思路可作为课程设计、毕业设计、大作业或工程实训的参考项目。压缩包共4个文件以2个Python脚本为核心分别对应Item_CF与User_CF两套算法实现另附一份csv数据文件用于读取与验证推荐结果以及一个gitignore配置项整体约6KB体量轻便便于快速阅读与本地调试。目前已有449人学习下载说明其在同类教学资源中具备一定参考价值。读者可借此理解相似度计算、邻居选取与评分预测等关键环节对照代码梳理两种协同过滤的差异与适用场景并在此基础上自行调整数据、添加功能或优化实现。需注意代码仅作参考建议具备Python基础后再进行调试与二次开发。1. 从一份只有四个文件的压缩包说起Python 协同过滤到底能跑出什么打开这个叫Collaborative-Filtering-Pytho.zip的包里面干净得有点反直觉User_CF.py、Item_CF.py、一个新建文本文档.csv再加一个.gitignore。没有 requirements、没有 README、没有 Flask 界面也没有任何可视化。很多人第一次看到这种结构会犯嘀咕——就这但恰恰是这种「裸算法」包最适合拿来把协同过滤的两条主线一次性吃透基于用户的协同过滤UserCF和基于物品的协同过滤ItemCF。它解决的不是「搭一个推荐系统平台」这种大问题而是让你在一份可读的 Python 代码里亲眼看到用户相似度矩阵、物品相似度矩阵、评分预测、TopN 推荐这几步是怎么一步步算出来的。适合谁正在做毕设、课程设计、大作业的学生或者想从零手写一遍推荐算法、不想一上来就被 Surprise、LightFM 这些库的黑匣子挡住的进阶学习者。下面我按「先跑通、再拆原理、最后避坑」的顺序把这份资源拆开讲。2. 环境准备与数据格式让两个脚本先跑起来2.1 依赖与 Python 版本选择这份代码是纯 Python 实现没有用到深度学习框架核心依赖基本只有pandas和numpy。常见做法是建一个干净的虚拟环境避免和你机器上已有的库版本打架。如果你还在纠结 python 安装教程、vscode python 环境配置这些前置问题先把解释器装好、把编辑器指向正确的解释器路径再回来跑算法否则报错会混在一起排查起来很痛苦。# 建议 Python 3.8 及以上3.10/3.11 实测都能跑 python -m venv venv # Windows venv\Scripts\activate # macOS / Linux source venv/bin/activate pip install pandas numpy逻辑说明虚拟环境把这份项目的依赖和你系统里的其他项目隔离pandas负责读 CSV 和做表格运算numpy负责矩阵和向量化计算。参数上没什么可调的版本别太老即可。如果你用的是 PyCharm配置解释器时记得选到venv里的那个 python而不是系统全局的否则会出现「命令行能跑、IDE 里报 ModuleNotFoundError」这种经典翻车。2.2 CSV 数据长什么样字段怎么对应包里的新建文本文档.csv是唯一的数据源。协同过滤的输入本质就是「谁 对 什么 打了 几分」这三元组所以 CSV 通常是三列用户 ID、物品 ID、评分。不同人拿到的文件列名可能不一样代码里一般会写死列名或用位置索引读取这就是第一个要确认的点。列含义典型取值注意user_id用户唯一标识1, 2, 3…不能有重复行冲突item_id物品唯一标识101, 102…类型要和代码一致rating用户对物品评分1~5缺失值要提前处理先跑一段探查代码确认数据能被正确读进来再动算法import pandas as pd # 读取数据注意编码中文 Windows 下常见 gbk df pd.read_csv(新建文本文档.csv, encodingutf-8) print(df.head()) print(df.shape) print(df.dtypes) print(df[user_id].nunique(), df[item_id].nunique())逻辑说明head()看前几行确认列对齐shape看数据量dtypes确认 ID 是不是被读成了 float一旦有 NaNpandas 会把整列变 float后面做字典键会出问题。参数上encoding是最容易踩的utf-8 报 UnicodeDecodeError 就换gbk或gb18030。如果列名和代码里写的不一致要么改 CSV 表头要么改代码里的列名别硬扛。2.3 两个脚本的入口与运行方式User_CF.py和Item_CF.py是两套独立实现各自能单独运行。常见做法是直接python User_CF.py脚本内部读 CSV、算相似度、打印推荐结果。运行前先确认脚本里的文件路径是相对路径还是绝对路径——很多人把脚本和 CSV 放在不同目录结果一跑就 FileNotFoundError。python User_CF.py python Item_CF.py逻辑说明两个脚本互不依赖可以分别验证。如果只想先看一个建议从User_CF.py入手因为「找相似用户」的直觉比「找相似物品」更好理解。运行后如果只打印了相似度矩阵却没打印推荐列表说明脚本里的推荐输出部分可能被注释掉了需要自己取消注释或补一段 TopN 逻辑这在学生作业类代码里非常常见。3. UserCF 拆解用户相似度矩阵是怎么算出来的3.1 用户-物品评分矩阵的构建UserCF 的第一步是把长表转成宽表行是用户、列是物品、格子里是评分。这一步决定了后面所有计算的形状。没打分的格子是 NaN代表「未知」不是 0——把未知当 0 是新手最常犯的错会让相似度整体失真。import pandas as pd import numpy as np df pd.read_csv(新建文本文档.csv, encodingutf-8) # 长表转宽表行用户列物品 user_item df.pivot_table( indexuser_id, columnsitem_id, valuesrating ) print(user_item.shape) print(user_item.isna().sum().sum()) # 统计缺失格子数逻辑说明pivot_table默认对重复的 (user, item) 做均值聚合如果你的数据里同一用户对同一物品有多条评分这里会自动合并避免报错。isna().sum().sum()告诉你矩阵有多稀疏——稀疏度往往在 90% 以上这正是协同过滤要面对的现实。参数上values必须是评分列index和columns换成你实际的列名。3.2 余弦相似度与皮尔逊相似度的选择用户相似度衡量的是「两个人口味像不像」。常见两种算法余弦相似度和皮尔逊相关系数。余弦看的是评分向量的方向皮尔逊先去均值再算相关能抵消「有人习惯打高分、有人习惯打低分」的偏置。这份代码里通常实现的是其中一种你需要看懂它用的是哪个。from numpy.linalg import norm def cosine_sim(u, v): # 只在对两个用户都非空的维度上计算避免 NaN 污染 mask ~np.isnan(u) ~np.isnan(v) if mask.sum() 0: return 0.0 uu, vv u[mask], v[mask] denom norm(uu) * norm(vv) return float(np.dot(uu, vv) / denom) if denom else 0.0 # 对前两个用户算一下 mat user_item.values print(cosine_sim(mat[0], mat[1]))逻辑说明关键在mask——只在两人都评过分的物品上算相似度否则 NaN 会传染整个结果。denom为 0 说明有人一个分都没打直接返回 0。参数上如果你换成皮尔逊就是先对uu、vv各自减均值再算余弦代码结构一样只多两行去均值。选哪个数据量小、评分尺度统一用余弦就够评分偏置明显时皮尔逊更稳。3.3 评分预测与 TopN 推荐生成有了相似用户就能预测「目标用户对没看过的物品会打几分」找和他最像的 K 个用户用他们的评分加权平均。权重就是相似度相似度越高话语权越大。def predict_user_cf(user_item, sim_matrix, target_idx, item_idx, k5): sims sim_matrix[target_idx].copy() sims[target_idx] -1 # 排除自己 # 只保留对该物品有评分的用户 rated ~np.isnan(user_item[:, item_idx]) sims[~rated] -1 top_k np.argsort(sims)[-k:] top_k [i for i in top_k if sims[i] 0] if not top_k: return np.nan num sum(sims[i] * user_item[i, item_idx] for i in top_k) den sum(abs(sims[i]) for i in top_k) return num / den逻辑说明sims[target_idx] -1把自己排除rated过滤掉没评过该物品的用户argsort取相似度最高的 K 个。num/den是加权平均分母用绝对值防止负相似度抵消。参数k是邻居数太小推荐不稳太大引入不相关的人一般 5~20 之间调。生成 TopN 时对目标用户所有未评分物品算预测分排序取前 N 即可。4. ItemCF 拆解物品相似度与推荐的可解释性4.1 物品-用户矩阵与相似度计算ItemCF 把矩阵转置过来看行是物品列是用户衡量的是「两个物品被同一批人喜欢的程度」。它比 UserCF 更稳定——物品的数量和属性变化慢用户口味变化快所以工业界 ItemCF 用得更广。# 转置行物品列用户 item_user user_item.T item_mat item_user.values def item_similarity(item_mat): n item_mat.shape[0] sim np.zeros((n, n)) for i in range(n): for j in range(i 1, n): mask ~np.isnan(item_mat[i]) ~np.isnan(item_mat[j]) if mask.sum() 0: continue a, b item_mat[i][mask], item_mat[j][mask] denom norm(a) * norm(b) if denom: sim[i, j] sim[j, i] float(np.dot(a, b) / denom) return sim逻辑说明双重循环对每对物品算余弦mask同样只取共同被评分的用户。sim[j, i] sim[i, j]保证对称省一半计算。参数上物品多时这个 O(n²) 会慢常见优化是先算共现次数、只对共现过的物品对算相似度能砍掉大量无效计算。4.2 基于物品相似度的推荐打分ItemCF 的推荐逻辑更符合直觉你喜欢物品 A那就推和 A 最像的物品。预测分是「你评过分的物品」与「候选物品」相似度的加权和。def recommend_item_cf(user_item, item_sim, target_idx, top_n5): scores {} rated_items np.where(~np.isnan(user_item[target_idx]))[0] for i in rated_items: for j in range(item_sim.shape[0]): if np.isnan(user_item[target_idx, j]): # 只推没评过的 scores[j] scores.get(j, 0) item_sim[i, j] * user_item[target_idx, i] ranked sorted(scores.items(), keylambda x: x[1], reverseTrue) return ranked[:top_n]逻辑说明外层遍历用户评过分的物品内层把相似度加权累加到候选物品上if np.isnan保证不推已经看过的。item_sim[i, j] * user_item[target_idx, i]里评分高的物品对推荐贡献更大。参数top_n控制返回条数。这套逻辑的好处是可解释——你能直接说「因为你看过 A而 B 和 A 很像」。4.3 UserCF 与 ItemCF 的适用边界两者不是谁替代谁而是场景不同。下面这张表是我实际选型时会对照的维度UserCFItemCF相似度对象用户之间物品之间实时性用户变化快需频繁更新物品稳定更新慢可解释性较弱相似的人还看了…强和你看过的很像冷启动新用户难新物品易新物品难新用户易适用场景社交、新闻电商、视频逻辑说明用户数远小于物品数时 UserCF 计算量小物品数可控、需要强解释时选 ItemCF。很多毕设会两个都实现做对比这份资源正好给了两套代码可以直接跑同一份数据看推荐结果的差异。5. 避坑与排查跑不通时先看这几条5.1 现象UnicodeDecodeError 读 CSV 直接崩原因CSV 是中文 Windows 下用 Excel 存的默认编码是 gbk而代码里写的是 utf-8。解决把encoding改成gbk或gb18030或者用 Excel 另存为 utf-8 编码的 CSV。别用记事本另存容易带 BOMpandas 读出来第一列列名会多个\ufeff。5.2 现象相似度全是 0 或全是 NaN原因ID 列被读成了 float或者评分列里有空值导致整列 NaN 传染。解决读入后用df.dropna()清掉缺失行用df[user_id] df[user_id].astype(int)强制转整型。如果相似度全 0检查是不是把「未评分」当成了 0 参与计算。5.3 现象推荐结果里出现用户已经看过的物品原因生成 TopN 时没有过滤已评分物品或者过滤条件写反了。解决在打分阶段加if np.isnan(user_item[target_idx, j])这类判断确保只对未评分物品排序。这是推荐系统最基本的约束漏了会让结果看起来很蠢。5.4 现象脚本跑得极慢几分钟没输出原因ItemCF 的双重循环在物品数上千时是 O(n²)纯 Python 循环扛不住。解决先用小样本比如前 100 个物品验证逻辑再考虑用 numpy 向量化或只对共现物品对计算。别一上来就拿全量数据硬跑容易以为代码坏了。5.5 现象两个脚本结果差异巨大怀疑有一个错了原因UserCF 和 ItemCF 本来就是两种视角结果不同是正常的不一定是 bug。解决先确认两者用的是同一份数据、同一个相似度定义再对比。如果差异大到离谱检查是不是一个用了余弦、一个用了皮尔逊或者 K 值、TopN 设置差太多。6. 进阶技巧把裸算法改成能验证、能扩展的版本跑通只是起点。这份代码最大的价值是「结构透明」你可以直接在它上面做实验。我一般会先加一个离线评估用留一法把每个用户的一条评分藏起来看推荐能不能命中这样调 K 值和相似度算法才有依据而不是凭感觉。def evaluate_user_cf(user_item, sim_matrix, k5): hits, total 0, 0 for u in range(user_item.shape[0]): rated np.where(~np.isnan(user_item[u]))[0] if len(rated) 2: continue # 留一藏起最后一个评分 test_item rated[-1] train_row user_item[u].copy() train_row[test_item] np.nan # 用训练数据预测 test_item pred predict_user_cf( np.vstack([user_item[:u], train_row, user_item[u1:]]), sim_matrix, u, test_item, k ) total 1 if not np.isnan(pred): hits 1 return hits / total if total else 0.0逻辑说明留一法把每个用户最后一条评分当测试集用剩下的数据预测命中率就是评估指标。total统计有效用户数hits是预测成功的次数。参数k可以循环几个值跑一遍看命中率曲线。注意这里为了演示简化了相似度矩阵的重算实际用的时候相似度矩阵也要基于训练数据重算否则有信息泄漏——这是评估里最隐蔽的坑我第一次做的时候就被它坑过指标虚高得离谱。另一个扩展方向是把结果落成 CSV 或接一个轻量网页端。比如用 Flask 起一个接口输入用户 ID 返回 TopN 推荐前端简单渲染一下就能从「命令行脚本」变成「能演示的系统」毕设答辩时这点很加分。但别本末倒置先把算法逻辑和评估跑对界面只是壳。从那以后我每次拿到这种裸算法包都强制先跑通、再留一法评估、最后才动界面顺序反了就会在错误的地基上盖楼。希望帮到你。本文还有配套的精品资源点击获取
返回列表