ARTICLE DETAIL

资讯详情

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

Python新闻推荐系统解析:协同过滤算法实现与工程落地

Python新闻推荐系统解析:协同过滤算法实现与工程落地 简介一个完整的基于Python协同过滤算法的新闻推荐系统毕业设计项目面向计算机相关专业学生及推荐系统学习者用于解决海量新闻内容下的个性化推荐与系统搭建问题。项目从数据爬取与清洗、用户行为分析、相似度计算到协同过滤算法实现、Top-N推荐和效果评估均有覆盖并包含Flask/Django后端服务与前端交互界面可作为毕业设计参考或算法复现起点。压缩包共146个文件包含vue前端页面32个、java/scala后端模块22个java、23个class、xml/sql配置与数据文件、properties/yaml配置文件、js脚本、markdown说明等包体约577KB。文件类型多样源码、配置、编译产物与数据文件齐备便于逆向研读项目结构与实现细节。目前已有353人学习浏览适用于需要学习协同过滤、Python数据处理或推荐系统完整设计的读者可通过源码跟踪、目录分析和配置解读快速理解系统架构与推荐流程。1. 这个 Python 新闻推荐系统到底能拿来做什么如果你打开这个 zip 之前以为里面是一个“能直接跑起来、点两下就推荐新闻”的成品 Web 应用那可能要失望但如果你是拿它当毕业设计骨架、想理解协同过滤在真实新闻数据上怎么落地那这个包的价值远大于一个花哨界面。它把新闻推荐系统最核心的链路——数据预处理、用户-新闻交互矩阵构建、UserCF/ItemCF 算法实现、Top-N 推荐——用 Python 完整串了一遍适合两类人一是要做推荐系统方向毕设、需要可解释代码和清晰模块划分的学生二是刚接触协同过滤、想在一个最小可运行项目上做实验的开发者。我拆完这个包后的感受是它不炫技但每一步都踩在协同过滤的标准流程上拿它改一改能省掉大量搭骨架的时间。2. 从原始新闻到用户-新闻矩阵数据链路是第一道坎2.1 行为数据的来源与预处理别指望数据是干净的这个项目里用户行为数据是最核心的输入没有之一。协同过滤的原理决定了你给算法的数据质量直接决定推荐效果的上限。常见的新闻行为数据来源包括数据库中的浏览记录、点击日志、收藏表甚至是爬虫抓下来的新闻内容配合模拟点击。这个压缩包里没有附带真实数据集但你用 Pandas 读一份典型的用户行为日志格式通常是这样的import pandas as pd # 假设行为日志长这样user_id, news_id, behavior, timestamp df pd.read_csv(user_behavior.csv) print(df.head()) # 过滤无效行为只保留浏览(1)和点击(2)两类权重最高的行为 df df[df[behavior].isin([1, 2])].copy() df[timestamp] pd.to_datetime(df[timestamp]) df df.sort_values([user_id, timestamp])逻辑说明behavior字段如果同时存在浏览、点击、收藏、分享通常的做法是先把行为映射成数值权重再决定哪些进入交互矩阵。浏览权重设为 1、点击设为 2、收藏设为 3这个项目里常用的做法是只保留浏览和点击因为新闻场景下收藏行为太稀疏反而会拉低矩阵质量。参数说明sort_values([user_id, timestamp])是为了后续按时间切分训练集和测试集。如果数据里有重复的(user_id, news_id)对建议先用drop_duplicates()去重不然同一个用户对同一条新闻的多次点击会被当成不同样本直接污染相似度计算。数据清洗的另一块是新闻内容本身。如果涉及爬虫抓取BeautifulSoup 或者 Scrapy 抓下来的 HTML 需要去标签、去空白、去停用词这个项目里用正则做最轻量的清洗就够了。别在这步过度设计——协同过滤算法本身不太依赖新闻正文内容清洗的目的只是为了让后续可扩展的基于内容推荐有干净的数据可用。2.2 构建用户-新闻交互矩阵稀疏矩阵是常态把行为日志变成矩阵是协同过滤的第一个关键节点。行是用户列是新闻单元格的值代表用户对新闻的交互强度。这个项目里直接用 Pandas 的pivot_table来构建# 构建用户-新闻矩阵值用行为权重聚合 interaction_matrix df.pivot_table( indexuser_id, columnsnews_id, valuesbehavior_weight, aggfuncmax, fill_value0 ) print(interaction_matrix.shape) print(稀疏度: {:.4f}.format(1 - (interaction_matrix 0).sum().sum() / interaction_matrix.size))逻辑说明aggfuncmax表示同一个用户对同一条新闻有多次行为时取最大权重而不是求和这样能避免点击次数被过度放大。fill_value0把无交互的位置补零形成标准评分矩阵。参数说明interaction_matrix.shape打印出来如果发现用户数或新闻数过万矩阵规模会迅速膨胀。比如一万用户乘以一万新闻就是一亿个单元格直接存成稠密矩阵会非常吃内存所以我一般建议先打印稀疏度。超过 99% 稀疏度时后续计算要优先用稀疏矩阵结构这个在后面代码里会体现。这里有一个容易被忽视的点新闻推荐和电影推荐不一样新闻的生命周期极短昨天的热门新闻今天可能已经没有推荐价值。因此构建交互矩阵之前最好给新闻数据加一个时间窗口过滤比如只保留最近 7 天有交互的新闻。这个项目如果直接用全量历史数据跑推荐结果里会出现大量过时新闻。2.3 训练集与测试集切分不能随机切要按时间切推荐系统里最常犯的错误之一就是用随机抽样来切分训练集和测试集。新闻数据有时序特性随机切分相当于让模型“偷看未来”评估出来的准确率虚高。正确做法是按时间切用前 80% 时间的交互做训练后 20% 做测试。cutoff df[timestamp].quantile(0.8) train_df df[df[timestamp] cutoff] test_df df[df[timestamp] cutoff] train_matrix train_df.pivot_table( indexuser_id, columnsnews_id, valuesbehavior_weight, aggfuncmax, fill_value0 ) test_matrix test_df.pivot_table( indexuser_id, columnsnews_id, valuesbehavior_weight, aggfuncmax, fill_value0 )逻辑说明quantile(0.8)是以全量数据的时间戳 80% 分位点为边界把数据切成两段。这样训练集和测试集在时间上严格隔离评估结果才真实反映“用过去预测未来”的能力。参数说明train_matrix和test_matrix的列可能不一致——测试集可能出现训练集里没有的新闻。后续计算推荐列表时要统一把测试矩阵的列对齐到训练矩阵的列上否则维度错位会直接报错。常见做法是test_matrix test_matrix.reindex(columnstrain_matrix.columns, fill_value0)。切分比例不是死的如果数据量小8:2 可能让测试集过薄可以适当调整为 7:3。但幅度不要太大测试集占比低于 10% 时评估指标方差会很大一次实验结果根本没法信。3. 两种协同过滤算法实现UserCF 与 ItemCF 的 Python 落地3.1 相似度计算余弦相似度与皮尔逊相关系数的选择协同过滤算法的核心是相似度计算。基于用户的协同过滤UserCF算用户之间的相似度基于物品的协同过滤ItemCF算新闻之间的相似度。这个项目里最常用的相似度是余弦相似度因为它对零值不敏感非常适合稀疏交互矩阵。import numpy as np def cosine_similarity(matrix): 计算矩阵行之间的余弦相似度 # 归一化每行除以自身模长 norm np.sqrt(np.sum(matrix ** 2, axis1)) matrix_norm matrix / norm[:, np.newaxis] # 相似度矩阵 归一化矩阵点乘其转置 sim_matrix np.dot(matrix_norm, matrix_norm.T) return sim_matrix # 用法user_sim cosine_similarity(train_matrix.values)逻辑说明余弦相似度的本质是计算两个向量夹角的余弦值取值在 -1 到 1 之间。值越接近 1说明两个用户的兴趣方向越一致。归一化的目的是消除用户打分尺度差异带来的影响——一个用户经常点 5 条新闻另一个用户只点 2 条不归一化的话前者的向量模长天然更大相似度会被虚高。参数说明np.sqrt(np.sum(matrix ** 2, axis1))计算每一行的 L2 范数matrix / norm[:, np.newaxis]用广播把每行除以对应范数。这里有个坑如果某行全是零新用户没有任何交互范数会是 0除零会得到 NaN。实践中要加一个极小值平滑比如norm[norm 0] 1e-10或者干脆把这些空行过滤掉因为空用户对相似度计算没有任何贡献。皮尔逊相关系数在用户评分尺度差异大时比余弦相似度更稳但它要求两个用户有足够的共同交互项新闻场景下共同点击的新闻往往极少皮尔逊系数容易因为分母太小而波动。所以我一般建议新闻推荐用余弦相似度做主力皮尔逊系数作为交叉验证时的对照。3.2 基于用户的协同过滤找到和你兴趣相同的“邻居”UserCF 的推荐逻辑很直观找到和目标用户兴趣最相似的 K 个用户把这 K 个用户喜欢过、但目标用户没看过的新闻按相似度加权汇总后推荐出去。def user_cf_recommend(user_id, train_matrix, user_sim, top_k10, top_n5): 基于用户的协同过滤推荐 Args: user_id: 目标用户 ID train_matrix: 训练集交互矩阵 user_sim: 用户相似度矩阵 top_k: 邻居数量 top_n: 推荐新闻数量 # 目标用户在矩阵中的行索引 user_idx list(train_matrix.index).index(user_id) # 获取相似度并排序排除自身 sim_scores list(enumerate(user_sim[user_idx])) sim_scores sorted(sim_scores, keylambda x: x[1], reverseTrue) sim_scores [s for s in sim_scores if s[0] ! user_idx][:top_k] # 目标用户已交互的新闻 interacted set(np.where(train_matrix.iloc[user_idx] 0)[0]) # 候选新闻得分 scores {} for neighbor_idx, sim in sim_scores: neighbor_items np.where(train_matrix.iloc[neighbor_idx] 0)[0] for item in neighbor_items: if item not in interacted: scores[item] scores.get(item, 0) sim # 取得分最高的 top_n 个新闻 ranked sorted(scores.items(), keylambda x: x[1], reverseTrue) return [train_matrix.columns[i] for i, _ in ranked[:top_n]]逻辑说明interacted用np.where找出目标用户所有已交互新闻的列索引这一步是为了避免推荐已经看过的内容。scores字典累加邻居的相似度作为权重一个新闻被越多个相似用户看过得分越高。参数说明top_k10是邻居数。新闻推荐场景下邻居数太大会引入噪声用户相似度很低但数量多太小则推荐结果不稳定。我测试下来 1020 是合理区间。top_n5是最终推荐条数实际前端展示时通常要 10 条左右可以调成 10。这个实现里有几个明显的性能瓶颈list(enumerate(user_sim[user_idx]))每次推荐都要遍历全部用户如果用户数上万响应时间会到秒级。项目里如果追求实时推荐需要改成预先计算好每个用户的 Top-K 邻居存到字典里查询时直接取。3.3 基于物品的协同过滤用新闻之间的相似度做推荐ItemCF 的思路和 UserCF 相反它计算新闻之间的相似度核心假设是用户喜欢某条新闻是因为它和他之前看过的新闻相似。新闻场景里 ItemCF 通常比 UserCF 表现更好因为新闻数量增长快用户兴趣变化也快而物品相似度相对稳定可以离线计算。def item_cf_recommend(user_id, train_matrix, item_sim, top_k10, top_n5): 基于物品的协同过滤推荐 user_idx list(train_matrix.index).index(user_id) # 用户已交互的新闻 interacted_items np.where(train_matrix.iloc[user_idx] 0)[0] scores {} for item in interacted_items: # 取与当前新闻最相似的 top_k 个新闻 sim_scores list(enumerate(item_sim[item])) sim_scores sorted(sim_scores, keylambda x: x[1], reverseTrue) sim_scores [s for s in sim_scores if s[0] ! item][:top_k] for sim_item, sim in sim_scores: if sim_item not in interacted_items: scores[sim_item] scores.get(sim_item, 0) sim ranked sorted(scores.items(), keylambda x: x[1], reverseTrue) return [train_matrix.columns[i] for i, _ in ranked[:top_n]]逻辑说明这个实现的思路是对用户看过的每条新闻找出它的相似新闻累加相似度得分。如果用户看过三条新闻这三条新闻的相似邻居里出现同一条新闻两次这条新闻的得分就会叠加排名自然靠前。参数说明item_sim是新闻相似度矩阵维度是新闻数 x 新闻数。新闻数多时这个矩阵会非常大一万条新闻就是一亿个浮点数内存直接爆炸。实际项目里普遍用sklearn.metrics.pairwise.cosine_similarity配合稀疏矩阵来算或者只保留每个物品的 Top-N 相似邻居把稠密矩阵变成稀疏字典。ItemCF 还有一个细节新闻热度会影响相似度质量。一条被所有人看过的爆款新闻它和任何新闻都有交互重叠算出来相似度虚高。处理方式是在相似度计算时引入惩罚项比如sim sim / np.log(1 item_popularity[item])降低热门新闻的权重。这个项目的代码里没有体现但你自己改的时候加上这行推荐多样性会有明显提升。3.4 UserCF 和 ItemCF 怎么选具体场景决定方案不是所有新闻场景都适合同一套算法这个选择直接影响推荐效果。UserCF 更擅长发现“圈子”里的热门内容——一群兴趣相似的人都在看某条新闻你也大概率感兴趣ItemCF 更擅长个性化推荐——你喜欢科技新闻系统不断给你推科技细分领域的内容。从计算角度看新闻平台用户数远大于新闻数UserCF 需要实时计算用户相似度代价高ItemCF 的相似度矩阵可以离线算好线上只查表响应更快。这也是为什么主流新闻 App 普遍优先采用 ItemCF。做毕设的话两种都实现、加一个对比实验是在答辩环节加分的点。两种算法都有冷启动问题新用户没有任何交互历史UserCF 找不到邻居ItemCF 没有可参考的物品。项目里通常用“热门新闻兜底”——冷启动用户直接推全站点击量最高的新闻榜单。这部分在评估时要注意把冷启动用户单独分组否则评估指标会被拉低。4. 避坑指南协同过滤新闻推荐系统的常见问题与排查4.1 相似度矩阵全是 NaN用户向量范数为 0现象跑完cosine_similarity发现结果矩阵里全是 NaN或者报错说存在无穷值。原因交互矩阵中有整行全为零的用户。这些用户的向量 L2 范数是 0归一化时matrix / norm触发了除零产生 NaN。NaN 会在后续矩阵乘法中传染导致整个相似度矩阵失效。解决在归一化前加一行保护norm np.sqrt(np.sum(matrix ** 2, axis1)) norm[norm 0] 1e-10 # 空行给一个极小值避免除零同时在建矩阵时把没有任何交互的用户直接过滤掉因为空用户对相似度计算和信息推荐没有任何贡献。如果确实需要保留新用户占位应该单独建一张用户表和交互矩阵分开管理。4.2 推荐结果全是最热新闻相似度计算没有做热门惩罚现象推荐列表里长期都是几篇爆款新闻个性化程度很低不同用户看到的推荐几乎一样。原因热门新闻被大量用户交互过在 ItemCF 中和几乎所有新闻都有共现关系相似度天然偏高。如果不做惩罚相似度矩阵会被热门新闻主导。解决在相似度计算后加一个热门惩罚系数。常见的做法是引入新闻流行度因子news_popularity np.sum(train_matrix 0, axis0) item_sim item_sim / np.log1p(news_popularity)[None, :]np.log1p是对数加一平滑避免取对数时出现负无穷。这个惩罚的作用是新闻越热门它的相似度权重被压制得越狠给长尾新闻更多露出机会。调完以后推荐列表的多样性会明显改善但注意别惩罚过度否则热门新闻全被压掉推荐结果会变得太冷门用户同样不爱看。4.3 评估指标虚高 30%测试集切分没有对齐时间现象在训练集上回测准确率有 60%但上线后实际点击率不到 5%差距大得离谱。原因大概率是训练集和测试集用了随机切分。随机切分时测试集里混入了训练集时间范围内的新闻模型在训练时已经“见过”这些新闻及其上下文评估时相当于开卷考试指标虚高。解决强制按时间切分用df[timestamp].quantile(0.8)找切分点。训练数据只取切分点之前测试数据只取切分点之后。另外切分完要检查测试集里新闻的列是否和训练集对齐用reindex(columnstrain_matrix.columns, fill_value0)统一列空间。评估代码里也要过滤掉冷启动用户——他们没有任何训练数据推荐结果本来就是猜把这些人算进准确率会拉低指标不是算法的问题。4.4 内存直接爆掉矩阵维度没控制现象数据量大概几万用户、几万新闻的时候程序跑着跑着就 OOM 了或者变得极慢。原因交互矩阵是用户数 x 新闻数的二维数组几万乘几万就是几亿个单元格一个浮点数占 8 字节算下来内存要好几 GB。如果再算相似度矩阵内存需求翻倍甚至指数级增长。解决优先用稀疏矩阵存储Scipy 的csr_matrix是最常用的选择。Pandas 的pivot_table生成的是稠密矩阵可以直接转成稀疏格式再运算。相似度矩阵也只保留 Top-N 邻居不要存全量from scipy.sparse import csr_matrix, issparse sparse_matrix csr_matrix(train_matrix.values) # 用 sklearn 计算余弦相似度自动处理稀疏 from sklearn.metrics.pairwise import cosine_similarity item_sim_sparse cosine_similarity(sparse_matrix.T, dense_outputFalse)dense_outputFalse是关键参数它让结果保持稀疏格式。取值时只取出前 N 个非零相似度作为邻居内存占用从 GB 级降到 MB 级。如果矩阵规模超过十万级下一步要考虑矩阵分解SVD降维这属于进阶优化不展开。4.5 新闻生命周期太短旧新闻霸榜现象推荐结果里出现几天前的旧闻用户点进去发现已经过时体验很差。原因新闻和电影的本质区别在于时效性。电影可以推荐十年前的老片但新闻晚半天推送就毫无价值。交互矩阵构建时如果没有时间过滤历史累积的点击量会把旧新闻顶到高位。解决构建矩阵前做时间窗口过滤。比如只保留最近 7 天的交互记录或者给时间衰减加权——越近的行为权重越高。常见做法是用指数衰减函数import numpy as np max_ts df[timestamp].max() df[recency_weight] np.exp(-(max_ts - df[timestamp]).dt.days / 7.0) df[final_weight] df[behavior_weight] * df[recency_weight]衰减半衰期设 7 天意思是一周前的行为权重衰减到原来的约 37%。参数按场景调如果是科技新闻这种更新快的品类半衰期可以缩短到 3 天如果是财经深度报道这种长尾内容放宽到 14 天更合适。5. 效果验证与调参把推荐结果从“能跑”优化到“能用”推荐系统做完不是终点验证和迭代才是真正花时间的部分。这个项目在评估环节留了扩展空间我用的时候补了一套评估流程你可以直接照搬。最基础的做法是把测试集里的真实交互作为“标准答案”看推荐列表里有多少命中。def evaluate_precision_recall(recommend_func, test_df, train_matrix, top_n5): 评估推荐结果的准确率与召回率 precision_list [] recall_list [] test_users test_df[user_id].unique() for user_id in test_users: if user_id not in list(train_matrix.index): continue # 跳过冷启动用户 # 用户真实交互的新闻 real_items set(test_df[test_df[user_id] user_id][news_id]) # 推荐的新闻 rec_items set(recommend_func(user_id, top_ntop_n)) if len(real_items) 0: continue hit len(rec_items real_items) precision_list.append(hit / top_n) recall_list.append(hit / len(real_items)) precision np.mean(precision_list) recall np.mean(recall_list) f1 2 * precision * recall / (precision recall) return precision, recall, f1逻辑说明准确率看的是“推荐的 5 条里用户真正点过的比例”召回率看的是“用户点过的新闻里被我们推荐出来的比例”。两者天然此消彼长推荐条数越多召回率越高但准确率下降所以 F1 值是最好的综合指标。参数说明rec_items set(recommend_func(user_id, top_ntop_n))这里的top_n要和推荐函数里的参数保持一致。real_items用的是测试集里用户真实交互的新闻注意测试集也要先做一遍行为过滤只保留权重最高的行为类型。调参的优先级我按实际经验排个序第一优先是推荐条数top_n默认 5 和 10 之间的 F1 差距通常最明显第二是邻居数top_k从 5 到 30 逐个试找出 F1 的峰值区间第三是相似度计算方式余弦和皮尔逊各跑一遍对比最后才是热门惩罚系数的时间窗口。这个项目给我最大的启发是协同过滤的代码实现大同小异真正的差距在数据处理细节。你不处理空行、不按时间切分、不管热门新闻算法再标准也是白搭。从那以后我每次搭推荐系统都强制自己先过一遍这几个检查项确认没有零范数用户、确认测试集列对齐、确认时间切分合理、确认热门惩罚生效。哪怕只是为了跑个 baseline这四步也不省。希望你拿到这个包以后照着这套流程走一遍把代码跑通再动手改比对着源码空想有效得多。希望帮到你。本文还有配套的精品资源点击获取
返回列表