ARTICLE DETAIL

资讯详情

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

基于Python的电影推荐系统课程设计:ItemCF协同过滤实现与调优指南

基于Python的电影推荐系统课程设计:ItemCF协同过滤实现与调优指南 简介面向正在完成课程设计与期末大作业的计算机相关专业学生这套基于 Python 的电影推荐系统源码提供了可直接复用的完整项目实现。压缩包共 10 个文件大小仅 2.37MB包含核心程序 movies.py、预处理后的电影与评分数据 CSV、经典的 ml-latest-small 数据集以及 TensorBoard 训练事件等代码、数据与配置结构清晰同时保留了 .idea 与 .iml 工程文件能够直接在 PyCharm 中打开运行。项目经过严格调试下载解压后即可运行可完整演示数据清洗、特征构建、模型训练与结果评估等推荐系统关键流程在课程设计或期末答辩中既可以直接展示也便于继续二次开发。目前已有 162 人学习下载无论是突击完成课设作业还是作为 Python 项目实战练习的参考起点这套源码都提供了足够完整的示例支撑与实用价值。1. 从课程设计题到可运行的推荐系统骨架这份源码在解决什么问题每年毕业季和期末周基于Python的电影推荐系统源码课程设计都是搜索榜上的常客。你在找的其实不是一段代码而是一条能交差、能答辩、能跑通的完整链路从原始评分数据到最终推荐列表中间每一步都要能说出“为什么这么做”。一个正常的课程设计推荐系统至少要覆盖数据预处理、相似度计算、评分预测、TopN 推荐和离线评估五个环节缺一个答辩时都会被追问到墙角。这个项目适合三类人一是正在做课程设计、需要一份结构清晰能改造的 Python 源码二是想搞懂协同过滤实际怎么落地、而不是只背公式的初学者三是准备在简历上写“推荐系统”项目、需要真实评估数字支撑的求职者。下文以 MovieLens 公开数据集为默认输入顺着标题里的技术点拆开讲每一段代码都给到可以直接粘贴运行的程度。2. 选型先立住电影推荐该选哪种算法评分预测公式怎么来的2.1 UserCF 与 ItemCF 的取舍为什么课程设计通常选基于物品的协同过滤推荐系统里最常被拿来当课程设计主算法的就是协同过滤Collaborative Filtering。它分两派基于用户的 UserCFUser-based Collaborative Filtering和基于物品的 ItemCFItem-based Collaborative Filtering。UserCF 的思路是“和你品味相似的人喜欢什么就推给你什么”先找目标用户的 K 个近邻用户再把这 K 个用户看过但目标用户没看过的电影按得分排序。ItemCF 的思路是“你喜欢的电影和哪些电影相似就推那些相似的”先找目标用户看过电影的各自主近邻物品再汇总推荐。选哪个主要看数据集规模和实时性要求。MovieLens 100K 有 943 个用户、1682 部电影、10 万条评分UserCF 要算 943×943 的用户相似度矩阵ItemCF 要算 1682×1682 的物品相似度矩阵。单看规模两者都扛得住但课程设计场景里常见的是“用户增长快、物品相对稳定”的假设——新用户不断进来老用户反复访问而电影库的变化要慢得多。此时 ItemCF 的優勢很明显物品相似度矩阵可以离线计算好存下来新用户一来只需要查他评分过的电影的近邻复杂度 O(N×K)N 是该用户评过的电影数实时性好而且推荐理由更好解释——“因为你喜欢《盗梦空间》所以推荐《星际穿越》”。答辩时这一条理由就够挡掉一半追问。我一般建议课程设计选题时直接用 ItemCF 打底后面如果要加分再叠加 SVD 或内容画像不必一上来就上深度模型。深度模型需要调参、需要更大数据量、需要 GPU在课程设计的时间预算里容易翻车。2.2 评分预测公式与相似度计算余弦、皮尔逊放在哪里用ItemCF 的评分预测核心公式是用户u对物品i的预测评分等于用户对与i相似的物品j的评分的加权和权重就是i与j的相似度。公式写出来是pred(u, i) sum_{j in N(i) ∩ R(u)} sim(i, j) * r(u, j) / sum_{j in N(i) ∩ R(u)} |sim(i, j)|其中N(i)是物品i的 K 个近邻集合R(u)是用户u评过分的物品集合r(u, j)是用户u给物品j的实际评分。分母是相似度绝对值之和作用是归一化避免热门物品的邻居数量多导致加权分偏高。相似度的计算课程设计里最常用两种余弦相似度Cosinesim(i, j) cos(i_vec, j_vec)把每个物品看作一个向量维度是所有用户值是该用户对这个物品的评分。余弦相似度对评分的绝对大小不敏感比较的是方向。皮尔逊相关系数Pearson先把每个物品的评分向量减去该物品的平均分再算余弦相似度相当于去均值后的余弦。皮尔逊能缓解“有人习惯打高分、有人习惯打低分”的评分偏差问题。实际写代码时如果评分矩阵已经做了均值中心化皮尔逊和余弦的结果会很接近。课程设计里直接用余弦相似度能少写几行去均值的逻辑但如果答辩老师追问“评分偏差怎么处理”你再补充说“可以将原始分减去物品平均分再算相似度”就能体现出理解深度。2.3 冷启动与热门偏差数据稀疏时推荐系统为什么翻车协同过滤有一个天然缺陷数据稀疏。MovieLens 100K 的用户-物品矩阵维度是 943×1682总格子数约 158 万但只有 10 万个非空评分稀疏度高达 93.7%。当一个用户只评过三五部电影他评过的每部电影对应的近邻物品在“用户已评分集合”里的交集就很小预测评分会很不稳定。冷启动有两层含义新用户没有评分记录系统无法给他建模新电影没有被任何人评分系统无法把它推出去。课程设计里如果只做协同过滤答辩时老师几乎必问“新用户怎么办”。常见的补救是在代码里加一个“热门电影全局榜”作为冷启动兜底——当用户评分数少于某阈值比如 5时直接返回全站评分次数 Top10 的电影不做个性化计算。这不算偷懒而是真实工业界也在用的降级策略。热门偏差则表现在另一个方向高频被评分的电影会被过度推荐。因为评分数大意味着它的近邻集合更饱满加权求和时更容易出现在推荐列表里。要压住这个偏差可以在相似度计算时对高频物品做惩罚比如乘以log(1 n_j)的倒数但课程设计如果时间紧张可以先不处理在论文里说明“该版本未引入流行度惩罚”即可。记住一个原则能说明白缺点的系统比假装完美的系统更能在答辩中拿分。3. 把 MovieLens 数据跑成推荐结果最小可运行代码与每行含义3.1 项目文件结构与数据预处理先搭一个标准的课程设计目录结构。你拿到的源码 zip 里一般会有这四类文件数据文件u.data、u.item、预处理脚本、算法核心脚本相似度计算、评分预测、推荐生成、评估脚本RMSE、MAE、Precision。下面是我惯用的组织方式movie_recommender/ ├── data/ │ ├── u.data # 评分数据user_id, item_id, rating, timestamp │ ├── u.item # 电影信息item_id, title, genres │ └── u.genre # 题材字典可选 ├── preprocess.py # 数据加载与矩阵构建 ├── similarity.py # 相似度计算 ├── recommend.py # 评分预测与 TopN 推荐 ├── evaluate.py # 离线评估指标 └── main.py # 串联全流程数据预处理是整个项目最不起眼但最容易出错的地方。u.data是 Tab 分隔的文本四列分别是用户 ID、电影 ID、评分1-5、时间戳。第一段代码先把数据读进来并划分训练集和测试集import pandas as pd import numpy as np def load_ratings(pathdata/u.data): 加载 MovieLens 评分数据。 列为 user_id, item_id, rating, timestamp。 返回 DataFrame并按 (user, item) 去重。 df pd.read_csv( path, sep\t, names[user_id, item_id, rating, timestamp], enginepython, ) # 同一用户对同一电影多次评分只保留最后一次按时间戳排序后去重 df df.sort_values(timestamp).drop_duplicates( subset[user_id, item_id], keeplast ) return df def split_by_user(df, test_ratio0.2, seed42): 按用户划分训练/测试集保证同一用户的评分不会全跑进同一份。 users df[user_id].unique() rng np.random.RandomState(seed) test_users rng.choice(users, sizeint(len(users) * test_ratio), replaceFalse) test_mask df[user_id].isin(test_users) train_df df[~test_mask].copy() test_df df[test_mask].copy() return train_df, test_df if __name__ __main__: df load_ratings() train, test split_by_user(df) print(f总评分: {len(df)}, 训练集: {len(train)}, 测试集: {len(test)})逻辑说明第一段做了两件容易被忽略的事——按时间戳排序后对同一(user_id, item_id)去重保证重复评分的场景下只保留最新一条按用户维度分割数据而不是按行随机切这样测试集里的每个用户都在训练集里有历史评分后续评估 TopN 推荐时才合理。enginepython是为了避免 Windows 环境下 C 引擎解析 Tab 分隔符时出现分隔符冲突如果你在 Linux 下跑去掉也能工作。要说明的一点这里按用户划分训练集和测试集没有用户重叠——测试集用户在训练集里完全没有数据协同过滤根本没法给他预测。课程设计里更常见也更合理的做法是按行随机切分即同一用户的部分评分进训练集、部分进测试集。上面代码是演示“按用户切”的逻辑实际评估时你会改成按行切原因在 4.3 节再展开。3.2 构建用户-物品评分矩阵与相似度矩阵构建矩阵是承上启下的关键步骤。用pivot把长表转成宽表行是用户、列是电影、值是评分缺失值填 0。这一步直接决定后续相似度计算的输入形态。from scipy.sparse import csr_matrix, save_npz def build_user_item_matrix(df, user_ids, item_ids): 构建用户-物品评分矩阵。 返回稀疏矩阵行用户, 列物品缺失评分记为 0。 user_idx {uid: i for i, uid in enumerate(user_ids)} item_idx {iid: j for j, iid in enumerate(item_ids)} rows df[user_id].map(user_idx).values cols df[item_id].map(item_idx).values values df[rating].values.astype(float) mat csr_matrix( (values, (rows, cols)), shape(len(user_ids), len(item_ids)), dtypefloat, ) return mat, user_idx, item_idx def compute_item_similarity(mat): 基于余弦相似度计算物品-物品相似度矩阵。 输入 mat用户-物品评分矩阵稀疏 输出 sim物品-物品相似度矩阵稠密形状 NxN。 # 对每个物品的评分向量做 L2 归一化 norm np.sqrt(mat.power(2).sum(axis0)) norm[norm 0] 1 # 防止除零 mat_norm mat.multiply(1.0 / norm) # 物品间余弦相似度 归一化评分矩阵的转置点乘自身 sim (mat_norm.T mat_norm).toarray() np.fill_diagonal(sim, 0) # 自己对自己的相似度置 0 return sim逻辑说明稀疏矩阵的作用是避免把 158 万个格子全铺成稠密二维数组——如果用pd.pivot得到稠密矩阵943×1682 还能忍换成 MovieLens 1M 的 6040×3706 就直接内存翻车。用csr_matrix只存非零元素10 万评分占的内存只有稠密版本的几十分之一。相似度计算的数学原理是余弦相似度等于两个向量归一化后的点积代码里对列做 L2 归一化后mat_norm.T mat_norm的每个元素正好就是两两物品的余弦相似度。这个写法比两重 for 循环快几个数量级课程设计里跑 1682×1682 的矩阵普通笔记本几秒就能出结果。np.fill_diagonal(sim, 0)是把物品自己和自己的相似度置零避免后续评分预测时把一个物品的评分加权到它自己身上那是典型的数据泄漏。漏掉这一步的常见症状是离线 RMSE 很低因为预测直接“抄”了真实评分但推荐列表却空得离谱。3.3 基于物品的评分预测与 TopN 推荐相似度矩阵算好之后就到了核心的评分预测环节。这里有一个关键参数 K——每个物品取多少个近邻参与加权常见取值 10 到 80。K 太小时预测方差大K 太大时会把不太相似的物品也拉进来稀释信号。def predict_rating(user_ratings, sim, K20): 为单个用户预测所有未评分物品的分值。 参数: user_ratings: dict {item_id: rating}该用户已评分的物品及其分值 sim: 物品相似度矩阵稠密 ndarray行/列下标对应 item 编号 K: 每个已评分物品参与加权的近邻数量。 返回: dict {item_id: pred_score} predictions {} rated_items list(user_ratings.keys()) # 找出该用户评分过的所有物品的候选近邻并集 # 对每个已评分物品 i取 sim[i] 中最大的 K 个作为它的近邻 neighbor_pool set() for i in rated_items: top_k_idx np.argsort(sim[i])[::-1][:K] neighbor_pool.update(top_k_idx.tolist()) # 只对用户没评过分的物品做预测 candidates [j for j in neighbor_pool if j not in user_ratings] for j in candidates: numer, denom 0.0, 0.0 for i in rated_items: w sim[i][j] if w 0: continue numer w * user_ratings[i] denom w if denom 0: predictions[j] numer / denom return predictions def top_n_recommend(user_ratings, sim, K20, N10): 包装 predict_rating返回 TopN 列表 [(item_id, score), ...]。 preds predict_rating(user_ratings, sim, KK) ranked sorted(preds.items(), keylambda x: x[1], reverseTrue) return ranked[:N]逻辑说明先收集用户已评分物品的 TopK 近邻取并集作为候选池再对候选池中每个未被用户评分的物品做加权求和。w sim[i][j]是物品 i 和物品 j 的相似度权重乘以用户给 i 的真实评分除以权重和得到预测分。这里把w 0直接跳过避免负相似度把预测分拉到不合理区间如果你希望保留负相关信号某些场景下“讨厌的电影的相似品”也值得考虑可以去掉这个判断但课程设计里不建议开这个口子。注意实际课程设计代码里通常不会把整个矩阵传给每次预测而是预先算好每个物品的 TopK 近邻表一个item_id - [(近邻, 相似度), ...]的字典预测时直接查表。上面代码为了可读性直接查矩阵跑 100K 数据时一个用户预测耗时约 20 毫秒600 个用户约 12 秒能接受。如果你要把 1M 数据也跑下来就需要改成近邻表模式。3.4 离线评估RMSE、MAE、PrecisionN 怎么算评估是答辩时拿数据说话的底气。课程设计常用两个回归指标和一个排序指标RMSE均方根误差、MAE平均绝对误差衡量评分预测准不准PrecisionN 衡量推荐列表里有多少是用户实际喜欢的。def evaluate_rmse_mae(train_df, test_df, sim, K20): 在测试集评分上评估预测误差。 返回 (rmse, mae)。 # 训练集里每个用户的评分字典user_id - {item_id: rating} user_ratings {} for uid, iid, rating in train_df[[user_id, item_id, rating]].values: user_ratings.setdefault(uid, {})[iid] rating preds, trues [], [] for uid, iid, rating in test_df[[user_id, item_id, rating]].values: if uid not in user_ratings: continue pred predict_rating(user_ratings[uid], sim, KK).get(iid) if pred is None: continue preds.append(pred) trues.append(rating) preds, trues np.array(preds), np.array(trues) mae np.mean(np.abs(preds - trues)) rmse np.sqrt(np.mean((preds - trues) ** 2)) return rmse, mae逻辑说明对测试集里每个真实评分用该用户在训练集里的历史评分做预测再和真实分对比。predict_rating返回的是字典.get(iid)找不到该物品时返回None并跳过这是因为测试集里可能出现训练集里从未出现过的冷门电影它们没有近邻、无法预测跳过它们是正确行为但要记录跳过数量写进论文——这本身就是对“冷启动问题”的数据支撑。RMSE 和 MAE 的差异值得在答辩时主动讲MAE 是绝对误差的平均RMSE 因为先平方再开方会给大误差更高的惩罚。同样的模型RMSE 通常比 MAE 大 0.1 到 0.2如果 RMSE 明显偏高说明模型在某些用户身上预测偏离严重而不是普遍误差大。指标本身并没有高低之分选定一个并从头到尾用同一个才有对比意义——别在论文里一个用 RMSE、另一个用 MAE横向比较会乱。PrecisionN 则需要额外的“喜欢”定义。常见做法是把用户评分大于等于 4 的电影视为“喜欢”推荐列表里命中“喜欢”的占比就是 PrecisionNdef evaluate_precision_at_n(test_df, train_df, sim, K20, N10, like_thr4.0): 对测试集用户计算 PrecisionN。 user_train_ratings {} for uid, iid, rating in train_df[[user_id, item_id, rating]].values: user_train_ratings.setdefault(uid, {})[iid] rating user_test_likes {} for uid, iid, rating in test_df[[user_id, item_id, rating]].values: if rating like_thr: user_test_likes.setdefault(uid, set()).add(iid) precisions [] for uid, liked_set in user_test_likes.items(): if uid not in user_train_ratings or len(liked_set) 0: continue recs top_n_recommend(user_train_ratings[uid], sim, KK, NN) rec_ids {item_id for item_id, _ in recs} hit len(rec_ids liked_set) precisions.append(hit / N) return np.mean(precisions)杀掉一个常见疑惑为什么top_n_recommend里用user_train_ratings[uid]而测试集里又需要用户有评分因为推荐的输入是用户的已有评分历史测试集的评分只用来验证推荐结果是否命中两者必须分开。如果拿测试集评分当输入去推荐评估就彻底失真了。4. 参数怎么调K 邻居数、正则化系数与数据集切分的联动4.1 邻居数 K 与 RMSE 的关系从 5 到 80 逐个试K 是最直观、也最容易在答辩时展示调参过程的参数。K 太小时加权平均只用了少数几个近邻预测值方差大评分极端化K 太大时大量相似度很低的物品混进加权和信号被稀释预测值向全局均值靠拢。在 MovieLens 100K 上K 从 5 升到 20 通常 RMSE 快速下降20 到 50 趋于平缓50 以上可能轻微回弹或进入平台期。课程设计里验证 K 的方式很简单写一个循环即可for k in [5, 10, 20, 30, 50, 80]: rmse, mae evaluate_rmse_mae(train_df, test_df, sim, Kk) print(fK{k:3d} RMSE{rmse:.4f} MAE{mae:.4f})参数说明K 的递增步长不需要太细先粗调找区间、再细调找最优。我的习惯是 10 到 50 之间每隔 10 试一次选 RMSE 最低的 K再在最优 K 附近 ±5 内做一次细调。如果你发现 RMSE 一直降不回升说明你的相似度矩阵可能有问题——正常场景下 K 增大到一定程度后新加入的近邻相似度极低加权时对结果的影响趋近于零RMSE 曲线必然变平。如果一直降大概率是 K 的邻居居然都高度相似这在物品相似度矩阵计算正确时不应该发生。4.2 评分归一化与均值中心化决定 RMSE 下限的操作ItemCF 直接预测用户的原始评分时会撞上一堵墙每个用户的打分习惯不同有人给 4 分是“不错”有人给 4 分是“一般般”。不处理这个偏差预测误差的下限就卡在用户的偏置上。常见的处理是均值中心化将每个用户的评分减去该用户的平均分再构建矩阵预测得到的是一个“偏移值”最后加上用户平均分还原成真实评分。另一种是物品均值中心化减去物品的平均分。课程设计里我建议两个都做但在论文里讲清楚差异def build_mean_centered_matrix(df, user_ids, item_ids): 构建用户均值中心化后的评分矩阵。 mat, user_idx, item_idx build_user_item_matrix(df, user_ids, item_ids) # 每个用户已评分的均值 user_means np.array( [ df.loc[df[user_id] uid, rating].mean() for uid in user_ids ] ) # 中心化对每个非零评分减掉对应用户的均值 rows, cols mat.nonzero() data mat.data.copy() for r, c in zip(rows, cols): data data # 实际需逐元素处理见下行注释 # 更高效的写法是直接按行做mat_c mat - user_means[:, None]再 mask mask mat.astype(bool).toarray() mat_c mat.toarray() - user_means[:, None] mat_c csr_matrix(mat_c * mask) return mat_c, user_means注意上面代码的 for 循环是示意真正跑建议用向量化写法即先转稠密相减再用 mask 还原成稀疏。矩阵小100K时怎么写都行但 1M 数据下逐元素循环会慢到怀疑人生。预测时要把中心化后的预测偏移值加上用户均值raw_pred predict_rating(user_centered_ratings, sim, K20) final_pred {item: score user_mean for item, score in raw_pred.items()}参数说明中心化之后同一个评分 5 在不同用户手里变成了不同的偏移量相似度矩阵才能真正反映“偏好方向”而不是“绝对分数高低”。你会看到 RMSE 明显下降通常能降 0.1 以上。如果中心化后 RMSE 反而升高先检查中心化时是否漏掉了 mask把原本为 0 的空位也减了均值——那会把缺失值错误地当成负分。4.3 训练集与测试集的切分策略随机切分与时间切分的差异切分方式直接决定评估数字的“含金量”。课程设计最常见的做法是按行随机切分比如 80% 训练、20% 测试。问题在于同一个用户的评分会被分到两侧训练集里该用户有历史、测试集里也有推荐器“认识”这个用户评估结果偏乐观。另一个极端是 2.1 节演示过的按用户切分测试集用户对推荐器完全是陌生人评估结果偏悲观。两个都不能说错但要在论文里写清楚自己用了哪种。MovieLens 自带时间戳更合理的做法是时间切分每个用户的前 70% 时间段的评分做训练后 30% 做测试模拟真实场景里“用历史预测未来”。课程设计里如果时间来得及我建议答辩时展示这个版本因为老师问“你为什么要这么分”时你能答出“推荐系统的本质是从过去推断未来随机切分把未来的数据混进了训练集会造成乐观偏差”——这一句话比调出一组漂亮 RMSE 更值钱。def split_by_time(df, train_ratio0.7): 按每个用户的时间戳排序前 train_ratio 进训练剩余进测试。 df df.sort_values([user_id, timestamp]) train_rows, test_rows [], [] for uid, group in df.groupby(user_id): cut int(len(group) * train_ratio) train_rows.append(group.iloc[:cut]) test_rows.append(group.iloc[cut:]) train_df pd.concat(train_rows) test_df pd.concat(test_rows) return train_df, test_df5. 避坑与排查这些坑我替你先踩一遍5.1 评分矩阵稠密化导致内存爆炸现象数据换成 MovieLens 1M约 100 万评分、6000 用户、3700 电影后程序运行到构建矩阵时内存飙升甚至直接卡死。原因预处理时用了df.pivot(indexuser_id, columnsitem_id, valuesrating)pandas 会生成一个 6000×3706 的稠密 DataFrame缺失值是 NaN占内存远超实际数据量。100K 还能忍1M 版本一下就把内存堆到几个 GB。更隐蔽的是pivot前如果忘了去重pandas 会抛出ValueError: Index contains duplicate entries这也是经典报错。解决换成稀疏矩阵如scipy.sparse.csr_matrix只存非零评分如果坚持用 pandas先groupby([user_id, item_id]).rating.mean().reset_index()去重再pivot_table并指定fill_value0。平时写代码养成习惯凡是“行×列”超过 1000×1000 的评分数据默认走稀疏路线。5.2 相似度矩阵全是 0推荐列表空荡荡现象代码跑完top_n_recommend返回空列表或者predict_rating里 denom 全是 0。原因最常出现在compute_item_similarity里归一化后直接点乘得到的是一个 u 矩阵但 u 矩阵的可视化成因是 sklearn 的cosine_similarity或pairwise_distances返回了全 0——多半是输入矩阵行方向向量化错误比如把转置矩阵传给了相似度函数物品向量维度是用户数而不是评分数。还有一种常见乌龙用户-物品矩阵本身构建错误csr_matrix((values, (rows, cols)))中 rows/cols 下标由字典映射产生如果映射时用了原始 ID 而不是连续整数索引矩阵形状会错乱。解决先打印mat.shape确认是 (用户数, 电影数)再打印sim的对角线值是否非 0如果对角线也是 0说明归一化时norm计算有误比如mat.power(2).sum(axis0)在稀疏矩阵上返回的是矩阵而非数组。用sim[:5, :5]人工抽查几行看值是否合理。5.3 文件编码问题Windows 下打开u.data就是乱码现象pd.read_csv(data/u.data, sep\t)在 Windows 上报错ParserError: Error tokenizing data或者读进来中文标签乱码。原因MovieLens 官方数据里有电影的原始标题和题材u.item文件的编码是 ISO-8859-1Latin-1而 Windows 默认用 GBK 解析u.data本身是纯 ASCII 一般没事但u.item里有些特殊字符比如电影名里的拐角引号在 GBK 下会解析失败。解决df_items pd.read_csv( data/u.item, sep|, headerNone, encodinglatin-1, enginepython, )注意u.item用|分隔而不是 Tabu.genre等其他辅助文件也要统一指定encodinglatin-1。顺带提醒存中间结果时不要用to_csv默认的utf-8乱码背锅建议encodingutf-8-sig写文件保证 Excel 也能正常打开。5.4 RMSE 很低但推荐列表“不好看”现象评估脚本打印 RMSE 在 0.9 以下看着不错但打开推荐列表一看全是训练集里最热门的几部电影和用户品味毫无关系。原因离线评估指标本身有漏洞。RMSE 评估的是“预测分和真实分的误差”但热门电影被评分的次数多测试集里命中的概率也大模型只要把高分推给热门电影就能把 RMSE 刷低PrecisionN 只统计“推荐列表里是否包含用户喜欢的电影”如果用户喜欢的恰好是热门电影预估的 Precision 也会虚高。换句话说评估出来的是“热度命中率”而不是“个性化能力”。解决给评估加一个覆盖率Coverage或多样性的指标。覆盖率可以简单定义为“推荐列表中出现的不同电影数 / 总电影数”如果覆盖率低于 10%基本可以断定推荐器退化成热门榜了。另一个常用做法是在推荐列表生成后做“去热”操作——对每个用户的推荐结果剔除总局出现频次前 20 的热门电影再看 Precision 变化。如果剔除热门后指标断崖式下跌说明你的模型本质上没有学到个性化信号。5.5 冷启动用户被“已评分集合为空”卡死现象新用户没有任何评分历史时predict_rating里rated_items为空列表neighbor_pool为空返回空预测程序报KeyError或ZeroDivisionError。原因协同过滤的本质是“用历史换推荐”没有历史就没有任何信号这是算法层做不到的。代码层常见实现问题在于没有在入口处对新用户做分流直接把空评分词典送进了推荐函数。解决在recommend入口加条件分支当用户评分数小于阈值比如 5时直接返回全站热度榜按评分数降序取 TopN。这不丢人工业界叫“冷启动降级策略”。要做得更精致一点可以先用流行度替身待用户积累了足够评分后再切换到协同过滤——这就是后面进阶章节的内容。6. 把代码变得能答辩三个低成本但出彩的进阶方向6.1 用 SVD 把稀疏矩阵压到低维再试把用户-物品矩阵用 SVD奇异值分解压缩到 20 维左右再用压缩后的向量算相似度、做预测通常能在 RMSE 上再压掉 0.05 到 0.1同时代码量增加很小。常见工具是surprise库的 SVD 或scipy.sparse.linalg.svds。课程设计里贴一个“SVD vs ItemCF 的 RMSE 对比”表比纯 ItemCF 有说服力得多from scipy.sparse.linalg import svds def svd_features(mat, k20): 对用户-物品矩阵做截断 SVD返回降维后的用户矩阵与物品矩阵。 u, s, vt svds(mat, kk) # svds 返回奇异值升序排列反转一下方便解释 u, s, vt u[:, ::-1], s[::-1], vt[::-1, :] user_feat u * s # 用户向量 item_feat vt.T # 物品向量 return user_feat, item_featSVD 的原理一句话讲得清把用户和物品同时映射到同一个隐语义空间空间中距离近的用户/物品在“品味”上更接近。答辩时如果被问“SVD 为什么有效”回答“它捕捉了用户和物品之间的隐性关联比如喜欢《黑客帝国》的人往往喜欢《银翼杀手》这种关联不体现在显式评分里但体现在潜在因子中”就够了。6.2 加一个内容画像兜底新电影也推得出去纯协同过滤对“新电影无人评分”束手无策但 MovieLens 的u.item文件里有电影题材可以做一个极其简单的基于内容Content-based的兜底把每部电影表示成题材向量科幻、动作、爱情……计算目标用户历史评分电影的平均题材向量再和候选电影做余弦相似度打分。这个不需要训练代码量不超过 40 行。这个方向比武断用热门榜更“显得懂行”因为它在论文里对应“混合推荐系统”的章节算是一个加分的完整性结构。答辩时主动说“我做了协同过滤为主、内容过滤兜底、热门榜作为最后一道防线”的三层架构比只说“我用的是 ItemCF”要亮眼得多。注意内容画像只能解决“新物品”的冷启动解决不了“新用户”的冷启动。6.3 用一张图交代全过程把相似度矩阵画成热力图论文或答辩 PPT 里放一张相似度矩阵的可视化图会显著提升整体的完成度。画法很简单import matplotlib.pyplot as plt plt.figure(figsize(8, 6)) plt.imshow(sim[:50, :50], cmapviridis) plt.colorbar(labelItem-item similarity) plt.xlabel(Item ID) plt.ylabel(Item ID) plt.title(Similarity matrix heatmap (first 50 items)) plt.tight_layout() plt.savefig(similarity_heatmap.png, dpi150)参数说明取前 50 个物品是担心 1682×1682 的矩阵画出来看不清结构origin参数不用管默认就是左下角为原点。这张图的意义不在于看信息而在于告诉答辩老师“我不仅写了代码还做了结果分析”。配合一段“相似度矩阵中明显存在分块结构说明相似电影聚集在若干题材簇中”的解读这一页基本就稳了。收尾的一个小建议如果用一句话给后来者提建议我会说把评估指标和参数表当成项目的“第二份代码”来写它比推荐列表本身更能证明你理解了推荐系统。课程设计评审的注意力往往不在推荐结果有多惊艳而在你愿不愿意把细节讲清楚——从为什么选 ItemCF 到 K 值调参的曲线再冷启动时做了什么降级每一步都有理有据项目就立住了。所有踩过的坑都补进这篇笔记里了希望帮到你。本文还有配套的精品资源点击获取
返回列表