ARTICLE DETAIL

资讯详情

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

Python电影推荐系统:协同过滤与内容推荐算法毕业设计实战

Python电影推荐系统:协同过滤与内容推荐算法毕业设计实战 简介基于Python实现的电影推荐系统完整项目涵盖物品协同过滤、用户协同过滤与基于内容的推荐算法并将三种方法结合为混合推荐系统适合作为毕业设计参考或推荐系统入门学习材料。包内含2000个文件以1977张图片多为电影海报、数据集样本或界面截图、11个Python脚本、5个HTML页面及配套样式脚本为主压缩包整体约257MB目录结构清晰便于对照学习。项目代码覆盖数据预处理、特征提取、模型训练与评价等完整流程并配有可交互的推荐展示页面能帮助读者直观理解不同算法在电影推荐场景下的实现差异与混合推荐思路。目前已有112人学习下载特别适合需要快速搭建推荐系统原型并进行算法对比的初中级开发者。1. 为什么这套电影推荐系统适合直接拿来做毕业设计每到毕业设计季推荐系统都是计算机类、数据科学类专业的常客电影推荐系统又格外合适数据现成、算法链路短、推荐结果还能直接看到界面反馈。这套基于 Python 的方案把物品协同过滤、用户协同过滤和基于内容的推荐算法放进同一个项目意味着论文里可以做算法对比实验而不只是演示一种算法。你用它跑通数据、调出 Top-N 推荐、画评估曲线、写进系统设计文档恰好覆盖毕业设计最关键的“理论 实现 评估”三段。适合两类人一是选了推荐系统题目的本科生需要一个结构清楚、能随时改参数的起点二是想快速把协同过滤落地成可演示系统的开发者需要一份不依赖重型框架的参考实现。2. MovieLens 数据清洗与评分矩阵推荐系统的地基怎么搭任何推荐算法吃的都是“谁在什么时间给哪个物品打了多少分”这件事。电影推荐系统最经典的数据集是 MovieLens它由明尼苏达大学 GroupLens 研究组维护100K 版本约 10 万条评分1M 版本约 100 万条覆盖 6000 个用户和 4000 部电影。毕业设计用 1M 版本写进论文更有说服力跑起来也不至于让普通笔记本内存吃紧。2.1 用 MovieLens 1M 做原始数据字段结构与加载流程MovieLens 1M 解压后有三个主文件users.dat、movies.dat、ratings.dat字段之间用双冒号::分隔和常见的 CSV 不一样直接pd.read_csv会踩坑。ratings.dat是核心每行四个字段UserID用户编号1 到 6040MovieID电影编号1 到 3952Rating评分1 到 5 的整数Timestamp评分时间戳可以算用户观看时间偏好也能用来做时间维度的训练集划分加载时不能按默认逗号分隔常见做法是手动指定分隔符和列名。import pandas as pd # 指定分隔符为 ::enginepython 是为了避开 C 引擎对多字符分隔符的报错 ratings pd.read_csv( ratings.dat, sep::, enginepython, headerNone, names[UserID, MovieID, Rating, Timestamp] ) movies pd.read_csv( movies.dat, sep::, enginepython, headerNone, names[MovieID, Title, Genres] ) print(ratings.head()) print(ratings.info())这段代码有两点值得注意。第一sep参数不能写:否则会把UserID::MovieID拆成更多列第二enginepython是必须的pandas 的 C 引擎只支持单字符分隔符遇到::会直接抛ParserError。ratings.info()能快速看有没有空值和内存占用如果数据行数和网上说明对不上多半是分隔符写错了。movies.dat里的Genres字段是竖线分隔的字符串比如Comedy|Romance后面做基于内容的推荐时要把这个字段拆成多种类型标签。加载完整之后建议做一步去重和类型转换把UserID、MovieID变成intRating保持整数为构建评分矩阵做准备。# 转类型并去重避免同一用户对同一电影出现两条评分记录 ratings[UserID] ratings[UserID].astype(int) ratings[MovieID] ratings[MovieID].astype(int) ratings[Rating] ratings[Rating].astype(int) duplicated_count ratings.duplicated(subset[UserID, MovieID]).sum() print(f重复评分记录数: {duplicated_count}) # 如果存在重复记录通常保留最近一条 ratings ratings.sort_values(Timestamp).drop_duplicates( subset[UserID, MovieID], keeplast )提示毕业设计论文里的“数据预处理”章节写清楚分隔符、异常值和去重这三个动作就足够不要堆砌无关操作。2.2 构建用户-物品评分矩阵稀疏矩阵与内存控制协同过滤算法在数学上都要处理一个行为矩阵行是用户、列是电影格子是评分。1M 数据集有 6040 个用户和 3952 部电影全量笛卡尔积约 2400 万个格子而实际评分只有 100 万条稀疏度超过 95%这就是“稀疏矩阵”的字面含义。构建矩阵有两个路线。路线一是用pivot_table生成稠密 DataFrame优点是调试方便、能直接打印肉眼检查路线二是用scipy.sparse的csr_matrix优点是内存占用低、后续算相似度更快。毕设项目建议先做稠密矩阵理解逻辑再换成稀疏矩阵写进最终代码。import numpy as np # 路线一pivot_table 生成稠密评分矩阵缺失值填 0 rating_matrix ratings.pivot_table( indexUserID, columnsMovieID, valuesRating ).fillna(0) print(评分矩阵形状:, rating_matrix.shape) print(非零元素数量:, (rating_matrix 0).sum().sum()) # 路线二稀疏矩阵只存非零位置 from scipy.sparse import csr_matrix user_ids ratings[UserID].values movie_ids ratings[MovieID].values scores ratings[Rating].values # 将 UserID 和 MovieID 重新映射为 0 开始的连续索引 user_encoder {uid: i for i, uid in enumerate(sorted(set(user_ids)))} movie_encoder {mid: i for i, mid in enumerate(sorted(set(movie_ids)))} row [user_encoder[uid] for uid in user_ids] col [movie_encoder[mid] for mid in movie_ids] sparse_matrix csr_matrix((scores, (row, col))) print(稀疏矩阵存储条目数:, sparse_matrix.nnz) print(内存占比约为稠密矩阵的:, round(sparse_matrix.nnz / (6040 * 3952), 4))参数说明pivot_table里的index和columns决定了矩阵的行列语义valuesRating指定填充值fillna(0)把未评分位置补齐。稀疏矩阵路线里最关键的是user_encoder和movie_encoder因为csr_matrix要求行和列索引必须是连续整数直接用原始 UserID最大 6040没问题但换成别的数据集时 ID 可能从 1000 开始不映射就会报索引越界或浪费空间。稠密矩阵适合调试稀疏矩阵适合做最终计算这是结构上最常见的分工。实际相似度计算里csr_matrix配合 sklearn 的cosine_similarity能直接加速稠密矩阵反而会因为内存溢出拖慢。2.3 四类相似度计算怎么选余弦、皮尔逊、Jaccard 与修正余弦协同过滤的“物以类聚”全靠相似度公式实现选错公式会让推荐结果出现明显偏差。常用四类对比相似度方法适用场景特点计算对象余弦相似度用户或物品向量忽略向量的模长只比方向评分向量或特征向量皮尔逊相关系数评分数据有个人评分尺度差异自动做均值中心化消除“从严打分”和“从宽打分”两个用户/物品的评分向量修正余弦物品评分受用户习惯影响明显时减去用户均值再算余弦物品相似度Jaccard 相似度布尔型偏好数据只看是否交集不看具体分数用户购买/收藏集合代码实现上sklearn.metrics.pairwise里的余弦和皮尔逊都有现成函数但皮尔逊和修正余弦需要手工做中心化。from sklearn.metrics.pairwise import cosine_similarity # 物品余弦相似度直接用稀疏矩阵的转置物品作为行向量 item_sim_cosine cosine_similarity(sparse_matrix.T) # 皮尔逊对每个用户评分做均值中心化 rating_mean sparse_matrix.mean(axis1) # 中心化把用户自己的平均分减掉消除打分尺度差异 rating_centered sparse_matrix - rating_mean item_sim_pearson cosine_similarity(rating_centered.T) # Jaccard先将评分转为二值是否看过 binary_matrix (sparse_matrix 0).astype(np.float64) intersection binary_matrix.dot(binary_matrix.T) # 共现次数 union intersection binary_matrix.shape[0] - binary_matrix.sum(axis0) jaccard_sim intersection / union参数说明cosine_similarity(sparse_matrix.T)表示把每部电影的所有用户评分当作向量计算电影间的余弦夹角输出矩阵形状是(电影数, 电影数)。皮尔逊的关键是sparse_matrix - rating_mean这一步把每个用户的评分减掉其个人均值让宽严尺度不同的用户可以比较。Jaccard 不用分数只看看过与否binary_matrix.dot(binary_matrix.T)算的是两两电影共同被评分的用户数分母是并集数量——注意这里的binary_matrix.sum(axis0)返回的是每部电影被评分的总次数。普通余弦适合内容推荐里的文本特征向量皮尔逊适合用户评分行为Jaccard 适合“是否看过”的点击流数据。实际做电影推荐系统时评分矩阵优先用皮尔逊或修正余弦因为 MovieLens 数据里用户打分习惯差异很明显——有人只打 4 和 5有人专门补低分。3. 把物品协同过滤从公式变成可跑的 Top-N 推荐物品协同过滤ItemCF的核心逻辑一句话就能讲清用户给电影 A 打了高分系统找出和 A 最相似的一批电影推荐给他。这套方案在电商和视频网站里用得最多因为它离线阶段把物品间相似度算好在线推荐时不需要实时遍历全量用户响应快还容易解释“因为你看过《盗梦空间》所以推荐《星际穿越》”。3.1 ItemCF 的推荐逻辑相似度矩阵到候选集过滤物品协同过滤分两步走。离线阶段计算所有电影两两之间的相似度存成矩阵在线阶段拿到某个用户打过分的电影列表把每部电影的前 K 个相似电影拉出来按加权得分排序过滤掉用户已经看过的。得分公式是最重要的参数对候选电影 j它被推荐给用户 u 的分数等于“用户给已看电影 i 的评分”乘“i 和 j 的相似度”再加权求和。def recommend_by_itemcf(user_id, rating_matrix, item_sim_matrix, top_k10, k_neighbors20): 物品协同过滤推荐 :param user_id: 目标用户 ID :param rating_matrix: 用户-物品评分矩阵 :param item_sim_matrix: 物品相似度矩阵 :param top_k: 最终返回的推荐电影数量 :param k_neighbors: 每个已看物品取最相似的多少部候选 # 取出该用户的全部评分记录非零位置 user_scores rating_matrix.loc[user_id] if hasattr(rating_matrix, loc) else rating_matrix[user_id] rated_items user_scores[user_scores 0] scores {} for item_id, rating in rated_items.items(): # 得到与当前物品最相似的 k 个邻居 sim_items item_sim_matrix.loc[item_id].sort_values(ascendingFalse)[1:k_neighbors 1] for neighbor_id, sim_score in sim_items.items(): # 已经看过的电影不再推荐 if neighbor_id in rated_items.index: continue # 用户评分越高相似物品的加权分越高 scores[neighbor_id] scores.get(neighbor_id, 0) sim_score * rating # 候选集按得分排序取前 top_k if not scores: return [] sorted_scores sorted(scores.items(), keylambda x: x[1], reverseTrue) return [movie_id for movie_id, _ in sorted_scores[:top_k]]逻辑说明代码里最容易被忽略的是sim_items的取值sort_values降序后要从第二个元素开始取因为第一个元素是物品自己和自己的相似度恒为 1必须跳过。scores[neighbor_id]用get累加而不是直接赋值是因为同一部候选电影可能通过多条路径到达用户——用户看过两部电影都和它相似得分要叠加。参数影响k_neighbors越小推荐越集中在少数热门电影上越大越容易混入相似度很低的噪音。MovieLens 1M 上常见做法是取k_neighbors20对应系统运行时间和效果的平衡点。top_k是界面展示数量通常取 10论文实验里取 5 和 10 各跑一轮做对比。3.2 计算电影相似度矩阵的真实代码与参数调优相似度矩阵是整个 ItemCF 的性能瓶颈。1M 数据有约 4000 部电影全量两两计算要跑约 800 万次纯 Python 双循环会卡到怀疑人生。正确做法是用 sklearn 的向量化实现并且考虑稀疏格式。from sklearn.metrics.pairwise import cosine_similarity # 假设 rating_df 是 pandas 的稠密评分矩阵行用户列电影 # 先转成 numpy 数组加速 rating_array rating_df.values # 计算物品相似度物品是列需要转置 # 这里用稀疏矩阵更稳数据量大时稠密矩阵会内存爆炸 from scipy.sparse import csr_matrix sparse_rating csr_matrix(rating_array) # min_row 过滤掉只有极少数用户看过的冷门电影减少噪音 item_sim cosine_similarity(sparse_rating.T, dense_outputFalse) # 转成 DataFrame 方便按行取值 item_sim_df pd.DataFrame( item_sim.toarray() if hasattr(item_sim, toarray) else item_sim, indexrating_df.columns, columnsrating_df.columns ) # 相似度对角线和阈值清理 np.fill_diagonal(item_sim_df.values, 0) item_sim_df[item_sim_df 0] 0 # 负相似度在推荐里没有意义直接置零参数说明cosine_similarity的dense_outputFalse让它在可能的情况下返回稀疏结果节省内存np.fill_diagonal把自相似度置 0避免后面推荐阶段把“自己”也列进候选负数相似度置零是因为负相关评分在电影推荐场景里通常表示“看过 A 的人讨厌 B”这种反向信息对 Top-N 推荐没有直接帮助反而会增加噪声。矩阵算完后落盘推荐矩阵能省下大量重复计算时间。毕业设计里把相似度矩阵存成.npy或者 CSV每次启动系统直接加载不用重新算一遍。3.3 消除热门物品偏差为什么要做物品活跃度惩罚ItemCF 有个著名的副作用《肖申克的救赎》《阿甘正传》这种被几百万人评分过的电影会和几乎所有电影都算出不低的相似度。原因是用户行为数据本身就偏向热门两部电影因为都被很多人看过而“被相似”不是因为内容真像。这让推荐结果趋同每个人都收到一样的榜单。学术界的标准解法是给相似度加“热门惩罚”最常见的是对共现次数做变换。# 物品被评分次数相当于物品热度 item_popularity (sparse_rating 0).sum(axis0).A1 if hasattr(sparse_rating, A1) else (sparse_rating 0).sum(axis0) # 将余弦相似度除以 log(1 热度) 进行惩罚 # 把稀疏矩阵转为稠密再计算 co_occurrence sparse_rating.T.dot(sparse_rating).toarray() item_pop np.array(item_popularity).flatten() # 热门惩罚系数log 防止过度惩罚中等热门物品 penalty np.log1p(item_pop) # log1p log(1x) penalized_sim co_occurrence / (penalty[:, None] penalty[None, :] 1e-9) # 归一化为标准相似度 norm_factor np.sqrt((sparse_rating 0).sum(axis0).A1) # 略去向量取模直接调用 sklearn 的 cosine_similarity 更稳实际上很多生产级实现直接用“带惩罚的共现计数”替代原始评分sim(i, j) 共同评分用户数 / (log1p(热度i) * log1p(热度j))。这个公式里log1p是关键它让热度从 1 到 100 万的变化被压到 0 到 14 的区间避免超级热门电影把冷门优质电影完全挤出推荐列表。注意加了热度惩罚之后推荐结果的多样性会提升但离线评估的精确率往往会略降。论文里写“采用惩罚系数后多样性提升 15%”比只写精确率更有说服力。4. 用户协同过滤与基于内容的推荐邻居选择与特征向量的落地物品协同过滤解决“看了 A 的人也会看 B”用户协同过滤则是“和你口味像的人看什么你也很可能喜欢”。基于内容的推荐走另一条路不看任何人的行为只看电影本身的属性。三个算法放一起做对比正好让毕业设计的创新点落在“多算法比较与混合策略”上。4.1 UserCF 的邻居选择K 值、评分归一化与交集用户数下限用户协同过滤的精髓是找到“相似用户”然后把这些邻居喜欢过的、目标用户没看过的电影按加权汇总推荐。和 ItemCF 相比UserCF 的相似度矩阵是(用户数, 用户数)1M 数据集是 6040x6040计算量反而比物品矩阵小。def recommend_by_usercf(user_id, rating_matrix, user_sim_matrix, top_k10, neighbor_k30): 用户协同过滤推荐实现 :param user_id: 目标用户 :param rating_matrix: 稠密评分矩阵 :param user_sim_matrix: 用户相似度矩阵 :param neighbor_k: 邻居数量 user_index rating_matrix.index.get_loc(user_id) # 取出该用户与所有用户的相似度自己排除掉 sim_scores user_sim_matrix.iloc[user_index].drop(indexuser_id) # 取前 neighbor_k 个相似用户 top_neighbors sim_scores.sort_values(ascendingFalse).head(neighbor_k) scores {} score_details {} for neighbor_id, sim in top_neighbors.items(): # 邻居的评分记录 neighbor_ratings rating_matrix.loc[neighbor_id] neighbor_rated neighbor_ratings[neighbor_ratings 0] for movie_id, rating in neighbor_rated.items(): # 跳过用户已看过的电影 if rating_matrix.loc[user_id, movie_id] 0: continue # 加权评分邻居的分越高、相似度越高贡献越大 scores[movie_id] scores.get(movie_id, 0) sim * rating score_details.setdefault(movie_id, []).append((neighbor_id, sim, rating)) sorted_items sorted(scores.items(), keylambda x: x[1], reverseTrue) return [mid for mid, _ in sorted_items[:top_k]]逻辑说明sim * rating是这里的核心乘法它假设“相似度高的用户的评分更有参考价值”。但这里有一个隐藏缺陷不同用户的评分尺度不同一个习惯打 5 分的用户和一个习惯打 3 分的用户即使口味完全一致贡献的分数也会差一大截。解决办法是在计算之前做评分归一化常见做法是减去用户均值后再加权也可以用 Z-score。归一化之后还要设置一个最小交集限制如果两个用户共同看过的电影不到 5 部算出来的相似度可能是巧合而不是共识。下面的代码是在选邻居时增加过滤条件。# 计算每个用户评分均值 user_mean rating_matrix[rating_matrix 0].mean(axis1) # 评分归一化减去用户自己的均值 rating_normalized rating_matrix.apply(lambda x: x - user_mean[x.name] if x.name in user_mean.index else x, axis1) # 再算用户相似度矩阵 from sklearn.metrics.pairwise import cosine_similarity user_sim_matrix cosine_similarity(rating_normalized.fillna(0)) # 设定最小共同评分数量阈值共同看过少于 5 部电影的用户对直接视为不相似 co_rated_count (rating_matrix 0).astype(int).dot((rating_matrix 0).astype(int).T) min_intersection 5 user_sim_matrix[co_rated_count.values min_intersection] 0参数说明min_intersection5这个值是经验阈值MovieLens 数据上取 3 到 5 比较合理取太大比如 20会让很多用户根本找不到邻居。它的作用是砍掉“只共同看过一两部电影但恰好评分相同”的虚假相似关系这类噪声是把垃圾推荐推给用户的元凶。4.2 基于内容的推荐把电影类型、导演、演员编码成特征向量基于内容的推荐不依赖用户行为历史它把电影转换为一组属性特征然后找“和你喜欢的电影在内容上相似”的影片。这也是处理新电影冷启动的唯一手段一部电影刚上线没有任何评分协同过滤全部失灵但它的类型、导演、演员已经在数据库里内容推荐可以直接把它推到相关用户面前。特征编码分两类。第一类是类别型字段比如电影类型Genres适合用 One-Hot 编码第二类是文本型字段比如Title里的关键词适合用 TF-IDF 向量。对于 MovieLens 1M最稳定的组合是类型字段。# 电影数据里 Genres 字段是 Comedy|Romance 格式 movies[genre_list] movies[Genres].str.split(|) # One-Hot 编码为每个类型建一列电影包含该类型则为 1 from sklearn.preprocessing import MultiLabelBinarizer mlb MultiLabelBinarizer() genre_matrix mlb.fit_transform(movies[genre_list]) genre_df pd.DataFrame(genre_matrix, columnsmlb.classes_) # 拼回电影 ID形成内容特征矩阵 content_features pd.concat([ movies[[MovieID]], genre_df ], axis1) print(content_features.head()) print(特征维度:, content_features.shape[1] - 1)编码完成后电影之间的内容相似度可以直接用余弦相似度算。这里有一个小巧思不同类型的特征权重不一样类型相同但一个是剧情片一个是纪录片用户接受度差异很大。可以做加权——给类型列乘一个权重系数再算相似度。毕业设计里如果只做基础版直接用 One-Hot 结果就好。# 基于内容特征的电影相似度 from sklearn.metrics.pairwise import cosine_similarity feature_array content_features.drop(columns[MovieID]).values content_sim cosine_similarity(feature_array) # 转成 DataFrame 便于检索 content_sim_df pd.DataFrame( content_sim, indexcontent_features[MovieID], columnscontent_features[MovieID] ) # 推荐函数用户打分过的电影各取相似电影汇总排序 def recommend_by_content(user_rated_df, content_sim_df, top_k10): user_rated_df: 用户给电影的评分记录只有 MovieID 和 Rating 两列 scores {} # 用户历史评分里权重最高的前 5 部 rated_sorted user_rated_df.sort_values(Rating, ascendingFalse).head(5) for _, row in rated_sorted.iterrows(): movie_id row[MovieID] rating row[Rating] if movie_id not in content_sim_df.index: continue sim_items content_sim_df.loc[movie_id].sort_values(ascendingFalse)[1:11] for candidate_id, sim in sim_items.items(): if candidate_id in user_rated_df[MovieID].values: continue scores[candidate_id] scores.get(candidate_id, 0) sim * rating sorted_rec sorted(scores.items(), keylambda x: x[1], reverseTrue) return [mid for mid, _ in sorted_rec[:top_k]]参数说明取“用户评分最高的前 5 部电影”作为内容推荐种子是一个常用压缩策略避免用户看过 300 部电影时把候选集撑爆。每个种子电影取最相似的 10 部候选和 ItemCF 里的k_neighbors是同一类参数控制召回范围。内容推荐有个天然局限它只能推荐和用户历史标签相似的电影跨类型探索能力弱这就是为什么真实系统里它要和协同过滤混合使用。4.3 三种算法的适用场景对比与推荐效果倾向把三种算法放到同一个项目里如果只用一句话描述各自倾向可以这样概括算法依赖数据冷启动表现推荐多样性计算开销结果解释性物品协同过滤用户-物品评分新用户差、新电影差中离线算物品矩阵在线快强“你看过 A 所以推荐 B”用户协同过滤用户-物品评分新用户差、新电影差较强能发现跨类型在线要取邻居用户多时慢中“与你相似的用户喜欢”基于内容物品属性特征新电影效果好弱只推同类型离线算特征在线快强“影片类型和导演相似”在 MovieLens 1M 上做离线实验一般结论是物品协同过滤的精确率略优于用户协同过滤因为它基于物品的相似关系更稳定用户协同过滤在用户行为稀疏时波动大基于内容的推荐精确率通常最低但能保证新电影的推荐覆盖。毕业设计若能跑出这个结论论文的“实验结果与分析”章节就有了扎实的素材。5. 毕业设计最容易翻车的四个位置冷启动、稀疏性、评估指标与展示不一致推荐系统跑通 demo 不难但做毕业设计要过“答辩 查重 工作量审核”三道关这里面的坑往往不是算法不会写而是忽略了一些工程细节。下面四条是我在实际跑数据时翻车最频繁的地方。5.1 冷启动翻车新用户、新电影没有评分推荐结果为空现象在系统里注册一个新账号或者往电影表里插入一部 2026 年上映的新片推荐列表直接空白前端展示什么都没有。原因协同过滤是纯行为驱动的新用户没有评分记录rating_matrix.loc[user_id]取出来全是 0UserCF 和 ItemCF 都找不到入口新电影没有用户看过它在相似度矩阵里和其他所有电影的相似度都是 0。解决推荐系统真相是“冷启动无法靠协同过滤解决只能靠内容推荐和策略兜底”。代码里要给推荐函数加一个兜底分支。def safe_recommend(user_id, rating_matrix, item_sim_df, content_sim_df): 冷启动安全推荐优先 ItemCF无数据时回退到内容推荐再没有就用热门榜 # 用户是否有评分行为 user_rated rating_matrix.loc[user_id] user_rated_items user_rated[user_rated 0] if len(user_rated_items) 3: return recommend_by_itemcf(user_id, rating_matrix, item_sim_df) elif len(user_rated_items) 0: # 评分记录太少ItemCF 结果不可信改用内容推荐 user_df pd.DataFrame({ MovieID: user_rated_items.index, Rating: user_rated_items.values }) return recommend_by_content(user_df, content_sim_df) else: # 完全没评分返回全站热门电影 movie_rating_cnt (rating_matrix 0).sum(axis0) hot_movies movie_rating_cnt.sort_values(ascendingFalse).head(10).index.tolist() return hot_movies注意这个兜底逻辑建议直接写进论文的核心模块说明里它能体现你对推荐系统边缘场景的理解比单纯实现三个算法加分更明显。5.2 稀疏矩阵让相似度失真电影共同评分人数过少产生巧合高分现象某部小众文艺片和一部同年的纪录片算出的相似度高达 0.95排在推荐榜第一位点进详情才发现两部电影只是被同 6 个人评过分且这 6 个人口味很像。原因MovieLens 数据稀疏度超过 95%大部分电影只有几十条评分。余弦相似度只看两个向量的夹角样本量为 6 和样本量为 6000 的电影对算出的相似度数值没有可比性样本少的对更容易产生极端接近 1 的值。解决在相似度计算后按“共同评分人数”设置过滤阈值这是最朴素也最有效的手段。# 计算电影间共同评分人数 co_rated (rating_matrix 0).astype(int).T.dot((rating_matrix 0).astype(int)) # 将共同评分人数少于 min_common 的相似度直接置零 min_common 10 item_sim_df[co_rated min_common] 0 # 再对每行做归一化让相似度范围更适合加权计算 item_sim_df item_sim_df.div(item_sim_df.sum(axis1), axis0).fillna(0)参数说明min_common10在这里表示两部电影至少要有 10 个共同评分用户它们的相似度才被采信。调参时如果推荐列表全是冷门长尾电影说明阈值太高把主流电影间的相似关系也砍掉了如果前排全是热门片说明阈值太低巧合高分混了进来。这是推荐系统里最典型的“玄学调参”位置。5.3 评估指标选错MAE 与 Top-N 命中率各说明什么问题现象论文里用 RMSE 评估三种算法结果基于内容的推荐表现“最好”但实际打开推荐页面觉得它推的东西明显更窄。原因RMSE 和 MAE 衡量的是“评分预测误差”也就是预测用户会给某部电影打几分与真实评分的差距。基于内容的推荐在做评分预测时容易往中间值靠误差反而小但它不负责“发现新兴趣”所以 Top-N 推荐命中率很差。你拿评分预测指标去验证推荐列表质量两套目标错位了。解决毕业设计里评估三类推荐算法要同时用两组指标。评分预测用 RMSE推荐列表用 PrecisionN、RecallN 和 HitRateN。这组指标专门衡量“用户真实看过且打高分的电影是否出现在你的推荐前 N 位里”。def evaluate_hitrate(recommender_func, rating_matrix, test_ratings, top_n5): HitRateN测试集中用户看过的电影有多少被推荐进 Top-N hits 0 total 0 for user_id, movie_id in test_ratings[[UserID, MovieID]].values: rec_list recommender_func(user_id, rating_matrix, top_ktop_n) if movie_id in rec_list: hits 1 total 1 return hits / total if total 0 else 0参数说明test_ratings是划分出的测试集只取真实存在的用户-电影评分对。hits / total的语义是测试集里这些电影被推荐系统命中前 N 的概率数值越高说明推荐算法越能在真实行为里生效。毕设论文里建议把 Precision、Recall、HitRate、RMSE 四个指标画成一张对比表逐项解释差异原因。5.4 离线评估与在线展示对不上的坑随机种子与数据划分现象离线实验显示 ItemCF 精确率 8%结果答辩演示时输入一个老用户 ID推荐出来的 10 部电影里有 7 部是这个用户已经看过的。原因数据划分时没有去掉训练集里已经出现的评分代码在计算推荐时只过滤了“用户当前输入时能看到的评分记录”训练集里的历史行为没有从展示库中排除。这是毕业设计里最容易出现的时间线错位。解决严格按时间顺序划分训练集和测试集推荐演示时把所有出现在训练集里的评分视为“已看过的历史”从候选集里过滤掉。# 按时间排序后取前 80% 作为训练集 ratings_sorted ratings.sort_values(Timestamp) split_idx int(len(ratings_sorted) * 0.8) train_set ratings_sorted.iloc[:split_idx] test_set ratings_sorted.iloc[split_idx:] train_matrix train_set.pivot_table( indexUserID, columnsMovieID, valuesRating ).fillna(0) # 演示系统里的用户历史 训练集里该用户的所有记录 # 候选集必须排除这些电影 user_history train_matrix.loc[target_user] user_history user_history[user_history 0].index # 推荐函数里已经跳过这些历史电影见 recommend_by_itemcf 的 continue 逻辑提示生成训练集矩阵后检查一下测试集中有多少“用户-电影”对在训练矩阵里根本不存在的边。占比超过 5% 时建议把冷启动用户单独拿出来做一组实验否则评估结果里混入了完全无法推荐的记录数值会被拉低。6. 毕业设计验收技巧用一份对比实验证明三个算法都有效系统跑通只是及格线答辩老师最看重的是“你有没有验证过它”。最后这一步建议花半天时间做出一张三个算法的评估对比表这比贴十行代码更值钱。6.1 用五折交叉验证和留一法给三个算法打分评估不能只跑一次随机划分会带来方差。常见做法是对全量评分做五折交叉验证每个算法跑五轮取均值。MovieLens 1M 上三个算法各跑五轮普通笔记本约一小时内能完成。如果时间紧可以退一步用留一法每个用户抽最近一条评分做测试其余做训练最后只报 HitRate10。留一法对冷启动用户惩罚更大但数据划分逻辑更容易写进论文。from sklearn.model_selection import KFold kf KFold(n_splits5, shuffleTrue, random_state42) records [] for fold, (train_idx, test_idx) in enumerate(kf.split(ratings)): train_data ratings.iloc[train_idx] test_data ratings.iloc[test_idx] # 用 train_data 构建矩阵 train_matrix train_data.pivot_table( indexUserID, columnsMovieID, valuesRating ).fillna(0) # 分别评估三种算法记录 HitRate10 和 RMSE hit_item evaluate_hitrate(lambda uid, m, k: recommend_by_itemcf(uid, m, item_sim_df), train_matrix, test_data, top_n10) records.append({ fold: fold, HitRate_ItemCF: hit_item })逻辑说明random_state42必须固定否则每跑一次实验划分不同论文里的指标无法复现。答辩时要能现场重跑实验并复现相同数值这比口头解释更有说服力。6.2 把推荐结果落成可视化对比表算法评估最终要落在可读的表格里。毕设系统里的推荐展示页面建议做成三列对比同一输入用户分别展示 ItemCF、UserCF、Content-based 的 Top-5 结果并标出每部电影的类型和豆瓣风格评分段。三列里能明显看出内容推荐“只推同类型”的局限以及协同过滤“能跨类型”的优势答辩时一句话就能讲清楚差异。用户历史高分看过《肖申克的救赎》《教父》《低俗小说》ItemCF Top-5UserCF Top-5Content Top-5结果倾向高评分影片的相似作品相似用户的泛兴趣犯罪/剧情类型影片预期命中情况偏高结果偏大众中偶尔有意外稳定但不新鲜在系统代码里把三个算法结果合并到同一个页面、用不同标签页切换就能同时满足“功能展示”和“论文对比实验”两个需求。6.3 我自己养成的两个习惯一是每次改参数都把前一次的实验结果存一个副本文件名带上参数值。推荐系统这行的指标浮动非常大没有记录调参就等于没有发生过。二是答辩前必做一次“随机用户抽查”随机抽 5 个用户把推荐结果和他们的真实评分历史对照着看一遍发现推荐列表里出现用户已经看过但没标记为过滤掉的电影要马上回去查历史过滤逻辑。这套基于 Python 的电影推荐系统价值就在于三个算法互相补位——ItemCF 稳定、UserCF 探索、Content 兜底冷启动毕业设计里既做出效果也留足了可扩展的混合推荐方向。希望你在这套代码上跑出来的第一份评估指标比自己预期的更好。希望帮到你。本文还有配套的精品资源点击获取
返回列表