从Attention到BERT:双向预训练语言模型到底解决了什么问题

从一次文本分类的困境说起

几年前我做过一个情感分类的小任务,数据量不大,大概几千条中文评论。当时第一反应是用 Word2Vec 训练词向量,再套一个 LSTM 做分类。结果跑出来准确率一直卡在 80% 出头,怎么调都上不去。

问题出在哪?我盯着几个错例看了很久。有一条评论是"这家店的服务态度真是没得说",模型判成了负面。原因很简单:Word2Vec 给"没"和"得"这两个字分配的向量是固定的,跟上下文无关。而"没得说"整体是褒义,模型却只看单个字。

这就是静态词向量的死穴------同一个词,不管出现在什么句子里,向量永远一样。而自然语言里,词义高度依赖上下文。

BERT 要解决的,正是这个问题。它不是第一个预训练语言模型,但它把"双向"这件事做成了标配,从此 NLP 的玩法变了。

单向语言模型的天花板在哪里

在 BERT 之前,比较有代表性的是 ELMo 和 GPT。

ELMo 的思路是用两个方向的 LSTM 分别读句子,一个从左往右,一个从右往左,然后把两边的隐状态拼起来。听起来像是"双向",但要注意,这两个方向是独立训练的,只是在最后拼接。模型在预测某个词的时候,左边那半并不知道右边看到了什么,右边那半也不知道左边看到了什么。这更像是"两个单向模型的拼盘",而不是真正的双向。

GPT 走的是另一条路:用 Transformer 的 Decoder,做标准的自回归语言模型,预测下一个词。这种结构天生是单向的,因为预测第 t 个词时,只能看到前 t-1 个词。

单向模型对生成类任务是合适的,写文章、续写句子,本来就只能从左往右。但对理解类任务,比如分类、问答、实体识别,句子是完整摆在面前的,凭什么只能看半边?

有人可能会想:那我把句子反过来再训一个模型,两个拼一起不就行了?ELMo 就是这么干的,效果确实比纯单向好,但前面说了,两个方向没有真正交互。

真正的问题在于:如果直接让模型同时看左右两边来预测中间词,会发生什么?

答案很简单------信息泄露。你要预测第 t 个词,如果模型能看到第 t 个词本身,那它什么都不用学,直接抄答案就行。这就是双向预训练最难绕过去的坎。

Masked LM:把答案藏起来再让人猜

BERT 的解法非常直接:既然不能让你看到要预测的词,那我就把它遮住。

具体做法是,随机选句子中 15% 的 token,然后:

  1. 其中 80% 替换成 [MASK] 标记;
  2. 10% 替换成一个随机的其他词;
  3. 10% 保持不变。

然后让模型去预测这些位置原本是什么词。

我第一次看到这个设计时觉得有点奇怪:为什么不全部换成 [MASK],非要留 10% 随机词、10% 不变?

这里其实是工程上的权衡。如果训练时全是 [MASK],那模型只在看到 [MASK] 的时候才认真预测,而下游微调时根本没有 [MASK] 这个标记,训练和推理的输入分布就对不上了。掺入随机词和原词,是为了让模型对每个位置都保持一定的预测能力,缩小预训练和微调之间的差距。

【注意】这个 80/10/10 的比例是 BERT 原论文里的设定,后来有不少工作质疑过它的最优性,比如 RoBERTa 就试过其他方案。但作为理解 BERT 的入口,这个设计足够说明问题。

用 HuggingFace 的 transformers 库可以很直观地看到 Masked LM 的效果。下面这段代码用的是 bert-base-chinese,transformers 版本我测试时是 4.40 左右:

python 复制代码
from transformers import pipeline

# 使用中文 BERT 做掩码填充
unmasker = pipeline(
    "fill-mask",
    model="bert-base-chinese",
    top_k=3
)

text = "这家店的 service 态度真是 [MASK] 得说"
results = unmasker(text)

for r in results:
    print(r["token_str"], round(r["score"], 4))

模型会给 [MASK] 位置填上概率最高的几个候选词。这里我想强调的是,BERT 在预测这个词的时候,是同时看了左边"这家店的服务态度真是"和右边"得说"的。左边告诉它这是评价场景,右边告诉它后面跟着"得说"这种固定搭配。两边的信息同时参与,这正是双向的价值。

注意力掩码:双向是怎么在代码里实现的

理解 BERT 的双向,绕不开注意力掩码(attention mask)。这也是很多人第一次读源码时容易懵的地方。

