ARTICLE DETAIL

资讯详情

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

Python协同过滤电影推荐系统源码解析:从UserCF到ItemCF实战

Python协同过滤电影推荐系统源码解析:从UserCF到ItemCF实战 简介基于Python的协同过滤电影推荐系统源码包面向推荐系统开发者、算法学习者及计算机相关专业学生可用于毕业设计、课程项目或技术入门。包内共228个文件以17个Python源码文件为核心覆盖数据预处理、用户与物品相似度计算、基于用户与基于物品的协同过滤、推荐结果生成与排序等完整流程另含csv评分与电影数据集、pyc缓存、js/css前端页面及SQL脚本等整体约6.92MB目录结构清晰便于按模块查阅。实现中涉及余弦相似度、皮尔逊相关系数等常用度量并配有简易交互界面便于直接运行与观察推荐效果。已有60人学习下载对希望快速上手电影推荐系统开发或深入理解协同过滤算法细节的读者而言是一份可运行、可改造的实践参考。1. 基于Python协同过滤算法的电影推荐系统这份源码到底能跑出什么假设你手上有一份MovieLens评分表几万行“用户-电影-评分”三元组。你要做的不是查SQL而是回答一个具体问题用户A没看过的电影里他最可能给出高分的是哪几部基于Python协同过滤算法的电影推荐系统正是用纯Pythonpandas numpy把这个问题落地的源码项目。它不依赖TensorFlow不引入深度模型核心是协同过滤算法里的UserCF与ItemCF两种经典变体。适合三类人刚学完Python基础、想做课程设计的学生需要一个快速基线模型的算法工程师以及想给原型系统加上“猜你喜欢”功能的产品开发者。接下来我会按源码的实现顺序把数据装载、矩阵构建、相似度计算、推荐生成和评估这条链路完整讲透。2. 协同过滤原理与选型UserCF、ItemCF和三种相似度度量的取舍2.1 基于用户的协同过滤UserCF先找相似的人再推他们的片单基于用户的协同过滤User-Based Collaborative Filtering简称UserCF的直觉来自“人以群分”。假设用户A给《肖申克的救赎》《阿甘正传》《低俗小说》都打了接近满分的评价用户B也恰好对《肖申克的救赎》和《阿甘正传》打了高分系统就有理由推断A和B的观影口味相近。此时B给过高分的《绿里奇迹》A还没看过它就会被选进候选推荐列表。这个流程在源码里通常拆成两段离线段和在线段。离线段要两两计算用户之间的相似度形成用户相似度矩阵在线段则从目标用户的K个最近邻居看过的电影里剔除已看过的再按预测评分排序取Top-N。核心代码常见这样写# UserCF 离线相似度计算 def compute_user_similarity(ratings): ratings: dict[user_id][movie_id] rating 返回值: dict[user_id] - [(neighbor_id, sim), ...] users list(ratings.keys()) sim_matrix {u: {} for u in users} for i in range(len(users)): for j in range(i 1, len(users)): u1, u2 users[i], users[j] # 取两个用户共同评过分的电影集合 common set(ratings[u1].keys()) set(ratings[u2].keys()) if len(common) 2: # 共同评过分至少2部电影结果才可信 continue sim cosine_similarity(ratings[u1], ratings[u2], common) sim_matrix[u1][u2] sim_matrix[u2][u1] sim return sim_matrix这段代码的关键在common交集。如果两个用户只共同看过一部电影算出的相似度是随机噪声把共同项阈值设成2或5是控制推荐结果稳定性的第一个开关。cosine_similarity内部实现是标准的余弦夹角先按共同电影取出两个评分向量再点积除以模长。这里要注意向量里只包含共同评分的电影不能用全量向量补0否则稀疏矩阵会把无数不相关用户拉成“高度相似”推荐质量会明显下降。选UserCF还是ItemCF实践里有个简单判断标准如果用户数远大于物品数计算用户相似度矩阵的代价就更大。电影推荐场景里电影数量在几千到几十万用户量却可能破千万工业界因此更倾向ItemCF。但在课程设计这种用户只有几百的场景UserCF反而更好调试因为你可以直接抽查某个用户看他的邻居画像是否合理。2.2 基于物品的协同过滤ItemCF先算电影邻居再借历史评分加权基于物品的协同过滤Item-Based Collaborative Filtering简称ItemCF思路恰好反过来它把“相似的电影”作为推荐单元。逻辑是如果很多人同时给《盗梦空间》和《星际穿越》打高分这两部电影在行为数据上就是相似的。之后用户对《盗梦空间》给出好评系统直接把《星际穿越》推给他不需要知道用户和谁口味相似。ItemCF相比UserCF有更直观的可解释性推荐理由能写成“因为你看过《盗梦空间》所以推荐《星际穿越》”。源码实现里离线计算从用户两两相似变成电影两两相似。同样的数据量下电影相似度矩阵的行列数由电影数量决定通常比用户矩阵更大但更稀疏。一份典型实现是# ItemCF 离线计算电影相似度矩阵 def compute_item_similarity(ratings): ratings: dict[user_id][movie_id] rating 返回值: dict[movie_id] - [(neighbor_id, sim), ...] movies set() for ratings_of_user in ratings.values(): movies.update(ratings_of_user.keys()) # 先统计每部电影被哪些用户评过分建立倒排索引 movie_users {m: set() for m in movies} for uid, movie_ratings in ratings.items(): for mid in movie_ratings: movie_users[mid].add(uid) item_sim {} movie_list list(movies) for i in range(len(movie_list)): for j in range(i 1, len(movie_list)): m1, m2 movie_list[i], movie_list[j] common_users movie_users[m1] movie_users[m2] if len(common_users) 2: continue # 用余弦相似度评分就是向量分量 dot sum(ratings[u][m1] * ratings[u][m2] for u in common_users) norm1 sum(ratings[u][m1] ** 2 for u in movie_users[m1]) ** 0.5 norm2 sum(ratings[u][m2] ** 2 for u in movie_users[m2]) ** 0.5 item_sim.setdefault(m1, {})[m2] dot / (norm1 * norm2) item_sim.setdefault(m2, {})[m1] dot / (norm1 * norm2) return item_sim这里有个性能上容易翻车的点双重循环遍历电影列表时间复杂度是O(M²)M是电影数量。MovieLens 100K数据集里电影数是1682全量两两计算只有约140万对纯Python勉强能扛。如果换成10万部电影的工业数据集这个双重循环会膨胀到百亿级别。所以业内通常用倒排索引加Top-N截断——只保留每部电影相似度最高的几十个邻居而不是全量矩阵。很多初学者把源码原样换成大数据集跑一晚上没结束就以为死循环了实际上是没估算复杂度。这里的movie_users倒排索引就是为后续优化预留的接口把内层循环改成只遍历候选电影能节省一个数量级的时间。2.3 余弦、皮尔逊、杰卡德三种相似度各自的适用边界相似度度量是协同过滤的灵魂选错公式推荐结果会变得很怪。三个度量各有侧重源码里通常都封装好留一个参数切换。余弦相似度只关心评分向量方向不关心长度。它天然适配“两个用户打分基准不同但趋势一致”的情况。实现上点积除以模长即可但要注意如果用户评分向量里没评分的位补0稀疏矩阵会让大量用户被算成“完全不相似”这也是源码里用共同评分集合而不补0的原因。皮尔逊相关系数会对每个用户的评分先做均值中心化减去个人平均分后再算余弦。这一步消除了“有人手松给分高有人手紧给分低”的系统性偏差。举个例子A的平均分是4.5B的平均分是3.0两人对同一部电影的评分都高于各自平均分0.5分余弦会认为他们不够相似皮尔逊却会觉得口味一致。在评分习惯差异明显的场景里皮尔逊通常优于余弦。杰卡德相似系数不考虑评分数值只看两个用户共同评过多少部电影交集除以并集。它适用于评分记录稀疏、数值本身不可信的场景比如隐式反馈点过1没点0。但如果评分是1到5的整数直接用杰卡德会丢掉大量信息属于信息利用不充分。我一般按这个顺序做实验先跑皮尔逊看RMSE是否合理效果差就换余弦对比一次数据若是隐式反馈则直接上杰卡德。三种度量不是可以随意替换的等价选项它们对应的是“评分习惯是否偏移”“数据是否稀疏”“是显式还是隐式反馈”三个不同假设。源码里推荐函数把这三个函数并列封装正是为了做对照实验时不改主流程。3. 复现这份源码MovieLens数据集装载、评分矩阵构建与推荐主流程3.1 u.data字段与装载脚本三个参数别抄错打开源码包里的data目录最常见的是三份文件u.data评分主表、u.user用户属性、u.item电影属性。u.data每一行是user_id movie_id rating timestamp制表符分隔。rating取值是1到5的整数timestamp是Unix时间戳推荐计算里基本用不到但保留着方便做时间维度的切分。先把评分表装进内存常见做法是用pandas的read_csvimport pandas as pd # 加载 MovieLens 100K 的 u.data df pd.read_csv( data/u.data, sep\t, # 注意是制表符不是逗号 headerNone, # 原文件没有列名必须指定 names[user_id, movie_id, rating, timestamp] ) print(df.head()) print(用户数:, df[user_id].nunique()) print(电影数:, df[movie_id].nunique()) print(评分总数:, len(df))参数说明sep\t必须写对这是网上抄代码时最容易出错的地方写成逗号后pandas会把整行读成一列headerNone表示文件本身没有列名列名由names参数补充。三个实际输出的数量在100K数据集上通常是943、1682、100000对不上就先检查数据集版本。字段装载是整套源码的地基这里读错后面的矩阵和相似度全是错位数据而且极难察觉——评分全对推荐出来的电影ID对不上这是典型的“数据没洗干净的玄学问题”。3.2 从DataFrame到评分矩阵稀疏矩阵与字典两种存储选型协同过滤的计算核心是“用户×电影”的二维矩阵。但直接用pandas构造943×1682的稠密DataFrame每个格子都填值矩阵里93%以上的位置是空值内存和算力都浪费在零上。源码里常见两种做法scipy.sparse稀疏矩阵和dict套dict结构。前者面向计算后者面向调试。from scipy.sparse import csr_matrix # 方式一scipy 稀疏矩阵适合相似度计算 user_ids df[user_id].astype(category).cat.codes.values movie_ids df[movie_id].astype(category).cat.codes.values ratings df[rating].values rating_matrix csr_matrix((ratings, (user_ids, movie_ids))) print(稀疏矩阵形状:, rating_matrix.shape) print(非零元素个数:, rating_matrix.nnz) # 方式二字典套字典适合写循环和调试 def df_to_dict(df): data {} for row in df.itertuples(indexFalse): uid, mid, rating row.user_id, row.movie_id, row.rating data.setdefault(uid, {})[mid] rating return data rating_dict df_to_dict(df) print(用户196的评分记录数:, len(rating_dict[196]))逻辑说明把user_id和movie_id用astype(category).cat.codes转成连续的整数编码是为了能用数组下标直接定位矩阵避免字符串查表拖慢后续的密集循环。稀疏矩阵只存非零元素形式上还是943行×1682列内存占用却只和10万个真实评分相关。字典版本是逻辑调试的入口写推荐循环时rating_dict[uid][mid]一眼能看懂但它的哈希表开销比稀疏矩阵大一个量级适合数据量小、需要频繁读单条记录的阶段。这里要提醒一句两种结构不能混用索引。稀疏矩阵的下标是重编码后的整数字典的key是原始user_id两者不一致。在数据处理流程里我会把“原始ID转编码”的映射关系保存为两个字典否则最后推荐结果输出给业务系统时根本没有办法把电影编码翻译回电影原名。3.3 UserCF预测函数逐行看懂加权平均评分这是整套源码最核心的一段逻辑。给定目标用户u先从相似度最高的K个邻居里收集候选电影再用邻居的评分做加权平均权重就是相似度。def predict_rating(u, i, user_sim, rating_dict, k20): 预测用户 u 对电影 i 的评分 u: 目标用户id i: 目标电影id user_sim: 用户相似度字典 {u: [(neighbor_id, sim), ...]} rating_dict: 用户评分字典 {u: {i: rating}} k: 参与预测的邻居数量 neighbors user_sim[u][:k] # 只取相似度最高的前 k 个邻居 total_sim 0.0 weighted_sum 0.0 for neighbor, sim in neighbors: # 邻居必须真的给电影 i 评过分 if i in rating_dict.get(neighbor, {}): r_ni rating_dict[neighbor][i] # 邻居对 i 的实际评分 weighted_sum sim * r_ni total_sim sim if total_sim 0: return None # 没有邻居评过放弃预测 return weighted_sum / total_sim # 相似度加权平均分参数说明k20是邻居数量是后续调参最关键的旋钮。weighted_sum / total_sim是相似度加权平均相似度越高的邻居说话越有分量。这里没有做均值中心化工程上可以先用这个版本跑通再看要不要引入中心化提高精度。返回None的情况会被上层跳过滤掉进入冷启动兜底分支——如果你发现预测结果大量是None先检查相似度邻居是不是为空而不是怀疑预测函数本身。值得注意这个预测函数是“逐部电影”计算的实际生成推荐列表时要枚举所有用户没看过的电影逐一预测再排序取前N。在943个用户、1682部电影的数据集上这个循环还能跑动用户和电影数量各放大100倍时就必须改成先聚合候选集再预测或者改用矩阵分解。这也是为什么源码的注释里通常会写“本实现面向教学场景生产环境请改用近似最近邻”。3.4 ItemCF推荐列表生成历史评分乘相似度再累加ItemCF在生成推荐时不需要实时遍历全部用户而是从用户的历史评分电影出发把每部电影的相似邻居捞出来按“历史评分×电影相似度”累加得分排序。def recommend_item_cf(rating_dict, item_sim, user_id, top_n10, k10): 基于 ItemCF 给用户推荐 top_n 部电影 item_sim: 电影相似度字典 {m: [(neighbor_movie_id, sim), ...]} k: 每部已看电影取多少个相似邻居 user_rated rating_dict.get(user_id, {}) scores {} # movie_id - 累计得分 for movie, r in user_rated.items(): # 从每部已看过的电影找它最相似的 k 部电影 for sim_movie, sim in item_sim.get(movie, [])[:k]: if sim_movie in user_rated: # 已看过的电影不再推荐 continue # 得分 历史评分 × 两部电影的相似度 scores[sim_movie] scores.get(sim_movie, 0.0) r * sim ranked sorted(scores.items(), keylambda x: -x[1]) return [mid for mid, _ in ranked[:top_n]]逻辑说明r * sim是ItemCF经典的打分公式。用户给《盗梦空间》的评分越高且《盗梦空间》与《星际穿越》越相似《星际穿越》的候选得分就越高。scores字典把同一部电影来自不同历史入口的得分累加自然融入“看过越多相似片排名越靠前”的信号。[:k]截断是性能开关实际使用中k取10到50太大不会显著提升效果反而会把相似度很低的噪音电影拖进来。到这里主干逻辑已经跑通。最小复现命令通常是python main.py --alg item_cf --top_n 10 --k 10如果输出里有一半电影是用户已经看过的检查recommend_item_cf里是否漏掉了user_rated过滤这一行决定推荐列表是惊喜还是废话。完整跑通后下一步就是调参看指标下面第4章会把最影响效果的四个参数逐一说清楚。4. 调好四个旋钮邻居数、均值中心化、相似度阈值与评估指标4.1 邻居数K从“覆盖”到“精准”的平衡点K是协同过滤里最直观也最影响效果的参数。K太小参与加权的邻居太少预测评分方差大偶尔会输出极端值K太大大量相似度很低的邻居进入加权把预测结果往全局平均分拉推荐列表变得中庸。MovieLens 100K上UserCF的K常见取值是10到80。K20时覆盖率不会太低推荐的电影相对精准K80时RMSE通常会略微下降但推荐列表里开始出现用户完全没兴趣的冷门片。实践里我用一组小实验来定K分别跑K10、20、30、40、50、80打印RMSE和Top-10命中率选两者乘积最大的K。如果你的源码实现了网格搜索也不难但要注意K调整后相似度矩阵本身不变不需要重新计算只是预测阶段取邻居数量不同所以跑一组K值的时间成本很低。提示K的调参结果高度依赖数据规模换一个数据集后原来的最优K就不作数了不要迷信别人博客里的“K20最优”。4.2 评分均值中心化防止“手松”和“手紧”干扰预测在2.3节提过皮尔逊协作就做了均值中心化。同样的道理UserCF的加权平均公式里如果不做中心化手松的用户平均分4.8天然比手紧的用户平均分2.5更有“话语权”。因为预测公式里邻居评分直接参与加权手松用户的评分普遍偏高他作为邻居时会系统性拉高预测值。改进后的预测公式长这样def predict_rating_centered(u, i, user_sim, rating_dict, avg_rating, k20): avg_rating: dict[user_id] - 该用户的平均评分 预测值 目标用户平均分 邻居评分偏差的加权平均 neighbors user_sim[u][:k] total_sim 0.0 weighted_sum 0.0 for neighbor, sim in neighbors: if i in rating_dict.get(neighbor, {}): r_ni rating_dict[neighbor][i] # 减掉邻居自身平均分只取“偏差”参与加权 weighted_sum sim * (r_ni - avg_rating[neighbor]) total_sim sim if total_sim 0: return None return avg_rating[u] weighted_sum / total_sim逻辑说明avg_rating[u]是目标用户自己的平均分代表他的基准打分习惯r_ni - avg_rating[neighbor]是邻居对这部电影的“额外喜好程度”。中心化之后手松和手紧的差异被抹平预测的是“这个用户在自己基准之上会对这部电影有多少额外好感”。这项改动通常能明显降低RMSE也是源码里“简单版”和“完整版”预测函数的主要区别。4.3 共同评分项阈值过滤“偶然重合”的伪邻居相似度计算里有一层隐藏参数共同评分项数量下限。两个用户只共同看过一部电影时余弦相似度不是1就是-1因为只有一维向量方向必然完全一致或完全相反。这种相似度没有任何统计意义却会把完全不搭边的用户配对成“最佳邻居”。源码里通常会对相似度计算加一个条件def similarity_with_min_common(ratings_u1, ratings_u2, min_common5): common set(ratings_u1.keys()) set(ratings_u2.keys()) if len(common) min_common: return 0.0 # 共同评分太少认为不相似 # 继续算余弦或皮尔逊 ...参数说明min_common5表示两个用户至少共同评过5部电影相似度才被接受。这个阈值在UserCF里建议设2到10之间设太高会把活跃用户的大部分邻居过滤掉推荐覆盖率暴跌设太低又会混入大量随机邻居。判断标准很简单打印一下每个用户的有效邻居数量如果大量用户邻居数不足K说明min_common设高了或者数据本身太稀疏。数据稀疏时这个阈值就是“后悔药”——先把阈值调低跑通再逐步调高看指标变化。很多源码里没有暴露这个参数如果你想改通常只需要在相似度函数入口处加这一行判断不需要动主流程。4.4 RMSE、PrecisionN与召回率一次把评估脚本写对推荐系统的评估最怕只用“预测准不准”一个维度因为RMSE低不代表推荐列表用户爱看。我一般会同时跑三个指标RMSE看回归精度PrecisionN和召回率看排序质量。from sklearn.metrics import mean_squared_error def evaluate(test_ratings, predict_func, k20): test_ratings: dict[user_id][movie_id] rating 返回 RMSE、Precision10、Recall10 y_true, y_pred [], [] hit_count 0 total_recommend 0 total_relevant 0 for uid, items in test_ratings.items(): # 收集该用户在测试集里的真实评分 recent_items list(items.keys()) if len(recent_items) 0: continue # 对候选电影预测评分 for mid, true_r in items.items(): pred_r predict_func(uid, mid, kk) if pred_r is None: continue y_true.append(true_r) y_pred.append(pred_r) # 取预测分数最高的前10部电影作为推荐列表 recommend_list top_n_recommend(uid, predict_func, top_n10, seenset(items.keys())) hit_count len(set(recommend_list) set(recent_items)) total_recommend 10 total_relevant min(len(recent_items), 10) rmse mean_squared_error(y_true, y_pred) ** 0.5 precision hit_count / total_recommend recall hit_count / total_relevant if total_relevant else 0.0 return rmse, precision, recall参数说明Precision10衡量推荐列表里有多少电影是用户确实看过的Recall10衡量推荐列表覆盖了用户真实观看行为的多大比例。这两项在电影场景里通常互斥选阈值时要看产品侧更在意“推荐的是不是精品”还是“能覆盖多少兴趣面”。这里必须强调评估数据分割方式如果随机打乱评分再分割成训练集和测试集同一用户对同一部电影的评分不会出现两次所以随机分割在这类数据上不算严重泄漏。但按用户分割更贴近真实场景——用用户前80%观看行为训练后20%验证模拟的正是“已知历史预测未来”。两种分割方式得出的RMSE差距通常不小源码里建议两种都保留对比着看。5. 避坑指南协同过滤源码跑起来之后的五个常见问题5.1 新用户推荐列表为空冷启动问题没有兜底方案现象新注册用户没有任何评分记录调用推荐函数直接返回空列表。原因协同过滤的推荐流程依赖历史评分。UserCF要和新用户算相似度ItemCF要拿新用户的评分记录去关联相似电影两者都要求用户先有行为数据这是协同过滤与生俱来的冷启动短板。解决在推荐入口加一个兜底分支无历史行为时按全局热度榜单推荐def recommend_with_fallback(rating_dict, user_id, top_n10): if user_id not in rating_dict or len(rating_dict[user_id]) 0: return global_hot_movies(top_n) # 兜底全局最热电影 return recommend_item_cf(rating_dict, item_sim, user_id, top_n)这套兜底方案在课程设计里足够但在生产里通常还会加上基于内容画像的召回通道协同过滤只作为其中一个召回来源。5.2 相似度普遍偏低甚至为0零填充把向量夹角拉偏了现象算出来的用户相似度大量是0Top-N推荐质量极差。原因写余弦相似度时把用户对没看过电影的评分填充成0再构建全量向量计算夹角。用户向量里大部分维度是0导致夹角几乎都接近90度余弦值趋近0。解决只用共同评分项构造向量即先取common集合再从集合中取值组向量。如果一定要用全量向量就把0项排除在点积计算之外。5.3 皮尔逊系数出现nan或报标准差为0评分完全一致的用户现象计算相似度时报nan或者输出莫名其妙的高分推荐。原因皮尔逊相关系数的分母是评分向量的标准差。当两个用户共同评分的电影恰好得分完全一致时标准差为0除零后产生nan。极端情况是两个用户只共同评过同一部电影且分数相同这种情况在稀疏数据里非常常见。解决相似度函数里加一个判断标准差为0或共同项太少时返回0而不是让nan污染整个相似度矩阵。def pearson_similarity(vec1, vec2): mean1, mean2 sum(vec1) / len(vec1), sum(vec2) / len(vec2) diff1 [v - mean1 for v in vec1] diff2 [v - mean2 for v in vec2] denom (sum(v**2 for v in diff1) * sum(v**2 for v in diff2)) ** 0.5 if denom 0: return 0.0 return sum(a * b for a, b in zip(diff1, diff2)) / denom这段代码里denom 0的早退判断避免的就是nan问题。很多源码教程省略了这个保护导致调参时相似度矩阵里一旦出现nan后续所有加权计算全被污染而且错误会在几层循环之后才暴露极难排查。5.4 预测评分全部挤在同一个值附近加权公式里漏了归一化现象预测评分几乎都收敛到3.5到4.0之间推荐列表排序全靠微弱差异支撑。原因计算加权平均时只累加sim * r没有累加sim做分母归一化或者归一化时除以的是总邻居数而不是有效评分邻居的总相似度。不同用户的邻居数量差异很大不归一化时邻居多的用户天然得分更高。解决回到3.3节的代码确认分母是total_sim参与评分的邻居相似度之和不是固定数字。这是新手最容易悄悄改坏的地方。5.5 离线评估结果虚高切分数据时混进了未来信息现象评估指标很好看RMSE很低但上线后推荐效果远不如离线结果。原因把整个评分表随机切成训练集和测试集可能同一个用户同一部电影同时出现在两边更常见的是用户早期的评分混进测试集后看的行为混进训练集模型相当于“提前看答案”。解决按时间戳切分。MovieLens每行评分都带timestamp按时间戳升序排序每个用户取前80%做训练、后20%做测试严格模拟预测未来的场景。切分函数可以参考# 按评分时间序为用户切分训练测试集 df df.sort_values([user_id, timestamp]) train, test [], [] for uid, group in df.groupby(user_id): if len(group) 2: train.append(group) continue cut int(len(group) * 0.8) train.append(group.iloc[:cut]) test.append(group.iloc[cut:]) train_df pd.concat(train) test_df pd.concat(test)这组代码处理完的train_df和test_df直接喂给后续评估函数你会发现RMSE会比随机切分高一些但那才是真实水平。6. 一个提升效果的进阶技巧在ItemCF中引入评分偏好加权调完参数、避开常见坑之后想让推荐结果再上一个台阶可以试一个改动成本极低的技巧给电影相似度计算加入热门惩罚项同时给用户历史评分做时间衰减。前者解决“热门电影和谁都相似”的偏置问题后者让近期的观影偏好权重更高。热门惩罚的原理很简单两部电影共同评分的人越多先验上相似度就越高——因为它们都是热门片。用1 / log(1 popularity)作为权重乘到相似度上可以有效压住热门片的过度扩散。时间衰减更直接在4.4节的recommend_item_cf里把历史评分r乘上一个随时间递减的系数近期评分权重更高。import math, time def recommend_with_time_decay(rating_dict, item_sim, user_id, timestamp_dict, top_n10, half_life_days30): timestamp_dict: dict[(user_id, movie_id)] - 评分Unix时间戳 半衰期 half_life_days30超过30天的评分权重衰减一半 now time.time() user_rated rating_dict.get(user_id, {}) scores {} for movie, r in user_rated.items(): ts timestamp_dict.get((user_id, movie), now) age_days (now - ts) / 86400.0 # 时间差换算成天数 decay 0.5 ** (age_days / half_life_days) # 指数衰减 for sim_movie, sim in item_sim.get(movie, [])[:10]: if sim_movie in user_rated: continue scores[sim_movie] scores.get(sim_movie, 0.0) r * decay * sim ranked sorted(scores.items(), keylambda x: -x[1]) return [mid for mid, _ in ranked[:top_n]]逻辑说明0.5 ** (age_days / half_life_days)是标准的指数半衰期衰减30天前评分的权重是现在的0.560天前是0.25。decay直接乘进得分既不改变原算法框架又让推荐结果更贴近用户当前兴趣。这个技巧在电影这类兴趣变化缓慢的领域提升有限但如果换成短视频、新闻这类时效性强的场景收益会非常明显。这两个技巧配合使用是我个人在这套源码上做提升最顺手的一套组合。先加热门惩罚压住头部效应再用时间衰减贴近当下口味两者都不需要改动相似度矩阵的主体结构工程量小、回滚也容易。如果你现在手里正跑着一份协同过滤的课程设计或实验基线不妨先备份一份原结果再按这个思路改用同一份测试集对比新旧版本的RMSE和Precision10。这套源码的价值不只是能跑通更重要的是它把推荐系统的几个关键决策点都暴露在你能看到、能改成的地方——希望帮到你。本文还有配套的精品资源点击获取
返回列表