互联网新闻情感分析实战:RoBERTa-wwm-large微调与课设包解析
2026/9/23 14:15:40 网站建设 项目流程

简介:这份资源是面向高校学生与初学者的互联网新闻情感分析完整项目工程,适用于课程设计、毕业设计、大作业、工程实训及学科竞赛等场景,也可作为NLP方向入门练手与项目立项的参考。包内共101个文件,以53个Python源码、18个Jupyter Notebook实验脚本、20个编译缓存文件为主,另含6个CSV数据集、1个已训练模型文件及说明文档,压缩包约33.37MB,覆盖数据预处理、模型训练到结果提交的完整流程。内容预览显示项目围绕RoBERTa预训练模型展开,包含多组不同层数embedding与cls策略的实验脚本,以及训练集、测试集与提交示例等数据文件,便于对照复现与调参对比。已有36人学习下载。读者可据此复刻出功能一致的情感分析系统,借鉴其设计报告与目录组织,并在现有代码基础上扩展新功能,遇到问题也可与作者联系获取帮助。

1. 从一份课设包说起:互联网新闻情感分析到底怎么落地

课程设计选题里,互联网新闻情感分析是出现频率极高的一个。原因很直接:数据好找、任务定义清晰、模型效果肉眼可见,而且能同时覆盖数据清洗、文本分类、预训练模型微调、结果提交这一整条链路。但真正动手时,多数人卡在同一个地方——不是不会调模型,而是不知道一份能跑通的工程应该长什么样。这份资源包给了一个完整答案:它包含训练集、测试集、标签文件、提交示例,以及四个基于 RoBERTa-wwm-large 的 notebook,覆盖了 last2embedding、last3embedding、cls 池化、标签替换等不同策略。换句话说,它不是一份“教学 demo”,而是一套可以直接复现、也可以在此基础上改造成自己方案的工程骨架。适合正在做课设、实训、大作业,或者想拿一个真实文本分类项目练手的人。

2. 数据与标签体系:先把输入输出对齐再谈模型

2.1 六个 CSV 文件的分工

拿到包之后别急着打开 notebook。先把数据文件的关系理清楚,否则后面调模型时连标签对不上都不知道错在哪。从文件命名看,这套数据的组织方式是典型的“训练/验证+测试+提交”三段式:

文件名作用关键列
Train_DataSet.csv训练集原文文本内容
Train_DataSet_Label.csv训练集标签标签列
Second_DataSet.csv第二份数据原文(验证或补充训练)文本内容
Second_DataSet_Label.csv第二份数据标签标签列
Second_TestDataSet.csv测试集原文文本内容
submit_example.csv提交格式示例预测列

这里有个容易翻车的地方:原文和标签是分开两个文件的。很多人习惯用 pandas 直接 read_csv 一个文件就开跑,结果发现标签列根本不存在。正确做法是按行号或索引对齐合并。常见做法是:

import pandas as pd train_text = pd.read_csv('Train_DataSet.csv') train_label = pd.read_csv('Train_DataSet_Label.csv') # 按索引对齐合并,避免行序错乱 train = train_text.copy() train['label'] = train_label.iloc[:, 0].values print(train.shape) print(train['label'].value_counts())

逻辑说明:先分别读取原文和标签,再用iloc[:, 0]取标签文件的第一列(通常标签就在第一列),通过索引对齐赋值。参数上需要注意的是,如果两个文件行数不一致,赋值会直接报长度不匹配的错误,这时候要回去检查数据是否被截断或有多余空行。value_counts()用来确认类别分布,如果发现某类样本极少,后面训练时就要考虑加权或重采样。

2.2 标签映射与提交格式

submit_example.csv是很多人忽略但极其关键的文件。它决定了你最终产出的格式能不能被评测系统接受。常见做法是先看一眼它的列名和取值:

submit = pd.read_csv('submit_example.csv') print(submit.columns.tolist()) print(submit.head()) print(submit.iloc[:, -1].unique()[:10])

如果提交示例里的预测值是整数编码(比如 0/1/2),而你的模型输出是概率或字符串,就需要做一次映射。我一般会先把标签编码关系固定下来,写成一个字典,训练和推理共用,避免两边不一致。这个字典最好直接写在 notebook 最前面,后面所有涉及标签的地方都引用它,而不是到处硬编码。

提示:标签文件如果只有一列且没有表头,pandas 会把第一行数据当成列名。读取时加header=None更稳妥。

3. RoBERTa-wwm-large 微调:四个 notebook 的差异与选择

3.1 last2embedding、last3embedding 和 cls 到底取哪个

