ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Python文章推荐系统实战:从TF-IDF到协同过滤

Python文章推荐系统实战:从TF-IDF到协同过滤 简介推荐系统是机器学习应用中的热门方向这份基于Python实现的文章推荐系统项目面向需要完成毕业设计或课程设计的高校学生覆盖了从数据采集、预处理、特征提取到模型训练与部署的完整流程。项目整合了Scrapy爬虫、NLP文本处理、TF-IDF相似度计算、贝叶斯分类等关键技术同时涉及MongoDB、MySQL等数据库应用帮助读者系统掌握推荐系统的工程实现。资源包共35个文件核心为15个Python源码文件另有7个编译生成的pyc文件、5个shell运行脚本、3个conf配置文件以及txt文本说明等整体仅89KB轻量易读其中sh脚本可一键执行爬虫、分类、训练等任务conf文件用于配置MongoDB等连接参数方便直接复用。目前已有108人学习该资源通过这个项目读者不仅能获得可运行的推荐系统代码还能深入理解相似度计算、协同过滤等算法在实际场景中的落地方式是一份理论与实践结合的高质量参考。1. 为什么课程设计我首选 Python 做文章推荐系统每年的毕业设计和课程设计里推荐系统都是出现频率最高的题目之一。很多人一开始想得很高级什么深度学习、知识图谱、图神经网络都往上堆结果一个月过去连像样的离线实验都没跑完。我的经验是文章推荐系统和商品推荐、视频推荐不一样它天然适合用 Python 配合文本向量化和传统协同过滤来做数据量不大效果却容易解释清楚。导师问起来每一个中间结果你都能说出来龙去脉这比丢出一个调好的黑匣子模型要稳妥得多。用 Python 实现文章推荐系统本质上是在解决这样一个问题当用户读完一篇技术博客、新闻稿或公众号文章后我们怎么从已有的文章池里找出他最可能接着读的一批文章。它要处理的是文本内容、用户历史行为以及这两者之间的匹配关系。整个系统可以拆成数据层、召回层、排序层三个部分每一层都有成熟的开源库支撑比如 sklearn 里的 TF-IDF 向量化工具、numpy 的矩阵运算、pandas 的数据清洗这些正是搜索引擎热词里经常被检索的几类组件。本文会从原理讲到可运行的代码再把我在实际调试中踩过的坑一个一个说清楚希望帮到你。2. 推荐系统选型基于内容、协同过滤还是混合方案2.1 三种主流方案的原理和适用边界文章推荐系统的核心任务是从用户的历史交互中推断兴趣再从文章库中找出匹配项。常见的做法分三类基于内容的推荐Content-based、协同过滤Collaborative Filtering以及混合推荐Hybrid。基于内容的推荐只依赖文章本身的特征不看其他用户的行为。流程是先把文章做分词、去停用词得到关键词向量然后计算用户历史阅读过的文章向量与候选文章向量的相似度推荐 Top-K。这种方案的优点是冷启动问题小一篇新文章只要有文本内容就能被推荐出去非常适合新闻资讯、技术博客这类内容更新频繁、物品生命周期短的场景。它的缺点是推荐结果过于同质化用户可能永远只看到自己已经感兴趣的领域缺少多样化探索。协同过滤则反过来它不看文本只看用户和文章的反馈矩阵。UserCF 找的是和你行为相似的其他用户把那些用户爱看但你还没看过的文章推荐给你。ItemCF 找的是和你读过的文章相似的其他文章但它定义的相似不是文本相似而是“被同一批用户共同阅读”的关系。协同过滤能带来惊喜度因为它的相似性来自群体智慧能挖掘出文本表面不相似但兴趣上相关的内容。它的硬伤是稀疏性和冷启动新用户没有行为新文章没有交互推荐就无从谈起。混合推荐的做法是把前面两者结合用协同过滤做召回扩大候选集用基于内容对召回结果重排保证相关性。毕业设计里做到这个程度已经超过 80% 的同题作品。多数公开数据集上纯协同过滤的指标在稀疏数据下会掉得很厉害混合方案则稳定得多。2.2 为什么课程设计我建议从“TF-IDF 余弦相似度”起步我知道很多人一上来就想用 BERT、Word2Vec 甚至 ChatGPT 的 embedding。但课程设计和毕业设计的核心时间是答辩和写文档不是调参。BERT 的 embedding 层动辄几亿参数本地没有 GPU 根本跑不动而且语义模型的效果评价口径复杂导师问“为什么这里用这个模型”你很难自圆其说。我一般会建议先做一版 TF-IDF 余弦相似度的基于内容推荐。TF-IDF 的核心思想是一个词在一篇文章中出现的次数越多同时在整个文章库中出现的次数越少它的区分度就越高。比如“算法”在技术文章里很常见权重会被压低而“协同过滤”只在部分文章出现就能获得高权重从而成为这篇文章的关键标识。sklearn 库的 TfidfVectorizer 一行代码就能完成分词、词频统计、逆文档频率计算和向量归一化整个推荐函数不超过三十行非常适合作为课程设计的第一版。这一版跑通之后你能看到每条推荐结果的相似度分数能解释为什么推荐这一篇。如果你愿意再做一版基于用户评分的 ItemCF就能形成对比实验两个方案在离线评估指标上的差异就是毕业设计最有价值的章节。这也是从“仿一个 demo”到“做一个工程”的真正分水岭。3. 用 Python 跑通文章推荐最小系统数据、向量化与相似度计算3.1 准备数据从公开数据集到自建文章库推荐系统离不开数据但课程设计不需要真的去爬几十万篇文章。最常用的做法是拿公开的新闻分类数据集比如 Reuters-21578、20 Newsgroups或者中文的 THUCNews 子集。中文场景下我习惯先用 sklearn 自带的 fetch_20newsgroups 或直接从 github 下载 CSV 格式的文章集合。这里面的文章已经带类别标签后续做评估时可以直接把类别当作“是否相关”的标注。如果你没有网络条件也可以自建一个小数据集准备 20~50 篇文章每篇是纯文本或带标题、正文两列存成 CSV。对于课程设计规模不是问题关键是流程完整。自建数据还有一个好处你知道每篇文章的内容验证推荐结果时能凭直觉判断相似度算得是否合理。import pandas as pd # 假设 CSV 中有 title、content 两列content 是纯文本正文 df pd.read_csv(articles.csv) df.dropna(subset[content], inplaceTrue) print(df.shape)这里只做两个动作读取 CSV删除正文为空的记录。真实数据里 NaN 出现在正文列的概率很高尤其是爬虫抓取时字段错位。招生简章类的文章经常只有标题没有正文不清理会直接影响后面的 TF-IDF 计算。3.2 中文分词与预处理不使用 jieba 的话TF-IDF 会输在起跑线英文文本可以直接按空格分词但中文不行。“我读过这篇文章”如果按单字切分会变成“我 / 读 / 过 / 这 / 篇 / 文 / 章”向量里全是噪声。中文推荐系统里最常用的分词库是 jieba这是事实标准。安装方式很简单直接在终端里执行pip install jieba。如果你还没装 sklearn顺便把pip install scikit-learn也执行了。import jieba import re def clean_and_cut(text): # 只保留中英文和数字去掉标点与换行 text re.sub(r[^\u4e00-\u9fa5a-zA-Z0-9], , text) # 使用 jieba 精确模式分词返回生成器 words jieba.cut(text, cut_allFalse) return .join(w for w in words if len(w.strip()) 1) df[cut] df[content].apply(clean_and_cut) print(df[cut].iloc[0])这段代码做什么第一是清洗文本把所有标点、特殊符号替换为空格防止后面出现“。”这样的单字词。第二是用 jieba 的精确模式分词cut_allFalse意味着尽量按最大概率路径切分比如“推荐系统”会被切成一个词而不是“推荐”和“系统”两个词。第三是过滤长度为 1 的词这类大多是标点残留或单个语气词。第四最终用空格把分词结果拼接成一个长字符串这是因为 TfidfVectorizer 默认按空白字符切分这样的输出可以直接喂给它。参数说明len(w.strip()) 1里的 1 是经验值。对于新闻和博客单字词的区分度极低砍掉能显著减小特征维度。如果你的文章库包含“俄乌冲突”“北上广深”这类本身是单字的关键词可以保留长度为 1 且属于自定义词典的词。3.3 构建 TF-IDF 矩阵并对新文章做增量映射分词之后用 sklearn 的 TfidfVectorizer 构建向量矩阵。注意一件事课程设计里最常见的问题是“我要给一篇新文章推荐相似文章要不要把新文章重新和整个库一起计算一遍”。不需要。正确做法是先用已有文章库 fit 得到词典和 IDF 值再用同一个 vectorizer 对新文章做 transform得到相同特征空间下的向量。from sklearn.feature_extraction.text import TfidfVectorizer vectorizer TfidfVectorizer(max_features5000, min_df2, max_df0.9) tfidf_matrix vectorizer.fit_transform(df[cut]) # 假设新来了一篇文章内容存在变量 new_article_cut 中 new_vec vectorizer.transform([new_article_cut])这里三个参数是必调的。max_features5000表示只保留出现频率最高的 5000 个特征。对课程设计的数据量来说5000 个关键词足够区分文章类别同时能把内存占用压到几十 MB。min_df2意味着只在 2 篇及以上文章中出现的词才被保留那些只出现一次的人名、错别字会被当作噪声去掉。max_df0.9表示在 90% 以上文章中都出现的词也会被忽略比如“文章”“内容”这类通用词它们的 IDF 接近于 0留着只会稀释真正的关键词。fit_transform和transform的区别经常有人混淆。fit_transform是学习词典和 IDF 并转换只能用在整个训练集上transform是直接沿用已经学好的规则用于新数据。如果不小心对新文章也调用了fit_transform你会得到一组维度完全不同的向量接下来算相似度必定报维度不匹配的错。3.4 余弦相似度计算与推荐函数有了 TF-IDF 矩阵之后相似度计算可以走 sklearn 的cosine_similarity也可以手动用归一化后的向量做点积。TF-IDF 向量内部已经做了 L2 归一化所以向量点积就等于余弦相似度。在大规模数据下手动点积加 argsort 比调用cosine_similarity省内存因为它不需要保存一个 N×N 的稠密矩阵。from sklearn.metrics.pairwise import cosine_similarity import numpy as np def recommend_by_content(article_index, tfidf_matrix, top_k5): # 取目标文章向量 vec tfidf_matrix[article_index] # 计算与所有文章的相似度 sims cosine_similarity(vec, tfidf_matrix).flatten() # 排除自身取前 top_k sims[article_index] -1 top_indices np.argsort(sims)[::-1][:top_k] return [(i, sims[i]) for i in top_indices] # 示例给第 0 篇文章找 5 个最相似的 results recommend_by_content(0, tfidf_matrix, 5) for idx, score in results: print(f{idx} - {df[title].iloc[idx]} - {score:.4f})cosine_similarity接收一个向量和一个矩阵返回 1×N 的相似度列表。注意这里返回的结果包含目标文章自己因为任何向量和自己的余弦相似度都是 1所以必须先把自己位置的分数设为 -1。argsort默认从小到大排[::-1]翻转为从大到小再取前 5 个就是相似度最高的候选文章。这个函数的复杂度是 O(N)N 是文章总数。如果你的量级在十万篇以内单次推荐都是毫秒级。数据量更高时你需要切换到倒排索引或近似最近邻库但课程设计完全不需要。如果你希望给用户做端到端推荐可以记录用户最近读过的 10 篇文章索引对每篇做一次相似推荐最后把分数加权求和取 Top-K 输出。这本质上就是基于内容的 User Profile 融合能明显提升推荐的相关性。4. 评价指标与调参离线评估到底看什么哪些参数不能乱动4.1 离线评估指标准确率、召回率与覆盖率很多课程设计做完推荐函数就结束这是大忌。推荐系统的价值在于可评估性你至少要说清楚推荐效果比随机推荐好多少。最常用的评估方式是留一验证从用户历史记录中随机隐去一篇文章用剩余历史做推荐看隐去的那篇是否出现在 Top-K 推荐结果里。def evaluate_recommendation(user_history, all_articles, top_k10): hits 0 total len(user_history) for user, held_out in user_history: # 用其余文章构造用户画像并推荐 recs recommend_by_history(user, top_k) if held_out in recs: hits 1 return hits / total这里的核心指标是 Top-K 命中率Hit Rate也叫召回率。能够被衡量才能支撑你后续说“我调大了 min_df召回率从 3.2% 提升到了 6.1%”。另一个值得关注的是覆盖率即推荐系统能推荐出多少不同的文章。如果系统永远只推荐最热门的十篇召回率可能不低但用户很快会看腻。覆盖率可用len(set(recommended_ids)) / len(articles)计算理想情况是覆盖率长期稳定在 50% 以上。如果你做的是基于内容的推荐还有一个专门的评估口径类别一致性。取一篇体育类文章看推荐结果中体育类占比多少。这个指标在新闻数据集上直观且好评判。4.2 三个必须调的参数min_df、max_features 与相似度阈值min_df和max_features直接决定文本特征空间的规模。min_df调大特征更精简每个词的代表性更强但可能丢掉一些低频却关键的主题词min_df调小特征更全但噪声也更多。我在新闻数据上常用min_df3在博客数据上常用min_df1因为博客的长尾词往往正是搜索价值所在。max_features从 3000 调到 8000推荐结果的多样性会变好因为更多长尾词参与相似度计算但内存占用几乎线性上升。课程设计的机器一般有 8GB 内存5000 这个数量级是安全的。相似度阈值是另一个容易被忽略的参数。默认top_k5会无条件返回 5 篇最相似的文章哪怕相似度只有 0.02。实际工程中当最高相似度低于 0.3 时这篇新文章可能根本不该出现在推荐池里。常见的做法是为推荐函数加一个min_sim参数低于该阈值时减少返回条数甚至返回“暂无可推荐内容”。def recommend_with_threshold(article_index, tfidf_matrix, top_k5, min_sim0.2): sims cosine_similarity(tfidf_matrix[article_index], tfidf_matrix).flatten() sims[article_index] -1 candidates [(i, s) for i, s in enumerate(sims) if s min_sim] candidates.sort(keylambda x: x[1], reverseTrue) return candidates[:top_k]这个阈值不是玄学它是可以标定的。我通常会随机抽取 100 篇文章人工查看每篇文章与最相似文章的相似度分布如果大量结果集中落在 0.1~0.2 之间说明文章库主题过于分散或者分词质量不对。一旦分布偏移先回头检查分词而不是继续调阈值。5. 避坑清单文章推荐系统最常见的五个翻车现场5.1 中文文本没有分词TF-IDF 算出的相似度全是单字匹配现象推荐结果和原文主题完全不相关比如一篇关于“Python 爬虫”的文章推荐出了“Python 安装教程”。原因直接在原始文本上调用 TfidfVectorizer默认的 token_pattern 按单词边界切分中文被拆成单个汉字。解决先按第二节的方式用 jieba 分词再交给 TfidfVectorizer。5.2 新文章重新 fit 了 vectorizer导致维度不匹配现象第一次训练一切正常新文章一来就报ValueError: dimension mismatch。原因对新文章调用了fit_transform产生了一个新的特征词典维度或顺序和旧矩阵不一致。解决坚持第一节的规矩fit_transform只用于训练集新数据一律走transform。可以打印vectorizer.vocabulary_的长度确认两次转换用的是同一个词典。5.3 相似度矩阵内存爆炸现象文章数量超过 2 万篇后cosine_similarity(tfidf_matrix)直接用整矩阵做主程卡死或报内存不足。原因N×N 矩阵在 2 万篇时就是 4 亿个浮点数如果使用 Float32也有 1.6GB 内存这还只是相似度矩阵不含原始向量。解决不要计算全量矩阵只算“单篇文章 vs 全量文章”的向量复杂度和结果完全等价。5.4 停用词过滤过度把专业术语也删了现象推荐结果的多样性大幅下降技术文章全都被推到同几个领域。原因有人在预处理时用了网上流传的“中文停用词表”其中包含“系统”“方法”“问题”这类词但在技术文章领域这些词恰恰是主题词。解决停用词表的选取必须结合领域。做新闻推荐可以用通用停用词表但做 CSDN 或知乎文章推荐时我建议只删除标点和单字不做领域停用词过滤让 TF-IDF 的 IDF 机制自己去判断权重。5.5 评估结果好得离谱细看发现是把训练数据泄露给了推荐模型现象离线评估命中率高达 95%实际体验一团糟。原因用户在留一验证中的数据被用于构造用户画像时还保留在训练集中。也就是说目标文章自己已经在用户的候选列表里等于开卷考试。解决评估推荐时必须从用户历史中真正删掉被预测的那条记录再重新构造画像。推荐系统领域有个专门的称呼叫“时间穿越”这是所有推荐评估里最容易犯、也最被扣分的错误。6. 进阶方向融入协同过滤与混合排序把毕设做出差异化如果你已经跑通基于内容的版本下一步建议做一版基于用户的协同过滤UserCF然后和你已有的内容推荐做混合。两版结果做一次对比实验这就能让你的设计从“一个推荐 demo”升级为“一个完整的推荐系统研究”。UserCF 的落地比内容推荐还简单构建用户-文章评分矩阵行是用户列是文章值是阅读次数或停留时长然后用cosine_similarity计算用户与用户的相似度再找最近邻用户读过、但你还没读的文章。from sklearn.metrics.pairwise import cosine_similarity # user_item_matrix: shape (n_users, n_items)值是出现次数 user_sim cosine_similarity(user_item_matrix) def recommend_by_usercf(user_id, user_item_matrix, user_sim, top_k5): # 找目标用户最相似的 5 个用户 similar_users np.argsort(user_sim[user_id])[::-1][1:6] # 汇总相似用户的阅读记录 rec_dict {} for uid in similar_users: items_read np.where(user_item_matrix[uid] 0)[0] for item in items_read: if user_item_matrix[user_id, item] 0: rec_dict[item] rec_dict.get(item, 0) user_sim[user_id, uid] sorted_recs sorted(rec_dict.items(), keylambda x: x[1], reverseTrue) return sorted_recs[:top_k]这里有个细节累加相似度分数时要乘上用户相似度否则每个相似用户的贡献权重完全一样推荐结果会偏向那些爱看文章的用户而不是真正和你口味相近的用户。参数[1:6]表示排除自己取相似度最高的 5 个邻居。你还可以加一步去重过滤掉用户已经读过的文章。混合排序最简单的做法是“分数加权融合”内容推荐分数归一化后乘以 0.6协同过滤分数乘以 0.4求和取 Top-N。加权因子不需要过度调优课程设计有对比实验足以说明问题。如果想让数据更有说服力多做一组消融只看内容推荐、只看 UserCF、混合推荐三者各自在 Top-10 命中率上的差异。做完这些再加一点工程化收尾用 Flask 包一个简单的 Web 接口接收用户 ID返回文章标题列表用 Matplotlib 画一张覆盖率随 Top-K 变化的折线图。这些内容是答辩时的加分项。整个项目做到这里你已经完成了从数据处理、特征工程、算法实现到评估验证的完整闭环这就是我对一份课程设计最朴素也最可靠的建议。希望帮到你。本文还有配套的精品资源点击获取
返回列表