第1章:BERT基础原理——从Transformer到BERT,预训练与微调流程
说起BERT,得先聊聊它的根基——Transformer。很多人一上来就啃,结果被Self-Attention搞晕了。我刚开始也是这样,花了一周才把那张“Attention is All You Need”的图看明白。其实说白了,Transformer就是个编码器-解码器结构,但它的核心不是RNN,而是注意力机制。
1.1 Transformer中的关键概念
为什么Transformer能打败RNN?因为它可以并行计算。RNN是串行的,一个词一个词往后传,训练慢不说,长距离依赖也容易丢。Transformer用Self-Attention,一个句子里所有词之间都能直接“对话”。
你想想看,输入"我喜欢吃苹果",要让模型知道"苹果"可能是水果也可能是手机品牌,传统RNN需要一步步传递,而Self-Attention一步到位。我在项目中遇到过一个问题:用LSTM做文本分类,长文本一超过128个效果就崩了。换上BERT后,512个依然稳。
Transformer编码器主要有几个组件:
- 多头自注意力(Multi-Head Self-Attention):让模型关注不同位置的不同表征子空间。
- 位置编码(Positional Encoding):因为注意力本身没有顺序感,得用正弦余弦或可学习的位置向量告诉模型词的位置。
- 前馈神经网络(FFN):每个注意力层后面跟一个两层全连接,增加非线性。
- 残差连接 & 层归一化:避免深层网络梯度消失或爆炸。
| 特性 | Transformer | RNN/LSTM |
|---|---|---|
| 并行计算 | 全并行 | 串行 |
| 长距离依赖 | 好(O(1)路径长度) | 差(O(n)路径长度) |
| 训练速度 | 快(GPU友好) | 慢(难并行) |
| 参数量 | 大(但高效) | 相对小 |
1.2 从Transformer到BERT
BERT全称是Bidirectional Encoder Representations from Transformers。注意那个“Bidirectional”——双向。之前的ELMo是双向但用了LSTM,是单向Transformer解码器。BERT直接把Transformer编码器拿来,用Masked Language Model(MLM)和Next Sentence Prediction(NSP)做预训练。
我个人习惯把BERT看作一个“填空+判断上下文”的巨人。它先在海量纯文本上玩“完形填空”(随机遮掉15%的,让模型预测),同时判断两个句子是否连续。你想想看,这就像让人在毫无标注的数据里自学语言规律。
BERT的预训练分为两个阶段:
- Masked Language Model (MLM):随机遮住输入中15%的,让模型预测被遮住的词。注意,这15%中80%用[MASK]替换,10%用随机词替换,10%保持不变。为什么要这么设计?为了减小预训练和微调之间的差异——微调时没有[MASK]标记。
- Next Sentence Prediction (NSP):输入两个句子,判断它们是否连续。这帮助BERT理解句子间关系,对QA、自然语言推理等任务特别有用。
微调阶段就简单多了:用预训练好的BERT权重,接一个分类层(或者做序列标注、问答等),然后在你自己的标注数据上训练几个epoch。我建议学习率调小一点,一般在2e-5到5e-5之间,太大容易让预训练的知识被冲掉。
1.3 整体流程全景图
下面这张图把从Transformer到BERT再到微调的全过程画在了一起。你一眼就能看清:左边是Transformer编码器结构,中间是BERT的双向预训练,右边是下游任务微调。
1.4 预训练与微调的核心差异
很多人混淆了预训练和微调,以为是一回事。我用个表格帮你理清:
| 维度 | 预训练 | 微调 |
|---|---|---|
| 数据 | 海量无标注文本(BooksCorpus + Wikipedia ≈ 3.3B词) | 少量标注数据(几百到几万条) |
| 目标 | 学习通用语言表示 | 适配具体任务(分类、QA等) |
| 训练开销 | 极大(TPU/GPU集群跑数天/周) | 小(单卡几小时到一天) |
| 任务头 | MLM + NSP 分类头 | 自定义任务头(取决于下游) |
| 学习率 | 动态(预热+衰减) | 固定小学习率(2e-5左右) |
| 典型优化器 | AdamW | AdamW |
1.5 避坑清单
做BERT相关项目,我踩过不少坑。这里列几个最典型的:
- 分词器不一致:预训练用的是WordPiece(BERT-base有30522个词),微调时一定要用同一个分词器,不能自己随便切。我用过jieba分词喂给BERT,结果一塌糊涂。
- 最大长度限制:BERT-base最大512个,超过会截断。我曾经做长文档分类,直接截断丢了关键信息。后来改用分层BERT(先分段编码再聚合)才解决。
- learning rate太大:刚开始用0.001直接崩了。BERT的微调学习率一般3e-5、2e-5甚至1e-5,我建议用验证集调一调。
- 忽略NSP负采样:有些框架实现的预训练NSP任务负样本比例不对(比如1:1),但实际上用随机句子做负例效果更好。不过后面RoBERTa直接去掉了NSP,证明有时NSP反而没用。嗯,这个有点争议,你自己试。
1.6 代码骨架:使用Hugging Face加载BERT
说了这么多理论,咱们上点实际代码。Hugging Face的transformers库是最常用的,我们用它快速加载BERT并做微调。下面是一个最简单的分类任务代码骨架:
from transformers import BertTokenizer, BertForSequenceClassification
from torch.utils.data import DataLoader
# 加载预训练模型和分词器
tokenizer = BertTokenizer.from_pretrained('bert-base-chinese')
model = BertForSequenceClassification.from_pretrained('bert-base-chinese', num_labels=2)
# 示例句子
texts = ["今天天气真好", "这个产品太差了"]
labels = [1, 0] # 1阳性 0阴性
# 分词 + 编码
encodings = tokenizer(texts, truncation=True, padding=True, return_tensors='pt')
# 输出:input_ids, attention_mask
# 简单的训练循环示意(省去优化器等细节)
for epoch in range(3):
outputs = model(input_ids=encodings['input_ids'],
attention_mask=encodings['attention_mask'],
labels=torch.tensor(labels))
loss = outputs.loss
loss.backward()
# optimizer.step() # 实际要用AdamW优化器
你想想看,就这么几行代码,就能把BERT用起来了。但别忘了,正式训练还需要DataLoader、学习率调度、验证集、early stopping。我曾经在一个项目里直接复制这份代码去跑500万条数据,结果内存爆炸了……最后一个一个batch读才搞定。
好了,这一章就到这里。Transformer是地基,BERT是房子,预训练和微调是装修方式。弄明白这些,后面的课程你就跟得上了。