☰
PyTorch新闻文本分类实战:从数据管道到预训练模型全链路解析
2026/10/10 1:31:32 网站建设 项目流程

简介:这份资源面向计算机相关专业学生与NLP入门开发者,提供一套基于PyTorch的新闻文本分类完整实现方案,可用于毕业设计、课程设计或综合实验等教学场景。压缩包共449个文件,约238.29MB,以357个pth模型参数文件为主,辅以74个zbak备份、8个py源码脚本,以及7z数据与词向量压缩包、png结构示意图、README说明和LICENSE授权文件,覆盖从数据到模型的完整链路。项目实现了文本预处理、特征工程、神经网络模型构建与性能评估等自然语言处理流程,代码经过多轮测试验证,运行稳定。已有56人学习关注。读者可据此获得可直接运行的分类系统源码、标注语料库、预训练模型参数及技术文档,对照TextCNN结构图理解模型设计,并借助词向量与数据包快速复现实验,适合作为NLP项目实践与技能训练的参考模板。

1. 从一份能跑通的新闻分类工程说起

如果你手头正缺一个能直接跑起来的文本分类基线,这套基于 PyTorch 的新闻文本分类系统值得花半小时拆一遍。它把完整源码、数据集和预训练模型打包在一起,覆盖了从分词、词表构建、Dataset 封装、模型定义到训练评估的全链路。适合两类人:一类是刚接触 NLP 工程、想找一个结构清晰的项目照着复现的新手;另一类是做舆情监控、内容审核、推荐冷启动的从业者,需要一个能快速替换数据、改改标签就能上手的脚手架。我见过太多人卡在「模型会写但数据管道搭不起来」这一步,这份资源恰好把最磨人的工程部分补齐了。下面按「它是什么、怎么跑、坑在哪、怎么改」的顺序拆开讲。

2. 拆开工程目录:数据管道与模型定义怎么分工

拿到一份源码包,第一件事不是急着python train.py,而是把目录结构和模块职责看清楚。这套工程的典型布局是data/放原始语料和预处理产物,models/放网络定义,utils/放分词、词表、指标计算,根目录下是train.py、predict.py、config.py。分工清晰的好处是,你换数据集时只动data/和配置,换模型结构时只动models/,互不牵连。

2.1 数据加载:从原始文本到张量的四步

文本分类和图像分类最大的区别在于,图像读进来就是数值,文本得先过一道「数字化」的工序。这套工程里,数据管道通常分四步走:读原始文件、分词、映射成 id、padding 成定长。核心逻辑封装在自定义 Dataset 里,配合DataLoader做批处理。

import torch from torch.utils.data import Dataset, DataLoader from collections import Counter class NewsDataset(Dataset): def __init__(self, texts, labels, vocab, max_len=128): self.texts = texts self.labels = labels self.vocab = vocab self.max_len = max_len # 统一截断/补齐长度,过长截断,过短补0 def __len__(self): return len(self.texts) def __getitem__(self, idx): tokens = self.texts[idx].split() # 常见做法是按空格切,中文需先分词 ids = [self.vocab.get(t, self.vocab['<UNK>']) for t in tokens] ids = ids[:self.max_len] # 截断 ids += [self.vocab['<PAD>']] * (self.max_len - len(ids)) # 补齐 return torch.tensor(ids), torch.tensor(self.labels[idx]) # 构建词表:只保留出现次数达阈值的词,避免词表爆炸 def build_vocab(texts, min_freq=2): counter = Counter() for text in texts: counter.update(text.split()) vocab = {'<PAD>': 0, '<UNK>': 1} for word, freq in counter.items(): if freq >= min_freq: vocab[word] = len(vocab) return vocab

这段代码里有两个参数值得盯住。max_len决定序列长度,设太小会丢信息,设太大显存吃不消,新闻标题类任务 64 到 128 通常够用,正文类任务可能要到 256 甚至 512。min_freq是词表过滤阈值,设成 1 会让词表膨胀到几万甚至十几万,嵌入层参数暴涨还容易过拟合,设成 2 或 3 能砍掉大量只出现一次的长尾词。我一般会先统计一下词频分布,看看覆盖 95% 语料需要多少词,再定这个阈值。

