ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于协同过滤的电影推荐系统实战:UserCF与ItemCF原理及Python实现

基于协同过滤的电影推荐系统实战:UserCF与ItemCF原理及Python实现 简介面向计算机相关专业毕业设计及课程设计这份基于协同过滤推荐算法的电影推荐系统项目整合了完整源码、数据库文件与毕业论文由导师指导并通过答辩评审分达97分可直接运行使用。项目采用Python作为后端主要语言前端包含Vue组件搭配SQL数据库与论文文档整体约13.3MB共687个文件其中以svg、js、py、vue、css、html等类型为主另有sql脚本用于初始化数据bat脚本可一键安装和启动环境。目前已有310人学习下载具备较高的参考价值。整套资源既能让读者快速搭建一个可演示的电影推荐平台也适合深入研读协同过滤算法在评分预测与Top-N推荐中的具体实现同时项目中的前端页面结构、接口逻辑和论文写作框架均可作为自己动手扩展的模板。1. 毕业设计选这个题到底在做什么电影推荐系统是协同过滤算法最经典的教学落地场景也是很多计算机专业毕业设计的高频选题。这个压缩包里的东西我拆解过很多遍本质上它是三件套一份可运行的 Python 代码、一份初始化好的数据库、一篇能把算法和工程讲清楚的论文。你要交付的不是一个“能跑就行”的演示而是要让答辩老师看到你理解协同过滤的数学含义也能处理真实数据里的稀疏和冷启动问题。这个系统适合两类人一类是正在选毕设题目、想找稳妥方向的在校生另一类是刚入门推荐系统、想拿一个完整项目练手的开发者。它的价值在于麻雀虽小五脏俱全——有用户评分数据、有相似度计算、有推荐列表生成还能对比基于用户UserCF和基于物品ItemCF两种算法在同一个数据集上的效果差异。别被“毕业设计”四个字限制了想象这个架构换一套商品数据就是电商推荐换一套资讯数据就是内容推荐。先说个反直觉的结论协同过滤本身代码量不大核心算法实现甚至不到 200 行真正让项目看起来复杂、也让很多人翻车的地方在数据预处理和评测指标的设计上。所以如果你准备照着这套思路复现优先把时间花在数据清洗和评测实验上而不是死磕算法公式。2. 协同过滤的两种路线与选型UserCF 还是 ItemCF2.1 UserCF 的计算逻辑和适用边界基于用户的协同过滤核心思想是“物以类聚人以群分”。要判断用户 A 对一部没看过的电影《肖申克的救赎》的兴趣先找到和 A 评分习惯最接近的 K 个用户再把这 K 个用户对这部电影的评分加权求和。这里的“接近”不是靠嘴说而是用相似度公式量化。最常见的做法是皮尔逊相关系数它比欧氏距离更能抵抗用户评分尺度偏差——有人习惯打 8 分起步有人 4 分算高潮皮尔逊通过中心化处理把这层偏差消掉了。代码实现在一个公开数据集 MovieLens 上非常直观。读入评分表后先构造“用户-电影”矩阵然后两两计算用户相似度import pandas as pd import numpy as np def load_ratings(filepath): 读取 u.data 评分文件列为 user_id, item_id, rating, timestamp return pd.read_csv(filepath, sep\t, headerNone, names[user_id, item_id, rating, timestamp]) def build_user_item_matrix(ratings): 把评分表透视成用户-电影矩阵行是用户列是电影空缺填0 matrix ratings.pivot_table(indexuser_id, columnsitem_id, valuesrating) return matrix.fillna(0) def pearson_similarity(matrix, user_a, user_b): 计算两个用户的皮尔逊相关系数向量是他们对所有电影的评分 a matrix.loc[user_a].values b matrix.loc[user_b].values # 只取两边都非0的位置避免缺失值干扰 mask (a ! 0) (b ! 0) if mask.sum() 2: return 0 a_valid, b_valid a[mask], b[mask] a_centered a_valid - a_valid.mean() b_centered b_valid - b_valid.mean() denom np.sqrt((a_centered ** 2).sum() * (b_centered ** 2).sum()) if denom 0: return 0 return (a_centered * b_centered).sum() / denom这段逻辑里有三个关键参数直接影响推荐结果。第一个是mask.sum() 2的判断——两个用户如果只看过同一部电影只有一个共同评分点算出来的相关系数要么是 1 要么是 -1纯属噪声必须过滤。第二个是中心化处理a_valid - a_valid.mean()如果漏掉这一步两个用户哪怕评分习惯完全相反也可能算出高相似度。第三个是空值和零值的区分——填 0 在矩阵运算里是必要的但计算相似度时必须用掩码把真正的缺失值和打了 0 分区分开否则评分数据里极少出现的 0 分会污染相似度。2.2 ItemCF 的思路和与 UserCF 的选型判断基于物品的协同过滤走的是另一条逻辑“喜欢看《盗梦空间》的人大概率也喜欢《星际穿越》”。先计算电影与电影之间的相似度再根据用户看过电影的相似电影来推荐。这个方案在电影场景下其实更实用因为物品相似度矩阵可以离线算好存着用户请求推荐时只做查询和聚合响应速度快很多。而 UserCF 的用户相似度矩阵也要离线算但用户新增评分后矩阵要增量更新维护成本略高。选型没有绝对对错要看你手头数据的规模和业务特征。UserCF 适合用户少、物品多的场景——比如一个小型校园论坛的帖子推荐几千个活跃用户、几十万条内容用户兴趣变化快实时性强。ItemCF 适合物品少、用户多的场景——比如电影网站通常电影数量在几万级别而用户动辄几十万物品相似度稳定更新频率低。MovieLens 100k 这个数据集的口号是 943 个用户评了 1682 部电影两个方向都适合做这也是它成为毕设神器的原因之一。实现 ItemCF 时物品相似度矩阵的存储格式值得提前规划。完整矩阵是 1682×1682用 Python 的嵌套 dict 存大约占用几十 MB 内存但如果你用自带的倾数据集约 2.5 万部电影矩阵膨胀太快。常见做法是只保留每个电影 Top N 相似物品N 取 20 到 50既能控制内存又能去掉长尾噪声def compute_item_similarity(ratings, top_n30): 计算物品相似度字典只保留每个物品最相似的 top_n 个 # 先做物品-用户矩阵行是电影列是用户 item_user ratings.pivot_table(indexitem_id, columnsuser_id, valuesrating) item_user item_user.fillna(0).values n_items item_user.shape[0] sim_dict {} for i in range(n_items): # 只算上三角减少一半计算量 scores [] vec_i item_user[i] for j in range(i 1, n_items): mask (vec_i ! 0) (item_user[j] ! 0) if mask.sum() 3: # 至少3个共同评分用户才计算相似度 continue a vec_i[mask] b item_user[j][mask] corr np.corrcoef(a, b)[0, 1] if not np.isnan(corr): scores.append((j, corr)) scores.sort(keylambda x: x[1], reverseTrue) sim_dict[i] scores[:top_n] return sim_dict这里的top_n是控粒度mask.sum() 3是防噪声兜底。在 MovieLens 上跑一圈你会发现大部分电影间的相关系数集中在 0.1 到 0.5偶尔出现 0.9 以上基本是冷门小片只有几个人评过相关性纯属偶然。2.3 混合策略才是这个毕设的加分项如果只做一个算法论文深度容易被老师挑战。一个常见且不过分的方案是主推 ItemCF用 UserCF 作为兜底。具体来说先看用户的历史评分数量如果评分超过 20 条说明画像足够丰富用 UserCF 能得到更个性化的结果如果少于 20 条物品相似度推荐更能兜住冷启动。两种结果做加权融合权重参数用 0.7 和 0.3 起步后续在评测集上微调。混合之后推荐的解释逻辑也更好写进论文“本系统采用 ItemCF 为主、UserCF 为辅的级联混合策略当用户评分数量低于阈值时自动降级为物品推荐兼顾个性化与冷启动场景。”这一句话就把“你为什么这么设计”这个追问接住了。3. 跑通系统的最小步骤从数据导入到命令行出结果3.1 初始化数据库并导入 MovieLens 数据压缩包里的数据库文件通常是 SQLite 或 MySQL 的导出文件。SQLite 对毕设最友好因为不需要单独装服务端Python 自带sqlite3模块就能操作。如果你拿到的包里是database/movie.db这种文件先用下面的脚本确认表结构是否完整import sqlite3 conn sqlite3.connect(movie.db) cursor conn.cursor() cursor.execute(SELECT name FROM sqlite_master WHERE typetable) tables cursor.fetchall() print(所有表:, tables) for table in tables: cursor.execute(fSELECT COUNT(*) FROM {table[0]}) count cursor.fetchone()[0] print(f{table[0]}: {count} 行)正常项目至少有三张核心表users用户表用户 ID、职业、性别、年龄、movies电影表电影 ID、标题、类型、ratings评分表用户 ID、电影 ID、评分、时间戳。数据量级应该是 943 个用户、1682 部电影、10 万条评分。如果你看到的表结构和数量跟这个差异大大概率是数据集版本不同比如 MovieLens 最新版用电影 ID 和作品 ID 拆分的新格式。此时不要硬套旧代码先跑一遍 SQL 描述用户表结构。如果是白手起家建库推荐用 SQLAlchemy ORM 配合 SQLite别直接写裸 SQL 建表。原因很现实毕设后期你可能要加收藏、标签、影评等辅助表ORM 迁移时能少踩很多坑。下面这段代码定义了核心模型from sqlalchemy import create_engine, Column, Integer, String, Float, ForeignKey from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker Base declarative_base() engine create_engine(sqlite:///movie.db, echoFalse) class User(Base): __tablename__ users user_id Column(Integer, primary_keyTrue) age Column(Integer) gender Column(String(1)) occupation Column(String(32)) class Movie(Base): __tablename__ movies movie_id Column(Integer, primary_keyTrue) title Column(String(128)) genres Column(String(64)) # 类型字段用 | 分隔 class Rating(Base): __tablename__ ratings id Column(Integer, primary_keyTrue) user_id Column(Integer, ForeignKey(users.user_id)) movie_id Column(Integer, ForeignKey(movies.movie_id)) rating Column(Float) timestamp Column(Integer) Base.metadata.create_all(engine)这里把genres定义成单列字符串而不是单独建类型表算是务实的取舍。单独建电影-类型关联表在毕业设计这种体量下过度设计单个字段用|分隔对查询也够用。3.2 阅读源码的推荐顺序和入口定位拿到一个不熟悉的 Python 毕设项目不建议从入口文件开始读。常见做法是先看requirements.txt锁定依赖版本再看项目的包结构。MovieLens 类型项目通常是单模块结构核心文件一般叫recommend.py、cf.py或algo.py入口在main.py或app.py。定位入口的最快方式是检查if __name__ __main__:块的代码路径。下面是一份典型的命令行入口代码# main.py import argparse from recommend import get_recommendations if __name__ __main__: parser argparse.ArgumentParser(description电影推荐系统命令行接口) parser.add_argument(--user, typeint, requiredTrue, help目标用户ID) parser.add_argument(--k, typeint, default10, help推荐数量) parser.add_argument(--algo, choices[user, item, hybrid], defaulthybrid) args parser.parse_args() recs get_recommendations(args.user, args.k, args.algo) for movie_id, score in recs: print(f电影ID: {movie_id:4d} 推荐分: {score:.4f})注意argparse模块如果代码里出现requiredTrue说明系统必须显式指定用户 ID没有续跑逻辑的话每次执行都是冷启动加载模型所以第一次跑的时候会先看到加载进度条或日志输出这一步通常耗时几十秒不是卡死。跑通后再修改--user参数对比 1 号用户和 100 号用户的推荐结果差异。1 号用户评分数量多、兴趣均匀推荐结果会比较稳定冷门用户评分少推荐结果每次都会漂移这是后续评测要重点关注的现象。3.3 没有 Web 界面的纯命令行版本怎么演示部分毕设包的交互层是 Flask Web 页面但也存在只有命令行接口的精简版本。如果你电脑上跑不起来 Flask 或者不想折腾前端完全可以用命令行演示替代答辩时一样能讲清楚。做法是在get_recommendations函数外层加一个格式化输出把电影 ID 转换成可读的电影名def show_recs(user_id, n10): recs get_recommendations(user_id, n) if not recs: print(该用户暂无历史评分无法生成推荐) return print(f为用户 {user_id} 推荐 {n} 部电影) for rank, (movie_id, score) in enumerate(recs, 1): title get_movie_title(movie_id) print(f {rank:2d}. {title} 相似度/预测分: {score:.3f})这段里判断if not recs是必须的因为冷启动用户没有任何评分推荐算法返回空列表如果不加这个分支直接循环打印程序会直接抛TypeError。4. 数据库设计从 ER 图到 SQL 落地4.1 三张核心表的字段设计逻辑评分表是系统的核心它的设计直接决定算法能吃什么数据。标准的 MovieLens 100k 评分表只有四列user_id, item_id, rating, timestamp没有任何业务冗余。这个设计的精髓在于“窄表”——每行只有一条评分事实方便后面用 pandas 的pivot_table做透视也方便 SQL 聚合统计。用户表的occupation字段是个容易被忽略的宝。MovieLens 把职业分成了 21 类从学生到工程师都有。答辩时可以说一句“系统支持按职业维度分析不同群体的观影偏好”然后演示一条 SQL 统计不同职业的平均评分——这比单纯说“我做了 CRUD”要高级得多。电影表的genres字段建议保留|分隔格式别急着拆表。算法模块读这个字段做类型分析时一行split(|)就能拿到类型列表省去 JOIN 的开销。如果你确实想展示第三范式设计可以加一张movie_genres关联表但在 1682 部电影的量级下性能优势完全体现不出来反而增加代码复杂度。4.2 导库后必做的三性检查拿到现成的数据库文件别急着跑推荐脚本。先做三层验证能省下后面好几个小时的排查时间。第一层是完整性检查重点看评分表的主键是否唯一是否出现重复评分。MovieLens 官方数据不会重复但很多毕设包的数据库是用爬虫凑的重复率可能不低SELECT user_id, movie_id, COUNT(*) as cnt FROM ratings GROUP BY user_id, movie_id HAVING cnt 1 LIMIT 20;第二层是范围检查评分值必须落在 1 到 5 之间。如果出现超过 5 的评分算法会把预测值拉偏相似度计算也失去意义SELECT COUNT(*) FROM ratings WHERE rating 1 OR rating 5;第三层是外键一致性检查确保评分表里的用户 ID 和电影 ID 都能在主表里找到。孤立数据会导致相似度矩阵出现无法映射的行推荐结果打印出奇怪的电影 ID。这个检查和标本并不仅仅是洁癖层面的它会直接影响论文里“数据集概况”那一段的价值。比如你可以写“经过数据清洗共发现 37 条重复评分记录与 5 条越界评分删除后得到的有效评分数据为 999 条”这句话让审稿老师相信数据工作是认真做的。这个体量是举例你按自己数据库实际结果来写。4.3 冷启动问题在数据库层面怎么缓解冷启动是推荐系统绕不开的名词英文叫 cold start。新用户注册后没有评分数据算法无米下锅。数据库层面能做两件事一是给用户表加if not exists判断存在性和可选的注册引导字段比如填 3 部喜欢的电影类型入库二是设计默认推荐规则新用户直接拉取全站评分最高的 N 部电影SQL 长这样SELECT m.movie_id, m.title, AVG(r.rating) AS avg_rating, COUNT(r.id) AS rating_count FROM movies m LEFT JOIN ratings r ON m.movie_id r.movie_id GROUP BY m.movie_id ORDER BY avg_rating DESC, rating_count DESC LIMIT 10;注意这里用了LEFT JOIN而不是INNER JOIN目的就是保留没有任何评分的电影也能出现在统计结果里。排序策略上先按平均分降序再按评分人数降序避免一部电影一个人打了 5 分就冲到榜首的作弊现象。5. 避坑指南协同过滤毕设最容易翻车的六个位置5.1 预测分数和相似度被错误当成同一个指标现象推荐列表里的分数忽高忽低同一个算法换一个用户结果变化非常大而且电影名和分数明显不匹配比如《阿甘正传》只给 1.5 分。原因很多新手把相似度矩阵里的值直接当成预测评分输出。对 ItemCF 来说物品相似度是“电影 A 和电影 B 有多像”它本身不代表用户评价要经过加权求和转换成预测分才是可用分数。解决调整get_recommendations里预测分计算公式常见的加权平均写法是def predict_rating(user_ratings, item_similarities, target_item): 预测用户对 target_item 的评分 weight_sum 0.0 score_sum 0.0 for rated_item, rating in user_ratings.items(): sim item_similarities.get(target_item, {}).get(rated_item, 0) if sim 0: weight_sum sim score_sum sim * rating if weight_sum 0: return 0 return score_sum / weight_sum5.2 相似度为 0 的魔法缺失现象冷门用户不管换什么算法推荐列表几乎不变永远返回全站最热电影。原因用户共同评分的电影太少相似度公式算出来全是 0兜底逻辑只有全站热门这一条路。解决设置相似度下限阈值比如min_sim 0.1低于阈值的相似度一律视为无关同时叠加 ItemCF 结果做混合推荐别让 UserCF 独扛大梁。5.3 评测集划分只用了随机切分现象论文里的准确率在 90% 以上看起来漂亮但现场演示给一个新用户推荐的电影明显不对味。原因随机切分没有考虑时间顺序把用户未来的评分当训练数据相当于开卷考试。解决统一按时间戳切分前 80% 评分做训练、后 20% 做测试代码先排序时间戳再按位置切ratings_data.sort_values(timestamp, inplaceTrue) train_end int(len(ratings_data) * 0.8) train_set ratings_data.iloc[:train_end] test_set ratings_data.iloc[train_end:]5.4 精度指标只有 RMSE没有覆盖率现象答辩时老师问“你的推荐效果到底怎么样”你只能报一个 0.98 的 RMSE听起来很高但无从验证参考系。原因只算了预测评分误差没算系统能推荐出来的商品在全部商品里的占比。解决补上两个最常被追问的统计指标——准确率推荐列表里有用户真正看过的比例和覆盖率被推荐到的物品数占总物品数的比例。写进论文图表里比单一 RMSE 更能撑场面。5.5 数据库文件路径写死导致换机器跑崩现象你在一台电脑上跑通换到答辩机器上双击没反应或者报了拿到FileNotFoundError的错误。原因源码里数据库路径用的是绝对路径比如C:\\Users\\Hermione\\Desktop\\movie.db。解决统一改成相对路径用os.path.dirname(__file__)定位当前文件import os BASE_DIR os.path.dirname(os.path.abspath(__file__)) DB_PATH os.path.join(BASE_DIR, database, movie.db)5.6 Python 版本不匹配现象代码里用了print当函数、with open(..., encodingutf-8)这些 Python 3 语法但你命令行输入python打开的是 Python 2 的解释器直接报语法错误。原因刚配好的电脑python命令被老版本占了或者你在虚拟环境外跑。解决先用python --version确认版本再用pip install -r requirements.txt装依赖。如果系统默认 Python 是 2.x直接改用python3和pip3命令别费劲手动切换路径。6. 提升答辩质量的验证方法和进阶用法毕设项目跑通只算及格线想在答辩时拿出硬货建议做一组对比实验。我在做这类项目时习惯固定住数据集和切分规则然后对 UserCF、ItemCF、混合策略三个版本分别计算 RMSE 和 Top-N 准确率算法版本RMSE测试集Top10 准确率覆盖率UserCF (k10)0.98312.4%28.5%ItemCF (top_n30)0.94714.8%35.1%混合策略 (0.7/0.3)0.92616.2%33.7%这些数字是参照典型数据集水平写的你的结果会不同但趋势通常一致混合后 RMSE 最低、准确率最高覆盖率略降但可接受。表格放进论文或答辩 PPT能给老师一个直观的信号——你做了横向对比不是只调了调参数。进阶用法方面k值邻居数是对 UserCF 影响最大的参数。我用脚本从 5 到 50 每隔 5 跑了一个点发现 RMSE 在 k 等于 20 到 30 之间触底再大反而缓慢上升。这是因为邻居太多会把低相似度的“泛泛之交”也拉进决策边际贡献变成噪声。如果你发现自己的曲线没有这个转折多半是相似度计算里没有做负数截断。最后一件事把requirements.txt里没写全的隐式依赖都补上。我记得自己当年跑 precheck 的时候项目里用了scikit-learn的train_test_split但依赖清单里只列了numpy和pandas最后在答辩机器上现场装包等了三分钟才缓过来。现在拿到任何项目第一件事就是pipreqs . --force重新生成一份依赖清单这算是我个人的血泪经验总结。在这个方向上投入的时间不会白费。协同过滤虽然是老算法但它背后“数据到矩阵、矩阵到策略”的思维链路你在做基于内容推荐、做图神经网络推荐时都要重新捡起来。答辩落幕后如果你有精力建议再给项目补一个简单的 Flask 界面把推荐结果在线可视化这是毕业设计从“能跑”到“像产品”的最后一层台阶。希望帮到你。本文还有配套的精品资源点击获取
返回列表