尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

机器学习新闻标题分类系统:TF-IDF与逻辑回归实战解析

机器学习新闻标题分类系统:TF-IDF与逻辑回归实战解析 简介这是一套面向毕业设计场景的新闻标题分类系统完整项目资源覆盖数据预处理、模型训练、Web展示与结果预测全流程适合自然语言处理方向学生参考或二次开发。压缩包共63个文件体积约10.94MB主要包含7个Python源码、16个HTML页面与13个CSS样式、9个文本数据及停用词表、8个前端JS脚本另含SQL脚本、ipynb分析笔记、PDF说明文档等目录按系统入口、应用模块、页面模板、静态资源、数据文件等划分便于快速定位功能逻辑。资源提供了完整的中文新闻标题处理链条从多种停用词表清洗过滤到数据切分与标签映射再到前端页面对分类结果的可视化展示同时保留了带标签的训练测试数据、词汇表及调试笔记有助于理解算法选型与实验过程。目前已有26人学习下载适合需要搭建同类系统或完成毕业设计文档的开发者参考。1. 为什么基于机器学习算法的新闻标题分类系统设计仍然值得照着做一遍晚上十一点我还在调一个新闻聚合工具的标题打标模块。两万条标题八个类别周六出测试结果。同事的第一反应是直接上 BERT 微调我最后交出去的却是一套传统机器学习方案TF-IDF 做特征、卡方降维、逻辑回归分类线上宏 F1 稳定在 0.88单条耗时不到 0.3 毫秒。这也是基于机器学习算法的新闻标题分类系统设计这个题目最值得复现的原因——它不追求模型最大而是把数据、特征、评估和避坑串成一条能跑的完整链路。无论你是拿它做课程设计、毕业设计还是给内部系统做快速分类这套流程都能让你用有限的数据拿到可信结果而不是只会在测试集上报一个虚高的准确率。2. 数据准备与预处理标题数据清洗、分词与类别平衡的处理路径标题分类的起点不是模型是数据。新闻标题的平均长度只有十几到三十个字原始数据里却混着大量杂质来源前缀、超链接、转载标记、全角符号甚至同一批数据里编码都不一致。做这个系统数据准备通常要占整个项目一半以上的工作量这部分做扎实后面模型对比才有说服力。2.1 标题数据从哪里来THUCNews 与自采数据如何选公开数据方面清华的 THUCNews 是文本分类最常见的起点。它按内容类别整理新闻标签规范拿来直接用可以省掉大量标注成本。但 THUCNews 的标题部分往往混有站点来源标记比如“- 新浪财经”“——来源网易”这类后缀必须清洗掉否则模型会把来源词学成分类特征线上遇到新来源就翻车。另一个选择是自己采集按站点分类或者用已有的栏目接口拉标题好处是数据更贴近实际场景坏处是类别分布完全不可控清洗成本更高。选型时我一般看三点。第一类别是否互斥如果一个标题既算科技又算互联网后面做评估会非常痛苦。第二各类别样本量是否悬殊财经和体育若差二十倍训练前就要先决定用重采样还是类别权重。第三标题是否带强噪声比如大量“本文转自”“点击查看”这类模板文本这些噪声会让 TF-IDF 学到一组几乎只跟来源相关的词。常见做法是先按 6:2:2 切成训练、验证、测试三份验证集只用来调参测试集从头到尾只碰一次防止调出测试集形状。2.2 预处理三步编码统一、内容去噪与结巴分词拿到原始数据后我习惯先把编码统一到 UTF-8。很多公开的新闻 CSV 是用 GBK 存的pandas 读取时直接指定 UTF-8 会报 UnicodeDecodeError先小文件试编码比盲猜省时间。清洗阶段用正则把 HTML 标签、URL、多余空白处理掉这一步不用过度清理——标点对标题分类有时是有信号价值的比如“突发”“大跌”这类语气词在财经与娱乐之间有区分度全删了反而丢信息。import pandas as pd import re def load_news(path, sep\t): # THUCNews 常见格式为“类别\t标题”这里先试 UTF-8失败再回退 GBK try: df pd.read_csv(path, sepsep, headerNone, names[label, title], encodingutf-8, enginepython) except UnicodeDecodeError: df pd.read_csv(path, sepsep, headerNone, names[label, title], encodinggbk, enginepython) return df def clean_title(title: str) - str: # 去 HTML 标签、去 URL、压缩空白全角标点统一转半角 title re.sub(r.*?, , title) title re.sub(rhttp\S, , title) title re.sub(r\s, , title).strip() title title.replace(, ,).replace(。, .).replace(, !).replace(, ?) return title这段代码的逻辑是加载时先尝试 UTF-8失败后回退到 GBK避免在数据入口被编码卡死。正则部分只清掉明确无意义的 HTML 与链接标点统一只做全角转半角不删标点因为后面的分词与特征阶段会自行处理这些符号。分词环节中文用结巴jieba.lcut英文只需要按空格切。这里有一个容易忽略的点停用词表不要为了省事把否定词全加进去。“不看好”“不达标”“未通过”里的“不”“未”一旦被删语义会直接反转财经类标题里这种表达非常多。我一般会先加载通用停用词表再把否定词显式从集合里去掉保住这部分信号。import jieba STOPWORDS set() with open(stopwords.txt, encodingutf-8) as f: for line in f: w line.strip() if w: STOPWORDS.add(w) # 保存否定词防止语义反转 KEEP_NEGATIVE {不, 没有, 没, 别, 无, 非, 未} STOPWORDS - KEEP_NEGATIVE def tokenize_title(title: str) - str: words jieba.lcut(title) keep [] for w in words: w w.strip() if not w or len(w) 1: continue if w in STOPWORDS: continue keep.append(w) return .join(keep)参数上len(w) 1的过滤会去掉单个字方便减少噪声特征但如果数据里出现“霾”“涨”“跌”这类单字强信号可以去掉这个过滤交给后面的卡方检验再筛一遍。分词后的输出用空格连接后续TfidfVectorizer直接按空白切词即可不需要再做一次分词。2.3 类别不平衡先处理再训练class_weight 与重采样新闻类别天生不平衡。财经、科技通常量大体育、军事可能只有前者的五分之一。如果直接训练多数类会把模型的决策边界带走测试准确率看着有零点九宏 F1 却可能只有零点四。我一般先打印各类别样本数再决定用哪种策略绝不跳过这一眼。先看分布df[label].value_counts()如果最大类与最小类差距超过十倍优先考虑两个做法。第一个是加权逻辑回归和 SVM 都支持class_weightbalancedsklearn 会根据样本频率自动给少数类更高的惩罚权重这个方案不动数据最省事。第二个是重采样对少数类做有放回抽样或对多数类做欠采样但重采样在标题这类短文本上容易引发轻微过拟合因为复制样本并不会引入新的语义。加权方案的实现很简单在模型训练那一节直接往分类器里传参数即可。如果是用朴素贝叶斯这类不支持class_weight的模型就改用阈值修正训练完看验证集上每个类别的精确率与召回率适度压低多数类的输出阈值。总之类别分布必须在训练前就暴露出来不能用准确率掩盖差异。3. 特征工程TF-IDF 向量化与卡方特征筛选的三个关键参数标题文本不能直接喂给线性模型得先转成向量。TF-IDF 在短文本标题分类里是默认方案因为它足够简单且结果可解释。特征工程的关键不在算法多新而在三个参数和一个筛选步骤能不能配合好。3.1 为什么标题分类选 TF-IDF 而不是 word2vec一个常见的误区是一上来就上词向量。word2vec 需要足够上下文才能学到词义而新闻标题平均只有二三十个字能覆盖的上下文非常有限如果只用预训练向量而不微调得到的词向量和当前分类任务并不对齐。TF-IDF 则不同它直接在标题词表上建模能捕捉“股市”“涨停”“比分”“进球”这类和类别强相关的词汇而且每个特征对应一个具体词训练完可以打印出来检查模型到底在学什么。对传统机器学习方案来说可解释性是刚需。课程设计和工业落地都要回答“为什么分到这一类”的问题TF-IDF 加线性分类器可以给出每个词的权重这是黑盒模型给不了的。标题分类的特征空间本身不大一万条标题做ngram_range(1,2)常见维度在三万到五万之间线性和朴素贝叶斯模型完全扛得住不需要复杂的特征学习。3.2 TF-IDF 必须调的三个参数min_df、max_df 与 ngram_range预处理已经把标题分词成了空格连接的形式所以向量器不再需要分词器直接用按空白切分即可。下面这组参数是我在多个新闻标题分类任务上验证过的起点。from sklearn.feature_extraction.text import TfidfVectorizer vectorizer TfidfVectorizer( tokenizerlambda s: s.split(), token_patternNone, # 使用自定义 tokenizer 时必须关闭默认正则 min_df3, # 至少在 3 篇标题中出现过才保留 max_df0.8, # 超过 80% 标题都出现的词多半是普适词丢弃 ngram_range(1, 2), # 单字词 双词组合 sublinear_tfTrue # 用 1 log(tf) 平滑削弱高频词压制 ) X vectorizer.fit_transform(texts)min_df3的直觉是只在一条标题里出现的词极可能是打错字或生僻写法保留只会增加噪声维度。max_df0.8解决的是“新闻”“今日”“最新”这类几乎每篇都出现的词它们对分类没有区分度却会占据很大的 IDF 权重。ngram_range(1, 2)是标题分类里最值得试的组合两个字以上的词组能抓住“碳中和”“监管层”“走出低谷”这类完整语义如果数据量很大再试(1, 3)但维度会明显膨胀。sublinear_tfTrue容易被忽略它把原始词频做1 log(tf)映射让一篇标题里重复出现的同一个词不至于无限放大。标题里常见“重磅重磅”这类叠词不加这个参数它会把整条向量拉偏。参数设置完可以用vectorizer.get_feature_names_out()打印词表扫一眼看有没有明显不该出现的符号或停用词残留。3.3 卡方检验压缩特征空间SelectKBest 的 k 值怎么定TF-IDF 给的维度里真正对分类有贡献的往往只有一小部分。卡方检验是朴素贝叶斯时代就常用的特征筛选手段它逐词计算词与类别之间的独立性得分越高说明两者关联越强。对标题这种短文本卡方特征筛选可以显著缩短训练时间同时对准确率几乎无损。from sklearn.feature_selection import SelectKBest, chi2 selector SelectKBest(chi2, k15000) X_selected selector.fit_transform(X, y) # 打印被选中的特征词验证筛选是否合理 mask selector.get_support() terms vectorizer.get_feature_names_out() selected_terms [t for t, keep in zip(terms, mask) if keep] print(selected_terms[:50])k值我一般取一万到两万之间。取太小会丢掉“不看好”“监管”这类低频但强区分的词取太大会把噪声放进来模型训练变慢但 F1 不涨。一万条标题、三万维特征时k15000是性价比很高的起点如果类别数少比如只有四个类k8000就够用。筛选完成后打印出来的特征词应该能看出一类词集中出现在某个类别里。如果你看到大量纯数字、单字碎片、或者和业务完全无关的词说明前面的预处理或min_df设置有问题这时候返回去修比继续调模型参数值。特征选择这一步虽然很短但在后续模型对比里起着稳定器的作用训练集和测试集应该用同一个已拟合的 selector 做transform避免信息泄露。4. 模型选择与训练朴素贝叶斯、逻辑回归与 SVM 的对比实验特征做完进入模型选择环节。标题分类任务里朴素贝叶斯、逻辑回归和线性 SVM 是三种最可靠的传统机器学习算法。它们在短文本上的表现往往非常接近真正的差别在训练速度、可解释性和超参敏感度上。4.1 三类常用模型在短文本分类上的适用边界朴素贝叶斯假设特征之间独立这个假设在词袋特征下明显不成立但它对 TF-IDF 这种稀疏输入格外宽容训练极快甚至在几千条小数据上也能给出不错的基准分。它的短板是不支持class_weight类别不平衡时只能靠数据层面或阈值修正。逻辑回归是短文本分类的首选主力它对特征的加权方式直观输出真实概率C 参数控制正则强度配合class_weightbalanced能有效缓解类别不平衡。线性 SVM 用 hinge loss 优化对高维稀疏数据很友好调好 C 之后往往和逻辑回归分数不相上下但没有predict_proba做置信度排序时只能借助decision_function。随机森林这类树模型在词袋高维稀疏特征上并不占优势而且训练慢、可解释性差在这个题目里我一般只作对比不当主力。整体排序通常是逻辑回归与线性 SVM 并列第一朴素贝叶斯略低零点零几。4.2 构建高可信对比实验宏 F1、交叉验证与随机种子评估指标必须用宏 F1不能用准确率。新闻标题类别不平衡准确率会被多数类带偏。宏 F1 对每个类分别计算 F1 再取平均少数类被忽略时分数立刻掉下来。交叉验证用 5 折数据量小的时候比单次划分稳定得多。from sklearn.model_selection import cross_val_score from sklearn.pipeline import Pipeline from sklearn.naive_bayes import MultinomialNB from sklearn.linear_model import LogisticRegression from sklearn.svm import LinearSVC def build_vec(): return TfidfVectorizer( tokenizerlambda s: s.split(), token_patternNone, min_df3, max_df0.8, ngram_range(1, 2), sublinear_tfTrue ) models { nb: Pipeline([ (vec, build_vec()), (clf, MultinomialNB(alpha0.1)) ]), lr: Pipeline([ (vec, build_vec()), (clf, LogisticRegression(C4, max_iter2000, class_weightbalanced)) ]), svm: Pipeline([ (vec, build_vec()), (clf, LinearSVC(C1.0, class_weightbalanced)) ]), } for name, pipe in models.items(): scores cross_val_score(pipe, train_texts, train_labels, cv5, scoringf1_macro, n_jobs-1) print(f{name}: {scores.mean():.4f} ± {scores.std():.4f})这段代码把向量器和分类器装在同一个 Pipeline 里交叉验证时每一折都会重新拟合向量器从源头上避免特征空间泄露。n_jobs-1让多核并行跑五折实验通常几秒到几十秒内完成。打印出的分数里标准差如果超过 0.03说明数据划分不稳定先检查类别分布是否在某几折里缺失。cross_val_score里的模型没有固定随机种子逻辑回归和 SVM 在数据量大时结果稳定不需要过分纠结但为了复现实验建议在分类器里显式加random_state42。分数的微小差异不要过度解读差 0.005 基本属于噪声差 0.02 以上才值得研究。4.3 常见调参区间alpha、C 与 n_estimators 的合理范围朴素贝叶斯的alpha是拉普拉斯平滑系数我习惯从0.1起步。alpha越小模型越依赖原始词频标题分类里0.01到1之间变化不大但超过1之后特征被过度平滑容易丢失强区分词的信息。逻辑回归的C是正则强度的倒数C越大正则越弱训练集拟合越充分。短文本高维特征下C在1到10之间比较安全个别任务里C4比1稳定提升零点零几。线性 SVM 的C同理默认1.0可作起点0.5到2之间微调。from sklearn.model_selection import GridSearchCV param_grid { clf__C: [0.5, 1, 2, 4, 8] } grid GridSearchCV( models[lr], param_grid, cv3, scoringf1_macro, n_jobs-1 ) grid.fit(train_texts, train_labels) print(grid.best_params_, grid.best_score_)网格搜索的内层交叉验证只用训练集数据外层再留出测试集做最终验证这是防止对测试集过拟合的基本纪律。如果多个C值分数相同选更小的C模型更稳。随机森林在这个任务里通常排在最后n_estimators到 200 以后收益很小max_featuressqrt是默认经验值不必单独调太久。5. 避坑排查标题分类最常翻车的 5 个现场与修复记录这个项目的坑大多不在模型而在数据处理和实验设定。以下五个案例都是我实际跑过的翻车现场每一条都按现象、原因、解决的顺序记录。5.1 训练集与测试集分别 fit TF-IDF特征空间对不上现象训练集宏 F1 0.89测试集直接跌到 0.51模型预测结果错得毫无规律。原因对训练集和测试集分别调用了fit_transform两个向量器的词表、IDF 值各自独立测试集里同一个词被编码成不同列模型根本看不到有效的特征。解决向量器只在训练集上fit_transform测试集只能transform最稳妥的做法是把向量器和分类器放进同一个 Pipeline交叉验证和预测时统一调用。# 错误写法测试集也 fit_transform X_train vec.fit_transform(train_texts) X_test vec.fit_transform(test_texts) # 正确写法测试集只 transform X_train vec.fit_transform(train_texts) X_test vec.transform(test_texts)特征工程的每一个步骤都要记住这个原则训练集定义规则测试集只套规则。卡方选择器同样是这个用法。5.2 GBK 与 UTF-8 混用标签对不齐、加载就报错现象数据加载时直接抛UnicodeDecodeError或者不报错但某些标题乱码标签列和标题列对不上。原因同一份 CSV 里部分行是 GBK 编码另一部分是 UTF-8最常见的来源是从不同网站爬虫合并导出。解决加载时先试 UTF-8失败再回退 GBK如果两种编码混杂在同一文件里建议用文本编辑器统一转码后再读。写完文件时统一指定encodingutf-8避免二次踩坑。5.3 否定词被当停用词删掉财经标题语义反转被误判现象财经类“机构不看好后市”被分到了体育或娱乐排查词表发现“不”“看好”里的“不”被通用停用词表删了分词后只剩“看好 后市”语义完全反向。原因停用词表是从网上捡来的里面把“不、没有、没、别”等否定词一并列进去了。解决加载停用词后把KEEP_NEGATIVE {不, 没有, 没, 别, 无, 非, 未}显式从集合里移除同时打开ngram_range(1,2)让“不看好”“未达标”作为整体被保留。这一步做完财经类的 F1 通常能回升两到三个点。5.4 类别不平衡被准确率掩盖全部预测“其他”也能拿高分现象整体准确率 0.92但宏 F1 只有 0.35分类报告显示多数类“其他”的精确率 0.93而“体育”类召回率只有 0.11。原因训练数据里多数类占八成模型学到最优策略就是把所有样本都判给多数类准确率高但没有任何实用价值。解决评估指标一律看宏 F1 和每类别的精确率召回率逻辑回归和 SVM 加class_weightbalanced朴素贝叶斯则在验证集上调整决策阈值压低多数类输出。先看分布再训练比事后补救省力得多。5.5 数据量小、标题太短模型泛化能力到顶了怎么办现象只有三千条标题每类平均不到四百条怎么调参宏 F1 都卡在 0.6 左右。原因信息量不足模型上限受限不是超参的问题。解决先合并相近类别比如把“科技”和“互联网”合成一类减少类别数能让每类样本更多再考虑扩充同场景语料从公开数据集里补充同分布的标题数据。同义词替换做数据增强要谨慎短文本里替换一个词就可能改变语义。如果数据实在不够可以试 FastText 或小型预训练模型但传统机器学习在几千条短文本上并不吃亏主要胜在训练快、可解释、不容易过拟合。6. 从模型到系统落盘、单条预测与上线前的回测技巧模型训练完成只是第一步这个标题叫“系统设计”最后一步是把模型封装成能调用的组件。训练结束后把向量器和模型分别用 joblib 落盘后续服务启动时加载一次不必每次预测都从磁盘读。import joblib # 训练完成后保存 joblib.dump(vectorizer, vectorizer.joblib) joblib.dump(model, model.joblib) def predict_one(title: str): cleaned clean_title(title) tokens tokenize_title(cleaned) vec joblib.load(vectorizer.joblib) clf joblib.load(model.joblib) X vec.transform([tokens]) proba clf.predict_proba(X)[0] idx proba.argmax() return clf.classes_[idx], float(proba[idx])predict_one把清洗、分词、向量化、预测串成一条流水线。这里有一个性能细节joblib.load应该放在服务启动阶段而不是每次预测都执行否则磁盘 IO 会占据单条耗时的绝大部分。如果模型是LinearSVC这类没有predict_proba的分类器改用decision_function取最大值对应的类别。上线前最后一个步骤是回测。除了固定测试集再抓最近两三天的新标题做盲测统计各类别的精确率与召回率打印错误样本。错误样本里如果集中出现“转自”“来源”这类模板词说明清洗还不够彻底如果集中在长尾新词说明数据采样时间跨度过窄。早期我习惯直接拿模型跑线上流量结果一个新来源后缀让整个分类偏掉后来才学会用新数据回测兜底。先跑通传统机器学习闭环等数据量涨到十万以上再考虑蒸馏或者微调更大模型那个阶段的路会清晰许多。希望帮到你。本文还有配套的精品资源点击获取
返回列表