☰
BERT情感分类实战:从环境搭建到模型训练的完整NLP流程
2026/10/14 4:59:36 网站建设 项目流程

BERT 已经发布好几年了,但现在回头看,它依然是绝大多数 NLP 入门和落地项目绕不开的基石。这个系列我打算从实际用例出发,带着大家一起用深度学习框架和 Python 把 BERT 用起来,而不是停留在“知道原理”的层面。

这篇是第一篇,核心目标很简单:跑通一个完整的 NLP 任务流程,选择情感分类作为切入点,同时把环境搭建、模型加载、数据预处理、训练评估这些环节一次讲透。内容会覆盖项目整体设计思路、开发环境准备、BERT 关键概念的手把手拆解、代码实现细节,以及我实际踩过的坑和排查方法。

不论你是刚接触 NLP 的学生、想转行做算法工程师的开发者,还是在工作中需要快速验证一个文本分类需求的测试人员,这套系列都可以帮你少走弯路。跟着做完第一篇,你至少能独立完成一个基于 BERT 的情感分类 demo,并且对后续迁移到其他任务有清晰的思路。

1. 内容整体设计与思路拆解

1.1 为什么第一篇选择情感分类作为入门任务

每个 NLP 项目落地前都要回答一个问题:这个任务能不能用预训练模型直接撑起来?我之所以把情感分类放在系列第一篇,是因为它具备三个非常适合入门的特性。

一是数据获取门槛低。情感分类的公开数据集非常丰富,无论是中文还是英文,都能找到大量带标签的语料。自己标注也不难,找几百条评论,打上“正向”和“负向”标签,就能支撑一个演示级别的项目。相比机器翻译、问答系统这些需要复杂标注体系的任务,情感分类的标注成本几乎可以忽略。

二是任务定义清晰。输入是一段文本,输出是一个类别。这种“文本到标签”的映射天然适合用 BERT 的序列分类能力来建模。模型的输出层只需要一个分类头,改动成本低,出了问题时排查链路也短。

三是评估指标好解释。准确率、F1 分数这些指标直接反映模型效果,不需要额外设计复杂的评价协议。这对我后续验证代码是否有 bug 有很大帮助——如果训练曲线一直不收敛,至少说明数据或者模型配置有问题,而不是评估方法本身有问题。

另一个原因是我希望这个系列先建立一条完整的技术链路,而情感分类恰好能覆盖链路的所有关键节点,包括数据加载、Tokenizer 处理、模型前向传播、损失计算、训练循环和结果保存。链路通了,后续无论换成命名实体识别还是文本相似度计算,都只是在局部模块上做替换。

1.2 技术选型背后的几个关键决策

在动手之前,我花了些时间确定整套方案的选型,因为选型直接决定了代码写法、调试方式,甚至部署难度。这里有几组决策值得展开说。

第一组是深度学习框架的选择。现在主流的选择是 PyTorch 和 TensorFlow,两者的生态都已经很成熟。我的建议很直接:新手优先考虑 PyTorch。原因不是 TensorFlow 不行,而是 PyTorch 的调试方式对我更友好,模型定义更接近 Python 原生风格,遇到问题可以直接用 Python 调试器定位。在跑 BERT 相关的开源项目时,大部分最新模型权重和示例代码都优先发布 PyTorch 版本,这样能减少很多“框架版本不匹配”的额外成本。

第二组决策是是否使用 Hugging Face Transformers 库。这个决定几乎不需要犹豫,直接用。Transformers 库封装了模型架构、Tokenizer 和预训练权重下载逻辑,如果不用它,我可能需要花费大量时间去实现注意力机制、处理词表映射和加载权重这些细节。而这些工作除了能帮助你复习模型结构,对完成实际任务并没有直接帮助。用 Transformers 库可以让整个实现的代码量缩小一个数量级,我后面会详细列出代码,你会发现核心逻辑其实非常短。

第三组决策是关于如何处理中文还是英文数据。我在这篇里选择使用中文数据作为演示对象,因为中文文本的分类任务在预处理上更贴近国内开发者的实际工作场景。当然,整个流程迁移到英文语料几乎是零成本的,只需要更换数据集和分词器名称。

