在自然语言处理领域,Transformer架构的出现彻底改变了序列建模的范式。作为其核心组件之一,编码器承担着将原始文本转化为富含上下文信息的向量表示这一关键任务。与传统的RNN/LSTM不同,Transformer编码器通过完全基于注意力机制的并行化设计,实现了对长距离依赖关系的有效捕捉和计算效率的大幅提升。
编码器的核心使命可以概括为:为输入序列中的每个token生成一个上下文感知的向量表示。这个表示不仅包含词汇本身的语义信息,还融入了该词汇在整个句子中的角色和关系。例如在处理"bank"这个多义词时,编码器能够根据上下文(是"river bank"还是"bank account")自动调整其向量表示的方向。
关键洞察:Transformer编码器的革命性在于,它摒弃了传统的序列逐点处理方式,转而采用全局注意力机制,使每个词都能直接与句子中的所有其他词建立联系,这种设计突破了传统模型在长距离依赖和并行计算上的瓶颈。
词嵌入技术将离散的词汇符号映射到连续的向量空间,这个过程可以形式化表示为:
E:V→Rd
其中V是词汇表,d是嵌入维度(通常d=512)。嵌入矩阵E∈R|V|×d是可训练参数,在训练过程中通过反向传播不断优化。
实际操作中,我们首先对输入文本进行tokenization:
python复制# 示例:使用WordPiece分词
tokens = ["The", "cat", "sat", "on", "the", "mat", "."]
token_ids = [1, 245, 789, 12, 1, 1024, 5] # 假设的词汇表ID
然后通过查表操作获取词向量:
python复制import torch
embedding = torch.nn.Embedding(vocab_size=10000, embedding_dim=512)
word_vectors = embedding(torch.tensor(token_ids)) # shape: [7, 512]
现代预训练模型通常采用子词切分(subword tokenization)策略,如WordPiece或BPE,这有效解决了OOV(out-of-vocabulary)问题,同时保持了合理的词汇表规模。
由于Transformer不包含递归结构,必须显式地注入位置信息。原始论文提出的正弦位置编码公式如下:
PE(pos,2i)=sin(pos/100002i/dmodel)
PE(pos,2i+1)=cos(pos/100002i/dmodel)
其中pos是位置索引,i是维度索引。这种编码方式具有以下优势:
实际实现示例:
python复制def positional_encoding(max_len, d_model):
position = torch.arange(max_len).unsqueeze(1)
div_term = torch.exp(torch.arange(0, d_model, 2) * (-math.log(10000.0) / d_model))
pe = torch.zeros(max_len, d_model)
pe[:, 0::2] = torch.sin(position * div_term)
pe[:, 1::2] = torch.cos(position * div_term)
return pe # shape: [max_len, d_model]
值得注意的是,现代模型如BERT、GPT更多采用可学习的位置嵌入(learnable positional embeddings),其效果通常优于固定编码,尤其是在训练数据充足的情况下。
自注意力机制的计算过程可以分为以下几个关键步骤:
线性投影:为每个token生成Q、K、V三元组
python复制Q = torch.matmul(X, W_Q) # [seq_len, d_k]
K = torch.matmul(X, W_K) # [seq_len, d_k]
V = torch.matmul(X, W_V) # [seq_len, d_v]
计算注意力分数:
python复制scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(d_k)
应用softmax归一化:
python复制attention_weights = torch.softmax(scores, dim=-1)
加权求和:
python复制output = torch.matmul(attention_weights, V)
技术细节:缩放因子1/√d_k的作用是防止点积结果过大导致softmax进入梯度饱和区,这对稳定训练至关重要。
多头注意力将d_model维的Q、K、V分割为h个头(通常h=8),每个头独立计算注意力:
python复制class MultiHeadAttention(nn.Module):
def __init__(self, h=8, d_model=512):
super().__init__()
self.d_k = d_model // h
self.h = h
self.W_Q = nn.Linear(d_model, d_model)
self.W_K = nn.Linear(d_model, d_model)
self.W_V = nn.Linear(d_model, d_model)
self.W_O = nn.Linear(d_model, d_model)
def forward(self, X):
Q = self.W_Q(X).view(batch_size, seq_len, self.h, self.d_k)
K = self.W_K(X).view(batch_size, seq_len, self.h, self.d_k)
V = self.W_V(X).view(batch_size, seq_len, self.h, self.d_k)
# 每个头独立计算注意力
attention_outputs = []
for head in range(self.h):
attn = scaled_dot_product_attention(Q[:,:,head,:], K[:,:,head,:], V[:,:,head,:])
attention_outputs.append(attn)
# 拼接多头输出
concat = torch.cat(attention_outputs, dim=-1)
return self.W_O(concat)
多头设计的优势在于:
编码器中的前馈网络(FFN)是一个两层的全连接网络,其典型实现为:
FFN(x)=max(0,xW1+b1)W2+b2
其中第一层将维度从d_model扩展到d_ff(通常d_ff=2048),第二层再投影回d_model。这种"扩展-收缩"的结构设计为模型提供了强大的非线性变换能力。
python复制class FeedForward(nn.Module):
def __init__(self, d_model=512, d_ff=2048):
super().__init__()
self.linear1 = nn.Linear(d_model, d_ff)
self.linear2 = nn.Linear(d_ff, d_model)
self.dropout = nn.Dropout(0.1)
def forward(self, x):
return self.linear2(self.dropout(F.gelu(self.linear1(x))))
现代变体常用GELU激活函数替代ReLU:
GELU(x)=xΦ(x),其中Φ是标准正态分布的累积分布函数
Transformer采用残差连接(Residual Connection)和层归一化(Layer Normalization)来稳定深层网络的训练:
python复制class SublayerConnection(nn.Module):
def __init__(self, size, dropout=0.1):
super().__init__()
self.norm = nn.LayerNorm(size)
self.dropout = nn.Dropout(dropout)
def forward(self, x, sublayer):
"残差连接后接层归一化"
return self.norm(x + self.dropout(sublayer(x)))
这种设计带来了以下好处:
层归一化与批归一化的区别在于,它是在特征维度而非批次维度上进行归一化,这对处理变长序列特别重要。
一个完整的编码器层包含以下组件:
python复制class EncoderLayer(nn.Module):
def __init__(self, size, self_attn, feed_forward, dropout=0.1):
super().__init__()
self.self_attn = self_attn
self.feed_forward = feed_forward
self.sublayer = clones(SublayerConnection(size, dropout), 2)
self.size = size
def forward(self, x, mask):
x = self.sublayer[0](x, lambda x: self.self_attn(x, x, x, mask))
return self.sublayer[1](x, self.feed_forward)
典型的Transformer编码器由N=6个相同的层堆叠而成,随着层数加深,模型学习到的特征也越来越抽象:
| 层数范围 | 主要学习内容 |
|---|---|
| 1-2层 | 局部语法模式(词性、短语结构) |
| 3-4层 | 句法关系(主谓宾、修饰关系) |
| 5-6层 | 语义角色、指代关系、篇章结构 |
这种层次化的特征学习过程使得模型能够逐步构建对输入文本的深层理解。
在实际应用中,我们需要处理两种主要掩码:
python复制def create_padding_mask(seq):
# seq形状:[batch_size, seq_len]
mask = (seq == 0).unsqueeze(1).unsqueeze(2) # [batch_size, 1, 1, seq_len]
return mask
# 在注意力计算中应用掩码
scores = scores.masked_fill(mask == 1, -1e9)
合理的初始化对Transformer训练至关重要:
python复制def initialize_parameters(model):
for p in model.parameters():
if p.dim() > 1:
nn.init.xavier_uniform_(p)
特别是注意力层的Q、K、V投影矩阵,通常采用Xavier初始化以保证方差稳定。
python复制lr = d_model**-0.5 * min(step**-0.5, step * warmup_steps**-1.5)
对于文本分类等任务,通常采用以下策略:
python复制# 添加分类token
input_ids = torch.cat([cls_token_id, token_ids], dim=0)
# 获取分类表示
cls_representation = encoder_output[0] # 第一个位置的输出
# 分类头
logits = classifier(cls_representation)
对于NER、POS tagging等任务:
python复制# 获取每个token的表示
token_representations = encoder_output # [seq_len, d_model]
# 标注头
tag_logits = tagger(token_representations) # [seq_len, num_tags]
对于阅读理解式问答:
python复制# 拼接输入
input_ids = [cls_id] + question_ids + [sep_id] + text_ids + [sep_id]
# 获取编码器输出
encoder_out = encoder(input_ids)
# 预测答案跨度
start_logits = start_head(encoder_out) # [seq_len]
end_logits = end_head(encoder_out) # [seq_len]
BERT在原始Transformer编码器基础上做了以下改进:
RoBERTa进一步优化了训练策略:
原始Transformer的注意力复杂度为O(n²),处理长序列时会遇到:
解决方案:
深层Transformer训练可能出现:
应对措施:
处理多语言数据时的挑战:
常见做法:
现代NLP采用两阶段范式:
编码器在这两个阶段都扮演核心角色:
预训练目标的演变:
在实际应用中,我发现编码器的微调需要特别注意以下几点: