**摘要:**本文从传统 RNN 与 Word2Vec 的痛点出发,系统梳理 BERT 的破局思路、MLM 与 NSP 两大预训练任务、双向性与微调范式,并深入拆解其底座 Transformer 的 Encoder 结构,包括 Self-Attention、Multi-Head Attention、位置编码、Add & Normalize 与 Feed Forward。最后给出 BERT 模型家族选型、环境配置踩坑与 GLUE 微调实战的关键参数说明,帮助读者从原理到工程完整跑通一遍。
一、为什么需要 BERT?背景与痛点
在 BERT 出现之前,NLP 领域的主流方案大致是「Word2Vec 词向量 + RNN/LSTM 编码器 + 任务特定输出层」。这套组合在不少任务上能跑出结果,但存在两个绕不开的结构性瓶颈。
1.1 传统 RNN 的局限:串联结构导致无法并行
RNN 是典型的串联结构,隐藏状态按时间步依次传递:h1 → h2 → ... → hn。第 t 步的计算必须等第 t-1 步算完才能开始,这意味着:
- 无法并行计算:序列有多长,就必须串行多少步,GPU 的并行算力被严重浪费。
- 训练时间极长:长序列场景下训练成本急剧上升,模型规模难以做大。
- 长距离依赖衰减:信息在逐步传递中容易丢失,越远的词对当前词的影响越弱。
虽然 LSTM、GRU 通过门控机制缓解了梯度消失问题,但「串行」这一根本约束并没有被打破。
1.2 传统 Word2Vec 的局限:静态词向量无法处理一词多义
Word2Vec 训练完成后,每个词对应一个固定的向量,无论出现在什么语境中都完全相同。这就带来一个典型问题:一词多义。
例如「机车」这个词,在「他骑着一辆机车」中指交通工具,在「这个说法很机车」中则带有完全不同的语义色彩。静态词向量只能给出同一个表示,模型无法区分。
本质上,Word2Vec 学到的是「词」的表示,而不是「词在具体上下文中的表示」。
1.3 BERT 的破局:双向编码 + 高度并行
BERT(Bidirectional Encoder Representations from Transformers)给出的答案是:
- 基于 Transformer 的编码器:抛弃循环结构,用 Self-Attention 让序列中任意两个位置直接建立联系,天然支持并行计算。
- 双向上下文建模:每个词的表示同时融合左侧和右侧的上下文信息,从根本上解决一词多义。
- 预训练 + 微调范式:在大规模无标注语料上预训练通用语言表示,再针对下游任务轻量微调。
一句话概括:BERT 把「静态词向量」升级成了「动态上下文表示」,同时把「串行训练」升级成了「并行训练」。
二、BERT 的核心概念
2.1 预训练任务一:MLM(Masked Language Model)
MLM 的思路是「完形填空」:随机 Mask 掉输入序列中 15% 的词,让模型根据上下文去预测这些被遮住的词。
- 被选中的 15% 中,80% 替换为
[MASK],10% 替换为随机词,10% 保持不变,避免预训练与微调阶段不匹配。 - 因为要预测被遮住的词,模型必须同时利用左右两侧的上下文,这正是「双向」的来源。
相比单向语言模型「只看左边预测右边」,MLM 逼着模型真正学习上下文语义。
2.2 预训练任务二:NSP(Next Sentence Prediction)
NSP 是一个二分类任务:判断句子 B 是否是句子 A 的真实下一句。输入格式为:
text
[CLS] 句子A [SEP] 句子B [SEP]
[CLS]位置的输出向量被用作整句/句对的聚合表示,下游分类任务直接接在它上面。[SEP]用于分隔两个句子。- 正样本取真实相邻句对,负样本随机采样另一篇文档的句子。
NSP 让模型学习句子间的关系,对问答、自然语言推理等句对任务有帮助。不过后续研究(如 RoBERTa)发现 NSP 作用有限,甚至去掉后效果更好,这一点在工程选型时值得注意。
2.3 双向性:与 GPT 的本质区别
| 对比项 | BERT | GPT |
|---|---|---|
| 结构 | Transformer Encoder | Transformer Decoder |
| 方向 | 双向(左右上下文同时看) | 单向(从左到右) |
| 预训练目标 | MLM + NSP | 自回归语言模型 |
| 擅长任务 | 理解类:分类、NER、抽取式问答 | 生成类:文本生成、对话 |
需要强调的是,BERT 的双向不是「两个单向模型拼接」,而是在同一层内让每个位置同时关注左右所有位置。
2.4 微调(Fine-tuning)
预训练好的 BERT 相当于一个通用的语言理解底座。做下游任务时,只需在 [CLS] 或各 token 输出之上接一个简单的输出层,用少量标注数据训练几个 epoch 即可:
- 文本分类 :取
[CLS]向量接全连接层 + Softmax。 - NER / 序列标注:取每个 token 的输出向量接分类层。
- 抽取式问答:预测答案在原文中的起始和结束位置。
这种「一次预训练、多任务复用」的范式,让 BERT 在多个榜单上刷新 SOTA,也大幅降低了每个任务的训练成本。
三、核心基石:Transformer 架构解析
BERT 的底座是 Transformer 的 Encoder 部分。要真正理解 BERT,必须先把 Transformer 的组件讲清楚。
3.1 Encoder-Decoder 框架
原始 Transformer 是为「序列到序列」任务设计的,例如机器翻译:输入英文序列,输出中文序列。
- Encoder:把输入序列编码成一组上下文相关的表示。
- Decoder:基于编码结果,自回归地逐词生成输出序列。
BERT 只取用 Encoder 堆叠(Base 为 12 层,Large 为 24 层),因为它的目标是「理解」而非「生成」。
3.2 Self-Attention:核心中的核心
Self-Attention 的核心思想是:让模型自己判断一句话里哪些词对当前词更重要,并据此做加权聚合。
Q、K、V 机制:每个词向量分别乘以三个可学习矩阵 WQ、WK、WV,得到:
- Query(查询向量):我要找什么。
- Key(键向量):我有什么特征等着被查。
- Value(值向量):我实际包含的内容。
计算流程:
- 计算 Q 与各个 K 的内积,得到匹配得分。
- 除以 √dk,防止数值过大导致 Softmax 梯度消失。
- 经过 Softmax 归一化,得到注意力权重。
- 将权重与 V 加权求和,得到输出特征 Z。
text
Attention(Q, K, V) = softmax(QK^T / sqrt(d_k)) V
可以把它理解为一个「带权重的加权求和」过程:权重由 Q 和 K 的匹配程度决定,求和的对象是 V。
3.3 Multi-Head Attention:多头注意力
单一注意力只能捕捉一种「关注模式」。Multi-Head Attention 类似 CNN 中使用多组卷积核提取不同特征:
- 使用多组(一般 8 个)不同的 WQ、WK、WV 矩阵,并行计算得到多个特征表达 Z0 ~ Z7。
- 将多个头的结果拼接(Concat)。
- 通过一个全连接层(FC)降维回原维度。
这样模型可以同时关注语法关系、指代关系、语义搭配等不同维度的信息。
3.4 其他关键组件
Positional Encoding(位置编码):Self-Attention 本身对词序不敏感,打乱顺序结果不变。Transformer 使用正弦(sin)和余弦(cos)函数生成位置编码,与 Word Embedding 逐点相加,既保留语义又注入位置信息。
text
PE(pos, 2i) = sin(pos / 10000^(2i/d_model))
PE(pos, 2i+1) = cos(pos / 10000^(2i/d_model))
Add & Normalize:残差连接(Add)缓解深层网络的梯度消失,LayerNorm 稳定训练分布。每个子层后都会执行一次。
Feed Forward:两层全连接神经网络,中间带 ReLU 激活,对每个位置独立作用,增强非线性表达能力。
一个 Encoder 层的典型数据流为:
四、实战与工程细节
4.1 BERT 模型家族与选型
| 模型 | 层数 | 隐藏维度 | 参数量 | 适用场景 |
|---|---|---|---|---|
| BERT-Base | 12 | 768 | 约 110M | 通用微调,性价比高 |
| BERT-Large | 24 | 1024 | 约 340M | 追求精度,算力充足 |
| BERT-Tiny 等小模型 | 2~4 | 128~256 | 极小 | 算力有限、边缘部署 |
除了 Base 和 Large,Google 还发布了 24 个较小的 BERT 模型(如 BERT-Tiny),适合算力有限的环境或对延迟敏感的场景。
4.2 环境配置踩坑
BERT 原生代码基于 TensorFlow 1.x 开发,环境配置是最容易卡住新手的环节。为了节约配置环境的时间,建议先用 CPU 版本跑通流程:
bash
pip install tensorflow==1.15 -i https://pypi.tuna.tsinghua.edu.cn/simple
提示:TensorFlow 1.x 对 Python 版本有要求(一般 3.7 及以下),建议使用 conda 单独创建虚拟环境,避免与现有环境冲突。若确实需要 GPU 版本,需匹配对应的 CUDA 与 cuDNN 版本。
4.3 数据准备
微调实验通常使用 GLUE 基准数据集,其中 MRPC(微软释义语料库)是常用的句对分类任务,适合快速验证流程是否跑通。数据会被转换为 BERT 要求的 [CLS] A [SEP] B [SEP] 格式,并统一到固定长度。
4.4 关键参数说明
| 参数 | 含义 | 实践建议 |
|---|---|---|
--uncased |
所有文本转为小写,Apple 与 apple 视为同一个词 | 大小写不敏感任务使用;NER 等对大小写敏感的任务用 cased |
--max_seq_length |
每句话的最大长度,超出截断,不足补齐 | 常用 128;长文本任务可调大,但显存占用随之上升 |
--train_batch_size |
训练批大小 | CPU 训练时尽量小(如 8 或 16),否则极易 OOM |
--learning_rate |
学习率 | 微调通常取 2e-5 ~ 5e-5,远小于从头训练 |
--num_train_epochs |
训练轮数 | 小数据集 3~4 个 epoch 即可,过多容易过拟合 |
4.5 一个最小可运行的微调命令
bash
python run_classifier.py \
--task_name=MRPC \
--do_train=true \
--do_eval=true \
--data_dir=./glue_data/MRPC \
--vocab_file=./uncased_L-12_H-768_A-12/vocab.txt \
--bert_config_file=./uncased_L-12_H-768_A-12/bert_config.json \
--init_checkpoint=./uncased_L-12_H-768_A-12/bert_model.ckpt \
--max_seq_length=128 \
--train_batch_size=8 \
--learning_rate=2e-5 \
--num_train_epochs=3.0 \
--output_dir=./mrpc_output
跑通这条命令后,你会得到评估指标与保存的模型文件,后续即可替换 data_dir 与 task_name 迁移到自己的业务数据上。
五、总结
回顾全文,BERT 的价值可以归纳为三点:
- 解决了并行问题:用 Transformer Encoder 替代 RNN,训练效率大幅提升,模型规模得以做大。
- 解决了上下文问题:通过 MLM 双向预训练,得到动态的上下文相关表示,真正处理一词多义。
- 解决了复用问题:预训练 + 微调范式让一个底座服务多种下游任务,工程落地成本显著降低。
理解 BERT,关键是把 Self-Attention 的 Q、K、V 计算流程吃透,再结合位置编码、残差与归一化理解整个 Encoder 的数据流。原理清楚之后,工程侧的重点就落在模型选型、环境版本匹配和超参数调优上。建议先按本文的最小示例跑通 MRPC 微调,再逐步迁移到自己的业务场景。