第1章 BERT模型基础:从Transformer到预训练任务

做BERT误码注入之前,得先把模型本身摸透了。我记得刚接触BERT那会儿,觉得这东西就是个黑盒子——输入句子,输出向量,中间发生了什么?嗯,其实核心就三块:Transformer架构、预训练任务、输入表示。咱们今天一个一个拆开聊。

1.1 Transformer架构回顾

BERT用的其实是Transformer的Encoder部分。你想想看,Transformer原版里那个“左膀右臂”的结构——Encoder负责读入,Decoder负责生成。BERT只需要理解文本,所以拿走了Encoder。

我个人习惯把Transformer Encoder画成一个“堆叠的单元盒”。每个盒子里两件套:多头自注意力和前馈神经网络。中间夹着残差连接和层归一化。

  • 自注意力:每个词跟所有词做关联计算,算出“谁该关注谁”。比如“我吃苹果”,“苹果”跟“吃”的关系要比跟“我”更近。
  • 多头:其实就是并行做多组自注意力,每组学到不同的关系。我见过有人比喻成“多个角度同时看一篇文章”。
  • 前馈网络:对每个位置的向量独立做一次非线性变换,说白了就是“再琢磨一下这个位置的意思”。

我记得有一次我在给模型调参时,发现层数越深,底层注意力图越来越模糊。后来才意识到,深层Encoder更像是在做“语义抽象”,而不是简单的词汇关联。

避坑指南: 我曾经以为BERT的12层Encoder是全共享权重的,结果训练出来完全不对。实际上每一层都有自己的参数,不能混用。

1.2 BERT的预训练任务

BERT最大的亮点就是那两个预训练任务:MLM(掩码语言模型) 和 NSP(下一句预测)。这两个任务让BERT在没有人工标注的情况下学会了句法和语义。

MLM——掩码语言模型

说白了就是“完形填空”。随机遮住输入句子中15%的,然后让模型去猜被遮住的是什么。但这里有个坑:如果只是简单地遮住再预测,模型在fine-tuning时就见不到[MASK]标记,会产生不匹配。所以BERT采用了这样的策略:

  1. 选中15%的位置做操作
  2. 其中80%替换成[MASK]
  3. 10%替换成随机词
  4. 10%保持不变

这样模型就不能单纯依赖[MASK]标记来判断是否需要预测,必须真正理解上下文。我曾在项目中试过只做80% [MASK],结果下游分类任务的F1掉了一个点——确实需要那点随机性。

NSP——下一句预测

这个任务更直观:给定两个句子A和B,判断B是不是A的下一句。BERT在预训练时构造50%正例(真实下一句)和50%负例(随机拼凑的下一句)。你可能会问:“这有什么用?难道就是学句子间的关系?”

是的。尤其对于问答、自然语言推理这类任务,理解句子之间的逻辑关系至关重要。我个人觉得NSP其实更像一个“附赠任务”,它迫使[CLS]向量去积累段落级别的信息。

任务 输入格式 预测目标 训练技巧
MLM 单个句子(带[MASK]) 被遮住的 80%[MASK]+10%随机+10%不变
NSP [CLS] A [SEP] B [SEP] IsNext或NotNext 50%正例+50%负例
注意: 后来的RoBERTa发现去掉NSP任务反而在某些场景下效果更好。但BERT原始是包含NSP的,动手做误码注入实验时建议保持原味。

1.3 BERT的输入表示

BERT的输入可不是简单的 ID。它把每个转成三个向量的和: Embedding、Segment Embedding、Position Embedding。然后才喂进Encoder。

Embedding

就是词汇表的查表操作。BERT用WordPiece分词,词表大小30,000左右。每个词或子词对应一个768维向量(base版)。嗯,这里要注意:像“[CLS]”和“[SEP]”这些特殊也有对应的向量。

Segment Embedding

用来区分句子A和句子B。比如在做NSP时,句子A的所有的segment id为0,句子B的为1。在做MLM单句输入时,全部设为0。这个操作我在做双句分类任务时经常忘记加——哦对,忘了它就等于模型不知道两个句子谁是谁。

Position Embedding

Transformer自身没有顺序感,所以需要手动注入位置信息。BERT用的是位置编码,直接学出一个0~511(最大长度)的位置向量表。你想想看,BERT base 512长度,每个位置学一个768维向量,参数量不小。

# 伪代码:BERT输入表示
_emb = _lookup(_ids)         # [batch, seq_len, 768]
segment_emb = segment_lookup(segment_ids)   # [batch, seq_len, 768]
position_emb = position_lookup(position_ids) # [batch, seq_len, 768]
input_emb = _emb + segment_emb + position_emb
# 然后加LayerNorm + Dropout,送Encoder
实战小技巧: 当我需要手动构造input_ids、_type_ids、attention_mask时,建议统一用HuggingFace Tokenizer,它自动返回这些字典。不要自己拼,容易出边界错。

本章知识结构图

最后,我用一张SVG图帮你理清BERT基础的核心脉络。注意看三个模块之间的依赖关系:输入表示决定了下游编码,预训练任务驱动了参数学习,而Transformer架构则是这一切的骨架。

BERT模型基础核心架构图 第一步:输入表示 Embedding Segment Embedding Position Embedding 三者相加 第二步:Transformer Encoder × 12(Base) 多头自注意力(Multi-Head Self-Attention) 前馈神经网络(FFN) + 残差连接 + 层归一化 第三步:预训练任务 MLM(掩码语言模型) NSP(下一句预测)

总结一下:BERT的输入表示像厨师备菜,Transformer Encoder像烹饪过程,预训练任务像食谱。三者缺一不可。做误码注入时,你可能会在输入表示环节动手脚,或者在预训练任务输出层做修改。但不管怎么改,必须理解这个三层结构。