Skip to main content

11.6.3 BERT 系列

BERT Masked Language Model 图

本节定位

BERT 是现代 NLP 进入“预训练大一统时代”的关键节点之一。 很多今天你看到的大模型概念,虽然形态已经演化,但不少理解基础都能从 BERT 身上找到。

学习目标

  • 理解 BERT 为什么会成为 NLP 的里程碑
  • 说清楚 BERT 和 GPT 这类自回归模型的核心区别
  • 掌握 [CLS][SEP][MASK]、双向上下文这些关键概念
  • 看懂一个最小 BERT 输入示例
  • 理解 BERT 常见的微调方式

历史背景:BERT 来自哪篇论文?

这一节最关键的历史节点是:

年份论文关键作者它最重要地解决了什么
2018BERT: Pre-training of Deep Bidirectional Transformers for Language UnderstandingDevlin 等把双向 Transformer 预训练 + 微调做成现代 NLP 理解任务的主线

对新人来说,最值得先记的是:

  • BERT 不是“又一个模型名”
  • 它代表的是一种非常重要的范式变化:

先在海量文本上做通用预训练,再把同一个底座微调到不同任务上。

这也是为什么你今天学大模型时,很多“先预训练、再适配”的感觉,会在 BERT 这里看到非常清楚的雏形。


一、BERT 到底解决了什么问题?

先看老问题:词义依赖上下文

单词不是总有固定意思。

例如英文里的 bank

  • “river bank” 是河岸
  • “bank account” 是银行

中文里也一样:

  • “苹果很好吃” 里的苹果是水果
  • “苹果发布了新设备” 里的苹果是公司

如果模型只能给每个词一个固定向量,就会很吃力。

BERT 的关键突破

BERT 的核心贡献之一是:

让一个词的表示真正依赖上下文。

也就是说,同一个词在不同句子里,可以得到不同的表示。

这就是“上下文化表示(contextual representation)”。

一个更适合新人的总类比

你可以把 BERT 理解成:

  • 一个读句子时会前后都看的“精读型选手”

它不像早期静态词向量那样只给词发一个固定名片, 而更像:

  • 同一个词放在不同句子里,会重新理解它现在扮演的角色

这就是为什么 BERT 特别适合理解类任务。


二、为什么 BERT 会被称为“双向”模型?

双向是什么意思?

看一句话:

“我昨天在银行旁边散步”

理解“银行”时,人并不会只看前面的“我昨天在”,也会看后面的“旁边散步”。

BERT 的重要特点就是:

当前 token 的表示,同时利用左边和右边的上下文。

和 GPT 的核心区别

粗略地说:

  • BERT:更偏理解,双向看上下文
  • GPT:更偏生成,只看左边历史

所以:

  • 做分类、抽取、匹配时,BERT 很强
  • 做续写、对话、生成时,GPT 路线更自然

三、BERT 的输入到底长什么样?

三个特别常见的特殊 token

token作用
[CLS]句子级任务的聚合位置
[SEP]句子分隔符
[MASK]预训练时被遮住的位置

一个最小输入例子

tokens = ["[CLS]", "我", "爱", "自", "然", "语", "言", "处", "理", "[SEP]"]
print(tokens)
print("序列长度:", len(tokens))

预期输出:

['[CLS]', '我', '爱', '自', '然', '语', '言', '处', '理', '[SEP]']
序列长度: 10

两个边界 token 也会算进真实序列长度。模型成本和 attention mask 都按完整 token 序列计算,不只按你肉眼看到的词计算。

如果是句对任务,比如问句匹配:

tokens = [
"[CLS]", "今", "天", "天", "气", "怎", "么", "样", "[SEP]",
"北", "京", "今", "天", "会", "下", "雨", "吗", "[SEP]"
]
print(tokens)

预期输出:

['[CLS]', '今', '天', '天', '气', '怎', '么', '样', '[SEP]', '北', '京', '今', '天', '会', '下', '雨', '吗', '[SEP]']

