简介:基于BERT的中文情感分类实验项目,面向自然语言处理初学者与研究者,系统展示了从预训练数据处理、模型构建到中文情感分类训练与预测的完整流程。资源包共22个文件,压缩后4.87MB,主体为11个Python脚本,分别承担建模、分类、分词、预训练数据生成与特征提取等关键环节;另有CSV数据文件、Shell运行脚本、文本说明与Markdown文档,其中CSV提供训练与评估数据,Shell脚本方便一键复现,文档则记录实验设计与结果。已有312人学习下载。通过这套源码,读者可以掌握BERT在中文文本分类任务中的实际用法,包括如何准备中文语料、完成分词与特征提取、训练分类模型并对新样本进行预测;文档与数据文件同时提供了实验记录和评估依据,适合作为课程设计、毕业设计或入门实践的参考模板。
1. 基于BERT的中文情感分类实验:从跑通到调优,一篇讲透
情感分类是中文自然语言处理里最常被拉出来做实验的任务,而BERT几乎成了这类实验的默认起点。很多人以为加载一个预训练模型、接上分类头、跑几个epoch就算“基于BERT的中文情感分类实验设计与源码实现”完成了,结果一验证发现准确率卡在80%上下不去,或者训练到一半显存爆掉、loss变成NaN。这篇笔记会顺着实验设计的完整链路走一遍:为什么选BERT而不是传统词向量、数据集怎么处理、源码怎么写、参数怎么调、哪些坑必须躲开。适合刚把深度学习框架装好、准备拿中文情感分类当第一个实战项目的人,也适合已经跑通过基础模型、想搞清楚边界和优化方向的从业者。目标是让你看完之后能照着复现,并且知道每行代码、每个超参数在干什么。
2. BERT中文情感分类的实验设计:先搞清楚模型在学什么
2.1 为什么中文情感分类总是先想到BERT
中文文本不像英文那样天然有空格分词,传统做法要先分词、去停用词、构造词向量,整个pipeline里的每一步都在损失信息。BERT通过子词(WordPiece)切分,配合预训练阶段的掩码语言模型和下一句预测,把中文的上下文语义提前编码进了参数里。做情感分类时,我们不需要自己训练词向量,只需要在BERT最后一层接一个分类头,微调整个模型就行。
这里有个关键认知:微调不是“用BERT提取特征然后训练分类器”,而是让BERT自身的参数也继续更新。很多初版实验把BERT的输出当成固定的特征向量,只训练后面的全连接层,效果会明显差一截。原因很简单——预训练模型学到的是通用语义,而情感分类任务需要激活特定方向的语义表征,比如“绝了”在“这电影绝了”里是褒义,在“这服务态度绝了”里是嘲讽。只有让BERT的参数在情感语料上继续反向传播,才能把这种语境差异压进模型里。
另一个设计要点是输入格式。BERT的中文输入要拼成[CLS] + 正文 + [SEP],其中[CLS]位置的输出向量会被当作整句的语义表示。实验设计时不要把整个数据集一股脑塞进去,要先确定最大序列长度。中文BERT默认支持512个token,但情感分类样本通常都是短文本,设成128或256既能保住信息,又能大幅降低显存占用和训练时间。我一般先统计训练集的长度分布,取95分位点作为max_len,避免为了个别超长样本拉高全量计算成本。
2.2 情感分类的标签体系与评估口径
中文情感分类实验里,标签体系直接决定模型结构。最简单的二分类(正向/负向)只需要一个线性层加sigmoid;三分类(正向/负向/中性)或者五分类(1到5星)则需要softmax。设计实验前要问清楚:业务上到底需要几个类别?中性的定义是什么?如果数据集本身是从评分映射来的,1到2分是负向、3分是中性、4到5分是正向,那就得注意类别不平衡问题——大部分点评数据都偏向正向,负样本占比可能不到20%。
评估口径上,只看准确率远远不够。我习惯同时输出精确率、召回率和F1分数,并且按类别分开看。比如“正向”类F1很高、“负向”类F1很低,就说明模型被中性或正向样本带偏了。这会直接影响源码实现里的评估函数写法:不能只算(pred == label).mean(),要用sklearn的classification_report或者自己逐类统计混淆矩阵。实验报告里如果只贴一个acc,答辩或业务评审时大概率会被追问到说不出话。
另外还要注意数据划分的随机种子。中文情感分类数据集经常出现同一条评论的重复样本,或者同一用户的多条评论。按文件顺序直接切分会把重复文本同时分进训练集和测试集,导致测试分数虚高。我在源码里会用train_test_split(..., shuffle=True, random_state=42),并在划分前按文本内容做去重。如果做严肃实验,最好按用户ID或评论ID分组划分,但这要看数据集里有没有这类字段。
2.3 源码实现的整体模块划分
实验源码不能只写一个训练脚本,至少要拆成配置、数据、模型、训练、评估、预测这几个模块。这不是为了“软件工程”,而是为了debug方便。BERT的源码实现里,最容易出问题的反而不是模型结构,而是数据加载的细节:tokenizer的padding方式、attention_mask有没有传、label的dtype对不对。拆分模块后,你能单独验证DataLoader输出是不是符合预期,再进训练环节。
常见做法是把超参数集中到一个config.py或者用argparse接收。BERT的源码实现里需要的参数包括:预训练模型路径、max_len、batch_size、learning_rate、epoch数、warmup比例、weight_decay、dropout、输出目录等。把这些写死在训练脚本里的做法很糟糕——每次调参都要改代码,很容易改坏。我一般会用dataclass写一个Config类,并支持从命令行覆盖,这样跑实验组时只需改启动命令。
3. 数据准备与预处理:把中文文本变成BERT能吃的张量
3.1 加载数据集与类别映射
以最常见的中文情感分类数据集为例,通常是一个CSV文件,包含text和label两列。第一步先把数据读进来,做基本清洗:去掉空行、去掉全空白文本、统一前后空格。注意不要过度清洗——不要在这里去标点、去停用词,BERT不是词袋模型,标点和语气词(“啊”“呢”“哈哈”)都可能携带情感信息。
import pandas as pd from sklearn.model_selection import train_test_split df = pd.read_csv("sentiment.csv", encoding="utf-8") df = df.dropna(subset=["text", "label"]) df["text"] = df["text"].astype(str).str.strip() df = df[df["text"] != ""].reset_index(drop=True) label2id = {"negative": 0, "neutral": 1, "positive": 2} df["label_id"] = df["label"].map(label2id) df = df.dropna(subset=["label_id"]).reset_index(drop=True) train_df, valid_df = train_test_split( df, test_size=0.15, stratify=df["label_id"], random_state=42 ) print(train_df["label_id"].value_counts())这段代码里最关键的是stratify=df["label_id"],它保证划分后训练集和验证集的类别比例一致。如果数据集类别分布是60%正向、20%中性、20%负向,不做分层抽样的话,小概率会把负向样本全分进训练集,验证集里就没有负向了。label2id的映射要写死,不能依赖DataFrame里的顺序,否则模型输出01和真实的类别对齐会错位。
3.2 分词与编码:tokenizer的正确用法
HuggingFace的transformers库提供了现成的中文BERT tokenizer。加载时要注意模型权重类型:bert-base-chinese是字级别的,而bert-base-multilingual-cased是按多语言子词切分的。我见过有人混用权重和tokenizer,导致max_len内塞不下、对齐错乱。源码实现里必须确保模型和tokenizer来自同一个预训练目录。
from transformers import BertTokenizer tokenizer = BertTokenizer.from_pretrained("bert-base-chinese") max_len = 128 def encode_text(text): encoded = tokenizer( text, max_length=max_len, truncation=True, padding="max_length", return_tensors="pt", ) return encoded sample = encode_text("这家店的服务态度非常好,上菜也快。") print(tokenizer.convert_ids_to_tokens(sample["input_ids"][0]))这里padding="max_length"会把所有样本都pad到128的长度,生成一个固定形状的批次,方便矩阵运算。truncation=True保证超长文本被截断。输出里有三个键:input_ids、token_type_ids、attention_mask。attention_mask会在self-attention计算中把padding位置设为0,源码里必须把它传给模型,否则模型会把[PAD]位置当真词参与语义计算。
3.3 构建PyTorch Dataset与DataLoader
把编码后的结果封装成Dataset类,这是BERT源码实现里最容易出错的地方之一:Dataset返回的是tokenizer编码好的字典加上label,DataLoader在收集batch时要求所有样本的张量形状一致,所以必须在编码阶段就统一max_len。另一个坑是return_tensors="pt"会让每个样本都带一个batch维度,DataLoader又会在前面加一维,最终变成三维张量。解决办法是在__getitem__里用torch.squeeze()去掉单例维度。
import torch from torch.utils.data import Dataset, DataLoader class SentimentDataset(Dataset): def __init__(self, texts, labels, tokenizer, max_len): self.texts = list(texts) self.labels = list(labels) self.tokenizer = tokenizer self.max_len = max_len def __len__(self): return len(self.texts) def __getitem__(self, idx): text = self.texts[idx] label = self.labels[idx] encoded = self.tokenizer( text, max_length=self.max_len, truncation=True, padding="max_length", return_tensors="pt", ) item = { "input_ids": encoded["input_ids"][0], "token_type_ids": encoded["token_type_ids"][0], "attention_mask": encoded["attention_mask"][0], "labels": torch.tensor(label, dtype=torch.long), } return item train_dataset = SentimentDataset( train_df["text"], train_df["label_id"], tokenizer, max_len ) train_loader = DataLoader( train_dataset, batch_size=16, shuffle=True, num_workers=0 )num_workers=0在Windows和macOS上最稳,避免多进程序列化报错。如果机器配置高,Linux上可以设成4或8,但要注意Python代码里Dataset构造函数传入了tokenizer对象,多进程时会重复加载。我通常把tokenizer初始化放在__init__里,每个worker进程都会继承一份,内存占用会翻几倍,所以数据集小就别开多进程。
4. 模型源码实现与训练:从加载BERT到输出分类结果
4.1 加载预训练模型并接分类头
transformers里最常用的是BertForSequenceClassification,它已经内置了一个分类头,直接输出每个类别的logits。这个类会加载BERT主干参数,并在[CLS]输出上接一个Dropout加线性层。用的时候只需要指定num_labels。但要注意,BertForSequenceClassification默认会随机初始化分类头权重,而BERT主干是从预训练权重加载的。如果忘记加载预训练权重,训练几千步也无法收敛。
from transformers import BertForSequenceClassification model = BertForSequenceClassification.from_pretrained( "bert-base-chinese", num_labels=3, ) # 如果你想修改dropout,可以调整config from transformers import BertConfig config = BertConfig.from_pretrained("bert-base-chinese", num_labels=3, hidden_dropout_prob=0.1) model = BertForSequenceClassification.from_pretrained("bert-base-chinese", config=config)自定义分类头的情况也有。有些人想用两层全连接加Tanh激活,或者想在分类之前把后几层BERT的特征拼接起来。常见做法是加载BertModel,取last_hidden_state[:, 0]作为[CLS]向量,然后过自己的分类器。但这样做需要额外处理池化、Dropout位置、梯度裁剪等细节,对新人容易翻车。我一般建议先用官方封装好的BertForSequenceClassification跑通基线,再考虑自定义结构。
4.2 优化器、学习率与warmup设置
BERT微调的优化器跟普通分类网络不一样。最核心的是学习率要小,常见取值范围是2e-5到5e-5,AdamW是默认选择。第二个关键是warmup:微调的前几百步用很小的学习率,让模型从预训练状态平稳过渡到任务状态,避免一开始就大幅改动BERT参数。HuggingFace提供了get_linear_schedule_with_warmup,它会在warmup阶段线性上升,之后线性衰减到0。
from transformers import AdamW, get_linear_schedule_with_warmup import torch.optim as optim optimizer = AdamW(model.parameters(), lr=3e-5, weight_decay=0.01) num_epochs = 5 total_steps = len(train_loader) * num_epochs warmup_steps = int(0.1 * total_steps) scheduler = get_linear_schedule_with_warmup( optimizer, num_warmup_steps=warmup_steps, num_training_steps=total_steps, )weight_decay=0.01是常见设置,但注意它不要对bias和LayerNorm参数生效,否则会破坏预训练好的归一化统计量。HuggingFace的AdamW允许传入correct_bias=False。经验上,直接在模型中筛选出需要weight decay的参数是更稳妥的写法,不过对情感分类这种小任务,weight_decay=0.01加上no_decay名单的差别不是特别大。学习率的选择要看batch size——如果显存只能塞下8个样本,学习率可以相应调低到2e-5。
4.3 训练循环与梯度累积
训练循环本身并不复杂,但有三个细节必须处理好。第一,模型要.train()模式,BERT内部的Dropout才会生效;第二,每次optimizer.zero_grad()避免梯度累加;第三,loss是CrossEntropyLoss,BertForSequenceClassification在前向传播时传入labels参数会直接返回loss,不用自己再算。如果想要梯度累积来模拟更大的batch,需要在累积步数达到后手动optimizer.step()和scheduler.step()。
from tqdm import tqdm device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model.to(device) accumulation_steps = 2 # 实际生效batch = batch_size * accumulation_steps for epoch in range(num_epochs): model.train() total_loss = 0 progress_bar = tqdm(train_loader, desc=f"Epoch {epoch + 1}") for step, batch in enumerate(progress_bar): input_ids = batch["input_ids"].to(device) attention_mask = batch["attention_mask"].to(device) labels = batch["labels"].to(device) token_type_ids = batch["token_type_ids"].to(device) outputs = model( input_ids=input_ids, attention_mask=attention_mask, token_type_ids=token_type_ids, labels=labels, ) loss = outputs.loss loss = loss / accumulation_steps loss.backward() if (step + 1) % accumulation_steps == 0: torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) optimizer.step() scheduler.step() optimizer.zero_grad() total_loss += loss.item() * accumulation_steps progress_bar.set_postfix({"loss": loss.item() * accumulation_steps})这段代码里梯度裁剪clip_grad_norm_是BERT微调里少不了的保险丝。情感分类的loss曲面有时会出现极端梯度,不裁剪的话,一两个批量就能把预训练权重冲飞,后面loss变成NaN。max_norm=1.0是常用值,显存紧张或学习率调大时需要相应收紧。另外注意token_type_ids要传,虽然bert-base-chinese只有一句输入时传不传影响不大,但保留这个参数能让代码兼容更多场景。
4.4 验证与保存:别只看训练loss
每轮epoch结束后要在验证集上评估。验证时模型要切换到.eval()模式,并且用torch.no_grad()关闭梯度记录,否则不仅慢,还会在推理时保留计算图,显存很快爆掉。评估指标应该包含每个类别的准确率、召回率、F1。保存模型时建议同时保存权重和tokenizer,这样后续加载预测不需要重新找到原本的tokenizer路径。
from sklearn.metrics import accuracy_score, f1_score, classification_report import numpy as np def evaluate(model, dataloader, device): model.eval() all_preds = [] all_labels = [] with torch.no_grad(): for batch in dataloader: input_ids = batch["input_ids"].to(device) attention_mask = batch["attention_mask"].to(device) labels = batch["labels"].to(device) outputs = model(input_ids=input_ids, attention_mask=attention_mask) logits = outputs.logits preds = torch.argmax(logits, dim=-1).cpu().numpy() all_preds.extend(preds) all_labels.extend(labels.cpu().numpy()) print(classification_report(all_labels, all_preds, digits=4)) return accuracy_score(all_labels, all_preds), f1_score(all_labels, all_preds, average="macro")保存模型时,我习惯把整个model和tokenizer都保存到同一个目录,方便部署复现:
save_dir = "bert_sentiment_model" model.save_pretrained(save_dir) tokenizer.save_pretrained(save_dir) torch.save(config.__dict__, f"{save_dir}/config_args.json")这里config是实验配置的dataclass,保存下来是为了记录当时用的max_len、标签映射等。很多实验翻车都是因为三个月后加载模型,忘了当初标签顺序,预测结果和业务线完全对不上。
5. 避坑与常见问题排查:BERT情感分类的8个血泪经验
5.1 训练loss不降或忽高忽低
现象:训练到第2个epoch,loss还在1.0附近波动,验证集准确率只有50%多,跟随机猜测差不多。
原因:最常见的三个原因——学习率太大导致参数震荡;warmup比例没设置,模型一上来就用大步长乱跑;数据标签映射错了,比如0和1对调,模型学到的恰好是相反语义。
解决:先把学习率降到2e-5,warmup占比调到0.1。再检查label2id:打印train_df.head()和train_loader里第一个batch的labels,手工确认某条明显正面评论的标签是2而不是0。我踩过最蠢的坑是CSV里的label列是字符串“positive”,直接astype(int)报错后改成map,但map时拼错了键名,导致大部分样本变成NaN后被drop,剩下一小撮样本硬生生训出一个“假模型”。
5.2 显存溢出(CUDA out of memory)
现象:训练刚开始,输出CUDA out of memory,代码崩在第几个batch处。
原因:batch_size太大,序列长度设成512,并且没有关闭验证阶段的梯度记录。BERT的显存占用跟序列长度近似线性,跟batch size也近似线性,两者一起涨很容易爆。
解决:把max_len从512降到128,batch_size从32降到16,并确认evaluate()里用了torch.no_grad()。如果还想加大batch,开显存优化,用torch.utils.checkpoint对BERT的Encoder层做梯度检查点,用时间换显存。另外检查代码里是否在循环内创建了model.to(device),那会保留一份CPU模型的额外开销。
5.3 验证时输入padding导致的结果偏移
现象:训练loss正常下降,但验证集F1比训练集低好几个点;单独推理单条文本时,结果跟验证集表现不一致。
原因:训练集的DataLoader默认shuffle=True,但验证集的DataLoader有时忘了设shuffle=False,模型在epoch内多次看到同一批验证样本的顺序变化,影响BatchNorm统计量。另外如果源码里在tokenizer处对训练和验证用了不同的padding参数,比如训练时截断、验证时不截断,长度不齐的情况下模型仍能跑因BertForSequenceClassification内部会自动padding,但attention_mask传错位置,就会导致结果异常。
解决:保证训练和验证的tokenizer参数完全一致,都传相同的max_len、truncation=True、padding="max_length"。验证集务必shuffle=False。排查时直接单步打印验证集第一个样品的input_ids和attention_mask,确认padding位置是0而不是1。
5.4 加载保存的模型时报键名不匹配
现象:from_pretrained加载训练好的模型时,报Error(s) in loading state_dict,而且键名差一个前缀module.或bert.。
原因:训练时用了DataParallel或多卡训练,保存下来的权重带module.前缀;或者加载的是BertModel,但保存的是BertForSequenceClassification,分类头的权重缺失。混合使用bert-base-chinese的tokenizer和bert-base-multilingual-cased的权重也会出现embedding维度不一致。
解决:训练时单卡就用torch.save(model.state_dict()),加载时保证类定义和保存时完全一致。如果已经保存了module.开头的文件,可以写一小段脚本把key名的前缀去掉:
from collections import OrderedDict state_dict = torch.load("model_state.pt") new_state_dict = OrderedDict() for k, v in state_dict.items(): new_key = k.replace("module.", "") if k.startswith("module.") else k new_state_dict[new_key] = v model.load_state_dict(new_state_dict)5.5 warmup步数超过总步数
现象:训练结束也没到达预设的完整训练步数,但scheduler报错或学习率一直很低,收敛慢。
原因:warmup_steps设为总的训练步数甚至更多,例如warmup_steps = len(train_loader) * num_epochs,就代表整个训练过程都在warmup,模型永远到达不了预设的学习率峰值。
解决:warmup步数一般取总步数的5%到10%。如果总步数才几百,就直接设成几十步。在打印scheduler当前学习率时确认峰值是否合理:
from transformers import get_linear_schedule_with_warmup total_steps = len(train_loader) * num_epochs warmup_steps = int(0.1 * total_steps) print(f"total_steps={total_steps}, warmup_steps={warmup_steps}")5.6 分类头随机初始化导致预测全偏向某一类
现象:所有验证样本都被预测成标签数量最多的那一类,比如全部预测为“正向”,precision为1.0但recall为0。
原因:一方面是类别不平衡,模型学到的最优策略就是把每个样本都判为多数类;另一方面,BertForSequenceClassification的分类头是随机的,如果训练不足,这个线性层没有学到有效的特征映射。
解决:要么做类别加权,把CrossEntropyLoss的weight参数按照类别频率的反比设置;要么干脆先做欠采样/过采样。对情感分类这种任务,我用过最省事的方法是class_weight传给模型:
class_weights = torch.tensor([1.0, 2.0, 1.5]).to(device) model = BertForSequenceClassification.from_pretrained( "bert-base-chinese", num_labels=3 ) # 训练时手动计算loss loss_fct = torch.nn.CrossEntropyLoss(weight=class_weights) logits = model(...).logits loss = loss_fct(logits.view(-1, 3), labels.view(-1))5.7 中文分字还是分词的影响
现象:同样一个文本,用bert-base-chinese和bert-base-multilingual-cased训练出来的效果相差5个百分点以上。
原因:bert-base-chinese直接用单个汉字作为最小单位,覆盖的词汇表是中文常用字的组合;而多语言版本按子词切分,中文字符可能会被拆成Unicode字节对,导致“点赞”“拉黑”这些词根本没法被完整编码。中文场景下优先选择bert-base-chinese,而不是英文BERT或多语言BERT。
解决:用BertTokenizer.from_pretrained("bert-base-chinese"),别追新。如果你手里的预处理模型是RoBERTa-wwm-ext或者Bert-wwm-ext这类中文全词掩码版本,tokenizer要对应使用BertTokenizer.from_pretrained("hfl/chinese-roberta-wwm-ext"),不要混用。
5.8 用pandas读完CSV后,标签变成float导致模型报错
现象:加载模型后前向传播正常,但计算loss时报错,类型不匹配。
原因:CSV里的标签列有缺失值,pandas会把整列读成float64,比如0.0、1.0。传给CrossEntropyLoss要求是long,直接转torch.long后缺失值会变成巨大的整数,导致分类类别越界。
解决:在数据清洗时加一行df["label_id"] = df["label_id"].astype(int),并且先丢弃空值。遇到不信任的数据集时,先打印df.dtypes,看到float64就该警惕。
6. 进阶:三招提升中文情感分类实战效果
先讲第一招:把阈值调参当成实验的一部分,不要默认argmax就是最优策略。BERT输出的logits经过softmax后,三个类别的概率分布往往不够尖锐,中性类概率普遍偏高。如果业务只关心“正向/负向”二分类,可以把中性类概率阈值从0.33上调到0.4,低于0.4的不给判断,这样虽然覆盖率降低,但准确率往往能提升两到三个点。验证方法是在验证集上遍历阈值,画一条准确率-覆盖率曲线,找到业务能接受的拐点。有人觉得阈值拍脑袋不科学,但这在线上系统里就是最直接的“后悔药”。
第二招是情感分类的数据增强,尤其适合数据量少于3万条的中文评论。不要用随机替换同义词这种会破坏语义的方式,BERT本身对语序敏感,替换后可能变成另一句话。我验证过有效的方式是回译——把中文翻译成英文再翻回来,但质量受在线翻译API影响。更稳妥的是在训练阶段对[MASK]做对抗式扰动,或在输入端随机遮挡5%的词元,让模型不过分依赖某些情感词。不过做增强时一定要保持标签不变,并且只在训练集上增强,验证集保持原始文本。
第三招是分层学习率。BERT底层学到的是通用语法,上层学到的是任务相关语义,微调时给顶层更大的学习率,底层更小的学习率,能减少灾难性遗忘。实现起来很简单,先拿到BERT的编码器参数分组:
no_decay = ["bias", "LayerNorm.weight", "LayerNorm.bias"] optimizer_grouped_parameters = [ { "params": [p for n, p in model.named_parameters() if not any(nd in n for nd in no_decay) and "encoder.layer.11" not in n], "weight_decay": 0.01, "lr": 2e-5, }, { "params": [p for n, p in model.named_parameters() if any(nd in n for nd in no_decay) and "encoder.layer.11" not in n], "weight_decay": 0.0, "lr": 2e-5, }, { "params": [p for n, p in model.named_parameters() if "encoder.layer.11" in n or "classifier" in n], "weight_decay": 0.01, "lr": 5e-5, }, ]这个写法是把最后一层Transformer和分类头单独拎出来,用5e-5的大学习率,其余层用2e-5。实测在小数据集上,这样调能比统一学习率快一到两个epoch收敛,F1有时能涨0.5到1个百分点。代价就是代码变复杂,需要自己写优化器分组。
验证阶段我还有个习惯:保留每次epoch结束后的验证集预测结果,把真实标签和预测概率存成CSV,最后对比每个epoch的错分样本。你会发现有些样本整个训练过程都错,比如“这家店环境不错但服务太差”,这种同时含正负情绪的句子,就连人判断都有分歧。这时候不要去强行拟合它,而是要思考是不是标签标错了,或者需要调整到更细的类别体系。模型进步的证据,应该是错误集中在真正模糊的样本上,而不是犯低级错误——把“太差了”预测成正向。
整个实验做完,建议你按这个顺序验收:先跑完一个epoch,用20条手工标注样本做预测,确认模型基本能分对;再看验证集分类报告,确认每个类别的F1都明显超过随机水平;最后打开预测错误的样本,判断是数据问题还是模型问题。情感分类没有银弹,BERT给了我们一个极强的起点,但真正决定效果上限的,还是你对待数据、源码细节和评估口径的态度。这些坑我都踩过一遍,希望帮到你少走点弯路。
本文还有配套的精品资源,点击获取