NLP

预训练语言模型 BERT 原理解析

基本理论学习

预训练语言模型 BERT 原理解析

内容总览

模块核心问题关键词
BERT 为什么重要为什么它会成为 NLP 通用方案预训练、微调、双向编码
BERT 的前置基础传统方法有什么局限Word2Vec、RNN、Transformer
Self-Attention 原理模型如何理解上下文关系Q、K、V、Softmax
Transformer 架构BERT 建立在哪个骨架上编码器、多头注意力、位置编码
BERT 训练方法模型如何学到语言知识MLM、NSP
下游任务使用预训练模型如何迁移到任务分类、阅读理解、微调

一句话理解 BERT

BERT(Bidirectional Encoder Representations from Transformers)本质上是一个基于 Transformer Encoder 的双向预训练语言模型。它先在大规模无标注文本上学习通用语言表示,再将这些表示迁移到分类、阅读理解、序列标注等下游任务中。

一、为什么要学 BERT

1. BERT 的定位

  • BERT 是自然语言处理中的通用解决方案之一,适合文本分类、情感分析、问答、句子关系判断等任务。
  • 它最大的价值不在于直接“做任务”,而在于先学会高质量的文本特征表达
  • 相比传统模型需要从头为每个任务设计特征,BERT 通过“预训练 + 微调”显著降低了任务开发门槛。
  • 从课程视角看,BERT 不只是一个模型名,更是一套**“先学语言、再做任务”**的通用方法论。

2. BERT 的几个关键优势

  • 双向上下文建模:同时利用左侧和右侧上下文,而不是只看单侧。
  • 上下文相关词向量:同一个词在不同句子里会得到不同表示。
  • 迁移能力强:先在大语料上训练,再迁移到具体任务。
  • 工程可用性强:官方与社区提供大量开源预训练模型和工具链。

3. 课程里的工程背景

课程里强调,BERT 不只是理论模型,也是一套可以直接落地的工程方案:

  • Google 提供了开源实现与预训练模型;
  • 常见代码入口包括 modeling.pyrun_classifier.pytokenization.py
  • 实际开发中重点通常不是“从零发明模型”,而是理解输入输出形式,再把预训练模型接到具体任务上;
  • 课程也特别强调应优先使用具备完整调试能力的 IDE,而不是只在 Notebook 里停留在演示层面。

4. 一个总览对比

方案主要优点主要问题BERT 相对优势
Word2Vec简单高效,能学词向量词向量静态,不看上下文能生成上下文相关表示
RNN适合序列建模串行计算、长依赖弱并行训练、全局建模
Transformer全局注意力、并行性强需要额外补位置信息为 BERT 提供最优骨架
BERT预训练 + 微调,通用性强训练成本高下游任务迁移效果强

二、BERT 之前的方法为什么不够好

1. RNN 的问题

RNN 适合处理序列,但有明显局限:

  • 每个时间步依赖上一步隐藏状态,天然串行,难以并行计算
  • 随着序列变长,早期信息容易衰减,长距离依赖建模能力有限
  • 网络层数通常不敢堆太深,训练效率和表达能力都受限制。

2. Word2Vec 的问题

Word2Vec 的词向量是静态的:

  • 一个词训练完以后向量固定不变。
  • 无法很好处理一词多义。
  • 不能根据上下文动态调整语义表示。

例如同一个词出现在不同语境时,真正需要的表示应该不同,而静态词向量做不到这一点。

3. Transformer 为什么成为突破口

Transformer 引入 Self-Attention 后:

  • 可以一次看完整个序列;
  • 可以直接建模任意两个词之间的关系;
  • 可以通过矩阵运算实现并行训练。

这为 BERT 提供了理想的底层架构。

三、Transformer 是 BERT 的基础骨架

1. Transformer 的整体结构

经典 Transformer 由两部分组成:

  • Encoder(编码器)
  • Decoder(解码器)

BERT 只使用其中的 Encoder 部分,因为它的目标是获得输入文本的高质量表示,而不是像机器翻译那样逐词生成输出。

2. 为什么 BERT 只用 Encoder

  • Encoder 可以看到整个输入句子。
  • 它更适合做特征抽取与语义编码。
  • ==BERT 的核心任务是“理解文本”,不是“自回归生成文本”==。

可以把 Transformer 的两部分粗略区分为:

  • Encoder 更偏“理解输入”
  • Decoder 更偏“根据已有信息一步步生成输出”

因此,BERT 只保留 Encoder,是因为它的重点是学到高质量文本表示;如果任务目标主要是生成文本,那么通常更依赖 Decoder 结构。

因此可以把 BERT 理解为:

用 Transformer 的编码器结构,专门训练一个强大的文本理解器。

四、Self-Attention 是 BERT 的核心机制

1. Self-Attention 在做什么

Self-Attention 的目标是:

计算序列中每个词与其他所有词之间的关系强弱,再融合全局上下文,生成新的词表示。

这意味着一个词最终的编码不只由它自己决定,还会受到全句其他词的影响。

可以把 Self-Attention 理解成:句子里每个词都在问,“为了更好理解我自己,我应该重点参考句子中的哪些词?”

例如句子:

小明把书放在桌子上,然后他离开了教室。

当模型处理“他”这个词时,它会和“小明”“书”“桌子”“教室”等词都计算相关性。如果最终“小明”的权重最高,模型就更容易知道“他”指代的是“小明”。