注意句对任务会有两个 [SEP]:一个结束句子 A,一个结束句子 B。真实 BERT 输入里还会用 segment id 帮模型区分两边。

一个很适合初学者先记的输入结构表

组件最值得先记住的作用
[CLS]句子级任务的聚合位置
[SEP]句子边界分隔
[MASK]预训练时要恢复的位置

这个表特别适合新人,因为它会把 BERT 输入从“神秘 token 串”重新变成几个能解释的部件。


四、BERT 预训练时在做什么?

最经典任务:Masked Language Modeling

BERT 最经典的训练目标是 MLM,也就是:

把句子中的一部分 token 遮住,让模型根据上下文猜回来。

例如:

“我爱 [MASK] 语言处理”

模型要根据前后文猜 [MASK] 是什么。

一个最小可运行示例

tokens = ["[CLS]", "我", "爱", "[MASK]", "语", "言", "处", "理", "[SEP]"]
mask_index = tokens.index("[MASK]")

candidates = ["自", "学", "看"]

print("tokens =", tokens)
print("mask index =", mask_index)
print("候选填空 =", candidates)

预期输出:

tokens = ['[CLS]', '我', '爱', '[MASK]', '语', '言', '处', '理', '[SEP]']
mask index = 3
候选填空 = ['自', '学', '看']

mask index 告诉你模型到底要在哪个位置预测。真实 MLM 模型会在整个词表上打分,而不是只从这里手写的三个候选里选。

这个例子虽然不是在真正训练模型,但已经在教你:

  • [MASK] 的位置是明确的
  • 模型的任务是恢复被遮住的信息
  • 当前词的预测依赖双向上下文

为什么这件事很重要?

因为它迫使模型真正去理解:

  • 左边说了什么
  • 右边说了什么
  • 当前被遮住的位置该是什么

这让 BERT 非常擅长“理解型任务”。

第一次学 BERT 时,最稳的默认顺序

更稳的顺序通常是:

  1. 先理解双向上下文到底在补什么
  2. 先看 [CLS] / [SEP] / [MASK] 这几个最常见 token
  3. 再看 MLM 在训练时要求模型学什么
  4. 最后再看微调是怎么接分类头的

这样会比一上来就盯论文细节和大模型参数更容易稳住主线。


五、BERT 的输入不只有 token

Token Embedding

每个 token 会先变成向量。

Position Embedding

模型还要知道顺序,所以要加位置编码。

Segment Embedding

在句对任务里,模型还要知道“哪些 token 属于句子 A,哪些属于句子 B”。

你可以把 BERT 的输入想成三部分相加:

最终输入表示 = token embedding + position embedding + segment embedding

这一步很重要,因为 Transformer 本身不自带序列顺序感。


六、一个真正可运行的离线 BERT 示例

下面这个示例不需要下载预训练权重,只需要安装 transformerstorch,就可以本地随机初始化一个小型 BERT,主要用来帮助你理解输入输出形状。

运行环境
pip install torch transformers
import torch
from transformers import BertConfig, BertModel

config = BertConfig(
vocab_size=100,
hidden_size=32,
num_hidden_layers=2,
num_attention_heads=4,
intermediate_size=64
)

model = BertModel(config)

input_ids = torch.tensor([
[1, 5, 8, 9, 2, 0, 0], # 一条较短样本,后面补 0
[1, 7, 6, 3, 4, 2, 0]
])

attention_mask = torch.tensor([
[1, 1, 1, 1, 1, 0, 0],
[1, 1, 1, 1, 1, 1, 0]
])

outputs = model(input_ids=input_ids, attention_mask=attention_mask)

print("last_hidden_state shape:", outputs.last_hidden_state.shape)
print("pooler_output shape :", outputs.pooler_output.shape)

预期输出:

last_hidden_state shape: torch.Size([2, 7, 32])
pooler_output shape : torch.Size([2, 32])

