☰
新闻标题分类实战:从TF-IDF到TextCNN的机器学习落地指南
2026/10/3 14:12:49 网站建设 项目流程

简介:面向自然语言处理学习者与毕业设计开发者,这份基于机器学习的新闻标题分类系统源码,可支撑文本分类、敏感词过滤、模型训练与评估等完整流程,适合课程设计、论文实验或工程原型搭建。压缩包共62个文件、约11MB,以Python脚本与Jupyter Notebook分析文档为主线,配合SQL数据库文件、停用词表、敏感词表和带标签的训练测试语料;另有HTML、CSS、JavaScript编写的前端页面用于结果展示,整体按数据准备、特征处理、模型训练、Web应用等模块组织,目录结构便于定位关键代码。内容涵盖文本预处理、分词与停用词过滤、特征构建、模型配置与分类演示,并附有中文停用词库、敏感词表和验证用数据集,可从数据清洗、模型训练到结果展示全链路复现,适合对照学习完整项目开发流程。已有566人学习下载,适合需要快速搭建分类原型或参考工程化源码组织的读者。

1. 新闻标题分类系统在解决什么问题:一个“一句话”分类器的真实分量

很多刚接触机器学习入门项目的人,第一次看到“新闻标题分类”,都觉得它是文本分类里最好捏的软柿子:一句话而已,类别就那么几个,跑个模型不就有了。但真把这套源码包接到业务里你会发现,标题分类是典型的“看起来简单、做起来全是细节”的机器学习项目。一个标题只有十几二十个字,没有正文上下文,口语缩略、品牌名、新造词全挤在一起,分类器既要理解“iPhone 新机发布”是科技还是财经,还得扛住“国足又输了”这种几乎没有特征词的样本。

这套源码体的落地价值很清楚:用机器学习模型把新闻标题自动映射到预定义栏目或话题标签,替代人工打标,服务内容分发、舆情监测和智能编辑审核。它能解决的不只是性能问题,而是把一条重复性极高的标注流水线变成一次训练和一次预测。适合三类人:想做文本分类入门拿真实数据练手的学生,要给内容库做自动打标的运营工程师,以及需要快速验证“这个分类需求到底能不能用模型做”的产品技术人员。下面按我自己的落地路径把这条线讲透。

2. 先立框架再动手:标题分类的建模路线与数据准备

2.1 短文本分类为什么难:新闻标题里的“一句话陷阱”

新闻标题分类本质上是最典型的短文本分类任务,但短文本带来的问题比长文本多得多。正文分类可以从上下文里找线索,标题没有这个条件。一条标题平均 10 到 25 个字,能承载的语义特征本来就少,再加上中文没有天然空格,分词一旦出错,后面的模型再强也白搭。

再一个问题是歧义。同一个词在不同语境下指向完全不同:比如“跨界”这个词,在“小米跨界造车”里是科技财经,在“歌手跨界演电影”里是娱乐;“涨停”出现在“苹果供应商涨停”里是财经,但放在“游戏股涨停潮”里又和文化产业相关。对关键词规则来说,这种词就是灾难,而机器学习模型需要足够多的样本才能自己学会区分上下文。

还有数据稀疏性。新闻标题是强时效文本,今天的新词、缩写、玩梗式表达,昨天的训练数据里可能完全没有。这也是为什么我在做这类机器学习项目时,从不指望一次训练管一辈子——数据分布会漂移,模型要留增量更新的口子。这也是标题分类和一般工业级文本分类最像的地方:真正难的不是模型结构,而是怎么让它在每天都在变的标题流里保持稳定。

2.2 选型路线:TF-IDF 基线先行,TextCNN 跟上,BERT 做兜底

我见过不少人拿到新闻标题分类的源码包,上来就调 BERT,结果训练时间长、显存不够,最后连基线效果都没对比过。我的原则是:先跑便宜模型验证数据和标签管线,再逐步加复杂度。第一个模型用 TF-IDF 加 LinearSVC 或者朴素贝叶斯,特征上把字级别和词级别的 n-gram 都加上,这一步在几万条标题数据上往往已经能跑到 0.8 以上的宏平均 F1,用来发现数据问题非常划算。