2.2 模型选型:为什么是 Embedding + 编码器 + 全连接

文本分类的模型骨架基本固定:嵌入层把 id 变成稠密向量,编码器提取序列特征,池化层压成定长向量,最后全连接输出类别 logits。区别在于编码器用什么。这份工程如果带预训练模型,编码器多半是 Transformer 或 LSTM;如果是轻量基线,可能就是 Embedding 平均池化。

import torch.nn as nn class TextClassifier(nn.Module): def __init__(self, vocab_size, embed_dim=128, hidden_dim=256, num_classes=10): super().__init__() self.embedding = nn.Embedding(vocab_size, embed_dim, padding_idx=0) self.encoder = nn.LSTM(embed_dim, hidden_dim, batch_first=True, bidirectional=True) self.fc = nn.Linear(hidden_dim * 2, num_classes) # 双向所以乘2 def forward(self, x): emb = self.embedding(x) # [B, L, E] out, (h, c) = self.encoder(emb) # h: [2, B, H] h = torch.cat([h[0], h[1]], dim=-1) # 拼接双向最后隐状态 return self.fc(h)

embed_dim是嵌入维度,128 或 256 是常见起点,太小表达力不够,太大在小数据集上容易过拟合。hidden_dim是 LSTM 隐层维度,双向的话实际输出是两倍。num_classes必须和你的标签数严格对齐,这个数字写错是最常见的翻车点之一,训练不报错但 loss 降不下去。如果换成预训练模型,嵌入层会被替换成预训练权重加载,此时要注意padding_idx和预训练模型的对齐问题。

2.3 训练循环:损失函数与优化器的搭配

训练部分看着模板化,但参数搭配有讲究。分类任务用交叉熵损失,优化器用 Adam 居多,学习率 1e-3 是常见起点,带预训练模型时通常要降到 1e-5 到 2e-5,否则预训练权重会被冲垮。

from torch.optim import Adam import torch.nn as nn model = TextClassifier(vocab_size=len(vocab), num_classes=len(label2id)) criterion = nn.CrossEntropyLoss() optimizer = Adam(model.parameters(), lr=1e-3, weight_decay=1e-4) for epoch in range(10): model.train() for batch_x, batch_y in train_loader: optimizer.zero_grad() logits = model(batch_x) loss = criterion(logits, batch_y) loss.backward() optimizer.step()

weight_decay是 L2 正则,1e-4 到 1e-5 之间调,能缓解过拟合。如果类别不均衡,CrossEntropyLoss可以传weight参数给少数类加权,这个在新闻分类里很常见,因为某些类别的样本天然就少。训练时记得每个 epoch 后在验证集上评估,别只看训练 loss,否则过拟合了都不知道。

3. 跑通训练与推理:参数配置和评估指标怎么定

工程能跑起来只是第一步,跑出合理结果才算数。这一章讲配置怎么改、指标怎么看、推理怎么接。

3.1 配置文件:把超参从代码里抽出来

好的工程会把超参集中在config.py或 yaml 里,而不是散落在各个文件。常见配置项包括数据路径、词表路径、模型保存路径、batch size、学习率、epoch 数、max_len、embed_dim 等。

# config.py class Config: train_path = "data/train.txt" val_path = "data/val.txt" vocab_path = "data/vocab.pkl" save_path = "checkpoints/best_model.pt" batch_size = 64 lr = 1e-3 epochs = 10 max_len = 128 embed_dim = 128 hidden_dim = 256 num_classes = 10

batch_size受显存限制,64 或 128 是常见值,太小训练不稳定,太大泛化可能变差。epochs别设太大,配合早停机制更稳。把配置抽出来的好处是,做对比实验时只改一个文件,不用满工程找参数。

3.2 评估指标:准确率不够,还得看 F1

