
简介面向新闻推荐场景的Python协同过滤算法毕业设计项目资料包适合计算机、软件工程等专业学生完成推荐系统方向的课题设计与开发。该项目围绕协同过滤推荐链路组织涵盖数据获取与清洗、用户行为矩阵构建、基于用户与物品的相似度计算、Top-N推荐生成以及准确率、召回率等评价指标并配有前端展示页面、后端服务代码与数据库脚本可辅助跑通从数据预处理到推荐结果的全流程。压缩包共146个文件主要由Vue前端页面、Java/Scala服务端源码、Python处理脚本、XML及properties配置、SQL建表语句和parquet样例数据组成整体约577KB。目前已有353人学习下载。文件按模块分层存放既包含算法训练与测试类文件也包含Web展示与运维部署配置目录结构清晰、便于对照改造可直接作为毕业设计参考模板或推荐系统入门实践材料。1. 协同过滤算法做新闻推荐为什么MovieLens的代码换个数据就跑不动了我见过不少毕业设计选这个题目觉得“协同过滤算法”听起来经典把开源的电影推荐代码换成新闻数据就能交差。结果一跑就发现问题推荐列表里全是半个月前的旧闻新用户点开根本没内容而且同一个事件的不同报道被当成完全不同的新闻推给同一个人。原因很简单电影是长生命周期物品用户评分稳定新闻是短生命周期内容用户行为稀疏还有强时效性。把协同过滤算法直接搬到新闻推荐系统不改造数据结构和时间衰减几乎必然翻车。这个标题下的项目真正要解决的是如何用Python对新闻点击行为做协同过滤让推荐结果既懂用户口味又赶得上热点。适合打算拿它当毕业设计、或者刚入门推荐系统想练手的开发者和学生。下文给出一套能从零落地的设计、代码和调参路径。2. 协同过滤在新闻推荐里的适用边界先搞懂为什么不能照搬电影推荐2.1 基于物品的协同过滤为什么比基于用户更适合新闻场景协同过滤分两类基于用户UserCF和基于物品ItemCF。基于用户的思想是找到兴趣相似的一群用户把他们点击过的新闻推荐给你基于物品的思想是找到与你历史点击新闻相似的新闻推荐给你。乍一看都有道理但在新闻场景下我一般不会选UserCF原因有三个。第一新闻平台的用户量远大于新闻量。一天的新增新闻可能只有几千条活跃用户却是百万级。UserCF需要实时计算用户之间的相似度行为一变就要更新计算成本很高ItemCF虽然也要算相似度但物品数量少得多而且新闻相似度可以每天离线算一次在线推荐时只用查表加求和响应速度能做到毫秒级。毕业设计的数据规模可能看不出差别但答辩时讲清楚这一点评委就知道你考虑过工程可行性。第二用户的兴趣在新闻场景下漂移很快。昨天看科技新闻的人今天可能关注体育赛事。UserCF保留的是长期兴趣画像对短期会话行为不敏感ItemCF直接基于用户最近点击的几篇新闻做推荐天然贴合这种短时兴趣模式。如果你想在论文里解释“为什么用ItemCF”这就是最有力的论据。第三新闻之间存在天然的同主题聚类关系。比如某款手机发布会的新闻用户看了A媒体的报道大概率会接着看B媒体的跟进报道。这样的物品对物品相似关系比用户对用户更容易解释也更容易做成“看了这篇还想看这些”的推荐流。因此本文后面说的协同过滤算法默认指基于物品的协同过滤ItemCF并用Python实现一个最小可用版本。经典ItemCF的推荐过程分两步第一步通过用户点击历史计算新闻两两之间的相似度第二步根据用户最近点击过的新闻找出相似新闻排除已读和过期内容生成Top N列表。这个流程看起来简单真正落地时难点在数据清洗和参数设置。2.2 从点击日志到行为矩阵数据清洗与用户-新闻表构建任何协同过滤都建立在用户行为数据之上。电影推荐常用1~5星评分但新闻场景几乎没有显式评分最自然的隐式反馈是“点击”。你要做的第一件事就是把原始点击日志整理成一张行为表字段至少包括下面这些字段示例作用user_idU1001用户唯一ID匿名化处理news_idN3042新闻唯一IDbehaviorclick点击、收藏、分享、停留时长等timestamp2025-05-12 10:30行为发生时间用于时间衰减publish_time2025-05-12 08:00新闻发布时间用于时效性过滤category科技新闻分类可作为辅助特征毕业设计的数据来源通常是自己写爬虫抓新闻网站或者用公开的新闻点击数据集。用爬虫时要注意抓下来的页面里经常有大量转载必须按标题和正文的相似度去重否则同一事件的多篇报道会被当成完全不同的物品推荐结果会变成同一个事件的刷屏列表。我一般用标题标准化加SimHash去重简单一点直接用标题里前20个字符去重也能凑合。拿到原始日志后清洗时按这几条来。第一过滤停留时长小于2秒的点击这类行为大概率是误触。怎么算停留如果前后两条点击间隔极短或者没有后续页面浏览记录就可以标记为无效。第二删除只出现一次行为的用户和只被点击一次的新闻。单个行为无法构成协同信号留着只会让矩阵更稀疏。第三对同一用户短时间反复点击同一新闻只保留第一次避免刷新行为影响权重。清洗完成后下一步是构造两个核心数据结构。第一个是user_items键是user_id值是该用户点击过的news_id列表第二个是item_users键是news_id值是该新闻被哪些用户点击过。协同过滤的相似度计算需要的是“共现关系”也就是两个新闻被同一个用户点击过所以user_items是基础item_users是索引用来加速。用pandas读取CSV再用defaultdict组织数据结构是最顺手的写法。下面这段代码把行为表变成协同过滤算法的输入import pandas as pd from collections import defaultdict # 读取清洗后的行为数据timestamp和publish_time必须转成datetime df pd.read_csv(news_behavior.csv, parse_dates[timestamp, publish_time]) # 按用户聚合user - [news_id, ...] user_items defaultdict(list) for row in df.itertuples(): user_items[row.user_id].append(row.news_id) # 按新闻聚合news - set(user_id) item_users defaultdict(set) for row in df.itertuples(): item_users[row.news_id].add(row.user_id) # 过滤极小众新闻只被1个用户点击过的新闻相似度计算没有意义 min_support 2 item_users {k: v for k, v in item_users.items() if len(v) min_support}这段代码有两个关键点。一是defaultdict(list)与defaultdict(set)的区别用户对新闻的点击可能有多次所以用list记录顺序一个新闻被一个用户点击多次不能算共现所以用set去重。二是过滤极低频新闻时阈值min_support设多少要看数据量。数据量在万级以下设2就够了数据量大到十万级可以设3~5能明显减少计算量。这个阈值直接影响相似度矩阵的稀疏程度后面调参会再提到。注意一点新闻推荐几乎不可能拿到真正的负反馈用户没点不代表不喜欢所以行为矩阵只记录正反馈。别去构造“未点击”作为负样本那会让相似度计算完全跑偏。如果你手里有收藏、分享、点赞行为可以后续作为行为权重加进去比如点击记1分、收藏记2分、分享记3分。但在协同过滤的第一步只考虑点击已经足够跑通整个流程。3. 用Python从零实现协同过滤算法相似度计算与推荐生成3.1 用户-物品倒排表与余弦相似度的Python实现上一章的item_users就是“物品-用户倒排表”。现在用它计算新闻之间的相似度。经典ItemCF的相似度公式是余弦相似度sim(i, j) |N(i) ∩ N(j)| / (sqrt(|N(i)|) * sqrt(|N(j)|))其中N(i)是点击过新闻i的用户集合。分子是两个集合的交集大小也就是共同点击用户数分母是各自用户规模的几何平均用来抵消热门新闻天然被更多人看过的偏差。注意这里不是新闻内容向量的相似度而是“用户行为相似度”所以叫协同过滤。计算时不能把每个物品和一个一维向量全量存下来时间复杂度是O(N^2)。常见做法是只对“出现在同一个用户点击列表里”的新闻两两统计共现次数。因为行为数据稀疏真正有共现的新闻对只占极小比例。下面是一个直观的Python实现import math from collections import defaultdict def compute_item_similarity(user_items, item_users): # user_items: {user_id: [news_id, ...]} # item_users: {news_id: set(user_id)} co_occur defaultdict(int) # 遍历每个用户的点击列表统计新闻对共现次数 for uid, items in user_items.items(): # 去重并按时间排序后的列表保留最近50篇控制计算量 uniq_items list(set(items))[:50] for i in range(len(uniq_items)): for j in range(i 1, len(uniq_items)): a, b uniq_items[i], uniq_items[j] co_occur[(a, b)] 1 co_occur[(b, a)] 1 # 分母每篇新闻对应的用户数 item_count {item: len(users) for item, users in item_users.items()} sim_matrix {} for (a, b), cnt in co_occur.items(): sim cnt / math.sqrt(item_count[a] * item_count[b]) sim_matrix[(a, b)] sim return sim_matrix这段代码里[:50]是“用户物品列表截断长度”。电影推荐通常取10~20新闻因为行为更稀疏可以取30~50。截断会丢失长尾信号但能显著降低计算量和内存占用。毕设数据集几万条行为不截断也能跑但为了体现规模化思路建议保留。另一个容易踩坑的地方是list(set(items))丢失了时间顺序。如果后续要加入“最近点击更重要”的逻辑最好先把用户点击列表按时间升序排序再取最后50个而不是直接set(items)。为了后续推荐时不用遍历全表我们把相似度矩阵转成“邻居表”对每个新闻只保留相似度最高的K个邻居。K通常取20这既是调参结果也是控制在线推荐计算量的手段。def build_neighbors(sim_matrix, k20): neighbors defaultdict(list) for (a, b), sim in sim_matrix.items(): if a ! b: neighbors[a].append((b, sim)) # 对每个新闻按相似度从高到低排序取前k for item, lst in neighbors.items(): lst.sort(keylambda x: x[1], reverseTrue) neighbors[item] lst[:k] return neighbors这里把(b, sim)直接存入列表推荐时只需要找这个邻居表不需要再访问原始矩阵。注意如果新闻总量很大build_neighbors同样要消耗内存但k20时每个物品只保留20个pair整体大小可控。3.2 基于ItemCF的推荐生成评分公式与Python函数拿到邻居表后给用户生成推荐的逻辑是找到用户最近点击过的新闻把这些新闻的相似邻居拿出来累加打分。经典公式为score(u, j) Σ_{i in I_u} sim(i, j) * w_i * time_decay(t_i)其中I_u是用户u点击过的新闻集合w_i是行为权重点击为1收藏为2分享为3time_decay是一个按行为时间衰减的系数。新闻场景下时间衰减比相似度本身更重要因为用户昨天看的新闻和三个月前看的新闻对当前推荐的影响应该完全不同。实现时先过滤用户已经看过的新闻再过滤发布时间太久的旧闻。下面这段代码是完整的推荐函数import datetime def recommend_for_user(uid, user_items, neighbors, news_info, top_n10, max_age_days3): # user_items: {user_id: [news_id, ...]}列表按行为时间升序 # neighbors: {news_id: [(neighbor_id, sim), ...]} # news_info: {news_id: {publish_time: datetime, title: str, ...}} seen set(user_items.get(uid, [])) if not seen: return [] # 冷启动由外部热门兜底 score defaultdict(float) now datetime.datetime.now() # 对每个用户只取最近7天内点击过的新闻作为触发源 recent_items [ nid for nid in user_items[uid] if news_info[nid][publish_time] now - datetime.timedelta(days7) ] for i in recent_items: # 行为时间衰减距离行为时间越近权重指数越高 behavior_gap now - news_info[i][publish_time] # 用发布时间近似行为时间 time_weight math.exp(-behavior_gap.days / 7.0) for j, sim in neighbors.get(i, []): if j in seen: continue # 过滤过期新闻 if news_info[j][publish_time] now - datetime.timedelta(daysmax_age_days): continue score[j] sim * time_weight ranked sorted(score.items(), keylambda x: x[1], reverseTrue)[:top_n] return [news_id for news_id, _ in ranked]这个函数有三个必调参数。第一个是max_age_days控制在推荐时排除发布超过多少天的新闻。新闻场景一般设2~3天设太长用户会看到“上周的旧闻”交互体验很差。第二个是time_weight这里用新闻发布时间近似了行为时间严格来说应该用用户点击行为发生的timestamp来算。如果你清洗后的数据里有timestamp字段把取user_items时的时间一并保存衰减会更准确。第三个是top_n推荐列表长度网页端一般是10~20移动端5~10。上面的实现有一个小问题recent_items里如果一篇新闻是昨天发布的另一篇是三天前发布的time_weight会差很多这符合预期。但如果你用发布时间来代替行为时间可能会把“三个月前发布但今天被点击的经典新闻”错误降权。更好的做法是在构建user_items时把每篇新闻的行为时间也存进列表比如存成[(news_id, timestamp), ...]推荐时直接用该时间来算衰减。这样会稍微增加内存但推荐逻辑更可信。另外在线推荐时如果每次都从user_items里取最近7天新闻再查邻居表速度已经够快。真正拖慢速度的是冷启动和新闻池过大。如果你发现推荐响应超过200毫秒可以先给每个用户缓存一个推荐结果每小时更新一次。毕业设计里这个优化值得写进论文因为面试官很喜欢问“你的接口能不能扛住并发”。4. 新闻推荐系统避坑指南5个常见问题与排查4.1 推荐全是热门新闻个性化失效现象所有用户的推荐列表都差不多全是点击量最高的新闻看起来像在复读热榜。原因ItemCF的经典陷阱。新闻越热门被越多用户点击它与其他新闻的共现机会越多相似度得分越高最终推荐时热门新闻会以绝对分数优势挤掉长尾新闻。余弦相似度的分母虽然惩罚了热门度但惩罚力度不够。解决在推荐打分时引入热门惩罚。常见做法是对候选新闻的用户规模取对数再乘到相似度上sup len(item_users.get(j, [])) # j是候选新闻 penalty 1.0 / math.log(1 sup) score[j] sim * time_weight * penalty这样越热门的新闻其作为候选时的得分被压制。注意不要过度惩罚否则推荐列表变得很偏门用户不认识那些小网站文章。惩罚强度可以用一个alpha系数控制penalty 1.0 / math.log(1 sup) ** alphaalpha在0.5到2.0之间调。我的经验值是从1.0开始再根据观察推荐列表的个性化程度微调。4.2 相似度矩阵撑爆内存现象跑完相似度计算后程序卡死内存占用飙升最后进程被杀。原因直接对全部新闻两两计算物品数上万时pair数量可能是上亿内存自然爆掉。很多新手直接在compute_item_similarity里返回所有pair导致进程被杀。解决在统计共现之前做三层截断。第一层过滤掉只被1个或2个用户点击的新闻用的是前面的min_support。第二层对每个用户的点击列表截断只保留最近50篇。第三层生成邻居表时只保留每个新闻Top20个相似邻居丢弃其余pair。这样万级新闻量下内存能控制在几百兆。如果数据量超过十万建议考虑用Spark或Flink的ALS但那已经超出纯Python毕设的范畴一般不会要求。4.3 新新闻永远排不上去现象发布不到1小时的新鲜新闻无论文章质量多好推荐列表里永远没有它。原因协同过滤依赖“用户已经点击过”的事实。新新闻没有任何行为记录无法与其他新闻建立相似关系所以永远不会被推荐。这是推荐系统的冷启动问题。解决给新新闻一个探索期。发布后的前几个小时用内容相似度兜底从新闻文本里提取关键词和用户历史点击新闻的关键词算文本相似度没有关键词的话就按分类返回热门新闻。毕业设计里最简单的做法是新新闻直接以一定概率插入推荐列表权重随时间递减等它积累少量点击后协同过滤自然接手。具体可以在推荐函数最后加一步从最近2小时发布且用户从未看过的新闻中随机抽1~2条排在推荐列表末尾。这样既不会太影响整体个性化又能给新内容曝光机会。4.4 离线评估高分线上点击率却很低现象用历史数据切分训练集和测试集精确率和召回率都很好但上线后用户不点推荐流也不好看。原因评估方式有误。如果随机把历史行为切80%训练、20%测试那么测试集里很多行为发生在训练集之前等于用未来预测过去造成时间泄漏。新闻推荐尤其严重因为新闻生命周期短昨天的新闻今天就失效了。随机切分下模型可以“记住”已经发生的事情线上自然表现差。解决必须按时序切分。把行为数据按时间排序前70%做训练后30%做测试。还要检查测试推荐结果里新闻的发布时间至少有一半是近2天发布的否则说明时间衰减没生效。这个切分方法很简单但能直接影响评估分数和答辩可信度。4.5 新用户没有点击历史推荐接口返回空列表现象新注册用户访问推荐接口返回空数组前端直接报错用户看到一片空白。原因协同过滤算法对新用户无计可施你没有准备兜底推荐。解决在推荐函数开头判断用户没有历史点击时返回全局热门新闻列表按最近24小时点击量排序同时混合该用户所选兴趣分类的热门新闻。这是最常规的“热门召回”策略。代码可以这样写def hot_fallback(categoryNone, top_n10): # 从新闻表中按最近点击量排序返回top_n ...冷启动策略不需要多复杂但必须在系统里存在。毕业设计答辩时这个问题被问到的概率极高。你只要在代码里保留一个hot_fallback函数并在文档里说明“新用户用热门兜底老用户用协同过滤”评委就不会觉得你漏了核心环节。5. 离线评估与参数调优推荐准不准用数字说话5.1 时间切分构建训练集与评估指标计算毕业设计里很多同学把“推荐准不准”停留在主观感受这很吃亏。评委一定会问你的准确率是多少所以我建议至少做两个东西时间切分和三个指标。时间切分的代码很简单df df.sort_values(timestamp) split_date df[timestamp].quantile(0.7) train df[df[timestamp] split_date] test df[df[timestamp] split_date]不要用随机切分原因前面已经说过。切分后对每个测试用户把他测试集中点击过的新闻记为真实正样本。用训练集训练模型生成每个用户的Top N推荐然后计算三个指标精确率 PrecisionN 推荐列表中命中的个数 / N召回率 RecallN 推荐列表中命中的个数 / 该用户在测试集点击的新闻总数覆盖率 Coverage 系统推荐出去的新闻个数 / 全部新闻个数评估代码可以参考下面这段def evaluate(train, test, recommend_func, top_n10): precision_list, recall_list [], [] hit_count 0 recommended_union set() all_news set(test[news_id]) for uid in test[user_id].unique(): test_items set(test[test[user_id] uid][news_id]) if not test_items: continue rec_items recommend_func(uid, top_ntop_n) if not rec_items: continue recommended_union.update(rec_items) hit len(set(rec_items) test_items) precision hit / top_n recall hit / len(test_items) precision_list.append(precision) recall_list.append(recall) if hit 0: hit_count 1 coverage len(recommended_union) / len(all_news) if all_news else 0 return { precision10: sum(precision_list) / len(precision_list) if precision_list else 0, recall10: sum(recall_list) / len(recall_list) if recall_list else 0, coverage: coverage, hit_rate: hit_count / len(precision_list) if precision_list else 0 }注意hit_rate表示至少命中1条的用户比例它比单点精确率更直观。如果只有10%的用户被推荐了相关内容说明系统覆盖能力不足。毕设里报指标时最好三个一起报比如“Precision100.21Recall100.18Coverage0.35HitRate0.62”。不要只报一个最高的数那样容易被追问。5.2 K值、阈值与热门惩罚三个必调参数的实验设计协同过滤在新闻场景下有几组关键参数直接决定推荐效果。调参顺序建议如下。第一个是邻居数K。K太小推荐结果窄K太大噪声多。新闻场景K取15~30比较常见。可以用一个循环去扫for k in [5, 10, 15, 20, 30]: neighbors build_neighbors(sim_matrix, kk) result evaluate(train, test, lambda uid, top_n: recommend_for_user(uid, user_items, neighbors, news_info, top_n)) print(k, result[precision10], result[coverage])第二个是相似度阈值。低于阈值的相似度加进来等于把不相关的新闻混入分数。阈值设0.1意味着至少约10%的共同用户比例才算相关。数据稀疏时阈值要降低0.05~0.15是常见区间。你可以在build_neighbors里先过滤一次neighbors[a] [(b, sim) for b, sim in neighbors[a] if sim threshold]第三个是热门惩罚强度。在4.1的惩罚公式里alpha取0.5、1.0、2.0分别对比。alpha越大热门新闻越被抑制个性化越强但覆盖率可能过大。和K、阈值一起调做一个简单的网格搜索每组参数跑一次指标。不必追求精确率最高因为新闻场景下覆盖率和新闻新鲜度同样重要。如果推荐列表内容太单一用户可能觉得系统“不懂我”覆盖率也反映选择多样性。调参表可以列在论文里方便评委阅读参数含义建议初始值调整范围K每个新闻保留的相似邻居数205~50sim_threshold相似度阈值0.10.05~0.2alpha热门惩罚强度1.00.5~2.0max_age_days推荐新闻最大发布天数31~7还有一个常被忽略的参数是参与相似度计算的行为时间窗口只取最近30天的点击行为。超过30天的点击对当前推荐几乎没有意义留着只会增加矩阵规模。写一个简单的“按时间窗口过滤行为数据”的函数在训练前执行。调参不追求一次到位但你要能说清楚“我调过这是调参后的结果”导师和评委就会认可。6. 进阶给你的协同过滤算法做一个能演示的Web界面毕设光有算法和评测还不够最好把推荐结果展示成网页。最简单稳妥的方案是Flask它轻量还能和你的算法代码在同一个Python环境里跑。下面这个最小例子把推荐函数封装成/api/recommend?user_idxxx接口from flask import Flask, request, jsonify from recommender import recommend_for_user, hot_fallback app Flask(__name__) app.route(/api/recommend) def api_recommend(): uid request.args.get(user_id) if not uid or uid not in user_items: return jsonify({code: 0, data: hot_fallback(10)}) rec recommend_for_user(uid, user_items, neighbors, news_info, top_n10) data [{ news_id: nid, title: news_info[nid][title], category: news_info[nid][category] } for nid in rec] return jsonify({code: 0, data: data}) if __name__ __main__: app.run(debugTrue, port5000)前端用一个简单的HTML表格展示即可不需要上Vue或React。工程结构建议把similarity.py、recommend.py、evaluate.py、app.py分开文件名一眼看出职责答辩时很好解释。如果你还有时间可以在推荐结果里加上“推荐理由”字段比如“因为你看过《xxx》所以推荐这篇《yyy》”这个细节会显著提升项目的演示效果。我自己的教训是第一次做这个题目时把新闻全文都加载进内存启动Flask时直接吃掉800M内存最后改成只加载news_id, title, category, publish_time四个字段内存降到100M以下。另一个教训是没做时间衰减演示时推荐出来的新闻比用户最新点击的还旧当时只能反复刷新页面掩饰。后来在推荐函数里加了max_age_days3这行过滤这个尴尬才消失。所以如果你也打算做这类系统一定要把“时效性”当核心需求来对待而不是锦上添花。希望帮到你。本文还有配套的精品资源点击获取