这也是 Self-Attention 能处理代词指代和长距离依赖的直观原因。

2. Q、K、V 三个矩阵的含义

输入词向量经过三个不同线性变换后,得到:

  • Query(Q):当前词“想关注什么”
  • Key(K):当前词“能提供什么索引信息”
  • Value(V):当前词真正承载的特征内容

可以粗略理解为:

  • Q 负责发问
  • K 负责匹配
  • V 负责提供信息

3. Self-Attention 的计算流程

第一步:词向量输入

  • 先把词或字转换成 embedding 向量。
  • 每个位置都会得到一个基础表示。

第二步:生成 Q、K、V

  • 每个输入向量分别乘以参数矩阵,得到对应的 q、k、v。

第三步:计算相关性分数

  • 用当前词的 q 与所有词的 k 做内积。
  • 内积越大,说明当前词与对方词越相关。

第四步:通过 Softmax 转成权重

  • 原始分数只是”强弱”,还不能直接做加权。
  • 用 Softmax 把它们归一化成概率分布。
  • 权重越大,说明那个词对当前词编码影响越大。

第五步:对 V 加权求和

  • 用这些权重对所有 value 向量做加权求和。
  • 得到当前词新的上下文表示。

如果把整个过程压缩成一句话,就是:

先用 Q 和所有 K 算”我该关注谁”,再用 Softmax 把关注程度变成权重,最后对所有 V 做加权求和,得到新的上下文表示。

也可以进一步记成:

  • Q / K 决定”看谁”
  • Softmax 决定”看多少”
  • V 决定”真正拿到什么信息”

5. Self-Attention 的数学表达

给定输入序列 X={x1,x2,...,xn}X = \lbrace x_1, x_2, ..., x_n \rbrace,其中每个 xiRdmodelx_i \in \mathbb{R}^{d_{model}}

线性变换: Q=XWQ,K=XWK,V=XWVQ = XW^Q, \quad K = XW^K, \quad V = XW^V

其中 WQ,WKRdmodel×dkW^Q, W^K \in \mathbb{R}^{d_{model} \times d_k}WVRdmodel×dvW^V \in \mathbb{R}^{d_{model} \times d_v}