这四个 notebook 的核心差异在特征提取方式上。RoBERTa 这类 Transformer 模型输出的是每个 token 的隐层表示,做句子分类时,常见做法有三种:取 [CLS] 位置的向量、取最后几层隐层做平均或拼接、或者把最后几层和 [CLS] 组合起来。文件名里的last2embeddinglast3embeddingcls说的就是这件事。

  • last2embedding:取最后两层隐层输出,通常做平均或拼接后接分类头。适合数据量中等、希望利用更多语义信息的场景。
  • last3embedding:取最后三层,信息更丰富,但参数量和显存占用也会上升。
  • cls:只取 [CLS] 向量,最简单直接,也是 BERT 系模型的默认做法。
  • replacement:从命名推测涉及标签替换或某种替换策略,可能是对类别不平衡或标签噪声的处理。

选哪个不是拍脑袋。如果显存够、训练时间充裕,last3embedding 通常比 cls 略好;如果数据量偏小,cls 反而更稳,因为额外层带来的参数更容易过拟合。我一般会先跑 cls 作为 baseline,再对比 last2 和 last3,看验证集上的 F1 有没有实质提升。

3.2 微调流程的关键参数

以其中一个 notebook 的结构为例,微调流程大致如下:

from transformers import BertTokenizer, BertForSequenceClassification from torch.utils.data import DataLoader, Dataset import torch # 加载 tokenizer 和模型 model_name = 'hfl/chinese-roberta-wwm-ext-large' tokenizer = BertTokenizer.from_pretrained(model_name) model = BertForSequenceClassification.from_pretrained( model_name, num_labels=3 # 根据实际类别数修改 ) # 文本编码 def encode(texts, labels=None, max_len=512): enc = tokenizer( texts, truncation=True, padding='max_length', max_length=max_len, return_tensors='pt' ) if labels is not None: enc['labels'] = torch.tensor(labels) return enc

逻辑说明:num_labels必须和实际类别数一致,改错会导致分类头维度不匹配。max_length=512是 RoBERTa 的上限,新闻文本通常不会超,但如果你的数据里有长文,截断策略就要考虑是取头部还是头尾拼接。padding='max_length'会统一补齐到 512,显存占用固定但浪费较多;如果显存紧张,可以改成动态 padding,在 DataLoader 的 collate_fn 里处理。

训练时的几个参数需要重点关注:

参数常见取值说明
learning_rate1e-5 ~ 3e-5large 模型用更小的学习率,避免灾难性遗忘
batch_size8 ~ 16受显存限制,large 模型 512 长度下通常只能到 8
epochs3 ~ 5太多会过拟合,看验证集早停
warmup_ratio0.1前 10% 步数做学习率预热
weight_decay0.01正则化,防止过拟合

这些值不是固定的,但如果你完全不知道从哪开始,就按这个表设。跑完第一轮看验证集 loss 和 F1,再决定要不要调。

3.3 从训练到提交的完整链路

训练完成后,推理和生成提交文件的步骤不能漏:

model.eval() predictions = [] with torch.no_grad(): for batch in test_loader: input_ids = batch['input_ids'].to(device) attention_mask = batch['attention_mask'].to(device) outputs = model(input_ids, attention_mask=attention_mask) preds = torch.argmax(outputs.logits, dim=-1) predictions.extend(preds.cpu().numpy()) # 生成提交文件 submit = pd.read_csv('submit_example.csv') submit.iloc[:, -1] = predictions submit.to_csv('submit.csv', index=False)

逻辑说明:model.eval()torch.no_grad()必须同时用,前者关闭 dropout 和 batch norm 的训练行为,后者省显存。torch.argmax取 logits 最大值对应的类别索引。写入提交文件时,用iloc[:, -1]定位最后一列,避免列名不一致导致覆盖错列。index=False防止多出一列索引,很多评测系统对格式敏感,多一列直接判错。

注意:如果提交示例的预测列不是最后一列,或者列名有特殊要求,一定要按示例来,不要自己想当然。

4. 避坑与排查:那些跑通之前一定会遇到的事

4.1 显存不够导致训练中断

现象:跑 large 模型时 CUDA out of memory,batch_size 降到 1 还是报错。 原因:512 长度下 large 模型的参数量和中间激活值占用很大,加上 last3embedding 这类操作会额外保留多层输出。 解决:先把 max_length 降到 256 试跑,确认能跑通后再逐步加长;开启梯度累积模拟大 batch;或者改用 base 模型先验证流程,最后再换 large。

4.2 标签对不齐导致准确率异常