新闻分类如果类别均衡,准确率够用;但类别不均衡时,准确率会被多数类带偏。这时候要看宏平均 F1 和各类别的混淆矩阵。

from sklearn.metrics import classification_report, confusion_matrix def evaluate(model, loader): model.eval() preds, trues = [], [] with torch.no_grad(): for batch_x, batch_y in loader: logits = model(batch_x) pred = torch.argmax(logits, dim=-1) preds.extend(pred.tolist()) trues.extend(batch_y.tolist()) print(classification_report(trues, preds, digits=4)) print(confusion_matrix(trues, preds))

classification_report会输出每个类别的 precision、recall、f1-score,重点看 f1-score 低的类别,往往是样本少或者特征不明显的类。混淆矩阵能看出哪些类别容易互相误判,比如体育和娱乐在某些标题上确实难分,这时候要么加特征,要么考虑合并类别。

3.3 推理接口:单条预测怎么封装

训练完要落地,得有个干净的推理函数。加载模型、加载词表、预处理输入、前向计算、返回类别和置信度。

def predict(text, model, vocab, label2id, max_len=128): model.eval() tokens = text.split() ids = [vocab.get(t, vocab['<UNK>']) for t in tokens][:max_len] ids += [vocab['<PAD>']] * (max_len - len(ids)) tensor = torch.tensor(ids).unsqueeze(0) # 加 batch 维度 with torch.no_grad(): logits = model(tensor) prob = torch.softmax(logits, dim=-1) pred_id = torch.argmax(prob, dim=-1).item() id2label = {v: k for k, v in label2id.items()} return id2label[pred_id], prob[0][pred_id].item()

unsqueeze(0)是给单条数据补上 batch 维度,模型 forward 里通常按[B, L]处理,少了这步会报维度错误。返回置信度是为了后续做阈值过滤,低置信度的预测可以转人工审核,这在内容审核场景里很实用。

4. 避坑与排查:那些让 loss 不降的细节

这一章是我踩过的坑合集,每条都按「现象 → 原因 → 解决」写,照着排查能省不少时间。

4.1 现象:loss 一直不降,准确率卡在随机水平

原因通常是标签和输入没对齐,或者词表映射出了问题。比如label2id里标签顺序和数据集里的标签不一致,模型学到的映射是乱的。另一个常见原因是padding_idx没设对,补齐的 0 参与了梯度计算,把嵌入层带偏了。

解决:先打印几条(input_ids, label)出来肉眼检查,确认 id 序列和标签对应正确。然后确认nn.Embedding的padding_idx=0和词表里<PAD>的 id 一致。再检查学习率是不是太大,1e-3 对某些模型偏大,降到 1e-4 试试。

4.2 现象:训练集准确率很高,验证集一塌糊涂

这是典型过拟合。原因可能是模型参数太多、数据太少、训练轮数太多,或者词表太大导致嵌入层记住了训练样本。

解决:先加 dropout,LSTM 和全连接层前都可以加,0.3 到 0.5 之间。然后加 weight_decay,1e-4 起步。再考虑减小embed_dim和hidden_dim。如果还不行,做数据增强,同义词替换、随机删除词都是文本分类里常用的手段。最后加早停,验证集 loss 连续几个 epoch 不降就停。

4.3 现象:中文分词后词表巨大,显存爆了

原因是用空格切中文等于没切,整句变成一个 token,或者用了不合适的切分方式导致词表爆炸。

解决:中文必须先分词,常见做法是用 jieba 或类似工具切好再存成空格分隔的格式。然后设min_freq过滤低频词,通常 2 或 3。还可以设max_vocab_size,按词频排序只保留前 N 个。如果显存还是紧张,减小batch_size和max_len,或者用梯度累积模拟大 batch。

4.4 现象:加载预训练模型后效果反而变差

原因通常是学习率没降下来,预训练权重被大学习率冲垮了。另一个原因是预训练模型的词表和当前数据的词表不匹配,映射错位。

