BERT 中文分类全流程:分词、编码、数据集与增量微调
中文分词与编码、AI 项目五阶段、用 ChnSentiCorp 制作自己的 Dataset,以及「冻结 BERT + 全连接头」的增量微调。
1. 中文分词:句子怎么变成数字
NLP 的第一步,是把句子转成词(token):
- 方式一:单个文字分词——按单字切分。扩展性最强,任何新词都能拆成已有字来表示,缺点是没有词级语义。
- 方式二:词组分词——按词表切分。语义更完整,但词表外的词会被切碎或变成
UNK。
BERT 使用的是 WordPiece 分词,词汇表 vocab.txt 里可以看到这样的条目:
白
##白
白日依山尽,
## 表示这个 token 是某个词的一部分(子词),比如「白日依山尽」会被拆成 白日依山尽 加特殊子词。
对句子编码后得到类似:
4635 3189 898 2255 2226 8024
注意:这不是词向量,而是字/词在词表中的索引(下标)。真正的词向量,是模型内部 Embedding 层拿着这些下标去查表得到的 768 维向量。
2. 编码:batch_encode_plus 参数详解
批量编码用 tokenizer.batch_encode_plus(),参数含义和默认值如下:
| 参数 | 含义 | 默认值 |
|---|---|---|
batch_text_or_text_pairs | 输入的文本列表(或文本对列表) | 必填 |
add_special_tokens | 是否自动添加 [CLS]/[SEP] 等特殊 token | True |
truncation | 超过 max_length 时是否截断 | 通常 False(看模型 config) |
max_length | 最大长度(token 数) | None |
padding | 是否填充补齐长度 | False;可选 "longest" / "max_length" / True |
return_tensors | 返回张量类型 | None(返回 Python list);可选 "pt" / "tf" / "np" |
return_token_type_ids | 是否返回 token_type_ids(区分句子 A/B) | True |
return_special_tokens_mask | 是否返回「哪些位置是特殊 token」的掩码 | False |
几个关键点:
- 多行向量成矩阵、对齐长度:一批句子长短不一,通过
padding填充到一样长,才能拼成矩阵喂给模型。 - 长度怎么算:五言诗带标点每句是 6 个 token(5 字 + 1 标点),加上句首
[CLS]和句尾[SEP]两个特殊 token,就是 8。 - 返回的是字典:
{"input_ids": [...], "attention_mask": [...], "token_type_ids": [...]}。 - 可以解码回文本:
tokenizer.decode(ids)能把编码结果还原成原文本。 - 版本差异:较新的 transformers(5.x)分词器不再接受旧的
batch_text_or_text_pairs写法,批量编码统一走batch_encode_plus(或直接调用tokenizer(...)),以你实际安装版本的 API 为准。 - 小纠正:笔记里「padding 取值 tf、pt、np」其实混淆了——
tf/pt/np是return_tensors的取值;padding取False/"longest"/"max_length"。
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("bert-base-chinese")
batch = tokenizer.batch_encode_plus(
["白日依山尽,", "黄河入海流。"], # 一批文本
add_special_tokens=True, # 自动加 [CLS] [SEP]
max_length=8, # 五言诗带标点 + 头尾特殊字符
padding="max_length", # 对齐成长度 8 的矩阵
truncation=True, # 超长截断
return_tensors="pt", # 返回 PyTorch 张量
return_token_type_ids=True, # 返回 token_type_ids
return_special_tokens_mask=False, # 不需要特殊 token 掩码
)
print(batch["input_ids"]) # shape [2, 8]:每行是一个句子
print(batch["attention_mask"]) # 1=真实 token,0=填充位
print(tokenizer.decode(batch["input_ids"][0])) # 解码还原(含特殊 token)
3. AI 项目开发的五个阶段
flowchart TB
A["需求 / 数据<br/>① 获取数据 ② 转格式(制作数据集)"] --> B["模型选择 / 设计<br/>增量微调等方案"]
B --> C["模型训练<br/>① 加载模型和数据开始训练 ② 观察训练状态"]
C --> D["效果评估"]
D --> E["部署"]
- 需求/数据:获取原始数据,转成统一格式(制作数据集)。
- 模型选择/设计:选基座模型,决定微调策略(增量微调等)。
- 模型训练:加载模型和数据开训,观察 loss、准确率等训练状态。
- 效果评估:在验证集/测试集上评估。
- 部署:把训练好的模型上线服务。
其中主要成本花销来自两处:硬件设备(GPU 等)和人工操作(数据清洗、标注)。
4. 数据集:ChnSentiCorp
ChnSentiCorp 是中文情感评价数据,每条包含两部分:
text:评论文本label:0负向(negative),1正向(positive)
国内网络建议先设置镜像环境变量再加载:
import os
os.environ["HF_ENDPOINT"] = "https://hf-mirror.com" # 使用国内镜像
from datasets import load_dataset, load_from_disk
# 在线加载(首次会下载到本地缓存)
dataset = load_dataset(path="lansinuote/ChnSentiCorp")
print(dataset)
# 保存到本地 / 从本地加载
# dataset.save_to_disk(r"D:\PythonProjects\my_saved_dataset")
# dataset = load_from_disk(r"D:\PythonProjects\my_saved_dataset")
# print(dataset)
下载后的路径:不指定 cache_dir 时会下载到默认缓存位置,Windows 下大致是:
C:\Users\<用户名>\.cache\huggingface\datasets\NousResearch___hermes-function-calling-v1\func_calling_singleturn\0.0.0\dae3e1d28cfbcf4b915c04ea1e072030529b4bda
目录里主要有两类文件:数据文件 hermes-function-calling-v1-train.arrow 和描述文件 dataset_info.json。
加载后的结构是 DatasetDict,例如:
DatasetDict({
train: Dataset({
features: ['id', 'conversations', 'tools', 'category', 'subcategory', 'task'],
num_rows: 1893
})
})
通常分为 train(训练)/ validation(验证)/ test(测试) 三部分。
一个工程经验:一般不在 Dataset 类内部做分词编码,而是直接返回原始文本,到模型训练时再编码。这样数据集与分词器解耦,可以复用、也可以换不同分词器。
5. 自定义 MyDataset:继承 Dataset
from datasets import load_from_disk
from torch.utils.data import Dataset
class MyDataset(Dataset):
def __init__(self, split):
self.dataset = load_from_disk(r"D:\PythonProjects\my_saved_dataset")
if split == "train":
self.dataset = self.dataset["train"]
elif split == "test":
self.dataset = self.dataset["test"]
elif split == "validation":
self.dataset = self.dataset["validation"]
def __len__(self):
return len(self.dataset)
def __getitem__(self, item):
text = self.dataset[item]["text"]
label = self.dataset[item]["label"]
return text, label # 返回原始文本 + 标签,训练时再编码
if __name__ == "__main__":
dataset = MyDataset("test")
for data in dataset:
print(data)
6. BERT 内部:从词向量到分类结果
flowchart LR
A["word_embeddings<br/>词 → 768 维向量"] --> B["position_embeddings<br/>位置编码(序列顺序)"]
B --> C["Transformer 编码器<br/>全连接 + 注意力机制"]
C --> D["pooler(BertPooler)<br/>768 维句特征"]
D --> E["全连接层 fc(768→2)<br/>下游任务模型"]
E --> F["2 类 logits → softmax"]
- word_embeddings(词向量):把词的索引查表转成 768 维向量,把词放到 768 维的空间中。大模型 / BERT 自带这套词向量编码,不需要自己训练。
- position_embeddings(位置编码):文本是序列问题,而全连接网络本身不具备处理序列顺序的能力,必须显式引入位置编码来弥补,告诉模型每个词在第几位。
- 高维空间的相似度矩阵问题:Transformer 内部靠注意力机制计算 token 之间的相似度(高维空间中的相似度矩阵),这是注意力机制的核心。
- pooler(池化层 BertPooler):把最后一层输出浓缩成一个 768 维的句级特征。
- 二分类:把 768 维结果映射成 2 维——这层「增量」的全连接就是下游任务模型。
- 增量微调:冻结 BERT 参数(
torch.no_grad(),BERT 不参与训练),只让新增的增量模型参与训练。 - NSV 记法:
N批次(batch)、S序列长度、V特征维度。out.last_hidden_state[:, 0]取的是每个样本第 0 个 token(即[CLS])的特征。 - 资源消耗:批次
N越大,显存与资源消耗越多。
import torch
from transformers import AutoModel
# 提前加载 BERT(不带分类头),pretrained 全局可用:
# pretrained = AutoModel.from_pretrained("bert-base-chinese")
class Model(torch.nn.Module):
def __init__(self):
super().__init__()
# 设计全连接网络,实现二分类任务:768 维 -> 2 维
self.fc = torch.nn.Linear(768, 2)
def forward(self, input_ids, attention_mask, token_type_ids):
# 冻结 BERT 参数,让其不参与训练
with torch.no_grad():
out = pretrained(
input_ids=input_ids,
attention_mask=attention_mask,
token_type_ids=token_type_ids,
)
# 增量模型参与训练:取每句第 0 个 token([CLS])的特征,经全连接输出 2 类 logits
out = self.fc(out.last_hidden_state[:, 0])
return out
7. 硬件与训练提示
- 如果你的机器是 RTX 2050(4GB 显存):
batch_size=90、max_length=512时每一步都会占满显卡且耗时很长,第一条epoch:0, i:0, ...日志可能要等几分钟才出现。 - 建议:先把
batch_size调小(如 16 / 32)、把max_length调小(如 128 / 256),或用梯度累积;先小规模跑通流程,再逐步放大。
8. 结语
这一篇把「文本 → 分词 → 编码 → 数据集 → 模型 → 增量微调」整条链路走了一遍:分词器负责把句子变成下标、padding 负责对齐成矩阵、Dataset 负责喂数据、冻结的 BERT 负责提取特征、新增的全连接层负责做二分类。
下一步建议:跑通完整训练循环(DataLoader + 优化器 + loss)、观察 loss 收敛情况、在 test 集上评估准确率,然后把训练好的模型保存下来做部署。
