赞
踩
模型理论讲解:
以下是自注意力在一个简单示例中的工作原理:
考虑一句话:“The cat sat on the mat.”
嵌入
首先,模型将输入序列中的每个单词嵌入到一个高维向量表示中。这个嵌入过程允许模型捕捉单词之间的语义相似性。
查询、键和值向量
模型为序列中的每个单词计算三个向量:查询向量、键向量和值向量。在训练过程中,模型学习这些向量,每个向量都有不同的作用。查询向量表示单词的查询,即模型在序列中寻找的内容。键向量表示单词的键,即序列中其他单词应该注意的内容。值向量表示单词的值,即单词对输出所贡献的信息。
注意力分数
一旦模型计算了每个单词的查询、键和值向量,它就会为序列中的每一对单词计算注意力分数。这通常通过取查询向量和键向量的点积来实现,以评估单词之间的相似性。
SoftMax 归一化
使用 softmax 函数对注意力分数进行归一化,以获得注意力权重。这些权重表示每个单词应该关注序列中其他单词的程度。注意力权重较高的单词被认为对正在执行的任务更为关键。
加权求和
最后,使用注意力权重计算值向量的加权和。这产生了每个序列中单词的自注意力机制输出,捕获了来自其他单词的上下文信息。
计算注意力分数
# 计算注意力分数 # 安装 PyTorch !pip install torch==2.2.1+cu121 # 导入库 import torch import torch.nn.functional as F # 示例输入序列 input_sequence = torch.tensor([[0.1, 0.2, 0.3], [0.4, 0.5, 0.6], [0.7, 0.8, 0.9]]) # 生成 Key、Query 和 Value 矩阵的随机权重 random_weights_key = torch.randn(input_sequence.size(-1), input_sequence.size(-1)) random_weights_query = torch.randn(input_sequence.size(-1), input_sequence.size(-1)) random_weights_value = torch.randn(input_sequence.size(-1), input_sequence.size(-1)) # 计算 Key、Query 和 Value 矩阵 key = torch.matmul(input_sequence, random_weights_key) query = torch.matmul(input_sequence, random_weights_query) value = torch.matmul(input_sequence, random_weights_value) # 计算注意力分数 attention_scores = torch.matmul(query, key.T) / torch.sqrt(torch.tensor(query.size(-1), dtype=torch.float32)) # 使用 softmax 函数获得注意力权重 attention_weights = F.softmax(attention_scores, dim=-1) # 计算 Value 向量的加权和 output = torch.matmul(attention_weights, value) print("自注意力机制后的输出:") print(output)
尽管Transformer模型具有强大的功能,但它缺乏对元素顺序的内在理解——这是位置编码所解决的一个缺点。通过将输入嵌入与位置信息结合起来,位置编码使模型能够区分序列中元素的相对位置。这种细致的理解对于捕捉语言的时间动态和促进准确理解至关重要。
在Transformer模型中,位置编码是一个关键组件,它将关于标记位置的信息注入到输入嵌入中。
与循环神经网络(RNNs)或卷积神经网络(CNNs)不同,由于其置换不变性,Transformers 缺乏对标记位置的内在知识。位置编码通过为模型提供位置信息来解决这一限制,使其能够按照正确的顺序处理序列。
位置编码的概念
通常在将输入嵌入传入Transformer模型之前,会将位置编码添加到嵌入中。它由一组具有不同频率和相位的正弦函数组成,允许模型根据它们在序列中的位置区分标记。
位置编码的公式如下:
假设您有一个长度为L的输入序列,并且需要在该序列中找到第k个对象的位置。位置编码由不同频率的正弦和余弦函数给出:
其中:
k:输入序列中对象的位置,0≤k<L/2
d:输出嵌入空间的维度
P(k,j):位置函数,用于将输入序列中的位置k映射到位置矩阵的索引(k,j)
n:用户定义的标量,由《Attention Is All You Need》的作者设置为10,000。
i:用于将列索引映射到0≤i<d/2的值,单个i值同时映射到正弦和余弦函数。
不同的位置编码方案
在Transformer中使用了各种位置编码方案,每种方案都有其优点和缺点:
# 位置编码的实现 import torch import torch.nn as nn import torch.optim as optim import torch.nn.functional as F class PositionalEncoding(nn.Module): def __init__(self, d_model, max_len=5000): # 继承nn.Module的初始化方法 super(PositionalEncoding, self).__init__() # 计算位置编码 # 初始化一个(max_len, d_model)的零张量用于存储位置编码 pe = torch.zeros(max_len, d_model) # 创建一个从0到max_len-1的张量,用于表示每个位置的索引,并在第1维增加一个尺寸 position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1) # 计算缩放因子,用于调整正弦波和余弦波的频率 div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-torch.log(torch.tensor(10000.0)) / d_model)) # 根据位置编码公式填充偶数列(正弦部分) pe[:, 0::2] = torch.sin(position * div_term) # 填充奇数列(余弦部分) pe[:, 1::2] = torch.cos(position * div_term) # 为位置编码增加一个批次维度,以便于后续与输入数据相加 pe = pe.unsqueeze(0) # 使用register_buffer将位置编码缓存为模型的一部分,不会被梯度更新 self.register_buffer('pe', pe) def forward(self, x): # 在前向传播中,将位置编码直接加到输入x上,注意重复或裁剪以匹配x的序列长度 x = x + self.pe[:, :x.size(1)] return x # 示例用法 d_model = 512 # 嵌入维度 max_len = 100 # 序列最大长度 # 位置编码实例化 pos_encoder = PositionalEncoding(d_model, max_len) # 创建一个示例输入序列,形状为(批量大小, 序列长度, 嵌入维度) input_sequence = torch.randn(5, max_len, d_model) # 应用位置编码到输入序列上 input_sequence = pos_encoder(input_sequence) print("输入序列的位置编码:") print(input_sequence.shape)
位置编码的Python实现-详细解释
# 位置编码的实现 import torch import torch.nn as nn import torch.optim as optim import torch.nn.functional as F # 位置编码 class PositionalEncoding(nn.Module): def __init__(self, d_model, max_len=5000): super(PositionalEncoding, self).__init__() # 计算位置编码 pe = torch.zeros(max_len, d_model) ''' pe = torch.zeros(max_len, d_model): 这里创建了一个形状为(max_len, d_model)的张量pe,并将其所有元素初始化为0。max_len代表序列的最大长度, 即模型能够处理的输入序列中的最大token数量;d_model表示模型的嵌入维度,也就是Transformer模型中每个词嵌入的维度。 这个张量pe最终会被填充成位置编码,用于给模型提供关于序列中每个位置的绝对位置信息。 ''' position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1) ''' position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1): torch.arange(0, max_len, dtype=torch.float): 生成一个从0到max_len-1的等差数列,类型为浮点数(dtype=torch.float)。这意味着每个位置将由一个唯一的浮点数值表示。 .unsqueeze(1):这个操作在张量的位置1(即列方向)增加一个新的维度。原本的张量是一维的,每个位置一个值, 经过unsqueeze后,它变成了形状为(max_len, 1)的二维张量。这样做是为了使其能与之后的张量进行广播运算(Broadcasting), 以便于计算位置编码时能直接与d_model维度相结合。 ''' div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-torch.log(torch.tensor(10000.0)) / d_model)) ''' torch.arange(0, d_model, 2) 生成一个从0开始,步长为2,直到d_model-1(如果d_model为偶数 ,则正好到达d_model-2)的一维张量。这是因为位置编码公式中对偶数和奇数维度使用了不同的计算方式(正弦和余弦)。 (-torch.log(torch.tensor(10000.0)) / d_model) 计算一个缩放因子,这里是利用自然对数计算出一个负数, 用于控制不同维度上的频率。10000是原始Transformer论文中选择的一个常数,用于按比例缩小频率。 torch.exp(...) 对上述计算结果取指数,得到最终的缩放因子数组,用于后续正弦和余弦函数的计算。 ''' pe[:, 0::2] = torch.sin(position * div_term) pe[:, 1::2] = torch.cos(position * div_term) ''' position * div_term 计算每个位置与对应维度的缩放因子的乘积,这正是位置编码公式的一部分。 pe[:, 0::2] = ... 和 pe[:, 1::2] = ... 将计算出的正弦值和余弦值分别填充到pe矩阵的偶数列和奇数列中。 这样,矩阵的每一行代表一个位置的编码,每一列则对应嵌入空间的一个维度。 ''' pe = pe.unsqueeze(0) ''' 这一步是为了适应深度学习模型中常见的四维张量格式(Batch, Sequence Length, Height, Width 或Batch, Sequence Length, Channel), 在这里即添加一个批次维度,使得位置编码可以被正确地广播到多个批次的输入数据上。 ''' self.register_buffer('pe', pe) ''' register_buffer是PyTorch中用来注册一个不被优化器更新的持久张量(如常量或状态变量), 作为模型的一部分保存。这意味着pe(位置编码矩阵)将会被保存在模型的状态字典中, 随模型一起保存和加载,但不会被视为模型的可训练参数(即不会在反向传播中更新)。 这样,一旦模型初始化完成,位置编码就固定下来,可以复用于不同输入序列,而不需要每次前向传播时重新计算。 ''' def forward(self, x): x = x + self.pe[:, :x.size(1)] return x # 示例用法 max_len = 100 d_model = 512 # num_heads = 8 # 位置编码 pos_encoder = PositionalEncoding(d_model, max_len) # 示例输入序列 input_sequence = torch.randn(5, max_len, d_model) # 应用位置编码 input_sequence = pos_encoder(input_sequence) print("输出序列的位置编码:") print(input_sequence.shape) # torch.Size([5, 100, 512])
Transformer模型的一个显著特征是它能够同时关注输入序列的不同部分——这是多头注意力实现的。
通过将查询、键和值向量分成多个头,并进行独立的自注意力计算,模型获得了对输入序列的细致透视,丰富了其表示,带有多样化的上下文信息。
多头注意力的重要性
多头注意力机制具有几个优点:
多头注意力的计算:
# 多头注意力的代码实现 # 定义一个多头注意力(Multi-Head Attention)类,继承自nn.Module class MultiHeadAttention(nn.Module): def __init__(self, d_model, num_heads): # 调用父类初始化方法 super(MultiHeadAttention, self).__init__() # 设置头的数量 self.num_heads = num_heads # 设置模型的嵌入维度 self.d_model = d_model # 断言检查d_model是否能被num_heads整除,确保可以平均分配给每个头 assert d_model % num_heads == 0 # 计算每个头的维度 self.depth = d_model // num_heads # 定义线性变换层,用于查询、键、值的转换 self.query_linear = nn.Linear(d_model, d_model) self.key_linear = nn.Linear(d_model, d_model) self.value_linear = nn.Linear(d_model, d_model) # 定义一个输出的线性变换层,用于最终的注意力输出 self.output_linear = nn.Linear(d_model, d_model) def split_heads(self, x): # 接收一个张量并将其按头分割 batch_size, seq_length, d_model = x.size() # 调整张量形状以便于分头,然后转置以适应多头注意力的计算 return x.view(batch_size, seq_length, self.num_heads, self.depth).transpose(1, 2) def forward(self, query, key, value, mask=None): # 对查询、键、值应用线性变换 query = self.query_linear(query) key = self.key_linear(key) value = self.value_linear(value) # 将变换后的查询、键、值张量按头部进行分割 query = self.split_heads(query) key = self.split_heads(key) value = self.split_heads(value) # 计算缩放点积注意力分数,除以深度的平方根是为了缩放,防止softmax函数因太大或太小的值而饱和 scores = torch.matmul(query, key.transpose(-2, -1)) / math.sqrt(self.depth) # 如果提供了掩码,用负无穷大填充那些需要被屏蔽的位置,以便在softmax中忽略这些位置 if mask is not None: scores += scores.masked_fill(mask == 0, -1e9) # 应用softmax函数得到注意力权重 attention_weights = torch.softmax(scores, dim=-1) # 使用注意力权重加权求和值得到注意力输出 attention_output = torch.matmul(attention_weights, value) # 将多头注意力的结果合并回原始形状 batch_size, _, seq_length, d_k = attention_output.size() attention_output = attention_output.transpose(1, 2).contiguous().view(batch_size, seq_length, self.d_model) # 对最终的注意力输出做线性变换 attention_output = self.output_linear(attention_output) # 返回注意力输出结果 return attention_output # 示例代码:使用定义的多头注意力类 # 设置模型的嵌入维度、序列最大长度、头的数量以及潜在的前馈网络维度(尽管这里未直接使用) d_model = 512 max_len = 100 num_heads = 8 d_ff = 2048 # 创建一个多头注意力实例 multihead_attn = MultiHeadAttention(d_model, num_heads) # 随机生成一个示例输入序列张量 input_sequence = torch.randn(5, max_len, d_model) # 应用多头注意力于输入序列自身(查询=键=值) attention_output= multihead_attn(input_sequence, input_sequence, input_sequence) # 打印输出注意力结果的形状 print("attention_output shape:", attention_output.shape)
多头注意力的代码解释
import math import torch import torch.nn as nn # 多头注意力的代码实现 class MultiHeadAttention(nn.Module): def __init__(self, d_model, num_heads): super(MultiHeadAttention, self).__init__() # d_model: 模型的嵌入维度(dimension of the model),指的是输入和输出数据的特征维度 self.d_model = d_model # num_heads: 多头注意力中头的数量。 self.num_heads = num_heads # assert 语句用来断言 d_model 能够被 num_heads 整除, # 这是多头注意力机制的一个重要前提,确保每个头的维度(depth)是一个整数。 assert d_model % num_heads == 0 # self.depth 计算了每个注意力头的维度,即将整个模型维度均匀分配给每个头。 self.depth = d_model // num_heads ''' 这三个 nn.Linear 实例分别对应查询(query)、键(key)和值(value)的线性变换层。 它们的作用是将输入数据映射到相同的维度空间中,为后续的注意力计算做准备。 ''' self.query_linear = nn.Linear(d_model, d_model) self.key_linear = nn.Linear(d_model, d_model) self.value_linear = nn.Linear(d_model, d_model) # 输出线性投影 ''' 这个线性变换层用于对经过多头注意力机制计算后的输出进行进一步的处理, 通常是为了调整输出的维度或者添加非线性特性,确保输出与模型的其他部分兼容 ''' self.output_linear = nn.Linear(d_model, d_model) def split_heads(self, x): ''' view() 方法用于改变张量的形状而不改变其数据,这里将张量 x 重新塑形为四个维度,其中: 第一维仍然是 batch_size,保持不变。 第二维现在是 seq_length,同样保持不变。 第三维变为 self.num_heads,即多头注意力中的头数。 第四维是 self.depth,即每个头的特征维度,等于原 d_model 分配给每个头的大小(d_model // num_heads)。 transpose(dim0, dim1) 方法交换张量中指定维度的位置。 在这个情况下,它交换了第二维(原序列长度维度)和第三维(头的数量维度), 使得张量的新形状变为 (batch_size, num_heads, seq_length, depth)。 ''' batch_size, seq_length, d_model = x.size() return x.view(batch_size, seq_length, self.num_heads, self.depth).transpose(1,2) def forward(self, query, key, value, mask=None): # 线性投影 ''' 通过self.query_linear, self.key_linear, self.value_linear三个线性层分别对query, key, value进行变换, 保持张量维度不变,依然是(batch_size, seq_length, d_model),但内容经过了非线性映射,为后续的注意力计算做准备。 ''' query = self.query_linear(query) key = self.key_linear(key) value = self.value_linear(value) # 分割头部 ''' 最终形状变为(batch_size, num_heads, seq_length, depth) ''' query = self.split_heads(query) key = self.split_heads(key) value = self.split_heads(value) # 缩放点积注意力 ''' 计算每一对query和key之间的点积然后除以math.sqrt(self.depth),这个操作称为缩放,旨在避免点积结果过大导致softmax函数的饱和 通过query和key.transpose(-2, -1)相乘实现, 其中.transpose(-2, -1)是为了将(batch_size, num_heads, seq_length, depth)张量的维度从(seq_length, depth) 调整为(depth, seq_length),以便点积。 ''' scores = torch.matmul(query, key.transpose(-2, -1)) / math.sqrt(self.depth) # 掩码 ''' 若提供了mask,则在scores上进行操作,将需要被掩盖的位置的分数设为-1e9, 这会在softmax中近似为0,意味着这些位置不会获得注意力。 ''' if mask is not None: scores += scores.masked_fill(mask==0, -1e9) # 计算注意力权重并应用softmax ''' 应用softmax函数沿最后一个维度(dim=-1)对缩放后的点积结果进行归一化,得到注意力权重分布。 ''' attention_weights = torch.softmax(scores, dim=-1) # 计算注意力到值 ''' 通过点乘注意力权重和value,实现对value的加权求和,得到初步的注意力输出形状仍为(batch_size, num_heads, seq_length, depth) ''' attention_output = torch.matmul(attention_weights, value) # 合并头部 ''' 将多头的输出重新整合回单个向量,通过转置和视图重塑操作,最终形状变为(batch_size, seq_length, d_model),恢复到原始维度布局 ''' batch_size, _, seq_length, d_k = attention_output.size() attention_output = attention_output.transpose(1, 2).contiguous().view(batch_size, seq_length, self.d_model) # 线性投影 ''' 通过self.output_linear对合并后的注意力输出进行最后的线性变换,确保输出维度与输入一致,便于后续的网络层使用。 ''' attention_output = self.output_linear(attention_output) return attention_output # 示例用法 max_len = 100 d_model = 512 num_heads = 8 # d_ff = 2048 # 通常代表前馈网络(Feed Forward Network)的隐藏层维度,在Transformer架构中的位置编码之后的层可能会用到。 # 多头注意力实例化 ''' 使用定义好的d_model和num_heads参数实例化一个MultiHeadAttention对象,这是实现多头自注意力机制的核心组件。 ''' multihead_attn = MultiHeadAttention(d_model, num_heads) # 示例输入序列 ''' 生成一个形状为(5, 100, 512)的随机张量,模拟5个样本,每个样本包含100个时间步,每个时间步有512维特征。 这通常代表了一组经过嵌入层处理后的文本序列数据。 ''' input_sequence = torch.randn(5, max_len, d_model) # 多头注意力 ''' 将input_sequence作为查询(query)、键(key)和值(value)输入到多头注意力模块中。 ''' attention_output = multihead_attn(input_sequence, input_sequence, input_sequence) print("attention_output shape:", attention_output.shape) # torch.Size([5, 100, 512])
与人类大脑能够并行处理信息的能力类似,Transformer模型中的每一层都包含一个前馈网络——一种能够捕捉序列中元素之间复杂关系的多功能组件。
通过使用线性变换和非线性激活函数,前馈网络使模型能够在语言的复杂语义景观中航行,促进文本的稳健理解和生成。
前馈网络的作用
每个Transformer层内的前馈网络负责对输入表示应用非线性变换。它使模型能够捕捉数据中的复杂模式和关系,促进了高级特征的学习。
前馈层的结构和功能
前馈层由两个线性变换组成,两者之间通过一个非线性激活函数(通常是ReLU)分隔。让我们来解析一下结构和功能:
Python实现前馈网络
# 前馈网络的代码实现 # 定义一个前馈网络(FeedForward Network)模块,作为Transformer模型的一部分,用于在多头注意力之后增加网络的非线性表达能力。 class FeedForward(nn.Module): def __init__(self, d_model, d_ff): # 继承nn.Module的初始化方法,并定义前馈网络的结构 super(FeedForward, self).__init__() # 第一层线性变换,将输入维度d_model映射到一个更大的维度d_ff,有助于模型学习更复杂的表示 self.linear1 = nn.Linear(d_model, d_ff) # 第二层线性变换,将中间维度d_ff映射回原始的输入维度d_model,保持输出与输入维度一致 self.linear2 = nn.Linear(d_ff, d_model) # 使用ReLU激活函数,为网络引入非线性 self.relu = nn.ReLU() def forward(self, x): # 线性变换1后接ReLU激活,增加网络的非线性表达能力 x = self.relu(self.linear1(x)) # 第二次线性变换,将激活后的特征映射回d_model维度 x = self.linear2(x) # 返回前馈网络的输出 return x # 定义模型使用的超参数 d_model = 512 # 模型的嵌入维度 max_len = 100 # 序列的最大长度 num_heads = 8 # 多头注意力中头的数量 d_ff = 2048 # 前馈网络中间层的维度 # 实例化多头注意力模块 multihead_attn = MultiHeadAttention(d_model, num_heads) # 实例化前馈网络模块 ff_network = FeedForward(d_model, d_ff) # 生成一个随机的输入序列,用于演示 input_sequence = torch.randn(5, max_len, d_model) # 形状为(批次大小, 序列长度, 嵌入维度) # 应用多头注意力机制到输入序列上 attention_output = multihead_attn(input_sequence, input_sequence, input_sequence) # 将多头注意力的输出传入前馈网络 output_ff = ff_network(attention_output) # 打印输入序列和前馈网络输出的形状,验证数据流正确无误 print('input_sequence shape:', input_sequence.shape) print("output_ff shape:", output_ff.shape)
在Transformer模型中起着至关重要的作用,其主要任务是将输入序列转换为有意义的表示,捕捉输入的重要信息。
每个编码器层的结构和功能
编码器由多个层组成,每个层依次包含以下组件:输入嵌入、位置编码、多头自注意力机制和位置逐点前馈网络。
输入嵌入:我们首先将输入序列转换为密集向量表示,称为输入嵌入。我们使用预训练的词嵌入或在训练过程中学习的嵌入,将输入序列中的每个单词映射到高维向量空间中。
位置编码:我们将位置编码添加到输入嵌入中,以将输入序列的顺序信息合并到其中。这使得模型能够区分序列中单词的位置,克服了传统神经网络中缺乏顺序信息的问题。
多头自注意力机制:在位置编码之后,输入嵌入通过一个多头自注意力机制。这个机制使编码器能够根据单词之间的关系权衡输入序列中不同单词的重要性。通过关注输入序列的相关部分,编码器可以捕捉长距离的依赖关系和语义关系。
位置逐点前馈网络:在自注意力机制之后,编码器对每个位置独立地应用位置逐点前馈网络。这个网络由两个线性变换组成,两者之间通过一个非线性激活函数(通常是ReLU)分隔。它有助于捕捉输入序列中的复杂模式和关系。
Python实现带有输入嵌入和位置编码的编码器层的代码
# 编码器的代码实现 # 导入必要的库 import torch import torch.nn as nn # 定义多头注意力模块(Multi-Head Attention)和前馈网络模块(FeedForward), # 这里假设它们已被正确定义在代码的其他部分,因为它们的具体实现未给出。 # 注意:实际使用中,你需要实现这些类。 # 位置编码类,为输入序列中的每个位置生成一个固定的向量表示 class PositionalEncoding(nn.Module): def __init__(self, d_model, max_len=100): super(PositionalEncoding, self).__init__() # 初始化位置编码矩阵 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() * (-torch.log(torch.tensor(10000.0)) / d_model)) pe[:, 0::2] = torch.sin(position * div_term) pe[:, 1::2] = torch.cos(position * div_term) pe = pe.unsqueeze(0).transpose(0, 1) self.register_buffer('pe', pe) def forward(self, x): # 将位置编码加到输入序列上 x = x + self.pe[:x.size(0), :] return x # 编码器层,结合自注意力机制和前馈网络,同时包含Layer Normalization和Dropout class EncoderLayer(nn.Module): def __init__(self, d_model, num_heads, d_ff, dropout=0.1): super(EncoderLayer, self).__init__() # 初始化自注意力层、前馈网络层、归一化层以及Dropout层 self.self_attention = MultiHeadAttention(d_model, num_heads) # 多头注意力层 self.feed_forward = FeedForward(d_model, d_ff) # 前馈网络层 self.norm1 = nn.LayerNorm(d_model) # 第一层归一化 self.norm2 = nn.LayerNorm(d_model) # 第二层归一化 self.dropout = nn.Dropout(dropout) # Dropout层 self.positional_encoding = PositionalEncoding(d_model) # 位置编码 def forward(self, x, mask): # 应用位置编码 x = self.positional_encoding(x) # 自注意力层处理并添加残差连接 attention_output = self.self_attention(x, x, x, mask) attention_output = self.dropout(attention_output) x = x + attention_output x = self.norm1(x) # 前馈网络处理并添加残差连接 feed_forward_output = self.feed_forward(x) feed_forward_output = self.dropout(feed_forward_output) x = x + feed_forward_output x = self.norm2(x) return x # 设置模型超参数 d_model = 512 # 模型维度 max_len = 100 # 序列最大长度 num_heads = 8 # 多头注意力中的头数 d_ff = 2048 # 前馈网络的隐藏层尺寸 # 实例化编码器层和位置编码模块 encoder_layer = EncoderLayer(d_model, num_heads, d_ff, 0.1) pos_encoder = PositionalEncoding(d_model) # 创建一个随机的输入序列张量,模拟数据输入 input_sequence = torch.randn(5, max_len, d_model) # 形状为(批量大小, 序列长度, 模型维度) # 使用位置编码增强输入序列 input_with_pe = pos_encoder(input_sequence) # 通过编码器层处理带位置编码的输入序列,此处未使用遮罩(mask) encoder_output = encoder_layer(input_with_pe, None) # 打印输出的形状 print("encoder output shape:", encoder_output.shape) # 预期输出为 torch.Size([5, 100, 512])
在Transformer模型中,解码器在基于输入序列的编码表示生成输出序列方面起着至关重要的作用。它接收来自编码器的编码输入序列,并将其用于生成最终的输出序列。
解码器的功能
解码器的主要功能是生成输出序列,同时注意到输入序列的相关部分和先前生成的标记。它利用输入序列的编码表示来理解上下文,并对生成下一个标记做出明智的决策。
解码器层及其组件
解码器层包括以下组件:
输出嵌入右移:在处理输入序列之前,模型将输出嵌入向右移动一个位置。这确保解码器中的每个标记在训练期间都能从先前生成的标记接收到正确的上下文。
位置编码:与编码器类似,模型将位置编码添加到输出嵌入中,以合并标记的顺序信息。这种编码帮助解码器根据标记在序列中的位置进行区分。
掩码的多头自注意力机制:解码器采用掩码的多头自注意力机制,以便注意输入序列的相关部分和先前生成的标记。在训练期间,模型应用掩码以防止注意到未来的标记,确保每个标记只能注意到前面的标记。
编码器-解码器注意力机制:除了掩码的自注意力机制外,解码器还包括编码器-解码器注意力机制。这种机制使解码器能够注意到输入序列的相关部分,有助于生成受输入上下文影响的输出标记。
位置逐点前馈网络:在注意力机制之后,解码器对每个标记独立地应用位置逐点前馈网络。这个网络捕捉输入和先前生成的标记中的复杂模式和关系,有助于生成准确的输出序列。
解码器的代码实现
# 解码器的代码实现 import torch import torch.nn as nn class DecoderLayer(nn.Module): def __init__(self, d_model, num_heads, d_ff, dropout): """ 初始化解码器层所需的所有子层和归一化层。 :param d_model: 模型的维度 :param num_heads: 多头注意力中的头数 :param d_ff: 前馈网络中间层的维度 :param dropout: Dropout比率 """ super(DecoderLayer, self).__init__() self.masked_self_attention = MultiHeadAttention(d_model, num_heads) # 掩码自注意力层 self.enc_dec_attention = MultiHeadAttention(d_model, num_heads) # 编码器-解码器注意力层 self.feed_forward = FeedForward(d_model, d_ff) # 前馈网络层 self.norm1 = nn.LayerNorm(d_model) # 第一层归一化 self.norm2 = nn.LayerNorm(d_model) # 第二层归一化 self.norm3 = nn.LayerNorm(d_model) # 第三层归一化 self.dropout = nn.Dropout(dropout) # Dropout层 def forward(self, x, encoder_output, src_mask, tgt_mask): """ 解码器层的前向传播方法,执行自注意力、编码器-解码器注意力和前馈网络操作。 :param x: 解码器的输入 :param encoder_output: 编码器的输出 :param src_mask: 源序列的遮罩 :param tgt_mask: 目标序列的遮罩 :return: 经过解码器层处理后的输出 """ # 掩码自注意力层 self_attention_output = self.masked_self_attention(x, x, x, tgt_mask) self_attention_output = self.dropout(self_attention_output) x = x + self_attention_output x = self.norm1(x) # 编码器-解码器注意力层 enc_dec_attention_output = self.enc_dec_attention(x, encoder_output, encoder_output, src_mask) enc_dec_attention_output = self.dropout(enc_dec_attention_output) x = x + enc_dec_attention_output x = self.norm2(x) # 前馈层 feed_forward_output = self.feed_forward(x) feed_forward_output = self.dropout(feed_forward_output) x = x + feed_forward_output x = self.norm3(x) return x # 定义DecoderLayer的参数 max_len = 100 d_model = 512 num_heads = 8 d_ff = 2048 dropout = 0.1 batch_size = 1 encoder_layer = EncoderLayer(d_model, num_heads, d_ff, 0.1) pos_encoder = PositionalEncoding(d_model) # 创建一个随机的输入序列张量,模拟数据输入 input_sequence = torch.randn(5, max_len, d_model) # 形状为(批量大小, 序列长度, 模型维度) # 使用位置编码增强输入序列 input_with_pe = pos_encoder(input_sequence) # 通过编码器层处理带位置编码的输入序列,此处未使用遮罩(mask) encoder_output = encoder_layer(input_with_pe, None) # 定义DecoderLayer实例 decoder_layer = DecoderLayer(d_model, num_heads, d_ff, dropout) ''' 1. torch.rand(batch_size, max_len, max_len) 生成一个形状为(batch_size, max_len, max_len)的张量, 其中每个元素都是从0到1之间的随机数。 2. > 0.5 是一个布尔操作,将张量中的每个元素与0.5比较,如果元素大于0.5, 则结果为True(通常在PyTorch中表示为1),否则为False(通常表示为0)。 因此,这行代码实际上生成了一个二值掩码,其中大部分情况下会是随机分布的True和False ,但实际上在Transformer的上下文中,src_mask常用于表示源序列中的padding部分或者 在某些情境下对序列进行特殊处理,此处的生成方式可能不直接对应标准实践, 标准情况下src_mask应依据实际的padding情况精确构建。 ''' src_mask = torch.rand(batch_size, max_len, max_len) > 0.5 ''' 1. torch.ones(max_len, max_len) 创建一个形状为(max_len, max_len)的全1张量, 这里的max_len和max_len应当理解为同一概念,表示序列长度。 2. torch.tril(...) 是一个三角函数,它返回下三角矩阵的下三角部分,其中下三角部分的所有元素为1, 其余为0。在这个上下文中,它生成了一个下三角掩码, 用于确保自注意力(在解码器的自注意力层)中只关注当前位置及之前的位置,符合自回归原则。 3. unsqueeze(0) 在张量的第一个维度(批维度)添加一个维度,使得掩码可以广播到任何批次大小, 最终形状变为(1, max_len, max_len)。 4. == 0 这个操作将下三角矩阵转换为掩码形式,其中下三角为False(0,表示可访问), 上三角为True(1,经过后续处理后变为0,表示不可访问),以确保自注意力不会看到未来的信息。 ''' tgt_mask = torch.tril(torch.ones(max_len, max_len)).unsqueeze(0) == 0 #将输入张量传递到DecoderLayer output = decoder_layer(input_sequence, encoder_output, src_mask, tgt_mask) print("Output shape:", output.shape) # torch.Size([5, 100, 512])
前几节讨论的各种组件的综合体。让我们将编码器、解码器、注意力机制、位置编码和前馈网络的知识汇集起来,以了解完整的 Transformer 模型是如何构建和运作的。
在其核心,Transformer模型由编码器和解码器模块堆叠在一起,用于处理输入序列并生成输出序列。以下是架构的高级概述:
编码器
解码器
连接和标准化
完整的Transformer模型通过将多个编码器和解码器层堆叠在一起来构建。每个层独立处理输入序列,使模型能够学习分层表示并捕获数据中的复杂模式。编码器将其输出传递给解码器,后者根据输入生成最终的输出序列。
Python实现完整的Transformer模型
# Transformer的实现 import torch.nn as nn class Transformer(nn.Module): def __init__(self, src_vocab_size, tgt_vocab_size, d_model, num_heads, num_layers, d_ff, max_len, dropout): super(Transformer, self).__init__() # 定义编码器和解码器的词嵌入层 self.encoder_embedding = nn.Embedding(src_vocab_size, d_model) self.decoder_embedding = nn.Embedding(tgt_vocab_size, d_model) # 定义位置编码层 self.positional_encoding = PositionalEncoding(d_model, max_len) # 定义编码器和解码器的多层堆叠 self.encoder_layers = nn.ModuleList( [EncoderLayer(d_model, num_heads, d_ff, dropout) for _ in range(num_layers)] ) self.decoder_layers = nn.ModuleList( [DecoderLayer(d_model, num_heads, d_ff, dropout) for _ in range(num_layers)] ) # 定义线性层 self.linear = nn.Linear(d_model, tgt_vocab_size) self.dropout = nn.Dropout(dropout) # 生成掩码 def generate_mask(self, src, tgt): ''' 通过比较src(源序列)与0,创建一个布尔掩码,其中1表示有效 token(非填充值,假设0为填充标记), 0表示填充部分。然后,使用.unsqueeze(1)和.unsqueeze(2)分别在第1维和第2维增加一维, 以便该掩码能够正确广播并与后续操作中的注意力权重矩阵相乘。 这主要用于排除填充部分在自注意力计算中的影响。 ''' src_mask = (src != 0).unsqueeze(1).unsqueeze(2) tgt_mask = (tgt != 0).unsqueeze(1).unsqueeze(2) seq_length = tgt.size(1) ''' 这里计算了一个“nopeek”掩码,用于阻止解码器中的一个时间步访问之后的时间步, 这是Transformer解码器中的典型做法,以维持自回归特性。 它通过创建一个上三角矩阵(torch.triu)并取其补(减去自身并取1减去结果)来实现, 然后转换为布尔类型。seq_length是从目标序列tgt中获取的实际序列长度。 ''' nopeak_mask = (1 - torch.triu(torch.ones(1, seq_length, seq_length), diagonal=1)).bool() ''' 将之前初始化的目标序列掩码(排除了padding)与刚计算的“nopeek”掩码进行按位与操作(&), 这样得到的tgt_mask既排除了padding, 又阻止了每个位置看到未来的信息,符合Transformer解码器的需要。 ''' tgt_mask = tgt_mask & nopeak_mask ''' ,函数返回两个掩码:一个是源序列掩码,用于在编码器中屏蔽padding; 另一个是经过调整的目标序列掩码,用于在解码器中同时屏蔽padding和未来信息。 ''' return src_mask, tgt_mask # 前向传播 def forward(self, src, tgt): src_mask, tgt_mask = self.generate_mask(src, tgt) # 编码器输入的词嵌入和位置编码 encoder_embedding = self.encoder_embedding(src) en_positional_encoding = self.positional_encoding(encoder_embedding) src_embedded = self.dropout(en_positional_encoding) # 解码器输入的词嵌入和位置编码 decoder_embedding = self.decoder_embedding(tgt) de_positional_encoding = self.positional_encoding(decoder_embedding) tgt_embedded = self.dropout(de_positional_encoding) enc_output = src_embedded for enc_layer in self.encoder_layers: enc_output = enc_layer(enc_output, src_mask) dec_output = tgt_embedded for dec_layer in self.decoder_layers: dec_output = dec_layer(dec_output, enc_output, src_mask, tgt_mask) output = self.linear(dec_output) return output # 示例用法 src_vocab_size = 5000 tgt_vocab_size = 5000 d_model = 512 num_heads = 8 num_layers = 6 d_ff = 2048 max_len = 100 dropout = 0.1 transformer = Transformer(src_vocab_size, tgt_vocab_size, d_model, num_heads, num_layers, d_ff, max_len, dropout) # 生成随机示例数据 ''' 这行代码的作用是生成一个形状为(5, max_len)的张量,其中的每个元素都是在1到src_vocab_size之间(不包括src_vocab_size) 的随机整数。这个张量可以用来模拟一个包含5个样本、每个样本长度为max_len的源序列数据, 适用于训练如机器翻译、文本生成等序列到序列(sequence-to-sequence)的学习任务中。 每个元素可以被视作词汇表中一个单词的索引,用于后续的词嵌入等处理。 ''' src_data = torch.randint(1, src_vocab_size, (5, max_len)) # (batch_size, seq_length) tgt_data = torch.randint(1, tgt_vocab_size, (5, max_len)) # (batch_size, seq_length) transformer(src_data, tgt_data[:, :-1]).shape # torch.Size([5, 99, 5000])
训练Transformer模型涉及优化其参数以最小化损失函数,通常使用梯度下降和反向传播。一旦训练完成,就会使用各种指标评估模型的性能,以评估其解决目标任务的有效性。
训练过程
梯度下降和反向传播:
学习率调度:
评估指标
困惑度:
BLEU分数:
PyTorch对Transformer模型进行训练和评估的基本代码实现
# Transformer 模型的训练和评估流程实现 # 导入必要的库 import torch import torch.nn as nn import torch.optim as optim # 初始化损失函数 CrossEntropyLoss,设置忽略索引为0,通常用于padding部分的mask处理 criterion = nn.CrossEntropyLoss(ignore_index=0) # 初始化优化器 Adam,用于更新Transformer模型参数 # 学习率为0.0001,betas=(0.9, 0.98)为动量项系数,eps为防止除零异常的最小分母值 optimizer = optim.Adam(transformer.parameters(), lr=0.0001, betas=(0.9, 0.98), eps=1e-9) # 开始训练循环 transformer.train() # 设置模型为训练模式 # 进行多个epoch的训练 for epoch in range(10): # 假设训练10轮 # 在每个批次开始前清零梯度,避免梯度累积 optimizer.zero_grad() # 通过Transformer模型得到输出,src_data为源序列,tgt_data[:, :-1]为目标序列去掉最后一个词作为输入 output = transformer(src_data, tgt_data[:, :-1]) # 计算损失,输出和目标序列都调整为一维向量形式以匹配CrossEntropyLoss要求 ''' output.contiguous().view(-1, tgt_vocab_size): .contiguous()确保张量的内存是连续的。在某些操作后(如transpose),张量可能在内存中变得非连续, 这会影响之后的.view()操作。虽然在现代PyTorch版本中这一步有时不是必需的,但在涉及改变张量形状时 保持习惯性地使用可以避免潜在问题。 .view(-1, tgt_vocab_size) 改变output的形状。-1是一个占位符,表示该维度的大小由其他维度自动推断得出, 以保证整体元素数量不变。tgt_vocab_size是你目标词汇表的大小。 这意味着将输出张量重塑为形状(batch_size * sequence_length, tgt_vocab_size), 其中每一行对应一个单词的预测概率分布。 ''' loss = criterion(output.contiguous().view(-1, tgt_vocab_size), tgt_data[:, :-1].contiguous().view(-1)) # 反向传播计算梯度 loss.backward() # 更新模型参数 optimizer.step() # 打印当前epoch的损失信息 print(f"{epoch + 1} 轮:损失= {loss.item():.4f}") # 准备虚拟数据用于演示,模拟实际的输入输出数据 src_data= torch.randint(1, src_vocab_size, (5, max_len)) # 源序列数据 tgt_data= torch.randint(1, tgt_vocab_size, (5, max_len)) # 目标序列数据 # 开始评估循环,设置模型为评估模式以关闭dropout等训练时特有的操作 transformer.eval() # 使用torch.no_grad()上下文管理器,避免在此阶段计算和存储梯度,节省内存并加速评估过程 with torch.no_grad(): # 对虚拟数据进行前向传播得到预测输出 output = transformer(src_data, tgt_data[:, :-1]) # 计算评估阶段的损失,注意这里tgt_data切片变化,与训练阶段对应未来词预测 ''' 切片操作[:, 1:]意味着取所有行(样本),但是从每个序列的第二个元素到最后一个元素,这是因为序列到序列任务中, 模型在训练时通常需要预测下一个词,所以标签序列相比输入序列少一个元素(第一个词不参与预测)。 ''' loss = criterion(output.contiguous().view(-1, tgt_vocab_size), tgt_data[:, 1:].contiguous().view(-1)) # 输出评估损失 print(f"\n虚拟数据的评估损失= {loss.item():.4f}")
Transformers 在自然语言处理(NLP)领域引发了大量先进概念和应用。让我们深入探讨其中一些主题,包括不同的注意力变体、BERT(来自 Transformers 的双向编码器表示)和 GPT(生成式预训练 Transformer),以及它们的实际应用。
不同的注意力变体
注意力机制是 Transformer 模型的核心,使其能够专注于输入序列的相关部分。各种注意力变体的提议旨在增强 Transformer 的能力。
缩放点积注意力:是原始 Transformer 模型中使用的标准注意力机制。它将查询和键向量的点积作为注意力分数,同时乘以维度的平方根进行缩放。
多头注意力:注意力的强大扩展,利用多个注意力头同时捕捉输入序列的不同方面。每个头学习不同的注意力模式,使模型能够并行关注输入的各个部分。
相对位置编码:引入相对位置编码以更有效地捕捉标记之间的相对位置关系。这种变体增强了模型理解标记之间顺序关系的能力。
BERT(来自 Transformers 的双向编码器表示)
BERT 是一个具有里程碑意义的基于 Transformer 的模型,在 NLP 领域产生了深远影响。它通过掩码语言建模和下一句预测等目标,在大规模文本语料库上进行预训练。BERT 学习了单词的深层上下文表示,捕捉双向上下文,使其在广泛的下游 NLP 任务中表现良好。
代码片段 - BERT 模型:
# BERT
from transformers import BertModel, BertTokenizer
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
model = BertModel.from_pretrained('bert-base-uncased')
inputs = tokenizer("Hello, world!", return_tensors="pt")
outputs = model(**inputs)
print(outputs)
# 引入BERT模型和分词器 # 我们从transformers库中导入BertModel和BertTokenizer两个类, # 这个库提供了预训练的BERT模型和相应的分词工具的接口。 from transformers import BertModel, BertTokenizer # 初始化BERT分词器 # 使用'bert-base-uncased'预训练模型的分词规则来初始化分词器, # 'uncased'表示模型在训练时将所有字母转换为小写,因此输入文本也会被转换为小写形式处理。 tokenizer = BertTokenizer.from_pretrained('bert-base-uncased') # 初始化BERT模型 # 加载'bert-base-uncased'预训练模型, # 这是BERT的一个基础版本,适用于英文文本理解任务。 model = BertModel.from_pretrained('bert-base-uncased') # 对输入文本进行分词和编码 # 使用分词器对文本"Hello, world!"进行分词处理,并将其转化为模型所需的输入格式(张量形式), # return_tensors="pt"指定返回的张量类型为PyTorch张量。 inputs = tokenizer("Hello, world!", return_tensors="pt") # 将编码后的输入传递给BERT模型获取输出 # 通过模型的forward方法,利用之前准备的输入张量得到模型的输出, # **inputs表示将inputs字典解包为关键字参数。 outputs = model(**inputs) # 打印模型输出 # 输出可能包含最后一层的隐藏状态、池化输出等信息,具体取决于BERT模型的结构和配置。 print(outputs)
GPT(生成式预训练 Transformer)
GPT 是一个基于 Transformer 的模型,以其生成能力而闻名。与双向的 BERT 不同,GPT 采用仅解码器的架构和自回归训练来生成连贯且上下文相关的文本。研究人员和开发人员已经成功地将 GPT 应用于各种任务,如文本完成、摘要、对话生成等。
代码片段 - GPT 模型:
# GPT from transformers import GPT2LMHeadModel, GPT2Tokenizer tokenizer = GPT2Tokenizer.from_pretrained('gpt2') model = GPT2LMHeadModel.from_pretrained('gpt2') input_text = "Once upon a time, " inputs=tokenizer(input_text,return_tensors='pt') output=tokenizer.decode( model.generate( **inputs, max_new_tokens=100, )[0], skip_special_tokens=True ) input_ids = tokenizer(input_text, return_tensors='pt') print(output)
# 导入GPT-2模型和分词器 # 从transformers库中导入GPT2LMHeadModel和GPT2Tokenizer, # GPT2LMHeadModel是GPT-2的语言模型头部,用于预测下一个单词,GPT2Tokenizer用于文本的编码与解码。 from transformers import GPT2LMHeadModel, GPT2Tokenizer # 初始化GPT-2分词器 # 使用预训练的'gpt2'模型配置来初始化GPT2Tokenizer, # 这将允许我们对文本进行编码和解码,适应GPT-2模型的输入输出格式。 tokenizer = GPT2Tokenizer.from_pretrained('gpt2') # 初始化GPT-2模型 # 加载预训练的'gpt2'模型,GPT-2是一个强大的语言模型,能生成连贯的文本。 model = GPT2LMHeadModel.from_pretrained('gpt2') # 输入文本 # 定义一个起始文本,模型将基于此文本继续生成后续内容。 input_text = "Once upon a time, " # 文本编码 # 使用tokenizer将输入文本转换为模型所需的张量格式,指定返回张量类型为PyTorch张量。 inputs=tokenizer(input_text,return_tensors='pt') # 生成文本 # 使用模型生成新的文本,基于输入文本后最多生成100个新token, # 并从生成的第一个序列中取第一个序列进行解码,skip_special_tokens=True移除特殊token。 output=tokenizer.decode( model.generate( **inputs, # 将inputs字典展开传入generate函数 max_new_tokens=100, # 生成新token的最大数量 )[0], # 选取生成序列中的第一个序列 skip_special_tokens=True # 解码时不包括特殊token,如CLS,SEP等 ) # 再次编码输入文本(此部分代码未使用,可能是为了展示目的或后续操作预留) # 与之前相同,再次对input_text进行编码,但实际未在后续使用此部分结果。 input_ids = tokenizer(input_text, return_tensors='pt') # 打印生成的文本 # 输出基于初始文本input_text生成的完整句子。 print(output)
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。