简介一份面向计算机相关专业毕业生与毕业设计初学者的论文文档主题为基于Python与协同过滤算法的电影推荐系统设计与实现。内容从研究背景、目的意义展开完整覆盖系统架构、Django框架应用、MySQL数据库设计、管理员与用户功能模块、电影评分推荐流程等关键章节并配有摘要、关键词与目录结构便于快速把握毕业设计写作框架和实现思路。资源为单个doc文档共1份文件包体大小2.43MB适合作为论文撰写、系统设计与答辩准备阶段的参考样例。目前已有80人学习读者可借助其中对协同过滤推荐机制、Python技术选型、数据规范化管理及安全问题的说明理解电影推荐系统从需求分析到设计落地的全过程也可借鉴其功能模块划分和界面布局思路迁移到其他信息管理系统类课题中。1. 想用 Python 做协同过滤电影推荐这篇能帮你少走三个月的弯路每年毕业季都能看到一批「电影推荐系统」方向的论文题目而其中九成以上的核心算法都落在协同过滤这个点上。说白了协同过滤就是靠「相似的人爱看什么」和「相似的电影被谁喜欢」来给你推片不需要理解电影内容只靠用户的历史行为矩阵就能出结果。这个方向之所以被毕设和简历项目反复选中是因为它数据好找、指标能算、代码量适中还天生带一个完整的工程链路数据清洗、相似度计算、召回排序、离线评估。如果你正卡在「论文题目有了但代码和实验不知道从哪下手」的状态这篇就是按一条可复现的路径把整个系统讲透数据集怎么来、UserCF 和 ItemCF 怎么落地、评估指标怎么算、哪几个坑会让你的实验结果忽高忽低。不管是拿来交差还是想真正搞懂照着做都能省下大量试错时间。2. 数据从哪来MovieLens 数据集和评分矩阵的构建2.1 为什么电影推荐项目首选 MovieLens而不是自己写爬虫做协同过滤第一步必须有用户对物品的评分行为数据。自己写爬虫去抓豆瓣或 IMDb 的评分一是反爬策略会让你怀疑人生二是爬下来的数据字段乱七八糟清洗成本远大于算法本身的成本。业内做推荐方向研究的默认数据集就是 MovieLens它由明尼苏达大学 GroupLens 研究组维护提供了多个规模档位MovieLens 100K、1M、10M 和 25M。其中 100K 适合算法验证1M 是毕设论文的黄金档位——数据量足够让训练和评估有意义又不至于让普通笔记本跑不动。MovieLens 1M 包含约 100 万条评分记录覆盖 6000 多个用户和 4000 多部电影评分区间是 1 到 5 的整数。数据文件是纯文本格式三个核心文件分别为users.dat、movies.dat、ratings.dat字段之间用双冒号::分隔。之所以反复强调用这个数据集是因为你的论文里需要交代数据来源和基本统计信息MovieLens 有配套的 README 文档里面的字段说明可以直接引用到论文的「数据集描述」章节。在动手写代码之前先把原始数据读进来并做一个基本的探查。这里直接用 pandas 来处理注意sep参数要指定为双冒号同时加上enginepython否则 pandas 的正则引擎会在这里报警告。import pandas as pd # 读取评分数据列名按 MovieLens 官方字段定义 ratings pd.read_csv( ratings.dat, sep::, enginepython, names[user_id, movie_id, rating, timestamp], encodinglatin-1 ) # 读取用户信息 users pd.read_csv( users.dat, sep::, enginepython, names[user_id, gender, age, occupation, zip_code], encodinglatin-1 ) # 读取电影信息 movies pd.read_csv( movies.dat, sep::, enginepython, names[movie_id, title, genres], encodinglatin-1 ) # 打印数据规模和前几行确认读取正确 print(fratings shape: {ratings.shape}) print(ratings.head()) print(funique users: {ratings[user_id].nunique()}) print(funique movies: {ratings[movie_id].nunique()}) print(frating distribution:\n{ratings[rating].value_counts().sort_index()})这段代码做了三件事把三个数据文件加载成 DataFrame、确认数据规模与官方描述一致、查看评分的分布情况。输出里ratings shape应该大致是 1000209 行四列unique users为 6040 左右unique movies为 3700 左右。如果数字出入太大说明数据集下载的不是 1M 版本后面所有实验基准都会受影响。接下来要生成论文里最常出现的那张图评分分布柱状图。这个图放在论文「数据探索」小节能有效充实现有工作评审老师也爱看。2.2 把评分表压缩成稀疏矩阵user-item 评分矩阵的两种表示协同过滤算法的输入本质上是一个矩阵 ( R )行是用户、列是电影单元格 ( R_{ui} ) 表示用户 u 对电影 i 的评分。但现实生活中大多数用户只看过极少一部分电影所以这个矩阵极度稀疏——1M 数据集里用户和电影的笛卡尔积大概是 6000×3700 约 2200 万个格子评过的只有 100 万格稀疏度高达 95% 以上。在 Python 里表示这个矩阵有两种常见做法。第一种是用pandas.DataFrame.pivot透视得到行是用户、列是电影的稠密表格没评过的位置填 0 或 NaN。第二种是直接用scipy.sparse的稀疏矩阵存储内存占用小几个数量级。毕设阶段用 DataFrame 就够了但如果之后想换更大的数据规模稀疏矩阵是必须掌握的。# 用 pivot 生成 user-item 矩阵缺失评分填 0 user_item_matrix ratings.pivot_table( indexuser_id, columnsmovie_id, valuesrating, fill_value0 ) print(fuser-item matrix shape: {user_item_matrix.shape}) print(fmatrix sparsity: {(user_item_matrix 0).sum().sum() / user_item_matrix.size:.4f}) # 转成稀疏矩阵格式为后续计算加速做准备 from scipy.sparse import csr_matrix sparse_matrix csr_matrix(user_item_matrix.values) print(fsparse data bytes: {sparse_matrix.data.nbytes})pivot_table里的fill_value0是刻意为之的虽然 0 不代表真实的「不喜欢」但在余弦相似度计算公式里0 的作用就是「该位置无行为」这符合协同过滤的隐含反馈假设。算出来的稀疏度大约是 0.9577论文里可以写「评分矩阵的稀疏度达到 95% 以上这直接影响了相似度计算的策略选择」。csr_matrix是稀疏矩阵的行压缩格式适合按行按用户做操作而 ItemCF 里按列按电影操作时可以用csc_matrix。2.3 数据集划分按比例切而不是随机切时间戳才是被忽略的关键协同过滤评估里最常见的错误就是直接random.shuffle然后按 8:2 切分。这在分类问题里没问题但推荐系统里存在信息泄露的隐患训练集里用了 t1 时刻的用户行为去预测 t 时刻的评分这在论文答辩时被问到「如何避免数据泄露」会非常被动。MovieLens 1M 自带timestamp字段正确做法是每个用户按时间顺序排序取前 80% 的行为作为训练集、后 20% 作为测试集。这样模拟的是「用历史推断未来」的真实推荐场景。评论文里把这句写清楚比堆十个公式都有说服力。# 每个用户按时间排序后切前 80% 做训练、后 20% 做测试 ratings_sorted ratings.sort_values([user_id, timestamp]) train_list [] test_list [] for user_id, group in ratings_sorted.groupby(user_id): split_idx int(len(group) * 0.8) train_list.append(group.iloc[:split_idx]) test_list.append(group.iloc[split_idx:]) train_data pd.concat(train_list, ignore_indexTrue) test_data pd.concat(test_list, ignore_indexTrue) print(ftrain size: {len(train_data)}, test size: {len(test_data)})这段代码用groupby按用户分组iloc[:split_idx]取每个用户时间最靠前的 80% 行为。要注意split_idx是整数截断如果某用户只有 4 条评分80% 就是 3 条训练、1 条测试。评分行为越稀疏的用户测试集越小这在最后算评估指标时要按用户加权而不是简单平均否则会被活跃用户主导。3. UserCF 和 ItemCF 的实现从相似度到 TopN 推荐3.1 相似度计算皮尔逊相关系数为什么在电影评分里比余弦相似度更稳协同过滤的灵魂在于相似度函数。入门教程里最常见的代码是直接对矩阵行做余弦相似度但做电影推荐时你会发现一个经典问题用户 A 打分全都偏高平均 4.5 分用户 B 打分全都偏低平均 2.5 分两个人实际品味相似但余弦相似度算出来很低。因为余弦相似度只关心向量夹角不关心向量整体的偏移。皮尔逊相关系数通过在计算前减去均值把每个人的打分习惯中心化才能真实反映「相对偏好」的一致性。计算皮尔逊相关系数的公式是 ( \text{sim}(u, v) \frac{\sum_{i \in I_{uv}} (r_{ui} - \bar{r_u}) (r_{vi} - \bar{r_v})}{\sqrt{\sum_{i \in I_{uv}} (r_{ui} - \bar{r_u})^2} \sqrt{\sum_{i \in I_{uv}} (r_{vi} - \bar{r_v})^2}} )。理解这个公式比背下来更重要分子是两个人共同评分过的电影交集上的偏差乘积分母是各自偏差的模长。如果两个人只在极少几部电影上有交集这个相关系数的方差会非常大所以还要引入一个「共同评分数」的置信下限。这是推荐系统里的基本节奏也是论文里可以展开讨论的点。在 Python 里如果你手写这个公式双层循环 6000 个用户会慢到让你怀疑电脑是不是坏了。正确姿势是对稀疏矩阵做向量化操作。下面用sklearn.metrics.pairwise提供的能力先对稀疏矩阵做中心化处理再算余弦本质上等价于皮尔逊。import numpy as np from sklearn.metrics.pairwise import cosine_similarity # 对 user-item 稀疏矩阵做中心化每行减去该用户的平均评分 user_mean sparse_matrix.mean(axis1) # 中心化时只减评过的位置0 保持为 0 user_centered sparse_matrix - user_mean # 计算用户之间的皮尔逊相似度矩阵 user_sim cosine_similarity(user_centered, dense_outputFalse) user_sim np.asarray(user_sim) # 打印某用户与其它用户的相似度分布检查是否有全 0 的情况 print(fuser_sim shape: {user_sim.shape}) print(fuser 0 top10 similar users: {np.argsort(-user_sim[0])[1:11]})user_centered这一步是关键sparse_matrix - user_mean利用了 numpy 的广播机制把每个用户的平均分广播到整个矩阵的行上但原来就是 0 的位置减去均值后变成负数这在数学上是错误的——用户没看过这部电影不应该产生负向偏好。所以更严谨的写法是先记录nonzero_mask中心化只对非零位置做再乘回掩码。这个小细节也是论文的差异化加分点后面第五部分会展开。计算出来user_sim的形状是 6040×6040内存规模大约 6040 的平方再乘以 8 字节接近 300 MB在可接受范围。如果你的机器内存吃紧只保留每个用户的 top K 邻居就是一个标准做法下面就有。3.2 UserCF 推荐找相似用户再找他们看过而你没看过的电影UserCF 的思想一句话「和你口味相似的人最近在看什么」。流程分三步第一步找到与目标用户最相似的 K 个用户第二步收集这些邻居评过分的电影第三步按加权分数排序排除目标用户已经看过的取前 N 个作为推荐列表。预测目标用户 u 对电影 i 的评分公式是 ( \hat{r}{ui} \bar{r_u} \frac{\sum{v \in N(u)} \text{sim}(u, v) \times (r_{vi} - \bar{r_v})}{\sum_{v \in N(u)} |\text{sim}(u, v)|} )。注意这里再次做了用户均值中心化否则评分离散都是整数会让结果偏向高分段用户。下面给出一个可以跑的 UserCF 推荐函数数据从刚才的user_item_matrix和user_mean来。def usercf_recommend(user_id, user_item_df, user_sim_mat, user_mean_arr, k10, top_n10): 基于用户的协同过滤推荐 :param user_id: 目标用户 id :param user_item_df: DataFrame行是用户列是电影值是评分 :param user_sim_mat: 用户相似度矩阵 :param user_mean_arr: 用户的平均评分数组 :param k: 取前 k 个相似用户作为邻居 :param top_n: 推荐电影数量 # 相似用户索引排序排除自身取前 k 个 sim_scores user_sim_mat[user_id].copy() sim_scores[user_id] -1 # 不把自己当邻居 top_k_users np.argsort(-sim_scores)[:k] # 目标用户的平均评分 u_mean user_mean_arr[user_id] # 目标用户已经评过分的电影要排除掉 rated_movies set(user_item_df.columns[user_item_df.loc[user_id] 0]) # 候选电影评分字典 score_dict {} sim_sum_dict {} for neighbor in top_k_users: sim sim_scores[neighbor] if sim 0: continue # 相似度为负的邻居直接跳过 # 取邻居评过分且目标用户没看过的电影 neighbor_rated user_item_df.columns[user_item_df.loc[neighbor] 0] for movie in neighbor_rated: if movie in rated_movies: continue r_vi user_item_df.loc[neighbor, movie] neighbor_mean user_mean_arr[neighbor] # 加权偏差累加 score_dict[movie] score_dict.get(movie, 0) sim * (r_vi - neighbor_mean) sim_sum_dict[movie] sim_sum_dict.get(movie, 0) sim # 预测评分 用户均值 加权偏差和 / 相似度和 pred_scores {} for movie, weighted_diff in score_dict.items(): if sim_sum_dict[movie] 0: pred_scores[movie] u_mean weighted_diff / sim_sum_dict[movie] # 按预测评分倒序取 top_n top_movies sorted(pred_scores.items(), keylambda x: x[1], reverseTrue)[:top_n] return top_movies注意我做了两个细节处理相似度 0 的邻居被直接过滤掉负相似邻居会直接抵消正邻居的投票在用户数量少时干扰极大预测分数用均值 偏差加权的形式而不是直接用原始评分加权这保持了预测在用户自身的评分习惯区间内。参数k10是常见起步值后面讲评估时你会看到 K 不是越大越好。3.3 ItemCF 推荐电影相似度矩阵和「喜欢 A 的人也喜欢 B」ItemCF 的商业应用更广泛因为物品数量相对用户数量稳定相似度矩阵可以离线定期更新。核心公式和 UserCF 几乎镜像先计算电影之间的相似度矩阵这时矩阵的「行」是电影然后目标用户 u 对电影 i 的偏好由「用户评过分的电影」与「候选电影的相似度乘积和」决定。常见做法是只用 Top K 个最相似的已看过的电影做累加减少噪音。利用用户评分数据的行表示到列表示的转换最容易踩的坑是索引对齐。user_item_matrix的列是 movie_id所以电影相似度矩阵要按user_item_matrix.T来算。Transpose 之后行是电影、列是用户和之前的用户中心化逻辑保持一致。# 用 item-user 矩阵计算电影相似度 item_user_matrix user_item_matrix.T # 对电影向量做中心化同样只中心化非零位置 item_mean item_user_matrix.mean(axis1) item_centered item_user_matrix - item_mean # 计算电影之间相似度 item_sim cosine_similarity(item_centered, dense_outputFalse) item_sim np.asarray(item_sim) print(fitem_sim shape: {item_sim.shape})电影相似度矩阵的规模取决于电影数量3700 的平方大约会占用 100 MB 级别的内存比用户相似度矩阵要小。这也是电影场景下 ItemCF 更高效的原因之一。ItemCF 的推荐函数对目标用户已经评过分的电影找到一个候选集合候选集合一般取「用户评过分电影的相似电影」的并集。如果用户看过的电影本身质量参差不齐候选集合可能迅速膨胀所以通常只取每个已看影片最相似的 top L 个电影再做汇总。def itemcf_recommend(user_id, user_item_df, item_sim_mat, k10, top_n10): 基于物品的协同过滤推荐 :param user_id: 目标用户 id :param user_item_df: 用户-电影评分矩阵行用户列电影 :param item_sim_mat: 电影相似度矩阵 :param k: 每个已看过的电影取多少个相似电影 :param top_n: 推荐数量 rated user_item_df.loc[user_id] rated_items rated[rated 0].index.tolist() score_dict {} for movie in rated_items: movie_idx user_item_df.columns.get_loc(movie) # 取该电影最相似的 k 部 sim_scores item_sim_mat[movie_idx].copy() # 排除自己和已看过的 sim_scores[user_item_df.columns.get_loc(movie)] -1 # 过滤掉已看过电影对应的列 for watched in rated_items: sim_scores[user_item_df.columns.get_loc(watched)] -1 top_k_idx np.argsort(-sim_scores)[:k] for idx in top_k_idx: candidate_movie user_item_df.columns[idx] weight item_sim_mat[movie_idx, idx] if weight 0: continue # 累加得分 score_dict[candidate_movie] score_dict.get(candidate_movie, 0) weight * rated[movie] top_movies sorted(score_dict.items(), keylambda x: x[1], reverseTrue)[:top_n] return top_moviesItemCF 的预测分数本质上是「用户历史评分的加权和」权重就是电影间相似度。这里的k指的是「相似电影截断数」而不是邻居数量参数含义和 UserCF 完全不同混用会导致理解偏差。3.4 离线评估指标准确率、召回率、覆盖率与流行度论文里只说「推荐了 10 部电影」是不够的需要量化指标来对比 UserCF 和 ItemCF。基础指标是 TopN 推荐场景下的准确率Precision和召回率Recall对测试集中每个用户把他的真实评分中阈值大于等于 4 的电影视为正样本推荐列表命中正样本的比例是准确率正样本被推荐列表覆盖的比例是召回率。def evaluate(recommend_func, user_item_df, test_df, threshold4, user_sim_matNone, user_mean_arrNone): 计算 TopN 推荐的准确率、召回率和覆盖率 precision_list [] recall_list [] # 收集所有推荐过的电影用于覆盖率计算 all_rec_movies set() test_users test_df[user_id].unique() for user_id in test_users: # 该用户测试集中真实评了高分4的电影 true_pos_set set(test_df[(test_df[user_id] user_id) (test_df[rating] threshold)][movie_id]) if len(true_pos_set) 0: continue # 生成推荐列表 rec_list recommend_func(user_id) rec_movies [movie_id for movie_id, _ in rec_list] all_rec_movies.update(rec_movies) # 命中数 hit len(set(rec_movies) true_pos_set) precision hit / len(rec_movies) recall hit / len(true_pos_set) precision_list.append(precision) recall_list.append(recall) avg_precision np.mean(precision_list) avg_recall np.mean(recall_list) coverage len(all_rec_movies) / len(user_item_df.columns) return avg_precision, avg_recall, coverage这段评估代码有个隐藏问题precision_list按用户平均计算时每个用户权重相同但不同用户测试集大小差异很大。更严格的 F1 指标需要先把命中数和测试样本总数分别求和再相除。论文里建议同时报告 macro 平均和 micro 平均答辩时可以解释这两者的差异这是常见加分点。覆盖率代表系统推荐结果的多样性计算公式是「被推荐出去的物品数 / 总物品数」。另一个论文里值得放的指标是流行度一般用推荐物品的流行度均值来度量推荐的「热门偏向程度」公式是 ( \text{Average Popularity} \frac{1}{|\text{Rec}|}\sum_{i \in \text{Rec}} \log(1 \text{count}_i) )里面count_i是物品在训练集中被评分的次数。这个指标能很直观地暴露协同过滤的「马太效应」问题后面避坑章节会专门提。4. 五个必踩的坑从评分失真到线上线下的评估偏差4.1 用户冷启动评分少于 5 条的用户让相似度计算变成随机数现象评估时发现某些用户的推荐结果完全不可理喻打开相似度矩阵后发现他的 top 相似邻居们和他在现实里毫无共同点。原因MovieLens 数据里有一部分用户只评了 3 部电影在 3700 部电影的维度上3 个非零值的向量算出的余弦相似度极其不稳定几个微小的共同评分就足以让相似度窜到 0.9 以上。这些伪高相似邻居主导了推荐结果。解决在训练和评估之前先按用户评分数量过滤掉边缘用户。常见做法是保留评分数量 ≥ 20 的用户。在groupby(user_id).size()之后做筛选再重建矩阵。# 过滤评分数量过少的用户 user_rating_count train_data.groupby(user_id).size() valid_users user_rating_count[user_rating_count 20].index train_data train_data[train_data[user_id].isin(valid_users)] test_data test_data[test_data[user_id].isin(valid_users)]这会导致评测用户数变少但换来的是指标稳定性。论文里要明确写「为保证相似度计算可靠性剔除了行为数不足 20 条的用户」这不是遮掩是标准的实验设定。4.2 K 值灵敏度K 从 5 调到 50准确率可能先升后降存在过拟合点现象有人把 K 设成 200结果准确率和召回率反而比 K20 时低于是一通乱调不知道哪个值是对的。原因K 太小邻居集合里包含的偏好信号不足K 太大大量弱相似用户被纳入加权噪音淹没信号。这是典型的偏置-方差权衡和机器学习里的模型复杂度如出一辙。解决把 K 做成实验变量分别取 5、10、20、30、50、80在验证集上观察准确率或 F1 的变化曲线。通常 10 到 30 之间有一个平坦的高原区选平原中心的那个值作为最终参数。这篇论文里需要放一张 K 值-准确率的折线图这是实验章节的核心图之一。4.3 流行度偏差推荐列表全是热门电影覆盖率低到不敢写进论文现象评估代码跑完覆盖率只有 0.08推荐列表里翻来覆去是那几部《星球大战》和《阿甘正传》。原因协同过滤天然偏向热门的头部物品因为热门电影和大量用户产生共现相似度权重高。评分矩阵的稀疏性让长尾电影几乎没有任何机会进入推荐候选集。解决在生成推荐结果时对热门物品做惩罚。具体做法是在打分时乘以一个与物品流行度负相关的因子比如 ( \text{score} \times \left(\frac{\log(1 \text{max_count})}{\log(1 \text{count}_i)}\right)^\alpha )alpha是惩罚强度系数。更偷懒的做法是直接把训练集中被评次数排在前 1% 的物品在推荐时权重乘 0.1。注意惩罚系数要放进实验对比展示加了惩罚前后覆盖率的变化这又是一组可控的实验数据。4.4 时间切分和随机切分的指标差异你复现不出别人的论文结果现象照别人的代码复现指标怎么都差一大截怀疑自己数据集不对。原因很多人分享的代码默认用train_test_split(test_size0.2, random_state42)随机切分这样测试集里包含了和训练集同一时间段的行为预测的是「已知结果的一部分」准确率被抬高。用时间切分后测试集是完全未见过的未来行为难度更高分数自然会低 5-10 个百分点。解决坚持时间切分并且在论文实验一节写清楚。如果想要两种口径都能交代可以分别跑随机切分和时间切分给出对比表说明协同过滤在时间演化环境下的性能衰减这是一个很有价值的真实研究结论。4.5 相似度矩阵内存爆炸用户量到 10 万级时普通方案直接 OOM现象把数据集从 1M 换到 10M 后程序在算相似度矩阵时内存涨到十几个 GB进程被杀。原因用户相似度矩阵是 O(N²) 的稠密存储10M 数据集用户数约 7 万产生约 49 亿个浮点数在内存里接近 40 GB。解决用 Spark 的 ALS 或近邻检索库如 Faiss来处理大规模场景。毕设阶段如果坚持用单机 Python可以只保留每行最大的 Top K 个相似度值其余置 0然后存储成稀疏矩阵格式。用scipy.sparse.csr_matrix存稀疏相似度能把 40 GB 压到几百 MB。这里点到为止具体工程方案不展开但至少在论文的「局限与展望」一节可以预告这个方向。5. 评分归一化与相似度计算的隐藏技巧用归一化把指标再抬一个台阶5.1 均值中心化到底该怎么做掩码版本的完整实现前面提过sparse_matrix - user_mean会错误地把未评分位置变成负数这里给出修正版本。原理是「只在有评分的位置做中心化未评分的位置保持 0」。计算步骤是先生成掩码矩阵对数据矩阵做中心化再乘回掩码让未评分位置归零。from scipy.sparse import csr_matrix def center_sparse_matrix(sparse_mat, axis1): 对稀疏矩阵按行axis1或按列axis0做均值中心化。 只中心化非零位置零位置保持为零。 if axis 1: # 按行求均值 mean sparse_mat.mean(axis1) # 生成掩码非零位置为 1零位置为 0 mask sparse_mat.copy() mask.data np.ones_like(mask.data) # 构建全零矩阵并填充均值到对应行 mean_sparse csr_matrix(mean) # 中心化后再乘回掩码 centered sparse_mat - mean_sparse centered centered.multiply(mask) return centered else: # 按列处理 mean sparse_mat.mean(axis0) mask sparse_mat.copy() mask.data np.ones_like(mask.data) mean_sparse csr_matrix(mean) centered sparse_mat - mean_sparse centered centered.multiply(mask) return centered这段代码的细节在于mask.data np.ones_like(mask.data)它保留了稀疏矩阵的非零位置结构但把值全部改为 1。centered.multiply(mask)是逐元素相乘能够把中心化后出现在原来零位置上的负值全部归零。这个版本比无脑广播合理很多也经得起评委追问。5.2 相似度矩阵的行归一化为什么推荐之前要再做一次归一化在 UserCF 中如果用户 A 有 5000 个邻居参与加权而用户 B 只有 20 个邻居参与加权直接累加会导致最终分数绝对值不可比。行归一化让每个用户的邻居权重之和为 1推荐分数落在合理区间排序时不会偏爱「邻居多」的用户。实现方式很简单对相似度矩阵每一行除以该行之和但对全零行要单独处理。def normalize_sim_matrix(sim_mat): 对相似度矩阵做行归一化保持方向一致。 对全 0 行不处理后续会被过滤掉。 row_sum sim_mat.sum(axis1, keepdimsTrue) row_sum[row_sum 0] 1 # 避免除零 return sim_mat / row_sum做归零保护的原因是冷启动用户可能没有任何邻居相似度行全为 0直接除会得到 NaN进而传染到预测评分。加了这层保护之后推荐函数要对全零行的用户单独走「热门兜底」逻辑保证返回结果不为空。5.3 把模型接口统一成同一个函数签名实验对比代码最少化论文实验部分的核心是对照实验。为了让 UserCF 和 ItemCF 以及不同 K 值之间的对比代码尽量简洁所有推荐函数统一包装成同一个接口这个工程习惯能让你后期跑批量实验时省下大量重复代码。def make_recommender(algorithmuser, k10): 返回一个统一的推荐函数 recommend(user_id) - list[(movie_id, score)] if algorithm user: def recommender(user_id): return usercf_recommend(user_id, user_item_matrix, user_sim, user_mean_arr, kk) return recommender else: def recommender(user_id): return itemcf_recommend(user_id, user_item_matrix, item_sim, kk) return recommender # 批量跑不同 K 值的指标对比 for k in [5, 10, 20, 30, 50]: rec_func make_recommender(user, kk) precision, recall, coverage evaluate(rec_func, user_item_matrix, test_data) print(fUserCF k{k}: precision{precision:.4f}, recall{recall:.4f}, coverage{coverage:.4f})结合实际经验初学者调参时最容易把k理解为唯一的超参数实际上评分阈值和相似度截断阈值对指标的影响同样明显。建议在论文实验部分至少列三组对比算法类型对比UserCF vs ItemCF、K 值灵敏度分析、相似度函数对比余弦 vs 皮尔逊。以上三条打通之后论文的「实验分析」章节基本就站得住了。我自己的习惯是把所有评估结果攒成一张 CSV 表因为最终写论文时你一定会反复回来对比数据别指望脑子记住了。这期的笔记就写到这把具体的调参和实验还得亲手跑一遍才有感觉希望帮到你。本文还有配套的精品资源点击获取