注意力输出: Attention(Q,K,V)=softmax(QKTdk)V\text{Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V

逐位置展开形式: 对于位置 ii 的输出: outputi=j=1nαijvj\text{output}_i = \sum_{j=1}^{n} \alpha_{ij} v_j

其中注意力权重: αij=exp(qikj/dk)k=1nexp(qikk/dk)\alpha_{ij} = \frac{\exp(q_i \cdot k_j / \sqrt{d_k})}{\sum_{k=1}^{n} \exp(q_i \cdot k_k / \sqrt{d_k})}

dk\sqrt{d_k} 缩放因子的作用:

  • 防止 QKTQK^T 内积结果过大,导致 Softmax 进入梯度消失区
  • 使训练更稳定,梯度更容易回传

4. Self-Attention 带来的本质变化

  • 词表示不再固定,而是依赖上下文动态生成
  • 每个词都可以直接与全句其他词交互。
  • 可以更好处理代词指代、语义歧义、长距离依赖等问题。

五、Softmax、多头注意力与特征增强

1. Softmax 的作用

Self-Attention 中先得到相关性分数,但这些分数不能直接表示“贡献比例”。

Softmax 的作用就是:

  • 把分数变成可比较的权重;
  • 突出更重要的词;
  • 保证所有权重之和为 1。

2. Multi-Head Attention 为什么有效

如果只用一组 Q/K/V,模型只能从一种角度理解句子。

多头注意力通过多组独立的 Q/K/V:

  • 并行学习多种关系模式;
  • 有的头更关注语法关系;
  • 有的头更关注语义搭配;
  • 有的头更关注远距离依赖。

可以把多头注意力理解成:同时安排多个“观察员”从不同角度分析同一句话。

  • 有的观察员更关注句法结构;
  • 有的观察员更关注语义搭配;
  • 有的观察员更关注长距离依赖;
  • 有的观察员更关注整句主题。

最终将多个头的结果拼接,再通过线性层融合,得到更丰富的表示。

3. Multi-Head Attention 的数学表达

设有 hh 个注意力头,每个头学习独立的参数:

ii 个头的输出: headi=Attention(QWiQ,KWiK,VWiV)\text{head}_i = \text{Attention}(QW_i^Q, KW_i^K, VW_i^V)

其中 WiQ,WiKRdmodel×dkW_i^Q, W_i^K \in \mathbb{R}^{d_{model} \times d_k}WiVRdmodel×dvW_i^V \in \mathbb{R}^{d_{model} \times d_v}

多头拼接与输出投影: MultiHead(Q,K,V)=Concat(head1,...,headh)WO\text{MultiHead}(Q, K, V) = \text{Concat}(\text{head}_1, ..., \text{head}_h) W^O

其中 WORhdv×dmodelW^O \in \mathbb{R}^{h d_v \times d_{model}} 是输出投影矩阵。

典型维度设置(BERT Base):

  • dmodel=768d_{model} = 768
  • h=12h = 12(12 个头)
  • dk=dv=64d_k = d_v = 64(每头 64 维)
  • 验证:12×64=768=dmodel12 \times 64 = 768 = d_{model}

4. 为什么要堆叠多层

单层注意力只能做一次上下文融合。

多层堆叠后:

  • 底层学局部关系;
  • 中层学短语和句法结构;
  • 高层学更抽象的语义信息。

这也是 BERT 能形成深层语言理解能力的关键。

六、位置编码、残差连接与归一化

1. 为什么需要位置编码

Self-Attention 只看词与词的关系,不天然保留顺序。

因为 Attention 本身只知道”谁和谁相关”,却不知道”谁在前、谁在后”。

如果没有位置编码,模型很难区分:

  • “今天下午去打篮球”
  • “下午去打篮球今天”

因此必须显式加入位置信息。

2. 位置编码怎么加入

经典 Transformer 中:

  • 为每个位置生成一个位置向量;
  • 再与词向量直接相加。

这样模型在关注词语内容的同时,也能感知词语在句子中的顺序位置。

3. 位置编码的数学表达(原始 Transformer)

原始 Transformer 使用正弦/余弦函数生成位置编码:

对于位置 pos{0,1,...,n1}pos \in \lbrace 0, 1, ..., n-1 \rbrace 和维度 ii

PE(pos,2i)=sin(pos100002i/dmodel)PE(pos, 2i) = \sin\left(\frac{pos}{10000^{2i/d_{model}}}\right) PE(pos,2i+1)=cos(pos100002i/dmodel)PE(pos, 2i+1) = \cos\left(\frac{pos}{10000^{2i/d_{model}}}\right)

这种设计的好处:

  • 不同位置产生不同的编码;
  • 相对位置关系可以被学习(因为 PE(pos+k)PE(pos+k) 可以表示为 PE(pos)PE(pos) 的线性函数);
  • 可以外推到比训练时更长的序列。

BERT 的位置编码:

BERT 不使用上述三角函数形式,而是学习一个可训练的位置嵌入矩阵 EposRL×dmodelE_{pos} \in \mathbb{R}^{L \times d_{model}},其中 LL 是最大序列长度(如 BERT Base 为 512)。

输入表示为: Inputi=Etoken,i+Epos,i+Esegment,i\text{Input}_i = E_{token,i} + E_{pos,i} + E_{segment,i}

其中:

  • EtokenE_{token}:词/token 嵌入
  • EposE_{pos}:位置嵌入
  • EsegmentE_{segment}:句子 A/B 段嵌入(用于区分句对任务)

3. 残差连接的作用

每个子层后都会保留原始输入,与子层输出相加。

如果每一层都完全覆盖上一层信息,深层网络会越来越难训练,因此残差连接相当于给原始信息保留了一条直通通道。

作用包括:

  • 避免深层网络退化;
  • 缓解梯度消失;
  • 保留原始信息,方便模型自主选择是否使用新特征。

4. 残差连接的数学表达

对于任意子层(如 Self-Attention 或 Feed-Forward),输出为:

Output=LayerNorm(x+SubLayer(x))\text{Output} = \text{LayerNorm}(x + \text{SubLayer}(x))

其中:

  • xx 是子层输入
  • SubLayer(x)\text{SubLayer}(x) 是子层的变换(如 Self-Attention)
  • 加法 x+SubLayer(x)x + \text{SubLayer}(x) 是残差连接
  • LayerNorm 在残差相加后应用

梯度回传视角:

损失函数对 xx 的梯度: Lx=LOutput(1+SubLayer(x)x)\frac{\partial \mathcal{L}}{\partial x} = \frac{\partial \mathcal{L}}{\partial \text{Output}} \cdot \left(1 + \frac{\partial \text{SubLayer}(x)}{\partial x}\right)

其中的 11 保证即使 SubLayerx\frac{\partial \text{SubLayer}}{\partial x} 很小,梯度也能直接回传。

4. Layer Normalization 的作用

LayerNorm 会对单个样本内部的特征做归一化。

它可以让每层输出的数值分布更稳定,减少训练过程中的震荡,帮助深层模型更容易收敛。

它的作用是:

  • 稳定训练;
  • 加快收敛;
  • 让深层网络更容易优化。

5. Layer Normalization 的数学表达

给定输入向量 xRdx \in \mathbb{R}^d,LayerNorm 的计算为:

μ=1di=1dxi\mu = \frac{1}{d} \sum_{i=1}^{d} x_i σ2=1di=1d(xiμ)2\sigma^2 = \frac{1}{d} \sum_{i=1}^{d} (x_i - \mu)^2 x^i=xiμσ2+ϵ\hat{x}_i = \frac{x_i - \mu}{\sqrt{\sigma^2 + \epsilon}} yi=γx^i+βy_i = \gamma \hat{x}_i + \beta

其中:

  • μ\mu 是均值
  • σ2\sigma^2 是方差
  • ϵ\epsilon 是很小的常数(如 10610^{-6}),防止除零
  • γ,βRd\gamma, \beta \in \mathbb{R}^d 是可学习的缩放和平移参数
  • yy 是归一化后的输出

与 BatchNorm 的关键区别:

BatchNorm 在批次维度上计算统计量: μj=1mi=1mxij\mu_j = \frac{1}{m} \sum_{i=1}^{m} x_{ij}

LayerNorm 在特征维度上计算统计量: μi=1dj=1dxij\mu_i = \frac{1}{d} \sum_{j=1}^{d} x_{ij}

因此 LayerNorm 不依赖批次大小,更适合序列模型。

5. Layer Normalization vs Batch Normalization

对比维度LayerNormBatchNorm
归一化方向对单个样本的所有特征做归一化对一批样本的同一特征做归一化
依赖批次大小不依赖,单样本即可计算依赖,批次太小效果会变差
适用场景RNN、Transformer、BERTCNN 图像任务更常见
训练/测试一致性训练和测试行为一致训练用批次统计,测试用移动平均
为什么 BERT 用 LayerNorm序列长度可变、批次可能较小,LayerNorm 更稳定图像任务中批次通常较大且固定

直观理解:

  • BatchNorm 像是"横向"归一化——看一批样本中同一个特征的平均和方差;
  • LayerNorm 像是"纵向"归一化——看单个样本内部所有特征的平均和方差。

对于 BERT 这种处理变长序列的模型,LayerNorm 不需要依赖批次统计量,因此更适合。

七、BERT 到底学到了什么

1. BERT 学的不是“标签”,而是语言表示

BERT 预训练阶段不依赖人工标注类别,而是通过设计自监督任务,让模型从海量文本中学习:

  • 词与词的关系
  • 句与句的关系
  • 上下文中的语义表达

2. BERT 输出的是上下文相关表示

和 Word2Vec 最大区别在于:

  • Word2Vec:每个词一个固定向量
  • BERT:同一个词在不同上下文下向量不同

这使得 BERT 更适合真实语言理解任务。

八、BERT 的两大预训练任务

1. MLM:Masked Language Model

核心思想

随机遮住输入中一部分词,让模型根据上下文预测原词。

课程中的关键点

  • 通常随机选取 15% 的词进行处理。
  • 被选中的词不一定全部替换成 [MASK],也可能:
    • 替换成 [MASK]
    • 替换成其他词
    • 保持原词不变

中文场景下怎么理解

在中文场景里,很多实现会更自然地以"字"或子词(subword)作为基本处理单位,因此 mask 时常常表现为对单字或若干子词进行遮蔽。

这说明 MLM 并不是死记某个完整单词,而是在当前 token 粒度上学习上下文恢复能力。

它训练了什么能力

MLM 迫使模型同时利用左右上下文,因此能学到真正的双向语义表示

例如模型看到一句话中间某个词被遮住,只有理解整句语义,才能较准确地恢复原词。

MLM 的数学表达

给定输入序列 X={x1,...,xn}X = \lbrace x_1, ..., x_n \rbrace,随机 mask 部分位置 M{1,...,n}M \subset \lbrace 1, ..., n \rbrace

模型预测: P(xiXM)=softmax(Wvocabhi+b)P(x_i | X_{\setminus M}) = \text{softmax}(W_{vocab} \cdot h_i + b)

其中:

  • XMX_{\setminus M} 是被 mask 后的输入
  • hih_i 是 BERT 对位置 ii 的输出表示
  • WvocabRV×dmodelW_{vocab} \in \mathbb{R}^{|V| \times d_{model}} 是词汇表投影矩阵
  • V|V| 是词表大小(BERT Base 约 30522)

损失函数(交叉熵): LMLM=iMlogP(xiXM)\mathcal{L}_{MLM} = -\sum_{i \in M} \log P(x_i^* | X_{\setminus M})

其中 xix_i^* 是位置 ii 的真实词。

15% mask 策略的细节:

对被选中的 15% token:

  • 80% 替换为 [MASK]
  • 10% 替换为随机词
  • 10% 保持不变

这样设计的原因:

  • 如果总是用 [MASK],训练和微调分布不一致
  • 随机替换和保持不变让模型不能过度依赖 [MASK] 标记

2. NSP:Next Sentence Prediction

核心思想

给模型两个句子,判断第二个句子是不是第一个句子的下一句。

输入形式

  • 使用 [CLS] 放在句首,作为分类汇总位置;
  • [SEP] 分隔两个句子;
  • 最终基于 [CLS] 对应向量做二分类判断。

它训练了什么能力

  • 学习句子间连贯性;
  • 学习段落级语义关系;
  • 为问答、检索、句对匹配等任务提供基础。

NSP 的数学表达

给定句子对 (SA,SB)(S_A, S_B),输入格式为:

[CLS],SA,[SEP],SB,[SEP][CLS], S_A, [SEP], S_B, [SEP]

模型预测: P(IsNextSA,SB)=σ(Wnsph[CLS]+b)P(\text{IsNext} | S_A, S_B) = \sigma(W_{nsp} \cdot h_{[CLS]} + b)

其中:

  • h[CLS]h_{[CLS]}[CLS] 位置的输出表示
  • WnspR2×dmodelW_{nsp} \in \mathbb{R}^{2 \times d_{model}} 是分类参数
  • σ\sigma 是 sigmoid 函数(二分类)

损失函数: LNSP=E(SA,SB)[logP(ySA,SB)]\mathcal{L}_{NSP} = -\mathbb{E}_{(S_A, S_B)}\left[\log P(y^* | S_A, S_B)\right]

其中 y{IsNext,NotNext}y^* \in \lbrace \text{IsNext}, \text{NotNext} \rbrace 是真实标签。

总预训练损失: Ltotal=LMLM+LNSP\mathcal{L}_{total} = \mathcal{L}_{MLM} + \mathcal{L}_{NSP}

九、BERT 中几个特殊标记的作用

1. [CLS] —— 全局表示聚合标记

位置: 始终放在输入序列的开头。

核心作用: 聚合整句或句对的全局语义表示,用于句子级任务。

为什么 [CLS] 能代表整句?

在 Self-Attention 机制中,[CLS] 位置的 Query 会与序列中所有 token 的 Key 计算注意力权重:

Attention(q[CLS],K,V)=j=1nα[CLS],jvj\text{Attention}(q_{[CLS]}, K, V) = \sum_{j=1}^{n} \alpha_{[CLS],j} \cdot v_j

其中: α[CLS],j=exp(q[CLS]kj/dk)m=1nexp(q[CLS]km/dk)\alpha_{[CLS],j} = \frac{\exp(q_{[CLS]} \cdot k_j / \sqrt{d_k})}{\sum_{m=1}^{n} \exp(q_{[CLS]} \cdot k_m / \sqrt{d_k})}

这意味着 [CLS] 的输出表示是全句所有 token 的加权和,权重由注意力机制学习得到。经过多层 Transformer 堆叠后,[CLS] 最终编码了整句的语义信息。

典型使用场景:

任务使用方式
文本分类h[CLS]h_{[CLS]} 接 softmax 分类层
NSP(下一句预测)h[CLS]h_{[CLS]} 接二分类层
句子相似度取句对的 h[CLS]h_{[CLS]} 计算相似度
情感分析h[CLS]h_{[CLS]} 接情感分类层

输入示例:

单句输入:

[CLS] 今 天 天 气 很 好 [SEP]

句对输入(如 NSP):

[CLS] 今 天 天 气 很 好 [SEP] 我 想 出 去 散 步 [SEP]
       └── 句子 A ──┘         └── 句子 B ──┘

2. [SEP] —— 句子分隔标记

位置: 放在句子末尾,用于分隔句子或标记序列结束。

核心作用:

  • 明确标识句子边界
  • 配合 Segment Embedding 区分不同句子
  • 在某些任务中标记序列结束

在 NSP 任务中的作用:

NSP 需要判断句子 B 是否是句子 A 的下一句,输入格式为:

[CLS] 句子 A [SEP] 句子 B [SEP]

模型通过 [SEP] 识别:

  • 句子 A 从哪里结束
  • 句子 B 从哪里开始
  • 两个句子的相对位置关系

在阅读理解中的作用:

输入"文章 + 问题"时:

[CLS] 文章段落 [SEP] 问题内容 [SEP]
       └─ 文章 ──┘   └─ 问题 ─┘

[SEP] 帮助模型区分:

  • 哪部分是待定位的文章
  • 哪部分是查询问题

Segment Embedding 配合:

BERT 为每个 token 添加句子段嵌入 EsegmentE_{segment}

  • 句子 A 的 token(包括 [CLS] 和第一个 [SEP]):EAE_A
  • 句子 B 的 token(包括第二个 [SEP]):EBE_B

完整输入表示: Inputi=Etoken,i+Epos,i+Esegment,i\text{Input}_i = E_{token,i} + E_{pos,i} + E_{segment,i}

这样模型可以明确区分两个句子的身份。

单句任务中的 [SEP]

即使只有一个句子,末尾也需要加 [SEP]

[CLS] 这 是 一 个 句 子 [SEP]

这是为了保持输入格式的一致性。


3. [MASK] —— 掩码标记

位置: 预训练时随机替换输入中的部分 token。

核心作用: 让模型通过上下文预测被遮蔽的词,训练双向语义理解能力。

15% Mask 策略:

对被选中的 15% token:

  • 80% → 替换为 [MASK]
  • 10% → 替换为随机词
  • 10% → 保持不变(不替换)

为什么这样设计?

策略原因
80% 用 [MASK]让模型学习根据上下文预测
10% 随机替换防止模型过度依赖 [MASK] 标记
10% 保持不变让模型学会"不需要预测"的情况,同时使训练/微调分布一致

如果总是用 [MASK],微调时没有这个标记会导致分布不一致,影响迁移效果。

[MASK] 的数学表达:

给定被 mask 的位置集合 MM,模型预测: P(xiXM)=softmax(Wvocabhi+b)P(x_i | X_{\setminus M}) = \text{softmax}(W_{vocab} \cdot h_i + b)

其中 hih_i 是位置 ii 的输出,它编码了左右双向的上下文信息。

十、BERT 如何用于下游任务

课程里强调的一个关键思想是端到端训练

  • 传统方法可能先单独训练词向量,再把词向量交给分类器;
  • 在 BERT 范式下,文本编码和下游任务训练通常放在同一个框架中联合优化;
  • 也就是说,BERT 不只是提供静态特征,而是会和任务头一起被微调。

1. 文本分类

典型流程:

  • 输入文本前加 [CLS]
  • 文本经过 BERT 编码;
  • [CLS] 的输出向量;
  • 接一个线性分类层完成情感分析、主题分类等任务。

文本分类的数学表达

给定输入序列 XX,BERT 输出 [CLS] 位置的表示 h[CLS]h_{[CLS]}

分类预测: P(yX)=softmax(Wch[CLS]+bc)P(y | X) = \text{softmax}(W_c \cdot h_{[CLS]} + b_c)

其中:

  • WcRC×dmodelW_c \in \mathbb{R}^{C \times d_{model}}CC 是类别数
  • h[CLS]Rdmodelh_{[CLS]} \in \mathbb{R}^{d_{model}}(BERT Base 为 768 维)

损失函数: Lcls=c=1CyclogP(y=cX)\mathcal{L}_{cls} = -\sum_{c=1}^{C} y_c^* \log P(y=c | X)

其中 ycy_c^* 是真实标签的 one-hot 编码。

微调时的参数更新:

同时更新 BERT 参数 θBERT\theta_{BERT} 和分类层参数 θc\theta_cθBERT,θcθBERT,θcηθBERT,θcLcls\theta_{BERT}, \theta_c \leftarrow \theta_{BERT}, \theta_c - \eta \nabla_{\theta_{BERT}, \theta_c} \mathcal{L}_{cls}


文本分类任务详解

任务定义: 给定一段文本,判断它属于哪个类别(如情感正/负、新闻主题、垃圾邮件识别等)。

输入格式:

单句分类:

[CLS] 这 部 电 影 太 精 彩 了 [SEP]
  │                       │
  └── 整句表示 → 分类标签   │
                          │
              Segment A ←─┘

句对分类(如语义相似度):

[CLS] 句子 A [SEP] 句子 B [SEP]  →  是否相似/蕴含关系

模型架构:

输入文本
    │
    ▼
┌─────────────────────────┐
│      BERT Encoder       │  12 层 Transformer (Base)
│  (双向上下文编码)        │  或 24 层 (Large)
└─────────────────────────┘
    │
    ▼
  [CLS] 向量  (768 维)
    │
    ▼
┌─────────────────────────┐
│   线性分类层 (Dense)    │  W_c: 768 → C
│   + Dropout (可选)      │
└─────────────────────────┘
    │
    ▼
  Softmax → 类别概率分布

训练流程(一个 batch):

  1. 数据准备:

    • 输入文本 tokenize → token IDs
    • 添加 [CLS], [SEP]
    • 生成 Attention Mask(区分真实 token 和 padding)
    • 生成 Segment IDs(句对任务)
  2. 前向传播:

    token_ids → BERT → [CLS]_embedding → Dense → logits → softmax → probs
    
  3. 损失计算: L=1Ni=1Nc=1Cyi,clogy^i,c\mathcal{L} = -\frac{1}{N}\sum_{i=1}^{N}\sum_{c=1}^{C} y_{i,c}^* \log \hat{y}_{i,c}

  4. 反向传播:

    • 计算梯度 θBERT,θc\nabla_{\theta_{BERT}}, \nabla_{\theta_c}
    • 同时更新 BERT 和分类层参数

典型超参数设置:

参数BERT Base说明
层数12Transformer 层数
dmodeld_{model}768隐藏层维度
学习率2e-5 ~ 5e-5比预训练小 10 倍
Batch Size16 ~ 32受显存限制
Epoch3 ~ 10数据少时用更多 epoch
Dropout0.1 ~ 0.3防止过拟合

训练技巧:

  • 学习率预热(Warmup): 前 10% 步数线性增加学习率,再衰减
  • 梯度裁剪(Gradient Clipping): 防止梯度爆炸,常用 max_norm=1.0
  • 分层学习率: BERT 底层用更小学习率,顶层用较大学习率
  • 早停(Early Stopping): 验证集性能不再提升时停止

代码伪代码示例:

# 前向传播
outputs = bert_model(
    input_ids=input_ids,      # [batch, seq_len]
    attention_mask=attn_mask, # [batch, seq_len]
    token_type_ids=seg_ids    # [batch, seq_len] (句对任务)
)

cls_embedding = outputs.last_hidden_state[:, 0, :]  # 取 [CLS]
logits = classifier(cls_embedding)                  # [batch, num_labels]
loss = cross_entropy(logits, labels)

# 反向传播
loss.backward()
optimizer.step()

多标签分类 vs 多类别分类

多类别(单标签):

  • 输出互斥,用 softmax
  • 如:情感分析(正/负/中性)

多标签:

  • 输出不互斥,用 sigmoid
  • 如:新闻主题(可同时属于"科技"和"财经")

P(ycX)=σ(Wch[CLS]+bc)P(y_c | X) = \sigma(W_c \cdot h_{[CLS]} + b_c)

损失用 Binary Cross-Entropy: L=c=1C[yclogy^c+(1yc)log(1y^c)]\mathcal{L} = -\sum_{c=1}^{C} \left[y_c^* \log \hat{y}_c + (1-y_c^*) \log (1-\hat{y}_c)\right]

2. 阅读理解(Machine Reading Comprehension)

课程中提到的做法是:

  • 输入”文章 + 问题”;
  • 模型分别预测答案的起始位置与终止位置;
  • 本质上是对序列中每个 token 做位置分类。

也就是说,BERT 不一定直接”生成答案文本”,而是从原文中定位答案片段

可以把它理解成两个位置分类问题:

  • 哪个 token 最可能是答案起点;
  • 哪个 token 最可能是答案终点。

训练时通常会分别学习一组”起始位置”和”终止位置”的判别参数,去和序列中每个 token 的表示计算得分。

一旦找到这两个位置,就能从原文中截取出答案片段。这也是课程里”阅读理解题”训练实例的核心思想。


阅读理解任务详解

任务定义(SQuAD 格式):

给定:

  • 文章(Context/Passage):D={d1,d2,...,dn}D = \lbrace d_1, d_2, ..., d_n \rbrace
  • 问题(Question):Q={q1,q2,...,qm}Q = \lbrace q_1, q_2, ..., q_m \rbrace

输出:

  • 答案在原文中的起止位置:(s,e)(s, e),其中 1sen1 \leq s \leq e \leq n

典型数据集:

  • SQuAD 1.1/2.0(Stanford Question Answering Dataset)
  • CMRC(中文机器阅读理解数据集)
  • DuReader(百度阅读理解数据集)

输入格式:

[CLS] 问题内容 [SEP] 文章段落 [SEP]
  │     │           │          │
  │     │           │          │
  │     │           └── 文章 ──┤
  │     │                      │
  │     └── 问题 ──────────────┤
  │                            │
  └──────── Segment A ─────────┤
                               │
                   Segment B ←─┘

注意: 实际实现中,问题和文章的顺序可能有两种:

  • [CLS] + Q + [SEP] + D + [SEP](常见于 HuggingFace)
  • [CLS] + D + [SEP] + Q + [SEP](部分实现)

两种格式都有效,关键是训练和测试保持一致。

模型架构:

┌─────────────────────────────────────────┐
│  问题:什么是机器学习?                  │
│  文章:机器学习是人工智能的一个分支...  │
└─────────────────────────────────────────┘
              │
              ▼
┌─────────────────────────────────────────┐
│           Tokenize + 拼接               │
│  [CLS] 什 么 是...[SEP] 机 器 学 习...[SEP] │
└─────────────────────────────────────────┘
              │
              ▼
┌─────────────────────────────────────────┐
│            BERT Encoder                 │
│  输出每个 token 的上下文表示 h_i         │
└─────────────────────────────────────────┘
              │
    ┌─────────┴─────────┐
    ▼                   ▼
┌──────────┐      ┌──────────┐
│ Start    │      │ End      │
│ Classifier│      │ Classifier│
│ W_start  │      │ W_end    │
└──────────┘      └──────────┘
    │                   │
    ▼                   ▼
  P_start(i)         P_end(j)
    │                   │
    └─────────┬─────────┘
              ▼
        答案:[s, e] = argmax P_start(s) × P_end(e)

阅读理解的数学表达

给定文章 D={d1,...,dn}D = \lbrace d_1, ..., d_n \rbrace 和问题 QQ,输入拼接为 [CLS],Q,[SEP],D,[SEP][CLS], Q, [SEP], D, [SEP]

BERT 输出每个位置的表示: H={h0,h1,...,hn+m+2}H = \lbrace h_0, h_1, ..., h_{n+m+2} \rbrace

其中只有文章部分的 token 参与答案预测(即 hm+2h_{m+2}hm+n+1h_{m+n+1})。

起始位置预测: Pstart(i)=softmax(Wstarthi+bstart)P_{start}(i) = \text{softmax}(W_{start} \cdot h_i + b_{start})

终止位置预测: Pend(i)=softmax(Wendhi+bend)P_{end}(i) = \text{softmax}(W_{end} \cdot h_i + b_{end})

其中:

  • hih_i 是序列中位置 ii 的输出表示
  • Wstart,WendR1×dmodelW_{start}, W_{end} \in \mathbb{R}^{1 \times d_{model}} 是起始/终止判别参数
  • Pstart(i)P_{start}(i) 表示位置 ii 是答案起点的概率

损失函数: Lqa=logPstart(s)logPend(e)\mathcal{L}_{qa} = -\log P_{start}(s^*) - \log P_{end}(e^*)

其中 s,es^*, e^* 是真实答案的起止位置。

预测时: s^,e^=argmaxse,esLmax(Pstart(s)Pend(e))\hat{s}, \hat{e} = \arg\max_{s \leq e, e-s \leq L_{max}} \left(P_{start}(s) \cdot P_{end}(e)\right)

通常约束答案长度(如 Lmax=30L_{max} = 30 个 token),避免预测过长的答案。


阅读理解训练流程

1. 数据准备:

对于每个训练样本 (D,Q,s,e)(D, Q, s^*, e^*)

  • Tokenize 问题和文章
  • 拼接为统一输入格式
  • 生成 Attention Mask
  • 生成 Segment IDs(区分问题段和文章段)
  • 将答案位置 (s,e)(s^*, e^*) 映射到 token 级别

2. 前向传播:

[问题 + 文章] → BERT → {h_0, h_1, ..., h_L}
                       │
           ┌───────────┴───────────┐
           ▼                       ▼
    W_start · h + b_start    W_end · h + b_start
           │                       │
           ▼                       ▼
       softmax                 softmax
           │                       │
           ▼                       ▼
    P_start(·)              P_end(·)

3. 损失计算:

L=1Ni=1N[logPstart(i)(si)logPend(i)(ei)]\mathcal{L} = \frac{1}{N}\sum_{i=1}^{N} \left[-\log P_{start}^{(i)}(s_i^*) - \log P_{end}^{(i)}(e_i^*)\right]

4. 反向传播:

同时更新 BERT 参数和两个分类器参数: θBERT,θstart,θendθηθL\theta_{BERT}, \theta_{start}, \theta_{end} \leftarrow \theta - \eta \nabla_\theta \mathcal{L}


训练技巧与注意事项

1. 最大序列长度:

BERT 限制最大长度为 512,但文章可能更长。处理策略:

  • 滑动窗口: 将长文章切分为多个片段,每个片段与问题拼接
  • 截断: 只保留文章前 512 个 token(可能丢失信息)
  • 稀疏注意力: 使用 Longformer 等支持长序列的模型

2. 无答案情况(SQuAD 2.0):

有些问题在文章中没有答案。处理方式:

  • 添加”无答案”分类头
  • 或用特殊位置(如 s=e=0s=e=0)表示无答案

3. 评估指标:

  • EM(Exact Match): 预测答案与真实答案完全匹配的比例
  • F1 Score: 预测答案与真实答案的 token 重叠度

4. 代码伪代码示例:

# 前向传播
outputs = bert_model(
    input_ids=input_ids,       # [batch, seq_len]
    attention_mask=attn_mask,  # [batch, seq_len]
    token_type_ids=seg_ids     # [batch, seq_len]
)

sequence_output = outputs.last_hidden_state  # [batch, seq_len, 768]

# 起始位置和终止位置预测
start_logits = linear_start(sequence_output).squeeze(-1)  # [batch, seq_len]
end_logits = linear_end(sequence_output).squeeze(-1)      # [batch, seq_len]

# 只考虑文章部分的 token(mask 掉问题和特殊 token)
start_logits = start_logits + attention_mask * -1e9
end_logits = end_logits + attention_mask * -1e9

# 损失计算
loss_fct = CrossEntropyLoss()
start_loss = loss_fct(start_logits, start_positions)
end_loss = loss_fct(end_logits, end_positions)
total_loss = (start_loss + end_loss) / 2

# 反向传播
total_loss.backward()
optimizer.step()

5. 预测后处理:

# 找到最可能的起止位置
start_idx = torch.argmax(start_logits, dim=-1)
end_idx = torch.argmax(end_logits, dim=-1)

# 约束:start <= end,且长度不超过阈值
for i in range(batch_size):
    if start_idx[i] > end_idx[i]:
        end_idx[i] = start_idx[i]  # 或跳过此样本
    
    # 截断过长答案
    if end_idx[i] - start_idx[i] > max_length:
        end_idx[i] = start_idx[i] + max_length

# 将 token 索引转回文本
answers = []
for i in range(batch_size):
    answer_tokens = tokens[start_idx[i]:end_idx[i]+1]
    answers.append(tokenizer.convert_tokens_to_string(answer_tokens))

阅读理解 vs 文本分类 对比

维度文本分类阅读理解
输入单句或句对文章 + 问题
输出类别标签答案起止位置
关键向量[CLS]全文每个 token
分类头1 个 Dense 层2 个 Dense 层(start/end)
损失函数交叉熵位置交叉熵 × 2
评估指标Accuracy/F1EM/F1
典型数据集IMDB, AG NewsSQuAD, CMRC

3. 其他任务

BERT 还可以迁移到:

  • 命名实体识别
  • 句子匹配
  • 问答系统
  • 信息抽取
  • 文本相似度计算

十一、BERT 的训练与使用思路

1. 预训练阶段

  • 在大规模无标注语料上训练;
  • 目标是学到通用语言知识;
  • 重点优化 Transformer Encoder 中的参数。

2. 微调阶段

  • 把预训练好的 BERT 接到具体任务头上;
  • 用相对较少的标注数据继续训练;
  • 模型参数会针对任务进一步调整。

这里的"微调"通常不只是训练最后一个分类器,而是让任务头和 BERT 参数一起更新,因此模型表示会越来越贴合当前任务。

3. 为什么这种范式有效

因为预训练阶段已经学到了大量通用语义结构,下游任务不再需要从零开始学习语言规律,只需要“在已有理解能力上适配任务”。

十二、BERT 的整体理解框架

可以用下面这条主线串起来:

  1. 传统方法存在瓶颈:RNN 难并行,Word2Vec 静态表示不足。
  2. Transformer 提供新骨架:依靠 Self-Attention 建模全局关系。
  3. BERT 继承 Encoder 结构:专注做上下文语义编码。
  4. 通过 MLM + NSP 预训练:在无监督语料中学习语言知识。
  5. 通过微调适配任务:把通用表示迁移到分类、问答等具体应用。

十三、易混淆点总结

概念容易混淆点正确认识
BERT vs Transformer以为 BERT 等于完整 TransformerBERT 主要使用 Transformer 的 Encoder
BERT vs Word2Vec都是词向量模型Word2Vec 是静态词向量,BERT 是上下文相关表示
Self-Attention vs 普通加权以为只是简单加权平均Self-Attention 是基于 Q/K/V 与 Softmax 的动态关系建模
MLM以为只是遮词填空技巧本质是训练双向上下文理解能力
NSP以为只是句子分类本质是训练句间关系建模能力
[CLS]以为只是普通 token它通常承担整句级任务表示与分类功能

十四、适合复习时记住的关键结论

  • BERT = Transformer Encoder + 双向预训练
  • 核心机制是 Self-Attention,不是 RNN。
  • 核心优势是上下文相关表示,不是静态词向量。
  • 核心训练任务是 MLM 和 NSP
  • 核心应用方式是预训练 + 微调

十五、我的理解版总结

如果用最朴素的话解释 BERT:

它先通过海量文本训练出一个“会结合上下文理解句子”的编码器,再把这个编码器迁移到各种自然语言任务中,因此效果强、适配广、工程价值高。

也可以进一步把它理解成:

  • Transformer 解决了“怎么高效看全句”的问题;
  • BERT 解决了“怎么学到通用语言表示”的问题;
  • 预训练 + 微调解决了“怎么把通用能力迁移到具体任务”的问题。

相关笔记

NLP

预训练语言模型BERT源码解读

预训练语言模型BERT谷歌老版的源码解读

打开笔记
预训练语言模型 BERT 原理解析 | 三火