1.3 项目整体流程的逐步规划

结构上我倾向于把整个项目拆成五个阶段,每个阶段对应一个明确的产品或验证目标。

阶段一是环境准备,包括 Python 虚拟环境、深度学习框架、CUDA 环境检查。目标是写出第一个可以加载预训练模型的脚本。

阶段二是数据获取与分析,目标是理解数据集分布,发现类别不均衡之类的潜在问题。

阶段三是预处理链路搭建,包括分词、编码、padding、attention mask 的构造,目标是让数据成为模型能够直接消费的张量格式。

阶段四是模型搭建与训练,包括加载预训练 BERT、定义分类头、设置优化器和学习率调度器,并完成训练循环。

阶段五是评估与错误分析,目标是不仅看到准确率数字,还能定位模型预测错误的文本模式,寻找改进方向。

这五个阶段的划分其实也是我多年养成的习惯:永远先保证一条最简链路能够跑通,再考虑优化效果。很多人一上来就追求高精确率,但实际遇到的情况往往是数据预处理环节就出错了,模型训练得再久也没有意义。先跑通,再调优,能省下大量排查问题的时间。

2. 开发环境搭建与依赖准备

2.1 Python 虚拟环境与基础工具链

无论你之前是否有过被依赖地狱折磨的经历,我都强烈建议在开始任何深度学习项目前先隔离开 Python 环境。计算机专业的同学可能有印象,当你同时维护多个项目时,不同项目之间对包版本的要求经常相互冲突。直接用全局环境的后果就是:A 项目需要 pandas 1.x,B 项目锁死 pandas 0.25,你每次切换项目都要重新安装,非常痛苦。

我推荐使用 conda 创建独立的虚拟环境,当然虚拟环境工具不唯一,virtualenv 或者 venv 都可以。关键在于隔离。

创建环境的命令非常简单,在命令行工具里执行这么一句:

conda create -n bert_demo python=3.9

为什么要指定 Python 3.9?因为这是一个经过大量深度学习库验证过的稳定版本,向下兼容性好,向上也不至于太老。激活环境后,后续在该环境中安装的所有包都不会影响系统全局环境。

激活方式:

conda activate bert_demo

接下来我通常会先安装 Jupyter Notebook 或者 Jupyter Lab。虽然写纯 Python 脚本也可以,但对于 NLP 项目来说,交互式环境能极大提升调试效率。你可以直接看到每一段代码的输出,直观检查分词结果、张量形状这些中间产物。现实情况是,我见过很多新人在写模型代码时用 print 一堆变量来调试,效率很低,用 Jupyter 逐个 cell 执行显然清晰很多。

安装指令:

conda install jupyter

2.2 深度学习框架安装与硬件确认

在这篇系列中我以 PyTorch 作为主框架,当然如果你更习惯 TensorFlow 也没有关系,核心思路完全一样。安装 PyTorch 前需要先确认你机器上是否有 NVIDIA 显卡,以及 CUDA 是否可用。这一步容易出问题,但是确实值得重视,因为 CPU 环境跑 BERT 训练会非常慢,如果机器配置一般,体验会受很大影响。

你可以先试试当前机器上有没有可用 GPU,可以运行下面的 Python 代码确认:

import torch print(torch.cuda.is_available())

如果输出为 True,说明 GPU 可用,可以安装 CUDA 版本的 PyTorch。如果输出为 False,也别着急,CPU 也能跑演示项目,只是训练时间会比较长。

安装命令以 PyTorch 官方给出的为准,一般类似:

pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118

这里补充一个我的实操经验:不要盲目追求最新版本的 PyTorch。最新的版本可能同时更新了一些底层行为,某些第三方库可能还没有适配。我通常会选择一个已经发布半年左右的稳定版本,安全性会好很多。

2.3 Transformers 库与数据工具库安装

PyTorch 装好之后,接下来安装本项目的绝对主角:Hugging Face Transformers。这个库一方面提供数以万计的预训练模型权重,另一方面统一封装了模型输入输出的处理逻辑。安装命令:

pip install transformers

同时建议安装它配套的数据集库:

pip install datasets