Transformer 的 Self-Attention 本质是每个 token 去"看"其他所有 token,然后加权求和。GPT 这类自回归模型需要一个上三角的掩码矩阵,把当前位置之后的位置全部屏蔽掉,保证只能看左边:

python 复制代码
import torch

def causal_mask(seq_len):
    # 上三角为 1,表示需要屏蔽的位置
    mask = torch.triu(torch.ones(seq_len, seq_len), diagonal=1)
    # 转换成 attention 用的形式:被屏蔽处为 -inf
    mask = mask.masked_fill(mask == 1, float("-inf"))
    return mask

print(causal_mask(4))

输出是一个 4x4 的矩阵,右上角全是负无穷,意味着第 0 个 token 不能看第 1、2、3 个 token。

而 BERT 用的是 Transformer 的 Encoder,它根本不需要这个上三角掩码。每个 token 都能看到包括自己在内的所有 token。代码上就是不做任何屏蔽,注意力矩阵是完整的。

所以你去看 BERT 的源码,会发现它处理的是 padding mask(把补齐的短句部分屏蔽掉),而不是 causal mask。这个区别看着小,但决定了模型是单向还是双向。

【关键结论】BERT 的双向不是靠什么特殊结构实现的,而是靠"不做因果掩码"加上"Masked LM 训练目标"这两件事配合出来的。结构上放开双向,目标上防止作弊,缺一不可。

CLS 和 NSP:句子级别的表示从哪来

Masked LM 解决的是 token 级别的理解,但很多下游任务是句子级别的,比如判断两句话是不是承接关系、整段文本是什么情感。

BERT 的做法是在每个输入序列最前面加一个特殊的 [CLS] 标记。经过多层 Transformer 之后,这个位置的输出向量会被当作整个句子的聚合表示,接一个简单的分类层就能做句子级任务。

为什么是 [CLS] 而不是把所有 token 的向量平均一下?因为 [CLS] 没有具体的词义,它在训练中唯一的作用就是汇聚全局信息。经过预训练,模型会学着把句子级别的语义塞进这个位置。这也是一种"让模型自己学怎么聚合"的思路,比人工设计池化规则更灵活。

另一个预训练任务是 NSP(Next Sentence Prediction)。构造训练样本时,一半的样本 B 是 A 的真实下一句,另一半是从语料里随机抽的句子,让模型判断 B 是不是 A 的下一句。这个任务是为了让模型学到句子之间的关系。

不过 NSP 后来争议很大。RoBERTa 的实验显示,去掉 NSP 反而效果更好,或者换成更难的句子顺序预测(SOP)效果更佳。所以现在如果你要自己预训练,NSP 不是必须的。但理解 BERT 的原始设计,NSP 是绕不开的一环。

微调:为什么 BERT 用起来这么省事

BERT 真正让它流行的,不只是预训练本身,而是"预训练 + 微调"这套范式。

以前做 NLP 任务,每个任务都要从头设计网络结构、从头训练。有了 BERT 之后,流程变成:加载预训练权重,在最后接一个任务相关的小输出层,用少量标注数据微调几个 epoch。

下面是一个最小可运行的文本分类微调示例,用的是 transformers 的 Trainer:

python 复制代码
from transformers import (
    BertTokenizer,
    BertForSequenceClassification,
    Trainer,
    TrainingArguments
)
from datasets import Dataset
import torch

model_name = "bert-base-chinese"
tokenizer = BertTokenizer.from_pretrained(model_name)
model = BertForSequenceClassification.from_pretrained(
    model_name,
    num_labels=2
)

# 假设已有文本和标签
texts = ["这家店服务很好", "东西太差了不会再买", "味道不错", "完全不推荐"]
labels = [1, 0, 1, 0]

def tokenize(batch):
    return tokenizer(
        batch["text"],
        padding="max_length",
        truncation=True,
        max_length=64
    )

dataset = Dataset.from_dict({"text": texts, "label": labels})
dataset = dataset.map(tokenize, batched=True)

args = TrainingArguments(
    output_dir="./bert_cls",
    num_train_epochs=3,
    per_device_train_batch_size=8,
    learning_rate=2e-5,
    logging_steps=10
)

trainer = Trainer(
    model=model,
    args=args,
    train_dataset=dataset
)

trainer.train()

几个工程上的注意点:

