BERT和Transformer的双向性理解
-
- 核心区别:注意力机制的限制
- [1. Transformer Encoder:实际上是**双向**的](#1. Transformer Encoder:实际上是双向的)
- [2. Transformer Decoder:这才是**单向**的](#2. Transformer Decoder:这才是单向的)
- [3. BERT:完全基于Encoder,真正的双向](#3. BERT:完全基于Encoder,真正的双向)
- [4. 通过MLM任务体现的"双向性"](#4. 通过MLM任务体现的"双向性")
- [5. 可视化对比](#5. 可视化对比)
-
- BERT的双向注意力
- GPT的单向注意力
- [原始Transformer Decoder的单向注意力](#原始Transformer Decoder的单向注意力)
- [6. 为什么这个区别如此重要?](#6. 为什么这个区别如此重要?)
- 总结表格
核心区别:注意力机制的限制
Transformer(原始论文)是"单向"还是"双向"?
答案是:看具体是哪一部分。原始Transformer论文包含Encoder和Decoder:
python
# 原始Transformer架构
class Transformer(nn.Module):
def __init__(self):
self.encoder = TransformerEncoder() # ✅ 双向的
self.decoder = TransformerDecoder() # ❌ 单向的(带掩码)
让我们看代码细节:
1. Transformer Encoder:实际上是双向的
python
class TransformerEncoderLayer(nn.Module):
def forward(self, src):
# 自注意力机制
# Q, K, V 都来自 src(输入序列)
attn_output = self.self_attn(src, src, src)
# ↑ ↑ ↑
# 查询 键 值
# 在计算注意力时,每个位置可以看到序列中的所有位置
# 包括它自己、它前面的位置、它后面的位置
关键代码:Encoder的自注意力没有限制
python
def attention(Q, K, V):
# Q.shape = K.shape = V.shape = [batch, seq_len, dim]
# 计算所有位置对之间的注意力分数
scores = torch.matmul(Q, K.transpose(-2, -1)) # [batch, seq_len, seq_len]
# scores[i, j] 表示位置i对位置j的关注程度
# ✅ 没有掩码!每个位置可以看到所有位置
attn_weights = F.softmax(scores, dim=-1)
# 每个位置的输出是所有位置的加权和
output = torch.matmul(attn_weights, V)
return output
例子:句子 "I love NLP"
- 当计算"love"的表示时:
- 可以看到前面的 "I"
- 可以看到后面的 "NLP"
- 也可以看到自己 "love"
- 这是真正的双向上下文!
2. Transformer Decoder:这才是单向的
python
class TransformerDecoderLayer(nn.Module):
def forward(self, tgt, memory):
# 第一步:带掩码的自注意力(单向的!)
tgt2 = self.self_attn(tgt, tgt, tgt, attn_mask=causal_mask)
# ↑ 关键!因果掩码
# 第二步:编码器-解码器注意力(双向的)
tgt2 = self.cross_attn(tgt2, memory, memory)
# 这里memory来自encoder,是完整的上下文
return tgt2
关键代码:因果掩码(Causal Mask)
python
def generate_causal_mask(seq_len):
"""
生成下三角矩阵,确保位置i只能看到位置j (j ≤ i)
"""
mask = torch.tril(torch.ones(seq_len, seq_len)) # 下三角矩阵
# 例如 seq_len=4:
# [[1, 0, 0, 0],
# [1, 1, 0, 0],
# [1, 1, 1, 0],
# [1, 1, 1, 1]]
return mask
在注意力中的应用:
python
def masked_attention(Q, K, V, mask):
scores = torch.matmul(Q, K.transpose(-2, -1))
# 应用因果掩码:将未来位置的分数设为负无穷
scores = scores.masked_fill(mask == 0, float('-inf'))
# mask=0的位置是未来位置,不能看
attn_weights = F.softmax(scores, dim=-1)
output = torch.matmul(attn_weights, V)
return output
例子:生成句子 "I love NLP"
- 生成第一个词 "I":只能看到起始符
<s> - 生成第二个词 "love":只能看到
<s> I,不能看到后面的 "NLP" - 生成第三个词 "NLP":只能看到
<s> I love,不能看到更后面的词 - 这是从左到右的单向!
3. BERT:完全基于Encoder,真正的双向
python
class BERTModel(nn.Module):
def __init__(self):
# BERT只使用Transformer的Encoder部分
self.encoder_layers = nn.ModuleList([
TransformerEncoderLayer() for _ in range(12)
])
# 没有Decoder,没有因果掩码!
def forward(self, input_embeddings, attention_mask):
# attention_mask只用于padding,不是因果掩码!
# attention_mask = [[1, 1, 1, 0, 0]] # 1=真实token,0=padding
for layer in self.encoder_layers:
# 每层都是完全双向的自注意力
output = layer(input_embeddings, attention_mask)
# attention_mask确保padding位置不被关注
# 但所有真实token之间都可以相互关注
return output
BERT中的注意力掩码 vs Transformer Decoder的掩码:
python
# BERT的attention_mask(用于padding)
bert_mask = torch.tensor([
[1, 1, 1, 1, 0, 0], # 前4个是真实token,后2个是padding
[1, 1, 1, 0, 0, 0] # 前3个是真实token,后3个是padding
]) # shape: [batch_size, seq_len]
# 转换为注意力掩码
bert_attn_mask = bert_mask.unsqueeze(1).unsqueeze(2) # [batch, 1, 1, seq_len]
# 广播后,每个位置对所有位置使用相同的掩码模式
# Transformer Decoder的causal_mask(用于单向)
causal_mask = torch.tril(torch.ones(seq_len, seq_len)) # [seq_len, seq_len]
# [[1,0,0,0,0,0],
# [1,1,0,0,0,0],
# [1,1,1,0,0,0],
# [1,1,1,1,0,0],
# [1,1,1,1,1,0],
# [1,1,1,1,1,1]]
4. 通过MLM任务体现的"双向性"
这才是BERT被称为"双向"的真正原因!让我们看预训练代码:
python
class BERTForPreTraining(nn.Module):
def forward(self, input_ids, mlm_labels):
# 输入: [CLS] 我 [MASK] 吃 苹果 [SEP]
# 101 1043 103 2003 3899 102
# 1. 获取BERT的隐藏状态
hidden_states = self.bert(input_ids) # [batch, seq_len, hidden_dim]
# 2. MLM预测
mlm_logits = self.mlm_head(hidden_states) # 预测每个位置的词
# 关键:预测[MASK]时,BERT能看到整个上下文!
# 位置2是[MASK],要预测它是"爱"
# 在计算位置2的表示时,BERT能看到:
# - 前面的 [CLS], "我"
# - 后面的 "吃", "苹果", [SEP]
对比单向模型(如GPT):
python
class GPTModel(nn.Module):
def forward(self, input_ids):
# 输入: [CLS] 我 [MASK] 吃 苹果
# 使用因果掩码,每个位置只能看到前面的词
# 当预测[MASK](位置2)时:
# 只能看到: [CLS] 我
# 不能看到: 吃 苹果
# 所以GPT无法像BERT那样利用完整上下文
5. 可视化对比
BERT的双向注意力
句子: [CLS] 我 [MASK] 吃 苹果 [SEP]
当计算[MASK]的表示时,注意力可以看向:
↑ ↑ ↑ ↑ ↑
[CLS] 我 [MASK] 吃 苹果 [SEP]
← 中心 →
双向都能看!
GPT的单向注意力
句子: [CLS] 我 [MASK] 吃 苹果
当计算[MASK]的表示时,注意力只能看向:
↑ ↑
[CLS] 我 [MASK] 吃 苹果
只能看左边!
不能看右边!
原始Transformer Decoder的单向注意力
生成过程:
步骤1: [CLS] → 预测"我" (只能看[CLS])
步骤2: [CLS] 我 → 预测"[MASK]" (只能看[CLS] 我)
步骤3: [CLS] 我 [MASK] → 预测"吃" (只能看[CLS] 我 [MASK])
6. 为什么这个区别如此重要?
对于理解任务:
python
# 完形填空:"中国的首都是[MASK]"
# BERT: 能看到"中国"和"首都" → 容易预测"北京"
# GPT: 只能看到前面的词 → 更难预测
# 命名实体识别:"[CLS] 张 三 去 北 京 [SEP]"
# BERT: "北"能看到前面的"去"和后面的"京" → 容易识别为地名
# GPT: "北"只能看到前面的词 → 更难识别
对于生成任务:
python
# 文本生成:"今天天气很好,"
# BERT: 不适合!因为训练时没见过[MASK]在中间的情况
# GPT: 完美适合!就是训练来做这个的
总结表格
| 特性 | Transformer Encoder | Transformer Decoder | BERT | GPT |
|---|---|---|---|---|
| 架构组件 | Encoder部分 | Decoder部分 | 只使用Encoder | 只使用Decoder |
| 注意力类型 | 完全双向自注意力 | 带掩码的自注意力(单向) | 完全双向自注意力 | 带因果掩码的自注意力(单向) |
| 能否看到未来 | ✅ 可以 | ❌ 不可以 | ✅ 可以 | ❌ 不可以 |
| 掩码作用 | 只掩码padding | 掩码未来位置 + padding | 只掩码padding | 掩码未来位置 + padding |
| 适合任务 | 理解任务(分类、NER等) | 生成任务(翻译、摘要等) | 理解任务 | 生成任务 |
| 预训练目标 | N/A | 自回归语言建模 | MLM(掩码语言建模) | 自回归语言建模 |
关键结论:
- 说"Transformer是单向的"不准确 ,应该说 "Transformer Decoder是单向的"
- BERT是双向的 ,因为它:
- 只使用Transformer的Encoder部分
- 没有因果掩码限制
- 通过MLM任务学习利用完整上下文
- 这个"双向性"是BERT在理解类任务上表现出色的根本原因
所以当人们说"BERT是双向的",他们真正想表达的是:"BERT在预测每个词时,都能利用该词左边和右边的所有上下文信息",而这正是通过去掉Decoder的因果掩码,结合MLM预训练任务实现的。