datasets库的价值不仅仅在于可以方便地下载公开数据集,更在于它提供了一套标准化的数据操作接口。你可以像操作 pandas DataFrame 一样对数据集做过滤、映射、切分,而且它在背后做了内存映射优化,不会因为数据量大而瞬间吃掉内存。

还需要安装的是分词工具。中文任务一般需要 jieba 或者直接使用 BERT 自带的 tokenizer,但有些数据集在构建时需要额外分词处理,所以我也会提前装好:

pip install jieba

所有这些工具都安装完成后,可以通过一个简单的导入测试确认环境正常:

from transformers import BertTokenizer, BertForSequenceClassification print("Transformers import OK")

如果这一句没有报错,说明你的核心依赖已经就绪了。

3. BERT 核心细节解析与实操要点

3.1 搞清楚 BERT 的输入到底是什么

如果说环境搭建是体力活,那么理解 BERT 的输入结构就是整个项目的智力核心。很多初学者跑代码报错,基本都是卡在这一环节:把字符串直接塞给了模型。

BERT 并不是直接吃原始文本。它吃的是三个张量,分别是input_ids、attention_mask和token_type_ids。

input_ids是文本经过 Tokenizer 映射后的数字序列。每个数字代表词表中的某个 token。比如中文里,“我喜欢自然语言处理”经过 BERT 分词后,可能变成['我', '喜', '欢', '自', '然', '语', '言', '处', '理'],然后每个字再映射到对应的数字 ID。需要注意的是 BERT 的分词粒度并不是固定的单词,中文场景下多数是单字或子词,英文场景下则是 WordPiece 子词。

attention_mask是用来标识哪些位置是真实文本、哪些位置是 padding 补零的。因为一个 batch 内的文本长度必须一致,短文本需要 padding 到和最长文本一样长,而 padding 的部分模型不能参与注意力计算,需要靠 attention_mask 把它遮住。

token_type_ids用来区分句子 A 和句子 B,在某些任务比如问答和句子对分类时使用。对于单句分类,这个张量全部是 0 也无妨。

如果还是觉得抽象,你可以这样理解:BERT 是一个阅读者,input_ids是它读到的文字内容,attention_mask告诉它哪些内容是假装存在的,哪些是真的,token_type_ids告诉它哪些字属于第一句话、哪些属于第二句话。三个信号拼在一起,模型才真正理解了输入。

3.2 Tokenizer 的调用方式与关键参数

在实践中,我们根本不需要手动构造这三个张量,而是通过tokenizer一步到位。Hugging Face 的 tokenizer 对象接收原始字符串,返回一个包含所有必要字段的字典。

一个非常典型的调用方式如下:

from transformers import BertTokenizer tokenizer = BertTokenizer.from_pretrained("bert-base-chinese") text = "这家餐厅的菜味道很好" encoded = tokenizer( text, truncation=True, padding="max_length", max_length=128, return_tensors="pt" ) print(encoded.keys()) print(encoded["input_ids"]) print(encoded["attention_mask"])

这段代码里,truncation=True表示超过最大长度的文本会被截断,padding="max_length"表示所有文本都会统一填充到max_length指定的长度,return_tensors="pt"表示返回 PyTorch 张量格式。

关于 max_length 的设置,我多说一句。128 是一个很常见的默认值,可以在效果和计算开销之间取得平衡。如果任务里的文本普遍较长,比如长新闻分类,可以适当调大,但代价是训练速度变慢、显存占用上涨。在文本长度分布比较集中的场景,建议先分析一下数据的长度分布再定值。

3.3 从预训练权重到分类模型的加载方式

BERT 原始模型是语言模型,输出结果不是直接对应分类标签的。要做情感分类,需要在其顶部接一个分类头。Transformers 库把这一步封装到了BertForSequenceClassification这个类里。

加载方式只需要一行代码:

from transformers import BertForSequenceClassification model = BertForSequenceClassification.from_pretrained( "bert-base-chinese", num_labels=2 )

这里的num_labels=2是因为我们的情感分类只有两个类别,正向和负向。加载这个动作背后做的事情是:下载预训练权重,加载 BERT 主体结构,然后在顶部随机初始化一个带 2 个输出维度的线性分类层。

