1. 从词向量到预训练:NLP 的范式转移
老读者都知道,我这些年一直在折腾自然语言处理相关的东西,从早期的统计机器翻译一路做到现在的深度学习方案。每次有朋友问我现在该从哪开始学 NLP,我的答案一直是同一个:先把第七章吃透,也就是自然语言处理与 BERT 那部分。倒不是说别的内容不重要,而是这一章恰好站在了整个领域的分水岭上——你理解了 BERT,就理解了这两年所有主流 NLP 模型的底层逻辑。
先聊一个很多人没意识到的事实:在 BERT 出现之前,NLP 的玩法基本是“每个任务训练一个专用模型”。你要做情感分析就训练一个情感分类器,要做命名实体识别就再训练一个序列标注模型,彼此之间互不相通。这种模式最大的问题是数据瓶颈——高质量的标注数据永远不够,而模型又必须依赖大量标注数据才能表现良好。我早期做一个中文分词项目的时候,光清洗和标注语料就花了两个月,真正调模型的时间反而只有一周。
BERT 带来的改变是根本性的:先用海量无标注文本做预训练,学出一套通用的语言表示,然后针对具体任务做微调。这套“预训练+微调”的范式,本质上把 NLP 的门槛从“你得有几十万条标注数据”降到了“你有几千条就够了”。这就好比以前你要学会做每一道菜都得从种菜开始,现在所有菜谱都给你配好了处理好的食材,你只要学会调味就行。
所谓自然语言处理,简单说就是让机器能读懂人类语言的任务集合。BERT 全称 Bidirectional Encoder Representations from Transformers,中文通常翻译成“基于 Transformer 的双向编码器表示”。2018 年一出来就直接刷新了 11 项 NLP 任务的最好成绩,当时整个圈子都沸腾了。后面几年你看到的几乎所有中文 NLP 项目,不管底层是 ERNIE 还是 Roberta-wwm,源头都能追溯到 BERT 这篇论文。
这个章节之所以值得你投入大量时间,不光是 BERT 本身牛逼,更关键的是它背后那一整套方法论你后面会反复用到:注意力机制、位置编码、层归一化、Adam 优化器、学习率预热策略……随便拎一个出来,在别的模型里也是标配。把这章吃透了,你再看 GPT 系模型、再看 T5、再看各种多模态模型,都会觉得只是 BERT 这套骨架的变体,心理负担立刻小很多。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 为什么是 BERT:双向编码的降维打击
2.1 预训练时代的“为什么”问题
做研究也好,做工程也罢,最忌讳的就是只会调包不理解原理。我见过太多人把 BERT 加载出来跑个分类任务就觉得自己会了,一问为什么 BERT 能理解语义就支支吾吾。这一节我想把最关键的那个点讲透:为什么 BERT 选择用双向编码器,而且效果碾压单向模型。
传统语言模型是单向的,从左往右逐个预测下一个词。这符合人类说话的顺序习惯,但对“理解”这件事来说是吃亏的。你读“这家餐厅的菜虽然贵,但是___”这句话,要判断空格里最可能出现什么词,光看左边你是推理不出来的,因为“但是”之后可能接“好吃”“不好吃”“值得”“服务好”等等,不确定性很大。但如果同时给你右边的“回头客特别多”,你立刻就能锁定“值得”这个方向。这就是双向上下文的意义——同时看左右两边,信息量翻倍,歧义大幅减少。
ELMo 和 GPT 都意识到了单向的限制,但 ELMo 是拿两个单向模型拼接,本质上还是割裂的;GPT 干脆只做了从左到右的注意力,虽然在生成任务上很适合,但对理解类任务的帮助有限。BERT 真正划时代的地方在于它把 Transformer 编码器完整用起来了,每个词都能同时看到句子里的所有词,而不是只看到左边。
这里有一个技术细节很多人会忽略:为了在双向注意力下仍然能让模型学会预测句子结构,BERT 引入了一个极其巧妙的设计——随机遮挡一部分输入的 token,然后让模型根据上下文去预测这些被遮挡的词。这个任务叫“掩码语言模型”(Masked Language Model,MLM),它的天才之处在于把语言理解任务转化成了一个猜词游戏,而猜词游戏天然需要双向信息。
2.2 两个任务撑起一个模型
MLM 是 BERT 的两条腿之一。具体实现方式是这样的:在预训练阶段,有 15% 的 token 会被选中参与遮蔽。其中 80% 的概率真正替换成一个特殊的[MASK]标记,10% 的概率替换成随机词,剩下 10% 保持不变。后面这个 10% 的设定很多人不理解,为什么要故意不遮蔽?其实这是为了防止模型过度依赖[MASK]这个标记本身,逼迫它真正学会利用上下文信息而不是偷懒。如果全部替换成[MASK],模型在微调阶段就懵了,因为微调的数据里根本没有[MASK]这个信号。
另一条腿叫做“下一句预测”(Next Sentence Prediction,NSP)。做法很简单:喂给模型两个句子 A 和 B,让模型判断 B 是不是 A 在原文中真实的下一句。这个任务的核心价值在于让模型学习句子级别的关联关系,这对问答和推理类任务至关重要。后面很多人尝试去掉 NSP 任务,发现大部分情况下影响不大,但这是后话——你初学的时候把它理解成“让模型学会句子间衔接”的训练信号就够了。
有了这两个任务加持,BERT 的预训练就不再是单纯的“背单词”,而是同时对词、句、篇章三个粒度做建模。这也是为什么它一张预训练权重就能适配那么多下游任务的本质原因——它学到的不是某个特定任务的能力,而是通用的语言理解能力。
2.3 BERT 的整体结构速览
BERT 的结构并不复杂,baseline 是 12 层 Transformer 编码器叠起来,hidden size 768,12 个注意力头,总参数大约 1.1 亿。Large 版本是 24 层,hidden size 1024,16 个注意力头,3.4 亿参数。
输入侧,BERT 对每个 token 的输入向量由三部分相加组成:token 嵌入(词本身的语义向量)、segment 嵌入(区分句子 A / 句子 B,用于 NSP 任务)、position 嵌入(标记 token 在序列中的位置)。
很多初学者想不通为什么 Transformer 要额外加 position embedding,原因其实很直观:注意力机制本身是“顺序无关”的,它对词序不敏感。你把“我打你”换成“你打我”,每个词对之间的注意力分数是一样的。为了让模型知道词的先后顺序,必须把位置信息显式编码进去,这就是 position embedding 存在的全部意义。
输出侧,BERT 在每个 token 位置都会产出一个隐藏状态向量,你可以把它理解成“结合了全句上下文的词表示”。同时第一个 token 位置(CLS)的输出向量还可以作为整个句子的语义表示,直接接一个分类层就能做句子级任务。从实用性角度看,cls 向量和平均池化向量是下游任务最常用的两种句子表示方式,我在实际项目里实测过,平均池化在大部分分类任务上表现往往更好,后面应用部分我会再展开讲。
3. 动手之前:环境准备与工具选型
3.1 算力需求与开发环境
不管你是做实验还是做实际项目,先有一个能跑起来的开发环境比什么都重要。BERT 的 baseline 模型推理大概需要 3~4GB 显存,如果你要做微调,建议显存不低于 8GB。我自己早期用一块 4GB 显存的显卡跑 BERT 微调,batch size 只能设到 8,训练速度慢得怀疑人生。后来换了 12GB 显存的卡,同样任务快了三倍不止,batch size 也能开到 32。
如果你手头只有 CPU,也别灰心——BERT 在小数据集上做推理还是能跑的,只是慢。我试过在 8 核 CPU 上对一条句子做预测,大概需要 2~3 秒,做演示和教学完全够用。训练就算了,CPU 训 BERT 基本是折磨自己。
开发环境我用得最顺手的是 Python 3.8 + PyTorch 1.10 + Transformers 4.x 的组合。这里有一句忠告:环境版本别追最新,能用就行。我踩过 Transformers 版本升级带来的接口不兼容坑,明明昨天还能跑的代码,升级后 API 直接变了。学术复现和工程开发永远把稳定性放在第一位。
3.2 模型加载方式的选择
Hugging Face 的 Transformers 库是当前最主流的选择,没有之一。它把模型结构、分词器、预训练权重、微调工具全部封装好了,你只需要几行代码就能把 BERT 跑起来。我见过有些教程让你从零手写 BERT 的模型结构,说实话对学习是有帮助的,但对应用来说纯属浪费时间。
用 Transformers 加载中文 BERT,最好的起点就是 bert-base-chinese。这个模型是在中文维基百科上预训练的,词汇表覆盖了绝大部分常用汉字。加载代码极简:
python复制from transformers import BertTokenizer, BertForSequenceClassification
tokenizer = BertTokenizer.from_pretrained("bert-base-chinese")
model = BertForSequenceClassification.from_pretrained(
"bert-base-chinese", num_labels=2
)
如果你要更快的加载速度,可以在 from_pretrained 里加一个 local_files_only=True 参数,配合你预先下载好的本地模型目录使用。这个在离线环境里非常实用,我此前做内网部署的时候就靠这一招省去了所有联网困扰。
另外需要提醒的是,bert-base-chinese 是字级别的模型,它直接把每个汉字当成一个 token,不存在分词环节。对中文来说这是很大的优势——英文模型需要分词器把单词切成子词,而中文按字切基本不存在词表爆炸的问题。但这也意味着你需要为中文任务重新设计 tokenizer 的截断策略,别贪心一次喂太长的文本,BERT 的位置嵌入最大只能支持 512 个 token。
3.3 文本预处理和三件套结构
对 BERT 而言,文本预处理的流程比传统 NLP 要简单得多,你几乎不需要做繁重的清洗工作。核心就三步:去掉不可见字符和乱码、按整句切分、交给 tokenizer 做编码。
这里必须强调一个我栽过跟头的细节:不要轻易对文本做拼音转换、繁简转换之外的重度改写,也不要随便对句子做截断。BERT 的字级 tokenizer 本身对噪声有一定容忍度,你过度清洗反而可能破坏语义结构。比如去停用词这种传统 NLP 必备操作,在 BERT 时代是完全不需要的,因为注意力机制会自动学会聚焦重要的词,你手动删掉那些“的、了、吗”反而可能让模型失去语法线索。
Tokenize 之后,你需要把样本整理成固定的输入格式。BERT 的输入是三段式的:input_ids(token 对应的索引编号)、token_type_ids(区分句 A 句 B)、attention_mask(区分真实 token 和 padding 填充)。这三个数组构成一个样本的完整输入,Pytorch 训练时把它们打包成一个 dict 喂给模型就行。
我强烈建议你写一个 Dataset 类来封装这些逻辑,每次从原始文本直接产出模型的输入,不要裸手写循环。Trainer 接口对这块也做了封装,把数据处理写好,训练部分就不用花太多时间了。
4. 实操演练:从零跑通一个中文文本分类任务
4.1 任务设定与数据准备
要真正理解 BERT 的用法,光看理论是不够的,必须动手做一个完整的任务。这几年我在分享经验时最爱拿来做演示的是中文情感分析:给定一条餐厅评论,模型判断它是好评还是差评。这个任务足够小、足够直观,模型结构也简单,很适合作为第一个上手项目。
我准备的模拟数据集大约 2000 条带标注的中文评论,分布大概是好评 1200、差评 800。训练集和验证集按 8:2 划分。如果你手头没有标注数据,最简单的办法是拿公开的影评或者电商评论数据,或者干脆自己标几百条,效果照样能看。
数据处理环节,我直接给你一套能跑通的代码框架:
python复制from torch.utils.data import Dataset
from transformers import BertTokenizer
import torch
class ReviewDataset(Dataset):
def __init__(self, texts, labels, tokenizer, max_len=128):
self.texts = texts
self.labels = labels
self.tokenizer = tokenizer
self.max_len = max_len
def __len__(self):
return len(self.texts)
def __getitem__(self, idx):
text = self.texts[idx]
encoding = self.tokenizer(
text,
truncation=True,
padding="max_length",
max_length=self.max_len,
return_tensors="pt",
)
return {
"input_ids": encoding["input_ids"].squeeze(0),
"attention_mask": encoding["attention_mask"].squeeze(0),
"labels": torch.tensor(self.labels[idx], dtype=torch.long),
}
max_len 这里我设成 128,对短评论已经绰绰有余。大原则是:能用 64 解决就不要到 128,能用 128 就不要到 256——长度越长,padding 越多,计算浪费越严重。BERT 的注意力复杂度是输入长度的平方,长文本对算力的消耗是指数级上涨的。
4.2 微调训练全流程拆解
数据准备好了,下面是训练部分。Transformers 库提供的 Trainer API 极大简化了训练流程,但我还是建议你先亲手写一遍训练循环,这能帮你真正理解优化器、学习率调度、梯度回传这些底层运作方式。等你理解了,再用 Trainer 偷懒。
我给的训练配置是经过多轮实验的:
- batch size:16,显存不够就降到 8
- 学习率:2e-5,BERT 微调一般在这个量级附近,太大了容易灾难性遗忘,太小了模型更新太慢
- 训练轮数:3 轮,小数据上 2~3 轮就够了,多了容易过拟合
- 优化器:AdamW,注意和普通 Adam 的区别,它把权重衰减和自适应学习率解耦了
- 学习率调度:先用 warmup 预热,先用小学习率走几步稳定梯度,再进入正常学习率区间,最后线性衰减到零
核心训练代码大概长这样:
python复制from transformers import BertForSequenceClassification
from torch.utils.data import DataLoader
from transformers import AdamW, get_linear_schedule_with_warmup
model = BertForSequenceClassification.from_pretrained(
"bert-base-chinese", num_labels=2
).to(device)
optimizer = AdamW(model.parameters(), lr=2e-5)
total_steps = len(train_loader) * num_epochs
scheduler = get_linear_schedule_with_warmup(
optimizer,
num_warmup_steps=int(0.1 * total_steps),
num_training_steps=total_steps,
)
for epoch in range(num_epochs):
model.train()
for batch in train_loader:
batch = {k: v.to(device) for k, v in batch.items()}
outputs = model(**batch)
loss = outputs.loss
loss.backward()
optimizer.step()
scheduler.step()
optimizer.zero_grad()
如果你细心观察会发现,我只写了 loss.backward() 到 optimizer.zero_grad() 的标准流程,没有做梯度裁剪。但实际训练里我建议加上 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0),尤其是你用的学习率偏大或者数据有噪声时,这一步能防止梯度爆炸把模型参数冲坏。这是我踩过很多次坑之后固定下来的习惯。
4.3 实验结果与关键指标解读
我按照上述配置训练了 3 轮之后,验证集准确率大约在 87% 左右。说实话,在 2000 条小数据上能做到这个水平,已经充分说明了预训练模型的价值。如果是传统方法,这么小的数据集能做进 80% 都费劲。
这里额外提一个非常有价值的观察:第一轮训练结束时,验证准确率已经能达到 82% 以上,后面两轮只是小幅提升。这说明 BERT 在预训练阶段已经具备很强的语言理解能力,微调更像是“适应任务的最后一脚”,而不是从零学习。也因此,二轮训练就够的情况在项目里很常见,加轮数可能反而导致过拟合。
训练完成后,保存和复用模型也是基本功:
python复制model.save_pretrained("./my_bert_sentiment")
tokenizer.save_pretrained("./my_bert_sentiment")
保存目录里会有 config.json、model.safetensors(新版)或 pytorch_model.bin(旧版)、vocab.txt 等文件。下次你要用的时候,直接 from_pretrained("./my_bert_sentiment") 就能加载,完全不需要重新下载和训练。
要是你想快一些,也可以直接用 Trainer 接口:
python复制from transformers import Trainer, TrainingArguments
training_args = TrainingArguments(
output_dir="./results",
evaluation_strategy="epoch",
learning_rate=2e-5,
per_device_train_batch_size=16,
per_device_eval_batch_size=16,
num_train_epochs=3,
save_strategy="epoch",
load_best_model_at_end=True,
metric_for_best_model="accuracy",
)
trainer = Trainer(
model=model,
args=training_args,
train_dataset=train_dataset,
eval_dataset=val_dataset,
compute_metrics=compute_metrics,
)
trainer.train()
Trainer 内置了分布式训练、混合精度、日志记录、检查点存储等一堆功能,做实验的时候极其实用。要是你想快速验证一个想法,直接把上面的 Dataset 类配好,然后跑 Trainer,几乎可以做到零失误出结果。
5. BERT 的工作原理深度拆解
5.1 注意力机制:模型凭什么“看得懂”上下文
BERT 最核心的模块就是自注意力机制。它解决了一个本质问题:如何让每个词在编码时参考句子中所有其他词的信息。
具体来说,每个输入 token 都会被映射成三个向量:查询向量 Q、键向量 K、值向量 V。注意力分数的计算方式是当前词的 Q 与句子中每个词的 K 做点积,点积越大说明两个词关联越紧密。这个分数经过 softmax 归一化之后变成权重,再去加权求和所有词的 V 向量,最终就是这个位置的新表示。
我习惯用一个生活化的类比来解释:假设你在一场聚会上想找“那个穿红裙子的人”,你会先把目光扫向全场,做一次注意力的分配——红裙子的人多停留几秒,无关的人快速略过。每个“词”都这样做一遍,全场的信息就完成了一次交互和更新。而 BERT 有 12 层这样的交互,每一层都可以被视为一次更深层次的语义聚簇和意义校准。
多头注意力就是在同样的流程里跑多次,每次用不同的 Q、K、V 投影矩阵,捕获不同视角的关联。比如“苹果”这个词,一个头可能关注到它跟“水果”的关系,另一个头关注到它跟“手机”的关系。这就让模型具备了理解一词多义的能力,这也是 BERT 比传统词向量强的原因之一——它的每一个词表示都是动态的,会根据上下文实时调整含义。
5.2 位置编码与残差连接的作用
我在前面已经提过位置编码,这里再展开讲一点:为什么 BERT 用的是“可学习的位置嵌入”而不是“公式计算的位置编码”。BERT 的做法是把位置也当成一个可学习的词典,直接把“位置 1、位置 2……”映射成向量向量加到 token 向量里。这样做的优效是灵活——模型可以自己学出适合自己任务的位置模式,不需要人为设计复杂的周期函数。
残差连接呢?它的存在意义比较直白,是为了解决深层网络的退化问题——网络太深,梯度回传路径太长,信息容易丢失。BERT baseline 有 12 层,如果层与层之间直接串联,训练时会面临严重的梯度消失。加一条从输入直接通向输出的旁路(也就是残差连接),信息就能高效地从底层传到顶层,每一层只需要学习“变化量”,而不是从头建立表示。这也是为什么 BERT 能堆到 12 层甚至 24 层还能顺利训练,而古代的 RNN 很难叠太多层。
层归一化(Layer Normalization)配合残差连接一起用,实现了每一层的输出分布稳定,训练更稳、更快。你在代码里看到的 model.encoder.layer 内部结构其实就是 AdderNorm、Feed-Forward 这样的组合模块。不必手动实现,但知道它在干嘛能帮你理解模型的整体计算流程。
5.3 双向掩码与自回归的对比
用 BERT 做理解任务时,双向编码器的优势是立竿见影的。前两年总有人问,为什么 GPT 系列能做生成任务而 BERT 不行,反过来也一样。答案是它们的训练目标先天决定了下游能力:BERT 用掩码语言模型训练,适合做“填空、判断、分类”这类理解任务;GPT 用自回归语言模型训练,适合做“续写、对话”这类生成任务。
有个词叫“无监督预训练”,现在你该明白它的真正含义了:不是不需要数据,而是不需要人工标注。BERT 预训练用的语料全都是现成的文本——维基百科、新闻网页、电子书等,自动从文本中构造训练目标,然后模型自己迭代学习。这本质上是一种“从文本自身吸取知识”的学习方法,也是这几年 NLP 能飞速发展的根本原因之一。
如果想把双向和单向的优势都拿到,后来的 BART 和解码器设计做了融合尝试,但那是进阶话题。你先把 BERT 的双向编码器理解到位,再看其他模型会顺很多。
6. 踩坑锦集:我在 BERT 实战中掉过的坑
6.1 经典坑位一:忽略 tokenizer 的截断逻辑
做长文本分类时最容易犯的错误就是“直接扔给 tokenizer 不管”。BERT 的上下文窗口只有 512,如果你输入一条 800 字的评论,tokenizer 默认会从尾部截断,把最后的信息全部丢掉。而文本的关键论点往往出现在文章中部或者结尾,这种方式会严重丢信息。
我的解决方案是分块处理:把长文本切成 400~500 token 的段落,分别过模型得到向量,然后做平均或者拼接。还有一种思路是取头尾各一半,对很多任务效果都不错。2020 年后的不少工作也专门研究了“如何让 BERT 吃下更长文本”的问题,比如对位置嵌入做插值,但现阶段用的最多的还是分块。我给项目做长文本分类时,对比过不处理、直接截断、分块三种方案的准确率,结果分块比直接截断高出差不多 5 个百分点。
6.2 经典坑位二:类别不平衡导致训练崩盘
做情感分析这类任务时,数据集往往很不均衡——好评远多于差评,或者反过来。模型在训练时很容易因为多数类主导梯度,直接退化成“全猜多数类”的保底策略,准确率看着还行,但少数类召回率惨不忍睹。
我常用的处理手段有三个,按优先级排序:
- 加权损失函数:给少数类更高的损失权重,代码上只需往损失函数传一个
weight参数。 - 类别均衡采样:每个 batch 里让每个类别的样本数尽量接近,数据加载时控制比例。
- 阈值偏移:预测时不去直接取 argmax,而是让少数类得分超过某个更低阈值(比如超过 0.4 就算少数类),提升召回率。
我实测下来,先用加权损失,再配合阈值偏移,组合效果比单一方案好得多。而且这种做法不用动模型结构,是性价比最高的修法。
6.3 经典坑位三:显存不够却被卡死
BERT 对显存的占用比较惊人,因为自注意力的中间结果都要留着反向传播。我的 4GB 老卡跑个 baseline 都费劲。这里分享几个我用过的省钱技巧:
第一,开混合精度训练。PyTorch 的 AMP(Automatic Mixed Precision)可以把部分计算切到 FP16,显存直接减半,速度略增,在我的项目上几乎没有精度损失。代码只需要在 Trainer 的参数里加 fp16=True,简单粗暴。
第二,减小 batch size 的同时增加梯度累积步数。比如 batch size 从 16 降到 4,梯度累积步数设 4,效果上等价于用一个 16 的 batch,显存却小了几倍。逻辑是用时间换空间,训练速度慢一些,但显存压力小得多。
第三,用 model.eval() 配合 torch.no_grad() 做推理。很多人推理不切 eval 模式,导致 dropout 层还在随机遮蔽,不仅结果不稳定,显存也白白占用。
6.4 经典坑位四:训练损失下降但验证一直在涨
这是我见过最多人困惑的问题——“训练时 loss 一直往下降,验证准确率却在倒退”。出现这种情况十有八九是过拟合,尤其是你用 BERT 这样的大模型在小数据集上训练。解决思路很清晰:
要么增强数据量——用同义词替换、回译等方案做数据增强,对小数据集的提升非常明显;要么降低学习率和训练轮数——小数据上 2 轮已经很多;要么加正则化——dropout 概率可以提高一些,BERT 默认 dropout 是 0.1,我试过在过拟合明显时调到 0.2 或 0.3,有奇效。
还有一个容易被忽视的检查点:验证集的划分要保证类别分布均匀,最好做分层采样。如果验证集恰好某一类的样本很多,即使你的模型没认真学也能得到偏高的准确率,这会严重误导你对模型真实能力的判断。
7. 从分类到全景:BERT 在各行各业的应用图谱
7.1 信息抽取与问答系统
文本分类只是 BERT 最简单的用法。在实际业务里,我最常接到需求是“从合同文本里抽取关键字段”和“从客服对话里抽取出用户的诉求”。这类任务本质上是命名实体识别和关系抽取,BERT 一样能扛。
做法是把序列标注问题套在 BERT 上:输入句子,BERT 在每个词的位置输出一个向量,然后接一个线性分类层对每个 token 做 BIO 标签预测。B 表示实体的开头,I 表示实体的中间或结尾,O 表示不是实体。微调目标就是把实体边界和类型识别出来。
问答系统是另一个高频应用方向。抽取式问答的做法是:把问题和包含答案的段落拼起来输入 BERT,然后模型输出两套概率分布——答案的起始位置和结束位置。选一段概率最高的区间作为答案。这种方案在阅读理解任务上表现很好,底层的逻辑就是 BERT 已经把问题和原文的语义对齐做好了。
7.2 文本生成与语义匹配
有人觉得 BERT 不能做生成任务,这个说法不太准确。BERT 做生成确实不如 GPT 顺手,但依然可以用于抽取式摘要和可控文本改写。比如你先让 BERT 做关键句抽取,再对关键句拼接做精简,就能得到一个可用的摘要系统。
语义匹配在搜索、推荐系统里是刚需。金融领域的相似问题识别、电商的相似商品匹配、检索时的 query 与 doc 相关性计算,本质都可以抽象成“计算两个句子的语义相似度”。做法就是拿 BERT 对两个句子分别编码,然后把两个向量拼起来或做点积,送给分类器判断是否相似。我做过一个商品检索项目,用 BERT 做语义召回之后,搜索准确率比单纯的词匹配方案提升了 20 个百分点,用户反馈也是天壤之别。
7.3 领域微调与中文 BERT 的变体
虽然 bert-base-chinese 能覆盖大多数通用场景,但专业领域里语言风格差异极大——医疗文本的术语密度、金融公告的格式规范、法律文书的表达方式,用通用 BERT 效果总会打折扣。解决方法是领域预训练:把通用 BERT 的权重初始化,在你的专属语料上继续做 MLM 预训练。这被称为“领域自适应预训练”,操作简单,收益却很可观。
中文 NLP 社区还诞生了一批优秀的 BERT 变体。其中一个比较有代表性的方向是引入全词掩码。标准 BERT 做 MLM 时是随机遮单个汉字,但中文的一个词可能有多个字——“苹果”被遮住一个偏旁,猜起来很怪。全词掩码的做法是把同一词内的所有汉字一起遮住,让模型真正猜测的是词而不是字,效果提升明显。还有模型在预训练阶段就引入知识图谱的信息,在百科类任务上表现更强。这些你都可以在 Transformers 的 model hub 里直接搜索到。
8. 常见问题速查表
为了让你用得顺手,我把平时被问得最多的几个问题整理成了一个速查表,直接照着排查:
| 问题现象 | 可能原因 | 解决办法 |
|---|---|---|
| 训练 loss 不降 | 学习率过大或过小 | 调整学习率到 1e-5 ~ 5e-5 区间;先调一个 batch 试跑 |
| 验证准确率上不去 | 数据量太小或预处理有问题 | 先检查数据标签是否一致;考虑做数据增强 |
| 输入特殊字符导致报错 | tokenizer 无法识别部分字符 | 预处理时统一过滤不可见字符、控制特殊符号 |
| 加载模型报“out of memory” | 显存不足 | 减小 batch size、开启混合精度、使用梯度累积 |
| 分类结果全部偏向某一类 | 类别不平衡 | 加权损失、分层采样、调整阈值 |
| 推理和训练结果差异大 | 忘了切 eval 模式 | 推理时添加 model.eval() 与 torch.no_grad() |
这份速查表是我在真实项目里攒下来的,不能说覆盖所有情况,但命中率极高。遇到问题时,先对照表里最贴近你现象的那条排查,很多时候问题就解决了一大半。
9. 下一步:超越 BERT 的方向
学完这一章,你已经站在了现代 NLP 的一个高地上。不过技术演进不会停,这里给你几条后续探索的路线参考。
BERT 当年靠的是大规模静态预训练,现代模型的共识则是“越大越好”。参数量从 1 亿涨到 100 亿甚至上千亿,涌现出的能力越来越强。不过对我们普通开发者来说,十几亿参数的模型已经够常用,跑千亿模型不是每个人都能承担的成本。
轻量化方向非常值得关注。知识蒸馏、模型剪枝和量化技术都在把大模型往小里做。我的一个生产实践就是把 BERT 蒸馏到一个参数量只有原来 1/3 的小模型,推理速度提升 2.5 倍,准确率只掉了一个百分点左右,在资源受限的部署场景里极其实用。
多模态和统一模型也是大势所趋,文本、图像、音频对齐到一个向量空间的方案越来越成熟。但不管技术怎么变,注意力和预训练这两个核心思想,都是你在这一章里打下的地基。
我个人认为,对初学者来说,现阶段最重要的事情不是去追最新论文里的花活,而是把 BERT 吃透:能从零解释它的结构,能独立完成一个任务全流程,能在遇到问题时自己定位是数据处理、模型结构还是训练策略的锅。你要是能做到这些,就已经比绝大多数只会在网上跑通示例的人强得多了。
最后忍不住再多说一句,这个领域最不缺的就是“新模型”,但真正通用的方法论从来不是玄学。你如果能把 BERT 做扎实,后面学习任何模型都会觉得举重若轻。这就是这一章值得你反复读、反复动手的全部理由。