第二个模型上 TextCNN。它用多个不同尺寸的卷积核去抓短文本里的 n-gram 局部特征,对标题这种短句特别合适,训练也比 RNN 稳定。PyTorch 实现 TextCNN 的代码量很小,不依赖外部预训练权重,机器配置要求也不高。最后一个模型才是 BERT 或者它的轻量变体,用预训练语义兜底。注意,BERT 对小数据集的提升未必比 TextCNN 大多少,而且它要求你把输入截断策略、分词器版本、标签映射都固定死,否则复现时很容易翻车。

选型结论可以概括成一句话:数据管线先跑通,TextCNN 做主力,BERT 做精调兜底。这样做的好处是每一步的投入产出都可控,出了偏差也容易定位是数据问题还是模型问题。

2.3 数据准备:CSV 读入、标签映射与分层切分的标准脚本

不管源码包里写的是什么模型,数据入口基本都是 CSV 或者 JSON 文件。新闻标题分类数据集的常见字段是两列:一列是标题文本,一列是类别标签,类别一般是体育、财经、娱乐、科技、健康、教育这类预设栏目。第一步先把数据读进来,去掉空标题,再把中文标签转成模型需要的数字索引,同时保存一份映射文件,这个映射后面会用很多次。

import pandas as pd from sklearn.model_selection import train_test_split import json df = pd.read_csv("data/headlines.csv", encoding="utf-8") # 只保留两列,丢掉空标题和空标签的行 df = df[["category", "headline"]].dropna() df["label"] = df["category"].astype("category").cat.codes # label2id 一定要落盘,训练和预测都要用同一份映射 label2id = dict(zip(df["category"], df["label"])) with open("data/label2id.json", "w", encoding="utf-8") as f: json.dump(label2id, f, ensure_ascii=False) # stratify 保证每个类别的比例在训练/验证集里保持一致 train_df, val_df = train_test_split( df, test_size=0.1, random_state=42, stratify=df["label"] ) train_df.to_csv("data/train.csv", index=False, encoding="utf-8") val_df.to_csv("data/val.csv", index=False, encoding="utf-8")

这段代码有几个关键点。dropna 不能省,标题列里的空值如果进了训练集,轻则 loss 波动,重则喂进 BERT 直接报错。astype("category").cat.codes 是 pandas 快捷编码方式,但注意它产生的数字顺序是按类别名排序的,所以必须显式保存 label2id,不能到时候靠猜。stratify 这个参数是很多新手的盲区,如果原始数据里财经类占 60%,娱乐类只占 5%,不做分层切分,验证集里可能连一条娱乐样本都没有,最后打印出来的 F1 会虚高得离谱。验证集一旦失真,后面所有调参判断全部失效,这个坑我踩过一次之后就不敢省了。

3. 把源码跑起来:从依赖安装到训练出第一个分类模型

3.1 一份标题分类源码包的典型目录:先认文件再动手

拿到 zip 解压之后,第一件事不是急着跑 train.py,而是把目录结构过一遍。这类源码包通常包含 data 目录、checkpoints 目录、几个 Python 模块和依赖清单。我一般会先看有没有 README,没有 README 就看文件命名,命名不规范的项目踩坑概率会高很多。

典型结构是下面这样,具体文件名可能有出入,但职责基本一致:

文件/目录职责我的检查点
data/原始 CSV、切分后的 train/val、label2id.json有没有原始数据,字段名是否匹配
checkpoints/模型权重保存目录是否为空目录,有没有预训练权重
config.py全局参数:路径、类别数、模型名模型名和类别数是否和 label2id 一致
utils.py文本预处理、加载数据、指标计算预处理是否被 train 和 predict 共用
train.py训练主入口参数是否用 argparse 暴露
predict.py单条/批量预测入口是否从 label2id.json 加载映射
requirements.txt依赖清单版本是否被固定,有没有缺 torch

