ARTICLE DETAIL

资讯详情

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

新闻标题分类实战:从TF-IDF到TextCNN的机器学习落地指南

新闻标题分类实战:从TF-IDF到TextCNN的机器学习落地指南 简介面向自然语言处理学习者与毕业设计开发者这份基于机器学习的新闻标题分类系统源码可支撑文本分类、敏感词过滤、模型训练与评估等完整流程适合课程设计、论文实验或工程原型搭建。压缩包共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, encodingutf-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, encodingutf-8) as f: json.dump(label2id, f, ensure_asciiFalse) # stratify 保证每个类别的比例在训练/验证集里保持一致 train_df, val_df train_test_split( df, test_size0.1, random_state42, stratifydf[label] ) train_df.to_csv(data/train.csv, indexFalse, encodingutf-8) val_df.to_csv(data/val.csv, indexFalse, encodingutf-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, defaulttextcnn, choices[bow, textcnn, bert], help基线模型或深度模型) parser.add_argument(--max_len, typeint, default32, help标题截断长度中文按字符数计算) parser.add_argument(--epochs, typeint, default10, help训练轮数配合早停使用) parser.add_argument(--batch_size, typeint, default64, helpDeep Learning 训练批次大小) parser.add_argument(--lr, typefloat, default1e-3, helpAdam 优化器默认学习率BERT 建议 2e-5) parser.add_argument(--seed, typeint, default42, 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(fepoch {epoch1} | train_loss{train_loss:.4f} | fval_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_sizeTextCNN 用 64 到 128显存不足就减半但注意 batch 太小的时候梯度噪声大F1 波动更明显。第五个是 learning_rateAdam 优化器下 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_weightbalanced, max_iter1000) 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(weighttorch.tensor( class_weights.values, dtypetorch.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 里加 encodingutf-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(dropTrue) 少写一行合并时也会错位。我的排查习惯是训练开始前先打印十行数据人工核对标签和文本是否匹配这一步只用一分钟但能省掉后面几小时的浪费。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_locationcpu) 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_labelslist(label2id.keys())) disp.plot(xticks_rotation45) plt.tight_layout() plt.savefig(report/confusion_matrix.png, dpi150)第二个动作是看概率输出而不是只看 argmax。很多业务场景里分类器需要说“我不确定”这时候要设置信度阈值。对二分类或多分类输出我一般用验证集跑 precision_recall_curve选一个宏平均 F1 最高的阈值作为默认拒绝线低于阈值的标题走人工审核。这样线上效果的表征是“分类精度”加“人工兜底比例”比单一 F1 更让业务方安心。第三个动作是给模型留增量更新的接口。新闻标题分类的数据漂移是常态每周都应该用新数据继续训练几轮。我现在的实现是加载 best_model.pt 作为初始化权重在新数据上用较低学习率继续训练同时保留旧验证集防止灾难性遗忘。还要把每次实验用的随机种子、数据版本记录到一个实验日志里不然两周后你看 log 都不知道自己调过什么参数。这个习惯帮我避开了无数“玄学调参”的坑也希望帮到你。本文还有配套的精品资源点击获取
返回列表