现象:训练 loss 正常下降,但验证集准确率始终在随机水平附近。 原因:原文和标签文件合并时行序错乱,或者标签编码和模型输出类别数不匹配。 解决:合并后打印前几行人工核对;用label.unique()确认标签取值范围;检查num_labels是否等于实际类别数。

4.3 提交文件格式被拒

现象:本地验证 F1 很高,提交后系统报格式错误或分数极低。 原因:提交文件的列名、列顺序、索引列与示例不一致,或者预测值用了概率而非类别。 解决:严格按submit_example.csv的结构生成,列名不改、不加索引列、预测值用整数类别。生成后先和示例做一次dtypesshape对比。

4.4 notebook 路径写死导致换机跑不通

现象:在自己电脑上能跑,换到服务器或同学机器上就报文件找不到。 原因:notebook 里用了绝对路径或相对路径依赖当前工作目录。 解决:统一用相对路径,并把所有数据文件放在 notebook 同级目录下;或者在开头定义一个DATA_DIR变量,后面所有路径都基于它拼接。

4.5 随机种子未固定导致结果不可复现

现象:同样的代码跑两次,结果不一样。 原因:PyTorch、numpy、python 的随机种子没有固定,数据划分和初始化权重每次都在变。 解决:在 notebook 最前面统一设置种子:

import random import numpy as np import torch def set_seed(seed=42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) torch.backends.cudnn.deterministic = True torch.backends.cudnn.benchmark = False set_seed(42)

逻辑说明:cudnn.deterministic=True会让 cuDNN 选择确定性算法,牺牲一点速度换可复现性。benchmark=False关闭自动调优,避免不同运行之间算法选择不同。这两个设置在做课设需要交报告、贴结果的时候特别重要。

5. 进阶技巧:把这份课设包变成你自己的项目

5.1 用不同池化策略做对比实验

如果你想让课设报告更有说服力,最直接的办法是把 cls、last2、last3 三种策略跑一遍,用同一份数据、同一个随机种子,只改特征提取部分,然后列一张对比表。常见做法是在模型外面包一层,把 hidden states 取出来自己处理:

from transformers import BertModel import torch.nn as nn class CustomClassifier(nn.Module): def __init__(self, model_name, num_labels, pool_type='cls'): super().__init__() self.bert = BertModel.from_pretrained(model_name, output_hidden_states=True) self.pool_type = pool_type self.classifier = nn.Linear(self.bert.config.hidden_size, num_labels) def forward(self, input_ids, attention_mask): outputs = self.bert(input_ids, attention_mask=attention_mask) hidden_states = outputs.hidden_states # tuple of (num_layers+1) tensors if self.pool_type == 'cls': pooled = outputs.last_hidden_state[:, 0, :] elif self.pool_type == 'last2': pooled = torch.mean(torch.stack(hidden_states[-2:]), dim=0)[:, 0, :] elif self.pool_type == 'last3': pooled = torch.mean(torch.stack(hidden_states[-3:]), dim=0)[:, 0, :] logits = self.classifier(pooled) return logits

逻辑说明:output_hidden_states=True让模型返回所有层的隐层输出。hidden_states[-2:]取最后两层,torch.stack后沿维度 0 求平均,再取 [CLS] 位置。这样改的好处是三种策略共用同一套训练代码,只改一个参数就能切换,对比实验的变量控制得干净。参数上注意hidden_size对 large 模型是 1024,base 是 768,换模型时分类头会自动适配。

5.2 标签替换策略的验证方法

文件名里带replacement的那个 notebook,从命名看涉及标签替换。这类操作通常是为了处理标签噪声或类别不平衡。验证它是否有效的方法很简单:在验证集上分别跑替换前和替换后的模型,看混淆矩阵的变化。如果替换后少数类的召回率明显提升且整体 F1 没掉,说明策略有效;如果整体 F1 下降,说明替换引入了新的噪声。

我一般会固定一个验证集不参与训练,每次改动只在这上面评估。这样即使训练集做了标签替换,验证集始终是干净的,对比才有意义。

5.3 从课设到可展示项目的最后一步

课设包能跑通只是起点。如果你想让这个项目在答辩或简历里拿得出手,建议做三件事:第一,把训练过程的关键指标(loss 曲线、F1 变化)用 matplotlib 画出来存成图;第二,把推理封装成一个函数,输入一段新闻文本直接输出情感类别和置信度;第三,写一个简短的 README,说明数据来源、模型选择理由和最终指标。这三件事花不了多少时间,但能让别人在三十秒内看懂你做了什么、效果如何。

从那以后我每次拿到这类课设包,都强制自己先跑通 baseline,再动任何改动的念头。因为只有 baseline 跑通了,后面所有的对比和优化才有参照系。希望帮到你。

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

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

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

立即咨询