我会把 checkpoints 里已有的文件先备份出来再动手训练,避免跑崩了把原模型覆盖掉,这是很多人忽略的后悔药。另外,如果 requirements.txt 里没有固定版本号,建议自己在环境里装完后再导出一次,不然半年后复现代码时你会发现结果对不上。

3.2 最小复现命令:数据处理、训练、评估一条线

环境准备和训练命令本身并不复杂,复杂度都在参数和预处理里。下面的命令是我跑这类机器学习项目的最小路径,先把整条链路走通,再做任何调优。

cd news-headline-classifier python -m venv venv source venv/bin/activate # Windows 下用 venv\Scripts\activate pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple # 先跑数据预处理,生成 train.csv / val.csv / label2id.json python prepare_data.py --input data/headlines.csv # 训练 TextCNN 模型 python train.py --model textcnn --max_len 32 --batch_size 64 --epochs 10 # 对一条新标题做预测 python predict.py --text "特斯拉上海工厂产能提升至每周两千辆" --model textcnn

我解释一下为什么这么安排。venv 是必须的,机器学习项目的依赖冲突很常见,尤其 torch 和 numpy 之间的版本耦合,直接装进系统 Python 环境相当于给自己埋雷。pip 用一个国内镜像源是因为 transformers、torch 这些包的体积大,默认源拉取实在太慢,换成清华镜像基本能省出一半安装时间。prepare_data.py 和数据格式绑定,如果你的原始 CSV 不是 category 和 headline 两列,就要先改这里。train.py 里的 --model 指定模型族,--max_len 控制标题截断长度,这几个参数的具体影响我在第 4 章展开。最后的 predict 命令验证的是整条链路是否通,如果这里能输出一个合理类别,说明数据、训练、推理已经完整闭环。

3.3 训练脚本里的关键参数:模型保存、日志与随机种子

训练脚本是所有调参的入口,所以它必须把参数暴露在命令行上,而不是写死在文件里。我通常要求训练脚本至少支持模型名、截断长度、训练轮数、批次大小、学习率、随机种子这六个参数,缺一个都会让人在复现时抓狂。

# train.py 中的核心参数定义 parser.add_argument("--model", default="textcnn", choices=["bow", "textcnn", "bert"], help="基线模型或深度模型") parser.add_argument("--max_len", type=int, default=32, help="标题截断长度,中文按字符数计算") parser.add_argument("--epochs", type=int, default=10, help="训练轮数,配合早停使用") parser.add_argument("--batch_size", type=int, default=64, help="Deep Learning 训练批次大小") parser.add_argument("--lr", type=float, default=1e-3, help="Adam 优化器默认学习率;BERT 建议 2e-5") parser.add_argument("--seed", type=int, default=42, help="随机种子,保证可复现") # 训练结束后保存权重和训练参数 torch.save({ "model_state": model.state_dict(), "vocab": vocab, "label2id": label2id, "config": vars(args), }, "checkpoints/textcnn_epoch10.pt")

这里有个容易被忽略的细节:模型权重、词表、标签映射、训练参数应该一起打包保存。很多人只保存 state_dict,结果预测阶段还要到处找词表文件,一旦文件丢了整个模型就废了。把 config 也存进去,加载时就能自动恢复 max_len、模型名等关键信息,而不需要靠记忆。随机种子这行也重要,深度学习模型在 GPU 上的计算本身有随机性,不固定种子的话,同一条命令跑两次 F1 可能差一个百分点,这会让你误以为是调参生效了,实际上是玄学波动。

验证环节我习惯在每个 epoch 结束后计算验证集损失和宏平均 F1,而不是只算 accuracy。因为新闻标题分类的类别分布通常不均衡,准确率会被多数类带偏。判断模型是否收敛,要看验证集 loss 是否还在下降,F1 也要盯着。下面这段回调逻辑是所有训练脚本里我第一优先检查的代码,它直接决定早停和最佳模型的选取。

