嵌入层
在 Transformer 中,嵌入表示层是处理输入数据的重要步骤,注意力机制要求连续的浮点向量,嵌入层完成从离散到连续的转换,使其以向量形式输入到后续的注意力机制和神经网络中。
Transformer中的嵌入表示层组成
Transformer的嵌入层由两部分组成:
- 输入嵌入层(Input Embedding Layer):
- 将输入序列中的每个标记(token)映射为一个固定维度的向量。
- 位置编码(Positional Encoding):
- 在嵌入向量中加入位置信息,帮助模型捕获序列顺序关系。
1. 输入嵌入层
- 每个输入标记(token)的索引通过嵌入矩阵查表获得对应的嵌入向量。
- 嵌入矩阵 的形状为 ,其中:
- 是词汇表大小(Vocabulary Size)。
- 是嵌入向量的维度,也是Transformer模型的维度。
公式:
其中, 是第 个标记的索引, 是嵌入矩阵的第 行。
2. 位置编码(Positional Encoding)
由于Transformer的注意力机制本质上是无序的(没有序列信息),需要通过位置编码显式地引入序列中每个标记的位置信息。
位置编码的计算公式:
-
对于位置 和维度 :
- 是标记在序列中的位置。
- 是嵌入向量的维度索引。
- 是嵌入向量的维度。
-
位置编码的特点:
- 每个位置的编码是一个固定的向量,维度与嵌入向量相同。
- 不同位置的编码具有独特的正弦和余弦模式,能够通过相对差异捕捉序列顺序。
嵌入层的整体计算过程
Transformer中的嵌入表示层的输出可以表示为:
-
生成词嵌入: 每个输入序列中的词(或子词)通过嵌入层映射到一个固定维度的向量空间,得到词嵌入 。
-
生成位置编码: 使用正余弦函数或可学习的方式,为每个位置生成与嵌入向量维度一致的编码向量 。
-
结合词嵌入和位置编码: 将词嵌入和位置编码按元素相加,得到包含位置信息的嵌入表示 :
这里 表示序列中的第 个位置,将位置编码和输入嵌入逐元素相加,使嵌入向量既包含语义信息,又包含位置信息。
嵌入表示层的特点
- 与模型维度一致:
- 输入嵌入和位置编码的维度与Transformer模型的维度 一致,方便后续模块的处理。
- 可学习性:
- 输入嵌入层的权重是可学习的(通过任务目标优化)。
- 位置编码通常是固定的(不可学习),也可以设为可学习(在某些变体中实现)。
- 处理序列输入的无序性:
- 注意力机制不关心序列的顺序,通过位置编码显式引入位置信息,补足这一缺陷。
代码实现示例
以下是嵌入表示层(包括输入嵌入和位置编码)的实现示例:
1. PyTorch实现
import torch
import torch.nn as nn
import math
class TransformerEmbedding(nn.Module):
def __init__(self, vocab_size, d_model, max_len=5000):
super(TransformerEmbedding, self).__init__()
# 输入嵌入
self.token_embedding = nn.Embedding(vocab_size, d_model)
# 位置编码
pe = torch.zeros(max_len, d_model)
position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)
div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model))
pe[:, 0::2] = torch.sin(position * div_term)
pe[:, 1::2] = torch.cos(position * div_term)
self.register_buffer('positional_encoding', pe.unsqueeze(0)) # 不参与梯度更新
def forward(self, x):
# 获取输入嵌入
token_embeds = self.token_embedding(x)
# 加入位置编码
seq_len = x.size(1)
embeddings = token_embeds + self.positional_encoding[:, :seq_len, :]
return embeddings
2. TensorFlow/Keras实现
import tensorflow as tf
import numpy as np
class TransformerEmbedding(tf.keras.layers.Layer):
def __init__(self, vocab_size, d_model, max_len=5000):
super(TransformerEmbedding, self).__init__()
# 输入嵌入
self.token_embedding = tf.keras.layers.Embedding(vocab_size, d_model)
# 位置编码
pos = np.arange(max_len)[:, np.newaxis]
i = np.arange(d_model)[np.newaxis, :]
angle_rates = 1 / np.power(10000, (2 * (i // 2)) / np.float32(d_model))
pos_enc = pos * angle_rates
pos_enc[:, 0::2] = np.sin(pos_enc[:, 0::2]) # 偶数
pos_enc[:, 1::2] = np.cos(pos_enc[:, 1::2]) # 奇数
self.positional_encoding = tf.constant(pos_enc[np.newaxis, ...], dtype=tf.float32)
def call(self, x):
seq_len = tf.shape(x)[1]
token_embeds = self.token_embedding(x)
embeddings = token_embeds + self.positional_encoding[:, :seq_len, :]
return embeddings
总结
- Transformer的嵌入表示层通过输入嵌入和位置编码结合,生成序列的高质量向量表示。
- 它不仅能捕获标记的语义信息,还能引入序列的位置信息,是Transformer中处理序列数据的基础。
自注意层
Transformer中的注意力机制是其核心组件之一,而查询(Query)、键(Key)、值(Value)是实现注意力的三个基本元素,源于输入向量,并通过以下方式参与计算。通过这些计算,Transformer能够动态地调整每个位置对其他位置的关注程度,实现对序列中各部分的依赖建模。
1. 查询(Query)
- 含义:查询是代表当前要“关注什么”的信息。
- 来源:查询通常由输入向量通过一个线性变换得到。
- 作用:用来与键(Key)计算相似度,从而确定当前输入应该关注哪些部分。
- 公式:
其中,是输入,是查询的权重矩阵。
2. 键(Key)
- 含义:键是代表“提供信息的标识”。
- 来源:键同样由输入向量通过一个线性变换得到。
- 作用:与查询进行点积计算,用来衡量不同输入之间的相关性或相似性。
- 公式:
其中,输入,是键的权重矩阵。
3. 值(Value)
- 含义:值是实际“传递的信息”。
- 来源:值由输入向量通过一个线性变换得到。
- 作用:根据查询和键的相似性计算的权重,对值加权求和,生成输出。
- 公式:
其中,是输入,是值的权重矩阵。
总结
| 查询(Q) | 键(K) | 值(V) |
|---|---|---|
| 决定“当前要关注什么” | 表示每个位置的“标识信息” | 包含实际传递的信息 |
多头自注意力
多头自注意力机制(Multi-Head Self-Attention, MHSA)是Transformer架构中注意力机制的增强版,它通过多个并行的注意力头,捕获输入序列中不同部分的多种依赖关系和特征。以下是多头自注意力机制的详细说明:
核心思想
- 相较于单一的注意力机制,多头自注意力通过多组查询、键、值计算不同的注意力分布,从多个视角对输入进行建模。
- 每个头独立计算注意力,最后将所有头的结果拼接并投影到输出空间。
- 这种机制使得Transformer具备强大的上下文建模能力,是实现其卓越性能的关键。
多头自注意力的计算步骤
1. 输入线性变换
-
对输入矩阵 分别线性变换生成查询()、键()和值($ V ))。
-
公式:
其中 , , 是第 个头的权重矩阵。
2. 单头注意力计算
每个头独立计算注意力:
- 计算查询和键的点积,衡量相似性。
- 对分数进行缩放(防止梯度过大)。
其中, 是每个头的键向量维度。
- 对缩放的分数进行Softmax操作,得到注意力权重。
- 用注意力权重对值向量加权求和。
3. 多头拼接
- 将 个头的输出拼接起来。
4. 输出线性变换
- 拼接结果通过一个线性变换生成最终输出。
其中 是投影矩阵。
关键优势
-
多视角捕获依赖:多个头独立学习不同的注意力分布,增强模型对复杂模式的捕捉能力。
-
维度分割减少计算负担:每个头的查询、键、值维度被缩小到总维度的 ,减轻单头计算的复杂性。
-
提升并行效率:每个头可以并行计算,加速训练。
公式总结
- 多头自注意力公式:
- 单个头的输出:
应用场景
- 编码器:每个位置关注输入序列的其他位置,捕捉全局上下文。
- 解码器:捕捉生成目标序列中各部分的依赖关系。
- 跨注意力(Cross-Attention):用于解码器关注编码器的输出。
示意图描述
多头自注意力可以形象地看作:
- 输入被映射到多个不同的子空间。
- 每个子空间独立计算注意力。
- 所有子空间的结果整合,生成综合视角的输出。
前馈层
在 Transformer 中,前馈层(Feed-Forward Layer,简称 FFN)是每个编码器和解码器层中的关键组件之一。它是作用于每个时间步单独计算的全连接网络,用于对特征进行进一步的非线性变换。
前馈层的结构
Transformer 前馈层是由两个全连接层(线性变换)和一个非线性激活函数组成的模块。它对每个时间步的特征独立应用相同的变换,不考虑序列维度。
公式表示
假设输入特征的维度为 ,前馈层的计算公式为:
其中:
- 是输入张量,形状为 。
- 和 是第一层线性变换的权重和偏置,。
- 和 是第二层线性变换的权重和偏置,。
- 是激活函数,应用非线性变换。
- 是前馈网络的隐藏层维度,通常远大于 。
实验结果表明,增大前馈网络隐藏层维度有利于提高最高翻译结果的质量,因此,前馈网络隐藏层的维度一般比自注意力层的要大。
前馈层的计算过程
-
第一层线性变换:
- 将输入 投影到一个高维空间(隐藏层维度 )。
- 公式:,其中 的形状为 。
-
非线性激活函数:
- 使用 ReLU 激活函数对每个元素进行非线性变换:
- 使用 ReLU 激活函数对每个元素进行非线性变换:
-
第二层线性变换:
- 将隐藏层的表示投影回输入维度 :
- 输出形状恢复为 。
- 将隐藏层的表示投影回输入维度 :
前馈层的作用可以理解为在高维空间(隐藏层维度 )中进行特征变换,提取到更复杂的特征后,再投影回原始维度 。
前馈层的作用
- 特征变换:
- 前馈层通过非线性激活函数和线性变换,学习更丰富的特征表示。
- 维度扩展与压缩:
- 通过扩展到高维(),网络能够捕获更多复杂的特征,再将其压缩回模型维度 。
- 独立时间步处理:
- 前馈层独立地对序列中的每个时间步进行变换,忽略序列的上下文信息。
Transformer中前馈层的实现
在 Transformer 编码器和解码器中,前馈层嵌套在层归一化(Layer Norm)和残差连接中,完整的前馈模块流程如下:
- 残差连接:
- 层归一化:
完整公式:
代码实现
1. PyTorch实现
import torch
import torch.nn as nn
class FeedForward(nn.Module):
# 初始化模块
def __init__(self, d_model, d_ffn, dropout=0.1):
super(FeedForward, self).__init__()
self.fc1 = nn.Linear(d_model, d_ffn) # 第一个线性变换
self.relu = nn.ReLU() # 激活函数
self.fc2 = nn.Linear(d_ffn, d_model) # 第二个线性变换
self.dropout = nn.Dropout(dropout) # 防止过拟合
# 调用模块
def forward(self, x):
x = self.fc1(x) # 线性变换 1
x = self.relu(x) # 激活
x = self.dropout(x) # Dropout
x = self.fc2(x) # 线性变换 2
return x
2. TensorFlow/Keras实现
from tensorflow.keras.layers import Dense, Dropout, ReLU
class FeedForward(tf.keras.layers.Layer):
def __init__(self, d_model, d_ffn, dropout=0.1):
super(FeedForward, self).__init__()
self.fc1 = Dense(d_ffn) # 第一个线性变换
self.relu = ReLU() # 激活函数
self.fc2 = Dense(d_model) # 第二个线性变换
self.dropout = Dropout(dropout) # 防止过拟合
def call(self, x, training=False):
x = self.fc1(x) # 线性变换 1
x = self.relu(x) # 激活
x = self.dropout(x, training=training) # Dropout
x = self.fc2(x) # 线性变换 2
return x
前馈层的关键超参数
- 模型维度 :
- 输入和输出的维度,通常与 Transformer 的主要维度一致。
- 隐藏层维度 :
- 决定中间层的宽度,通常是 的 4 倍(如 )。
- 激活函数:
- 默认使用 ReLU,也可以尝试其他激活函数(如 GELU)。
总结
Transformer 的前馈层:
- 是一个作用于每个时间步的两层全连接网络。
- 通过非线性激活和扩展维度,帮助模型学习更加丰富的特征。
- 与残差连接和层归一化结合,构成了 Transformer 的核心模块之一。
在 Transformer 中,残差连接(Residual Connection) 与 层归一化(Layer Normalization) 是模型的重要组成部分。它们的结合可以有效缓解梯度消失问题,提升训练稳定性和收敛速度,同时改善深层网络的表达能力。
残差连接与层归一化
残差连接
定义与作用
残差连接是一种跳跃连接技术,它将子模块(例如,自注意力机制或前馈网络)的输入直接加到其输出上,形式化表示为:
其中, 表示当前子模块对输入 的变换结果。
优点
- 缓解梯度消失问题:
- 在深层网络中,梯度可能在反向传播时逐层衰减。残差连接使用一条直连通道直接将对应子层的输入连接到输出,避免在优化过程中因网络过深而产生潜在的梯度消失问题。
- 提高特征传递能力:
- 跳跃连接允许模型学习增量特征(residual),而不是直接学习完整的变换,从而更容易优化。
- 稳定训练:
- 残差连接让模型在训练初期更容易接近恒等映射,降低了模型的训练难度。
在 Transformer 中的应用
Transformer 的每一层都包含两个残差连接:
- 用于多头自注意力模块:
- 用于前馈网络模块:
2. 层归一化(Layer Normalization)
残差连接后,为了使每一层的输入/输出稳定在一个合理范围内,归一化技术被引入每个Transformer块中。
定义与作用
层归一化是对每个输入样本的特征维度进行归一化的技术。对于输入 ,层归一化的计算公式为:
其中:
- 和 分别是输入的均值和方差,用于将数据平移缩放到均值为0、方差为1的标准分布。
- 和 是可学习的缩放和平移参数。
- 是一个小值,用于防止分母为零。
优点
- 提高训练稳定性:
- 消除了特征在不同层间分布的差异(即内部协变量偏移问题)。
- 加速收敛:
- 归一化后的输入更加稳定(稳定在一个合理范围),模型在训练过程中能够更快达到最优状态。
- 适用于小批量数据:
- 与批归一化(Batch Normalization)相比,层归一化只依赖于单个样本的特征,适合小批量甚至单样本训练。
3. 残差连接与层归一化的组合方式
在 Transformer 中,残差连接与层归一化的组合非常关键。以下是标准的实现方式(以自注意力为例):
-
子模块的输出计算:
-
残差连接与层归一化:
这被称为 "Pre-Norm" 结构,因为层归一化在输入到下一层前完成。
总结
- 残差连接:用于缓解梯度消失问题,增强特征传递能力。
- 层归一化:用于提高训练稳定性和收敛速度。
- 在 Transformer 中,残差连接与层归一化紧密结合,成为每个子模块(自注意力、前馈网络)的标准处理流程。现代实现更倾向于使用 Pre-Norm,以改善深层网络的稳定性。
编码器和解码器结构
编码器的总体组成
Transformer 的编码器由堆叠的 NNN 个编码器层组成,每个编码器层包括两个子层:
-
多头自注意力子层(Multi-Head Self-Attention Sub-layer)
- 捕获输入序列中各位置之间的相关性。
- 每个位置的表示能够综合序列其他位置的信息。
-
前馈网络子层(Feed-Forward Network, FFN)
- 对每个位置独立地进行非线性变换,提升表示能力。
每个子层后都加了 残差连接 和 层归一化(Layer Normalization) ,以提高梯度传播的稳定性和模型的训练效果。
单个编码器结构
编码器层的具体流程如下:
- 输入嵌入与位置编码: 输入序列通过嵌入层生成词嵌入,并与位置编码相加,得到序列表示。
-
多头自注意力机制: 输入经过多头自注意力层,生成综合上下文信息的表示:
-
前馈网络: 注意力输出通过全连接前馈网络:
最终,编码器层输出 ,作为下一层编码器或解码器的输入。
编码器堆叠
将上述编码器层堆叠 层,每一层的输出作为下一层的输入,最后输出整个输入序列的上下文表示。
编码器输出: 编码器的最终输出是一个包含输入序列中每个位置信息的上下文表示矩阵,形状为:
其中 是输入序列的长度,是嵌入向量的维度。
解码器的总体组成
解码器同样由堆叠的 个解码器层组成,每个解码器层包括三个子层:
-
多头自注意力子层:
- 对目标序列(或其部分)的每个位置进行自注意力计算。
- 使用 掩码(Masking) 确保解码器只能访问目标序列的当前和之前的位置,而不能看到未来的位置。
-
编码器-解码器注意力子层:
- 将目标序列的表示与编码器生成的上下文表示结合。
- 帮助解码器获取输入序列中的相关信息:编码器-解码器注意力机制负责将编码器的输出(上下文表示)与解码器的当前状态结合起来,帮助解码器生成更加准确的预测。
- 通过编码器-解码器注意力,解码器可以动态查询输入序列的相关部分,从而保证生成的输出序列与输入序列的语义一致,生成合理的目标语言序列。
-
前馈网络子层:
- 和编码器中的前馈网络子层类似,对每个位置进行非线性变换。
每个子层后也使用残差连接和层归一化。
单个解码器层的结构
解码器层的具体流程如下:
-
目标嵌入与位置编码: 解码器接收目标序列的词嵌入与位置编码:
-
掩码多头自注意力机制: 使用目标序列的自注意力机制进行上下文建模,并对未来位置进行屏蔽:
-
编码器-解码器注意力: 将解码器的中间表示与编码器的上下文表示结合:
其中编码器生成的上下文表示 是解码器的重要输入。
-
前馈网络: 应用非线性变换得到最终输出:
最终,解码器层输出 ,作为下一层解码器的输入。
解码器堆叠
将上述解码器层堆叠 层,最终输出一个表示目标序列的分布矩阵。
解码器输出: 解码器的最终输出是一个概率分布矩阵,经过线性层和 softmax 层生成每个位置的词预测:
其中 是目标序列的长度, 是词汇表的大小。
解码器的生成机制
解码器通过逐步预测目标序列中的下一个词:
- 初始解码输入为特殊的开始符号
<start>。 - 解码器逐步生成目标序列的每个位置,前一步的输出作为下一步的输入。
- 停止生成条件为输出特殊的结束符号
<end>。
预训练模型GPT
受计算机视觉领域的启发,自 ImageNet 推出以来,模型通过海量图像数据进行预训练以学习特征提取能力,然后根据具体任务目标进行微调的范式大获成功。这一理念在自然语言处理领域也得到了广泛应用,开启了语言模型预训练的新篇章。以 GPT 和 BERT 为代表的基于 Transformer 的大规模预训练语言模型的出现,标志着自然语言处理正式进入“预训练-微调”范式的新时代。
预训练语言模型通过结合海量训练数据、自监督预训练任务和 Transformer 等深度神经网络结构,能够高效学习词汇、语法以及语义信息,为多种语言处理任务奠定基础。
在将预训练模型应用于下游任务时,可以采用以下方式:
- 微调预训练模型:只需利用具体任务的标注数据对预训练模型进行监督训练。
优势:无需深入了解任务细节,也无需设计专门的神经网络结构即可取得优秀性能。
无监督预训练
GPT 的无监督预训练采用 自回归语言建模(Autoregressive Language Modeling)方法,其目标是让模型通过预测序列中下一个词的概率来学习语法、语义和上下文关系。具体来说,GPT 的无监督预训练主要基于以下损失函数:
自回归语言建模目标
GPT 的核心训练目标是最大化给定前 个词 的条件下,下一个词 的概率:
模型学习整个序列的联合概率分布,通过链式法则,将其分解为一系列条件概率:
这意味着每次预测都只依赖于之前的词,而不是未来的词,确保生成的结果是自回归的。
- 输入编码
给定文本序列 ,GPT 首先在输入层中将其映射为稠密向量:
其中, 是词 的词向量, 是词 的位置向量, 为第 个位置的单词经过模型输入层(第0层)后的输出。
经过输入层编码,得到向量序列,随后将 送入模型编码层。
- 模型编码
编码层由 个 模块组成,每个模块通过自注意力机制和前馈神经网络对输入表示进行处理。
在自注意力机制的作用下,每一层的每个表示向量都会包含之前位置表示的向量信息,每个表示向量都具备丰富的上下文信息。
并且,经过多层编码,模型能得到每个单词层次化的组合式表示。
其中, 表示第 层的表示向量序列, 为序列长度, 为模型隐藏层维度, 为模型总层数。
- 模型输出
GPT 模型的输出层基于最后一层的表示 ,预测每个位置上的条件概率,其计算过程可以表示为
其中,为词向量矩阵, 为词表大小, 是输出层的偏置项。
- 损失函数
单向语言模型按照阅读顺序输入文本序列 ,用常规的语言模型目标优化 的最大似然估计,使之能根据输入历史序列对当前词做出准确的预测:
其中 代表模型参数,这里也可以基于马尔科夫假设,只使用部分过去词作训练。通过最小化损失函数,模型不断调整权重,使其能够更准确地预测下一个词的概率。
有监督下游任务微调
下游任务微调的目的是在通用语义表示的基础上,根据下游任务的特性进行适配。
下游任务需要利用有标注的数据集进行训练,每个样例由输入长度为 的文本序列 和对应的标签 构成。
将文本序列 输入 GPT 模型,获得的最后一层的最后一个词所对应的隐藏层输出 ,在此基础上,通过全连接层变换结合 函数,得到标签预测结果。
其中 为全连接层参数, 为标签个数。
通过对整个标注数据集 优化如下目标函数精调下游任务:
在微调过程中,下游任务针对任务目标进行优化,很容易使得模型遗忘预训练所学习的知识,从而损失模型的通用性和泛化能力,导致出现 灾难性遗忘(Catastrophic Forgetting) 问题。
解决方法:采用混合预训练任务损失+下游微调损失的方法
其中 的取值为 ,用于调节预训练任务损失占比。