这里的模型是随机初始化的,所以数值本身不代表真实预测。真正要看的信息是形状:2 条样本,每条 7 个位置,每个 token 有 32 维隐藏表示。

输出怎么理解?

  • last_hidden_state

    • shape: [batch, seq_len, hidden_size]
    • 每个 token 都有一个上下文化表示
  • pooler_output

    • shape: [batch, hidden_size]
    • 通常可理解为整句摘要表示之一

这也解释了为什么 BERT 适合:

  • token 级任务:看 last_hidden_state
  • 句子级任务:看 [CLS] 或句级表示

七、BERT 怎么拿来做分类?

典型套路

最常见的做法是:

  1. 输入句子
  2. 经过 BERT
  3. [CLS] 或句子表示
  4. 接一个线性分类头

这就是经典的 fine-tuning 方式。

一个概念级的小例子

import torch
from torch import nn

# 假设这是 BERT 输出的 [CLS] 表示
cls_embedding = torch.randn(4, 32) # batch=4, hidden=32

# 接一个分类头
classifier = nn.Linear(32, 2)
logits = classifier(cls_embedding)

print("logits shape:", logits.shape)

预期输出:

logits shape: torch.Size([4, 2])

这表示 batch 里有 4 条样本,分类头为每条样本输出 2 个原始分数。真实分类训练时通常会在这些 logits 上接 softmax 或交叉熵损失。

这段代码很简单,但它教你一个很重要的事实:

BERT 往往不是任务的终点,而是“强表示层”。

如果把 BERT 放进项目里,最值得先展示什么

最值得展示的通常不是:

  • “我用了 BERT”

而是:

  1. 输入文本长什么样
  2. [CLS] 表示怎么接分类头
  3. 它比传统表示或轻模型好在什么地方
  4. 哪些错例它仍然会错

这样别人会更容易看出:

  • 你理解的是 BERT 在任务链里的角色
  • 不只是换了个模型名

八、BERT 适合哪些任务?

特别适合

  • 文本分类
  • 句对匹配
  • 命名实体识别
  • 抽取式问答

不那么自然的地方

BERT 本身不是为了长文本自由生成设计的。 如果任务重点是:

  • 长对话生成
  • 续写
  • 大段文本创作

那 GPT 路线通常更自然。


九、BERT 为什么后来不再是唯一主角?

原因不是它没用,而是生态继续往前走了

后面 NLP 和 LLM 发展出了:

  • 更大规模的预训练
  • 更强的生成模型
  • 更统一的任务接口

所以今天很多应用更常讨论 GPT、T5、Llama 这类路线。

但 BERT 仍然非常值得学

因为它能帮你真正理解:

  • 上下文化表示
  • encoder-only 模型
  • 预训练 + 微调范式
  • token 级和句子级任务的区别

这些都是后面继续学大模型的重要地基。


十、初学者最常踩的坑

把 BERT 和 GPT 混成一个东西

它们都很重要,但训练目标和擅长任务并不一样。

以为 [CLS] 是“天然最佳句向量”

在很多任务里它好用,但并不是放之四海皆准。

只知道“用 BERT 做分类”,不知道它到底学了什么

真正要掌握的是:

  • 为什么它是双向的
  • 为什么 MLM 有效
  • 为什么它更适合理解任务

小结

这一节最重要的不是记住 BERT 的全称,而是抓住三件事:

  1. BERT 是双向上下文建模的代表
  2. 它通过 MLM 学会“基于上下文理解 token”
  3. 它非常适合理解型任务和微调范式

理解了这三点,你后面再学 GPT、T5、LLM 时,很多差异就会自然清楚。


练习

  1. 自己构造一个带 [MASK] 的中文句子,写出你认为最合理的候选词。
  2. 把离线 BERT 示例里的 hidden_size 改成 64,再看输出 shape 怎样变化。
  3. 想一想:为什么“我爱 [MASK] 语言处理”这种训练目标,能让模型学会双向理解?
  4. 用自己的话解释:BERT 和 GPT 在“看上下文”的方式上有什么核心差别?