best_f1 = 0.0 for epoch in range(args.epochs): train_loss = run_epoch(train_loader, model, optimizer) val_loss, val_f1 = evaluate(val_loader, model) print(f"epoch {epoch+1} | train_loss={train_loss:.4f} | " f"val_loss={val_loss:.4f} | macro_f1={val_f1:.4f}") if val_f1 > best_f1: best_f1 = val_f1 torch.save(model.state_dict(), "checkpoints/best_model.pt")

注意,我保存的是验证集 F1 最高的权重,而不是最后一个 epoch 的权重。很多源码包默认保存 last epoch,如果训练后期过拟合,验证集 F1 已经开始掉了,你拿到的模型其实是退化版本。这个细节能直接决定最终交付模型的水平。

4. 模型对比与参数调优:新手的默认值,老手的调参路径

4.1 三组机器学习模型的实测对比:精度、训练速度与显存

拿到一套能跑通的源码之后,下一步就是决定到底用哪个模型。我在新闻标题数据上常用的对比维度是验证集宏平均 F1、训练耗时和资源占用,因为这三项直接对应业务上线后的性价比。以大约五万条标题的中等规模数据集为参照,三组模型的经验区间大致如下:

模型验证集 macro-F1训练耗时显存/内存占用适用场景
TF-IDF + LinearSVC0.84 - 0.87秒级到分钟级CPU 内存即可快速验证、冷启动基线
TextCNN(词向量随机初始化或预训练)0.87 - 0.90分钟级1GB 左右常规主力模型
BERT / 轻量预训练模型0.90 - 0.93小时级8GB 起步对精度要求高、标注质量好

这个表不是让你直接选最后一行,而是帮你建立心理预期:如果 TextCNN 只跑出 0.82,那大概率不是模型的问题,而是数据或预处理的问题,换了 BERT 也只是把错误放大。TF-IDF 基线在这个任务里永远值得先跑,它训练快、可解释性强,万一业务方问“模型凭什么把这条标题分到体育”,你还能把特征词拿给他看。

对文本分类这个任务,我还有一个私人心得:如果 TF-IDF 基线能做到 0.85,说明线性可分性不错,这时候 TextCNN 的提升空间通常在 2 到 3 个点;如果基线只有 0.7,说明数据本身有问题,先去查标注质量。把精力花在数据清洗上,回报比换任何模型都高。

4.2 五个必调参数:max_len、embedding_dim、epoch、batch_size、learning_rate

新闻标题分类的参数调优面比图像任务小得多,但五个参数每个都能让结果大起大落。第一个是 max_len,中文新闻标题平均长度只有 15 到 25 个字符,我一般设 32,少数超长标题截断后丢失的信息基本不影响分类。设 128 不是不行,但序列变长后 TextCNN 的池化特征会被稀释,训练耗时增加,精度反而可能下降。

第二个是 embedding_dim。源码包里默认值常见是 100 或 128,五万条以下数据用 100 足够了。预训练词向量在这个任务上收益不如想象中明显,因为新闻标题里人名、品牌名、新词太多,静态词表永远覆盖不全,我现在的做法是随机初始化词向量,靠模型自己去学。第三个是 epoch,默认 10 轮已经跑得动,但必须配早停,否则第 12 轮的过拟合模型会让你怀疑人生。第四个是 batch_size,TextCNN 用 64 到 128,显存不足就减半,但注意 batch 太小的时候梯度噪声大,F1 波动更明显。第五个是 learning_rate,Adam 优化器下 1e-3 是常青树;切换 BERT 家族必须降到 2e-5 左右,这个数量级差异是 BERT 微调的第一原则,拿 1e-3 跑 BERT 大概率 NaN loss。

把这五个参数做成配置文件而不是散落在脚本里,是我现在做机器学习项目的硬性习惯。config.py 里写默认值,命令行参数覆盖默认值,实验记录里每次都打印实际生效的参数组合,这样回看实验结果时不需要猜。

4.3 类别不均衡怎么救:class_weight、过采样与 Focal Loss

