第1章:BERT基础原理——从Transformer到BERT,预训练与微调流程

说起BERT,得先聊聊它的根基——Transformer。很多人一上来就啃,结果被Self-Attention搞晕了。我刚开始也是这样,花了一周才把那张“Attention is All You Need”的图看明白。其实说白了,Transformer就是个编码器-解码器结构,但它的核心不是RNN,而是注意力机制。

一句话总结:BERT = Transformer的编码器 + 海量无监督预训练 + 下游任务微调。

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编码器 vs RNN/LSTM
特性TransformerRNN/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可以直接拿来做任何任务,结果发现输入格式和分词器必须严格对齐。比如中文要用全角标点,英文要加[CLS]和[SEP]。很多新手栽在这个细节上。

BERT的预训练分为两个阶段:

  1. Masked Language Model (MLM):随机遮住输入中15%的,让模型预测被遮住的词。注意,这15%中80%用[MASK]替换,10%用随机词替换,10%保持不变。为什么要这么设计?为了减小预训练和微调之间的差异——微调时没有[MASK]标记。
  2. Next Sentence Prediction (NSP):输入两个句子,判断它们是否连续。这帮助BERT理解句子间关系,对QA、自然语言推理等任务特别有用。

微调阶段就简单多了:用预训练好的BERT权重,接一个分类层(或者做序列标注、问答等),然后在你自己的标注数据上训练几个epoch。我建议学习率调小一点,一般在2e-5到5e-5之间,太大容易让预训练的知识被冲掉。

1.3 整体流程全景图

下面这张图把从Transformer到BERT再到微调的全过程画在了一起。你一眼就能看清:左边是Transformer编码器结构,中间是BERT的双向预训练,右边是下游任务微调。

Transformer → BERT → 微调流程 Transformer → BERT → 微调流程 输入词嵌入 + 位置编码 Embedding + Segment + Position 多头自注意力(12头) 并行捕捉全局依赖 残差连接 + 层归一化 Add & Norm 前馈神经网络(FFN) 两层线性 + ReLU 残差连接 + 层归一化 Add & Norm Transformer 编码器层(×12) 输入:[CLS] 我 [MASK] 苹果 [SEP] 带Mask的句子对 Masked LM 预测 预测被遮住的词([MASK]→喜欢) NSP 二分类 判断两句是否连续(IsNext / NotNext) 预训练损失:MLM + NSP BERT预训练(无监督) 分类层 线性 + Softmax 序列标注 CRF或逐分类 问答 / 相似度 Span预测/双塔 小学习率微调 下游任务微调 核心思路:用Transformer编码器做双向无监督预训练 → 学到通用语言表示 → 下游任务只需加简单分类层微调

1.4 预训练与微调的核心差异

很多人混淆了预训练和微调,以为是一回事。我用个表格帮你理清:

预训练 vs 微调
维度预训练微调
数据海量无标注文本(BooksCorpus + Wikipedia ≈ 3.3B词)少量标注数据(几百到几万条)
目标学习通用语言表示适配具体任务(分类、QA等)
训练开销极大(TPU/GPU集群跑数天/周)小(单卡几小时到一天)
任务头MLM + NSP 分类头自定义任务头(取决于下游)
学习率动态(预热+衰减)固定小学习率(2e-5左右)
典型优化器AdamWAdamW
小技巧:我习惯在微调开始时先冻结BERT前几层,只训练分类层和最后几层,跑1-2个epoch后解冻全部层。这样能避免梯度太大冲坏预训练权重。特别是当你的标注数据很少时(比如不到100条),这个方法很管用。

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是房子,预训练和微调是装修方式。弄明白这些,后面的课程你就跟得上了。