这里有一个关键点:虽然分类头是随机初始化的,但 BERT 主体的权重已经在大规模语料上预训练过。这意味着模型已经具备了很强的文本理解能力,我们只需要用少量标注数据去微调顶部的分类头以及微调 BERT 部分层即可。这个特性是迁移学习的核心价值,也是 BERT 项目能在小数据集上表现不错的原因。

4. 实操过程与核心环节实现

4.1 准备数据集并完成数据探查

在动手写训练代码前,我先准备好了一份电影评论情感分类数据集。格式非常简单,是两列:一列是文本内容,另一列是标签,0 表示负向情绪,1 表示正向情绪。这样的数据放在 CSV 文件里即可。

我会把完整流程放在一个 Notebook 里逐步演示。第一步是加载数据并做基础的分布检查:

import pandas as pd df = pd.read_csv("movie_reviews.csv") print(df.shape) print(df["label"].value_counts())

了解标签分布非常关键。我提醒一下,如果两个类别的样本数量差距很大,模型可能会学到“无脑预测多数类”的捷径,在评估时准确率虚高但实际没有泛化能力。此时就需要考虑过采样、欠采样或者调整类别权重。在这个演示项目里,如果发现基础数据基本平衡,那就继续往下走。

接下来看几条样本数据,确认文本没有明显异常,比如空值或者乱码:

print(df.head(10))

4.2 数据预处理与 Dataset 对象构建

Hugging Face 的datasets库可以把 pandas DataFrame 直接转换成 Dataset 对象,操作起来非常顺手。

from datasets import Dataset dataset = Dataset.from_pandas(df[["text", "label"]])

然后对数据集做训练集和验证集的切分:

dataset = dataset.train_test_split(test_size=0.2, seed=42) train_dataset = dataset["train"] eval_dataset = dataset["test"]

接下来实现一个核心的预处理函数,这个函数将原始文本转换成模型所需的所有输入张量:

def preprocess_function(examples): encoded = tokenizer( examples["text"], truncation=True, padding="max_length", max_length=128 ) encoded["label"] = examples["label"] return encoded

然后把这个函数映射到完整数据集上:

train_dataset = train_dataset.map(preprocess_function, batched=True) eval_dataset = eval_dataset.map(preprocess_function, batched=True)

这里的batched=True表示对一批样本同时做处理,会大幅提升速度。处理完成后,每个样本里都包含了input_ids、token_type_ids、attention_mask和label四个字段,可以直接被模型消费。

如果要在 PyTorch 下训练,还要构造 DataLoader。需要注意,我们在训练时不应该手动处理 padding,因为 DataLoader 里自带了一个非常关键的工具,叫做DataCollatorWithPadding。它会在每个 batch 内部动态地把文本 padding 到该 batch 的最长长度,这比我们提前把所有样本都填充到最大长度要节省空间。

from transformers import DataCollatorWithPadding data_collator = DataCollatorWithPadding(tokenizer=tokenizer)

为什么不直接全部 pad 到 max_length?因为批次内动态 padding 可以明显减少计算量,尤其是当数据集中文本长度差异明显时,这一项能节省不少显存。

4.3 模型搭建与训练参数配置

模型加载方式已经在前一节说过,这里直接组合起来。我们需要把 BERT 模型、优化器、学习率调度器还有训练参数集中定义好。

优化器我选择 AdamW,这是 BERT 系列最常用的优化器。它的特点在于修正了 L2 正则化与权重衰减之间的关系,在预训练模型微调场景下表现稳定。

学习率设置上,一般 micro 调的区间是 2e-5 到 5e-5 之间。我直接把数值设为 2e-5。说实话,这个值不用太纠结,关键是要配合学习率调度器使用。我使用的是线性衰减调度器,在训练过程中让学习率从初始值逐渐降到 0,有助于训练后期收敛。

核心代码如下:

from transformers import Trainer, TrainingArguments from transformers import AdamW, get_linear_schedule_with_warmup training_args = TrainingArguments( output_dir="./results", evaluation_strategy="epoch", save_strategy="epoch", learning_rate=2e-5, per_device_train_batch_size=16, per_device_eval_batch_size=16, num_train_epochs=3, weight_decay=0.01, logging_dir="./logs", load_best_model_at_end=True, )