新闻标题分类数据几乎天然不均衡:娱乐、体育往往很多,财经和健康类占比可能只有 5% 到 10%。不处理的话,模型会把高频类学得极好,低频类直接牺牲,整体 accuracy 看着有 0.9,但低频类的 F1 只有 0.3。第一个处理手段是 sklearn 里现成的 class_weight,成本最低。

# sklearn 模型:直接传入 class_weight from sklearn.svm import LinearSVC model = LinearSVC(class_weight="balanced", max_iter=1000) model.fit(tfidf_matrix, train_labels) # PyTorch 模型:给损失函数传权重 class_counts = train_df["label"].value_counts().sort_index() class_weights = 1.0 / class_counts class_weights = class_weights / class_weights.sum() import torch.nn as nn criterion = nn.CrossEntropyLoss(weight=torch.tensor( class_weights.values, dtype=torch.float32 ))

class_weight 的原理是给少数类的损失乘以一个较大系数,让模型更重视它们。但要注意,权重太大模型会变得激进,把很多样本分到少数类,所以权重归一化以后一般还需要调一个缩放系数。如果加了权重后低频类的 F1 还是上不去,再考虑过采样:对少数类样本做简单的重复采样,或者在 TextCNN 这种模型上用更大比例的 dropout 防止对少数类过拟合。Focal Loss 是从目标检测里借用过来的思路,把易分类样本的损失压低、难分类样本的损失突出,对标题分类这种噪声多的短文本效果不错,代价是要多调一个 gamma,默认 2 起步。我的顺序永远是先 class_weight,再看 F1 分布,最后才上 Focal Loss,能少调参就少调参。

5. 标题分类避坑指南:编码、错位与过拟合

5.1 CSV 编码与全角符号:第一个翻车点

现象:训练脚本跑起来以后,日志里出现一堆不属于任何类别的文本,或者 predict.py 加载数据直接抛 UnicodeDecodeError,还有的标题输出乱码,像“鍥炲槈QQ”这种。原因:CSV 文件在 Windows 下保存成了 GBK 或 GB2312 编码,而 pandas 默认按 UTF-8 读取;还有一部分标题里混着全角括号、全角数字,分词器处理不了直接变成脏数据。解决:读文件时先探测编码,pandas 里加 encoding="utf-8-sig" 是最稳的,因为 utf-8-sig 会自动去掉 BOM 头;文本规范化时把全角字符统一转半角再进入特征阶段,这一步放在预处理流水线的最前面。

我后来把这段逻辑写成公共函数放在 utils.py 里,训练和预测都调它,保证两边处理逻辑完全一致。很多源码包里的预处理是复制粘贴的,训练阶段做了全角转半角,预测阶段忘了做,线上结果自然对不上账。

5.2 标签与句子错位:shuffle 时才能暴露的隐性 bug

现象:训练损失一直不降,验证集 F1 在 0.5 左右徘徊,像随机猜。检查数据却发现 train.csv 里每一行的标签和标题根本对不上。原因:源码里先对句子数组做了 np.random.shuffle,又单独对标签数组做了 shuffle,两个数组的顺序从此分道扬镳,这种 bug 只在 shuffle 之后才会暴露。解决:不要分开打乱,要么用索引数组一次洗牌,要么直接交给 train_test_split。

import numpy as np # 错误写法:两行 shuffle 打乱了句子和标签的对应关系 # np.random.shuffle(texts) # np.random.shuffle(labels) # 正确写法:只打乱索引 idx = np.random.permutation(len(texts)) texts = texts[idx] labels = labels[idx]

这段代码看起来简单,但它是文本分类项目里最隐蔽的翻车点。还有个变体是加载数据时 dropna 之前和之后的索引不一致,reset_index(drop=True) 少写一行,合并时也会错位。我的排查习惯是训练开始前先打印十行数据,人工核对标签和文本是否匹配,这一步只用一分钟,但能省掉后面几小时的浪费。

5.3 验证集漂亮、线上翻车:过拟合和数据泄漏

现象:训练日志里验证集 F1 冲到 0.93,测试集却只有 0.72;或者模型在历史数据上完美,遇到当天新标题就乱分。原因有两种。第一种是过拟合,模型把训练集里的噪声模式背下来了,常见于训练轮数太多又没做早停;第二种是数据泄漏,比如按时间切分测试集时,测试集前几天的标题和训练集里的重复新闻同时出现,模型看到过答案了。解决:划分数据时用 stratify 保证类别分布一致,同时保证切分时间维度上测试集是更晚的数据;训练时早停只看验证集损失,别把测试集拿来选 epoch,否则测出来就是虚高。

另一个我常踩的泄漏细节是去重。新闻标题的转发率很高,同一条标题可能重复出现十几次,如果不做标题去重,训练集和测试集里就存在同源样本,测试 F1 会虚高两个点以上。我现在的做法是在数据准备阶段先按标题文本去重,再做时间切分,这样评估结果才真实反映“未来新闻”的分类难度。

5.4 类别映射写死在代码里:上线后最痛的返工

现象:模型已经上线运行,运营提来一个新类别需求“辟谣”,你要么改代码重新训练,要么发现 predict.py 里 label2id 是手工写的字典,模型就是分不出新类别。原因:训练阶段把 label2id 直接硬编码在脚本顶部,没有落盘;或者预测脚本里用 enumerate 重新生成了一遍映射,和训练时的顺序不一致。解决:映射文件必须由数据准备阶段生成,模型保存时把 label2id 也打进 checkpoint,预测脚本只从这两个地方读取。

# 加载模型时恢复标签映射,而不是硬编码 checkpoint = torch.load("checkpoints/best_model.pt", map_location="cpu") label2id = checkpoint["label2id"] id2label = {idx: label for label, idx in label2id.items()} # 预测时输出可读标签 pred_id = model.predict_one(text) print("预测类别:", id2label[pred_id])

这个坑的隐性成本很高。如果预测脚本和训练脚本的 label2id 不一致,哪怕都叫“财经”,一个映射成 0 一个映射成 1,模型精度就是零。我吃过一次亏以后,把所有类型的映射文件统一放到 data/ 目录,checkpoint 里再存一份副本,加载时优先读取 checkpoint 里的,双重保险。

6. 推到线上前先做三件事:看混淆矩阵、调阈值、留增量接口

模型训练完不等于项目完事。我现在的固定流程是先出一份混淆矩阵,看哪些类互相打架。新闻标题分类里最常见的错误是“科技”和“财经”互分,因为企业新闻同时带两个属性;还有“娱乐”和“社会”边界模糊。把这些错例列出来给业务方看,他们能判断是标注体系本身就分不清,还是模型欠拟合。

from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay import matplotlib.pyplot as plt cm = confusion_matrix(y_true, y_pred) disp = ConfusionMatrixDisplay(cm, display_labels=list(label2id.keys())) disp.plot(xticks_rotation=45) plt.tight_layout() plt.savefig("report/confusion_matrix.png", dpi=150)

第二个动作是看概率输出,而不是只看 argmax。很多业务场景里,分类器需要说“我不确定”,这时候要设置信度阈值。对二分类或多分类输出,我一般用验证集跑 precision_recall_curve,选一个宏平均 F1 最高的阈值作为默认拒绝线,低于阈值的标题走人工审核。这样线上效果的表征是“分类精度”加“人工兜底比例”,比单一 F1 更让业务方安心。

第三个动作是给模型留增量更新的接口。新闻标题分类的数据漂移是常态,每周都应该用新数据继续训练几轮。我现在的实现是加载 best_model.pt 作为初始化权重,在新数据上用较低学习率继续训练,同时保留旧验证集防止灾难性遗忘。还要把每次实验用的随机种子、数据版本记录到一个实验日志里,不然两周后你看 log 都不知道自己调过什么参数。这个习惯帮我避开了无数“玄学调参”的坑,也希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询