解决:带预训练模型时学习率降到 1e-5 到 2e-5,并且可以考虑先冻结嵌入层几个 epoch 再解冻。词表方面,要么直接用预训练模型的词表做映射,要么确认自定义词表和预训练权重的 id 对齐。这个对齐问题很隐蔽,错了不报错但效果差,建议加载后先跑几条已知样本验证输出是否合理。

4.5 现象:推理时单条预测结果和批量预测不一致

原因是预处理不一致。训练时用了截断和补齐,推理时如果忘了补齐,或者max_len设得不一样,输入分布就变了。

解决:把预处理逻辑抽成一个函数,训练和推理共用同一份代码。max_len从配置里读,别在两处各写一个数字。这个坑很隐蔽,因为单条推理不补齐时模型也能跑,只是结果飘。

5. 进阶玩法:换预训练模型与处理类别不均衡

跑通基线之后,真正拉开效果差距的是两件事:换更强的编码器,和处理类别不均衡。这一章讲具体怎么改。

5.1 把 LSTM 换成预训练 Transformer

如果资源包里带了预训练模型,通常是为了替换编码器部分。以常见的 Transformer 类模型为例,改动集中在模型定义和输入处理上。

from transformers import AutoModel, AutoTokenizer class TransformerClassifier(nn.Module): def __init__(self, model_name, num_classes, dropout=0.3): super().__init__() self.encoder = AutoModel.from_pretrained(model_name) hidden = self.encoder.config.hidden_size self.dropout = nn.Dropout(dropout) self.fc = nn.Linear(hidden, num_classes) def forward(self, input_ids, attention_mask): out = self.encoder(input_ids=input_ids, attention_mask=attention_mask) cls = out.last_hidden_state[:, 0] # 取 [CLS] 向量 return self.fc(self.dropout(cls))

这里的关键变化是输入多了attention_mask,用来告诉模型哪些位置是真实 token、哪些是补齐的。少了这个 mask,模型会把补齐的 0 也当有效信息处理,效果会打折。学习率要降到 2e-5 左右,batch_size可能要降到 16 或 32,因为 Transformer 比 LSTM 吃显存。训练轮数通常 3 到 5 个 epoch 就够,多了容易过拟合。

5.2 类别不均衡的三种处理方式

新闻分类里类别不均衡是常态,比如财经类样本可能是小众类别的十倍。三种常见处理方式,按改动成本从低到高排。

第一种是给损失函数加权,少数类权重高,多数类权重低。

from sklearn.utils.class_weight import compute_class_weight import numpy as np weights = compute_class_weight('balanced', classes=np.unique(train_labels), y=train_labels) class_weights = torch.tensor(weights, dtype=torch.float) criterion = nn.CrossEntropyLoss(weight=class_weights)

compute_class_weight的balanced模式会按类别频率反比给权重,频率越低权重越高。这个改动最小,通常能带来明显提升。

第二种是重采样,对少数类过采样或对多数类欠采样。过采样简单但容易过拟合,欠采样会丢信息。折中做法是用数据增强给少数类生成变体。

第三种是换评估和决策方式,不只看 argmax,而是按类别调阈值。比如少数类阈值调低,宁可多召回一些再人工过滤。这个在内容审核场景里很常见,召回优先于精确。

5.3 一个验证改动是否有效的习惯

每次改完模型或数据处理,别只看最终准确率。我一般会固定一个验证集,跑三组对比:基线、只改模型、只改损失函数。然后看混淆矩阵里哪些类别变了。如果某个类别的 f1 从 0.6 涨到 0.75,说明改动对这个类有效;如果整体涨了但某个类跌了,可能是类别间此消彼长,得权衡。

还有个小技巧是固定随机种子,torch.manual_seed(42)加上 numpy 和 random 的种子,保证每次跑的结果可复现。不然调参时你分不清是改动生效了还是随机波动。从那以后我每次做对比实验都强制固定种子,跑三遍取平均,虽然费时间但结论靠谱。希望这套拆解帮你少走点弯路,把这份资源真正用起来。

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

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

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

立即咨询