训练器直接使用 Transformers 库的 Trainer 对象,它把训练循环、日志输出、模型保存这些流程全部封装好了。

trainer = Trainer( model=model, args=training_args, train_dataset=train_dataset, eval_dataset=eval_dataset, data_collator=data_collator, tokenizer=tokenizer, )

如果想完全自己写训练循环也可以,但 Trainer 在处理分布式训练、混合精度、日志监控等细节时已经足够成熟,直接把精力花在业务逻辑上更省事。

4.4 执行训练并通过评估函数观察效果

启动训练只需要一行:

trainer.train()

在我的机器上,大约两千条样本训练 3 轮,一块中端 GPU 大概几分钟就跑完了。训练过程中日志会打印每个 epoch 的训练损失和验证损失。

训练完成后,可以调用评估方法获得验证集指标:

result = trainer.evaluate() print(result)

由于 Trainer 默认的评估指标是损失值 loss,而没有任何分类指标,所以我还会自定义一个计算函数,用来计算准确率和 F1 值:

from sklearn.metrics import accuracy_score, f1_score def compute_metrics(eval_pred): predictions, labels = eval_pred predictions = predictions.argmax(axis=-1) acc = accuracy_score(labels, predictions) f1 = f1_score(labels, predictions, average="binary") return {"accuracy": acc, "f1": f1}

然后在 Trainer 初始化时传入这个函数:

trainer = Trainer( model=model, args=training_args, train_dataset=train_dataset, eval_dataset=eval_dataset, tokenizer=tokenizer, data_collator=data_collator, compute_metrics=compute_metrics, )

这个环节是很多人容易忽略的,因为默认评估函数不会告诉你模型到底分对了多少。加上准确率和 F1 指标以后,你对模型的真实表现才有直观判断。

4.5 模型推理与新样本预测流程

训练完模型之后,最有成就感的时刻就是拿新数据去测试。这里不需要重新加载 Trainer,直接用 pipeline 一行代码即可:

from transformers import pipeline classifier = pipeline("sentiment-analysis", model=model, tokenizer=tokenizer) result = classifier("这部片子剧情拖沓,演技尴尬,不推荐") print(result)

需要注意的是,pipeline 里的标签映射可能会与我们的 0 1 标签不完全一致,所以输出里的 label 是模型自己学习出来的映射。如果需要对标签做精准映射,可以使用更底层的调用方式。

基础版本是这样的:

model.eval() encoded = tokenizer( "这部电影太精彩了,值得二刷", truncation=True, padding=True, return_tensors="pt" ) with torch.no_grad(): outputs = model(**encoded) logits = outputs.logits predicted_class_id = logits.argmax(dim=-1).item() print(predicted_class_id)

这里有一个经验要分享:推理时务必加torch.no_grad(),这样会关闭梯度计算,大幅降低显存和内存占用,提高推理速度。

5. 常见问题与排查技巧实录

5.1 Tokenizer 相关报错与处理

第一个高频问题就是这样一段报错:Some weights of the model checkpoint were not used。看到这个信息不用慌,这通常是因为我们加载的是一个带语言模型头的 BERT,而现在换成了分类模型。模型检测到顶部分类权重不存在,于是随机初始化。这完全符合预期,不算错误。

第二个高频问题与文本编码相关,出现样式多为Token indices sequence length is longer than the specified maximum sequence length。这个错误的含义是输入文本过长,超过了固定的最大位置编码。解决方式非常直接,在 tokenizer 调用时指定truncation=True,或者设置max_length参数强制截断。在这篇文章的代码中我们已经提前处理了,但如果你在自己写脚本时忘记加,这个报错出现的频率很高。

第三个比较隐蔽的问题是 tokenizer 的 padding 方式不一致导致模型性能波动。如果你在 DataLoader 中用了动态 padding,但在评估或者推理时却用了 max_length padding,input_ids 的分布会略有变化,个别情况下会影响模型效果。我的建议是训练、验证、推理三个环节的 padding 策略尽量保持一致。

5.2 显存溢出与 batch size 调整