学习率一定要小。BERT 微调常用的学习率是 2e-5 到 5e-5 这个量级,比从头训练小一两个数量级。因为预训练权重已经学到了很好的表示,学习率太大会把学到的知识冲掉。

epoch 不要多。通常 2 到 4 个 epoch 就够了。BERT 参数量大,在小数据集上很容易过拟合,跑十几个 epoch 反而掉点。

max_length 要按任务选。BERT 的输入上限是 512 个 token,但很多任务用不到那么长。短文本设 64 或 128 就够,能明显省显存和时间。

BERT 和它的同类,到底怎么选

到这一步,可以用一张表把几个常见方案的差异理清楚:

方案 核心思路 优点 缺点 适用场景
Word2Vec/GloVe 静态词向量 轻量、快、可离线 词义与上下文无关 资源极受限、简单任务
ELMo 双向 LSTM 拼接 引入上下文相关表示 两方向独立、交互弱 早期 NLP 任务
GPT 自回归单向 生成能力强 理解类任务受限 文本生成、续写
BERT 双向 Encoder + MLM 理解类任务强、微调方便 不擅长生成、输入长度受限 分类、问答、NER、句对任务

【关键结论】BERT 不是万能的,它的强项是"理解",弱项是"生成"。如果你的任务是写文案、续写、对话生成,单向的 GPT 系模型更合适。选型要看任务性质,不是看谁更新。

实际使用中容易忽略的几点

第一,中文要用中文预训练权重。bert-base-chinese 是在中文语料上训的,直接用英文版 bert-base-uncased 处理中文,tokenizer 会把汉字拆得乱七八糟,效果会差很多。

第二,BERT 的 tokenizer 是 WordPiece,会把罕见词拆成子词。这对处理未登录词有好处,但也意味着 token 数和你直觉上的"字数"不一样。做长度统计时要以 token 数为准。

第三,输入长度 512 是硬限制,超过就要截断或分段。长文本任务(比如长文档分类)需要额外设计,比如分段后聚合、或者换用 Longformer、BigBird 这类支持长序列的变体。

第四,[CLS] 向量直接拿来做相似度效果一般。原论文就提到过,句子相似度这类任务用 [CLS] 不如用句对输入效果好。后来 Sentence-BERT 专门针对这个问题做了改进,用孪生网络结构训练出更适合相似度计算的句向量。

写在最后

BERT 的价值不在于它有多复杂,恰恰相反,它的核心设计简洁得有点朴素:用 Transformer Encoder,放开双向,然后用 Masked LM 防止作弊。但就是这套组合,把预训练语言模型从"能用的工具"变成了"标配的基础设施"。

回头看,它真正改变的是工作方式。以前每个 NLP 任务都要单独设计模型,现在大多数任务的第一步都是"先加载一个预训练模型看看 baseline"。这种范式的转变,比任何单个技术点都重要。

如果你现在要上手,我的建议是先跑通一个微调例子,再回头读源码里的 attention mask 部分。搞清楚"为什么 BERT 不需要因果掩码",比记住多少参数有用得多。至于后续演进,RoBERTa、ALBERT、ELECTRA 都是在 BERT 基础上做的改进,理解 BERT 是理解它们的前提。

相关推荐
正经教主1 小时前
【FDE系列】阶段3:Day 56:评测体系入门 — 建立你的黄金评测集
人工智能·fde
鲲穹AI种草1 小时前
自媒体矩阵批量剪辑怎么选?鲲剪短视频批量处理工具横向评测
人工智能·音视频·媒体
迅猛龙办公室1 小时前
实现第一个python程序(HelloWorld)
开发语言·python
正经教主1 小时前
【FDE系列】阶段3:Day 58:Prompt 安全 — 注入、越狱与防护
网络·人工智能·安全·prompt·fde
拉格朗日(Lagrange)1 小时前
【第2 章】WorkBuddy 从入门到高手
开发语言·python
TechEdu2026062 小时前
[人工智能]Python09:numpy.random.Statistics统计实战指南
人工智能·numpy
可乐ea2 小时前
AI Agent 工具调用准确性评测:选择错误与参数错误分开测
大数据·人工智能·算法·大模型·工具调用·ai智能体·agent评测
昨日之日20062 小时前
yovoice:本地配音工具箱,支持音色克隆与情绪控制,专为旁白、有声书和视频配音打造
人工智能·音视频
DP DPharness2 小时前
选型时怎么比:dsh-knowledge 与三类 RAG 方案的维度对照
人工智能·dpharness