训练过程中最常见的崩溃原因,就是CUDA out of memory。这个单词出现在屏幕上时,先别急着降低 batch size,而是按顺序检查三件事。

第一,是不是 DataLoader 里每个 batch 的文本被 pad 到了固定 128 长度。如果是,说明动态 padding 没生效,可以检查 data_collator 是否正确传入。

第二,是不是数据加载时把整个数据集都 pad 了。如果你在预处理时使用了padding="max_length",但没有让 tokenizer 适应 DataLoader 的动态策略,显存会被大量无意义的 padding token 占满。建议改成只在tokenizer调用时不做 padding,或者让data_collator接管。

第三,如果以上都没问题,再考虑降低per_device_train_batch_size,从 16 降到 8 或 4。BERT 对显存的占用确实比较大,小显存显卡上 batch size 8 也是完全可以接受的。

5.3 训练损失不下降的诊断思路

比起显存溢出,更让人头疼的问题往往是模型训练了好久,loss 完全没变化。这种情况打开 Debug 的顺序通常是这样。

先检查标签是否传递正确。如果 label 没有传到模型,模型的损失计算就会出错,或者出现隐藏的 NaNs。确保预处理函数里确实构造了label字段,并且在 DataLoader 迭代时能看到该字段。

再检查学习率设置。学习率过高会导致损失震荡不降,过低则可能收敛非常慢。BERT 微调一般推荐 2e-5 到 5e-5,不要用默认的 1e-3。

最后检查数据是否干净。文本列里如果有大量空值、异常符号或者标签错位,模型学到的一定不是语义规律而是噪声。先打印前 50 条样本,手动检查一下数据质量,往往能发现很多问题。

5.4 结果复现与随机种子固定技巧

如果你希望实验结果可复现,那么务必在训练流程的开头固定所有随机源,包括 Python 的 random 模块、NumPy 的随机种子和 PyTorch 的随机种子。示例代码如下:

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) set_seed(42)

实测下来固定种子之后,多次训练出来的准确率波动一般在零点几个百分点以内。这个细节在项目汇报或者写实验报告时非常重要,因为别人复现你的结果会更顺利。

还有一个我自己习惯的小技巧:训练结束后不要只保存一个模型路径,把 tokenizer 文件也保存下来。推理时用同一个 tokenizer,能避免因为词表不一致导致的分词错位问题。

6. 从第一个用例到后续任务的扩展思路

这个系列的第一篇到这里已经把情感分类的完整流程跑通了。从环境搭建到训练评估,你现在手里应该有一条可以复用的 NLP 任务流水线。

需要特别强调,这套流水线的价值不局限于情感分类。你在本文学到的 tokenizer 调用、数据预处理、模型加载、训练器配置、评估函数设计,几乎可以直接迁移到其他 BERT NLP 用例中。

举几个简单的例子。如果要做文本多分类,比如新闻主题分类,只需要把num_labels从 2 改成实际的类别数量,其他部分基本不用动。如果要做命名实体识别,模型需要换成BertForTokenClassification,标签从文本级别变成 token 级别,数据处理部分会复杂一些,但训练和评估的大框架依然一致。如果要做文本相似度计算,则通常需要改成句子对输入,在tokenizer里传入两段文本,并利用token_type_ids来区分两句话。

按照系列规划,后面的文章我会逐一展开这些任务场景,同时在已跑通流程的基础上,补充更多优化策略,比如学习率分层、对抗训练、模型蒸馏和部署上线等内容。

在做完这个情感分类项目后,我个人最大的体会是:NLP 项目的核心难点往往不在模型结构,而在数据流的组织方式。只要把 BERT 的输入三个张量理解透了,整个项目的骨架就已经搭建起来了。后续无论任务怎么变,都是在这个骨架上填充新的业务逻辑和数据模式。

如果你已经把这套代码跑下来了,可以试着换一个自己熟悉领域的数据集,比如商品评论分析、微博情感分类或者客服工单分类,把同样的流程再走一遍。这一步的收获会比看十篇文章都大。下一篇我会选择一个更有挑战性的 NLP 任务继续拆解,到时候见。

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

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

立即咨询