Transformer相关知识

记录自己学习中遇到的transformer模型知识


前言

Transformer是一种深度学习模型架构,最初由Vaswani等人在2017年的一篇论文中提出,用于处理序列到序列的任务,如机器翻译。它是一种基于注意力机制的模型,相比传统的循环神经网络(RNN)或卷积神经网络(CNN),Transformer在处理长序列时表现更好,并且可以更好地并行化处理。Transformer已经成为自然语言处理领域的重要基础模型,并且在其他领域也有广泛的应用。

一、Self-Attention Mechanism(自注意力机制)

自注意力机制允许模型在处理序列时对不同位置的信息进行加权。对于每个输入位置,模型计算一个权重向量,该向量指示了与当前位置相关的其他位置的重要性。这使得模型能够在不同位置之间建立长距离的依赖关系。
(好困啊,想睡觉,一学习就想睡觉)
自注意力机制包括以下模块:
查询(Query):
通过线性变换将输入序列转换为查询向量,用于计算注意力权重。

键(Key):
通过线性变换将输入序列转换为键向量,用于计算与查询的相关性。

值(Value):
通过线性变换将输入序列转换为值向量,用于根据注意力权重加权求和得到最终的输出。

注意力权重计算:
通过计算查询向量与每个键向量之间的相似度,得到注意力权重,进而对值向量进行加权求和。
输出:
将加权求和后的值向量作为自注意力机制的输出。

import torch
import torch.nn.functional as F

class SelfAttention(nn.Module):
    def __init__(self, embed_size, heads):
        super(SelfAttention, self).__init__()
        self.embed_size = embed_size  # 设定嵌入向量的维度
        self.heads = heads  # 设定注意力头的数量
        self.head_dim = embed_size // heads  # 计算每个注意力头的维度
        
        # Make sure embedding size is divisible by the number of heads
        assert (
            self.head_dim * heads == embed_size
        ), "Embedding size needs to be divisible by heads"  # 确保嵌入向量的维度能被注意力头的数量整除
        
        # Linear transformations for values, keys, and queries
        self.values = nn.Linear(self.head_dim, self.head_dim, bias=False)  # 值的线性变换
        self.keys = nn.Linear(self.head_dim, self.head_dim, bias=False)  # 键的线性变换
        self.queries = nn.Linear(self.head_dim, self.head_dim, bias=False)  # 查询的线性变换
        
        # Final linear transformation for output
        self.fc_out = nn.Linear(heads * self.head_dim, embed_size)  # 最终输出的线性变换
        
    def forward(self, values, keys, query, mask):
        N = query.shape[0]  # 获取输入张量的第一个维度大小(样本数量)
        value_len, key_len, query_len = values.shape[1], keys.shape[1], query.shape[1]  # 获取序列的长度信息
        
        # Split the embedding into self.heads different pieces
        values = values.reshape(N, value_len, self.heads, self.head_dim)  # 将值的张量进行重塑,以便进行自注意力操作
        keys = keys.reshape(N, key_len, self.heads, self.head_dim)  # 将键的张量进行重塑,以便进行自注意力操作
        queries = query.reshape(N, query_len, self.heads, self.head_dim)  # 将查询的张量进行重塑,以便进行自注意力操作
        
        # Linear transformations
        values = self.values(values)  # 值的线性变换
        keys = self.keys(keys)  # 键的线性变换
        queries = self.queries(queries)  # 查询的线性变换
        
        # Calculate energy (attention scores)
        energy = torch.einsum("nqhd,nkhd->nhqk", [queries, keys])  # 计算能量(注意力分数)
        
        # Apply mask if provided
        if mask is not None:
            energy = energy.masked_fill(mask == 0, float("-1e20"))  # 如果提供了遮罩,则应用遮罩
        
        # Apply softmax to normalize the attention scores
        attention = torch.nn.functional.softmax(energy / (self.embed_size ** (1 / 2)), dim=3)  # 应用softmax函数对能量进行归一化,得到注意力权重
        
        # Weighted sum of values using attention scores
        out = torch.einsum("nhql,nlhd->nqhd", [attention, values]).reshape(
            N, query_len, self.heads * self.head_dim
        )  # 使用注意力权重对值进行加权求和
        
        # Final linear transformation
        out = self.fc_out(out)  # 最终的线性变换
        return out  # 返回输出结果

二、multi-head attention(多头注意力机制)

多头注意力(multi-head attention)是自注意力机制的一种扩展形式,它在自注意力机制的基础上引入了多个注意力头。每个注意力头都可以学习到不同的注意力权重,从而捕捉到序列中不同方面的信息。

自注意力机制是一种注意力机制,它在处理序列数据时能够捕捉序列内部的长距离依赖关系。在自注意力机制中,通过计算查询向量和键向量之间的相似度来计算注意力权重,然后将注意力权重应用到值向量上,得到最终的输出。自注意力机制的优点是能够并行化处理,同时能够捕捉到序列中不同位置之间的依赖关系。

多头注意力在自注意力机制的基础上,将输入数据通过线性变换分别投影到多个子空间,然后在每个子空间中独立计算注意力权重,最后将多个子空间中得到的输出拼接起来,得到最终的多头注意力输出。通过引入多个注意力头,多头注意力能够同时学习到不同方面的信息,从而提高模型的表示能力。

在多头注意力机制中,虽然每个注意力头的线性映射层输出维度相同,但它们学习到的是不同的权重矩阵。这意味着每个注意力头的映射层学习到的特征表示是不同的,因此可以被视为不同的子空间。

具体来说,每个注意力头的映射层包含了一个权重矩阵,这个权重矩阵决定了该注意力头对输入特征的映射方式。即使输出维度相同,不同的权重矩阵会导致不同的特征表示,因此可以认为它们对应不同的子空间。

举个例子,假设我们有一个输入特征维度为 128,希望使用 8 个注意力头。每个注意力头的映射层都是一个线性变换,输入维度为 128,输出维度为 16。尽管输出维度相同,但是每个映射层的权重矩阵是不同的,因此它们学习到的特征表示是不同的。例如,第一个注意力头可能学习到了序列中的位置信息,而第二个注意力头可能学习到了序列中的语义信息。因此,尽管它们的输出维度相同,但是它们学习到的特征表示是不同的,因此可以被视为不同的子空间。

通过这种方式,多头注意力机制可以通过多个不同的子空间捕捉到序列中不同方面的信息,从而提高了模型的表示能力。

1、怎么确定多头注意力的数量

以及子空间是按照什么划分的,如果是随机划分的话,为什么不能直接用原来的自注意力机制计算两两之间的相关性。
确定多头注意力的数量通常是一种超参数,需要通过实验调节来确定。一般来说,可以在验证集上进行实验,尝试不同数量的注意力头,然后选择在验证集上性能最好的模型作为最终模型。通常情况下,多头注意力的数量不宜过多,以免增加计算复杂度和参数量,同时也要考虑模型的表示能力。

子空间的划分通常是通过线性变换来实现的。在多头注意力中,输入数据会经过多个线性变换(例如线性映射和拆分操作),将输入数据映射到多个不同的子空间中。每个子空间都可以学习到不同方面的特征,然后在每个子空间中独立计算注意力权重,最后将多个子空间中得到的输出拼接起来,得到最终的多头注意力输出。

如果采用随机划分子空间,那么每个注意力头学习到的特征可能会相互重叠,导致模型的表达能力下降。此外,随机划分子空间可能会导致模型学习到不一致的特征,从而影响模型的泛化能力。因此,通常情况下,多头注意力会采用固定的线性变换来划分子空间,而不是随机划分。

原始的自注意力机制计算所有位置之间的相关性,但在多头注意力中,不同的子空间可能学习到不同方面的特征,因此需要在每个子空间中独立计算注意力权重。多头注意力能够通过同时学习多个子空间中的特征来提高模型的表达能力,从而取得更好的效果。
我的理解是对于多头注意力机制,将其分为不同的子空间,每部分学习模型的内容不同,会增加模型的表示能力

三、位置编码(Positional Encoding)

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
当然,让我们通过一个具体的代码示例来说明位置编码的实现。以下是一个简单的Python代码示例,演示了如何使用正弦和余弦函数来计算位置编码:

import torch
import torch.nn as nn
import math

class PositionalEncoding(nn.Module):
    def __init__(self, d_model, max_len=5000):
        super(PositionalEncoding, self).__init__()
        self.d_model = d_model
        self.max_len = max_len
        
        # 初始化位置编码矩阵
        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('pe', pe.unsqueeze(0))

    def forward(self, x):
        # 将位置编码加到输入张量中
        return x + self.pe[:, :x.size(1)]

# 测试代码
pe = PositionalEncoding(20)
input_tensor = torch.zeros(1, 10, 20)
output_tensor = pe(input_tensor)
print(output_tensor.shape)

这段代码实现了一个名为 PositionalEncoding 的PyTorch模块。在初始化函数中,我们计算了位置编码矩阵 pe,然后将其作为模块的一个缓冲区(buffer)注册。在前向传播函数中,我们将位置编码矩阵加到输入张量中,然后返回结果。

在计算位置编码时,我们首先创建了一个大小为 (max_len, d_model) 的零矩阵,其中 max_len 是序列的最大长度,d_model 是位置编码向量的维度。然后,我们使用正弦和余弦函数来计算位置编码值,并将其填充到矩阵中。最后,我们通过一个可学习的参数来调整位置编码的幅度,这样模型就可以根据需要调整位置编码的影响程度。

在测试代码中,我们创建了一个输入张量,大小为 (1, 10, 20),表示一个批次大小为1、序列长度为10、特征维度为20的输入。然后,我们将这个输入张量传递给 PositionalEncoding 模块,并打印输出张量的形状,以确认位置编码已经成功添加到输入中。

四、Feed-Forward Neural Networks(前馈神经网络,简称FFNN)

Feed-Forward Neural Networks(前馈神经网络,简称FFNN)是一种常见的神经网络结构,用于对输入数据进行非线性变换和特征提取。在Transformer模型中,每个注意力层后面都包含一个前馈神经网络,用于对注意力层的输出进行进一步处理和特征提取。

前馈神经网络通常由两个线性层和一个激活函数组成。首先,输入数据经过一个线性变换(通常称为隐藏层)产生一个中间表示,然后通过一个激活函数进行非线性变换,最后经过另一个线性变换(通常称为输出层)得到最终的输出。这样的设计能够使前馈神经网络学习到更复杂的特征表示,并增强模型的非线性拟合能力。

下面是一个简单的Python代码示例,演示了如何在PyTorch中实现一个前馈神经网络:

import torch
import torch.nn as nn

class FeedForwardNN(nn.Module):
    def __init__(self, input_dim, hidden_dim, output_dim, dropout=0.1):
        super(FeedForwardNN, self).__init__()
        self.fc1 = nn.Linear(input_dim, hidden_dim)  # 第一个线性层
        self.fc2 = nn.Linear(hidden_dim, output_dim)  # 第二个线性层
        self.dropout = nn.Dropout(dropout)  # dropout层
        self.relu = nn.ReLU()  # 激活函数

    def forward(self, x):
        x = self.fc1(x)  # 第一个线性层
        x = self.relu(x)  # 非线性变换
        x = self.dropout(x)  # dropout
        x = self.fc2(x)  # 第二个线性层
        return x

# 测试代码
input_tensor = torch.randn(64, 512)  # 输入张量大小为(batch_size, input_dim)
ffnn = FeedForwardNN(512, 2048, 512)  # 创建前馈神经网络实例
output_tensor = ffnn(input_tensor)  # 前向传播
print(output_tensor.shape)

在这个代码示例中,我们定义了一个名为 FeedForwardNN 的前馈神经网络类,它包含一个输入层(线性层)、一个隐藏层(线性层)、一个激活函数(ReLU)和一个输出层(线性层)。在 forward 方法中,我们按照线性层-非线性变换-线性层的顺序进行数据流动,并使用了dropout来防止过拟合。

在测试代码中,我们创建了一个输入张量 input_tensor,大小为 (64, 512),表示一个批次大小为64、输入维度为512的输入。然后,我们创建了一个前馈神经网络实例 ffnn,并对输入张量进行前向传播,得到输出张量 output_tensor,最后打印输出张量的形状。
Dropout层是一种用于防止神经网络过拟合的正则化技术。它在训练过程中以一定的概率(通常是0.1或0.5)随机将神经网络中的部分神经元(或节点)置为零,即将它们“丢弃”,不参与当前前向传播和反向传播。这样可以强制网络学习到更加鲁棒的特征表示,提高其泛化能力,减少过拟合的发生。

Dropout层通常被放置在神经网络的隐藏层之间。在训练过程中,每个神经元都有一定的概率被丢弃,而在测试或预测时,所有的神经元都会保留,但其输出值会按照丢弃概率进行调整,以保持期望输出值的一致性。

具体来说,Dropout层可以起到以下作用:

  1. 减少神经网络的复杂性:通过随机丢弃部分神经元,Dropout层可以减少神经网络的复杂性,防止过拟合,提高模型的泛化能力。

  2. 强制学习更鲁棒的特征表示:由于在训练过程中某些神经元被随机丢弃,网络不得不在每次训练迭代中学习到更加鲁棒的特征表示,从而提高模型的泛化能力。

  3. 减少神经元间的共适应性:通过随机丢弃部分神经元,Dropout层可以减少神经元之间的依赖关系,降低共适应性,从而使网络学习到更多的特征信息。

总之,Dropout层是一种有效的正则化技术,可以提高神经网络模型的泛化能力,减少过拟合的发生。

五、Layer Normalization and Residual Connections(层归一化和残差连接)

在这里插入图片描述

import torch
import torch.nn as nn

class SublayerConnection(nn.Module):
    def __init__(self, size, dropout):
        super(SublayerConnection, self).__init__()
        self.norm = nn.LayerNorm(size)  # 层归一化
        self.dropout = nn.Dropout(dropout)  # Dropout层

    def forward(self, x, sublayer):
        # 残差连接
        return x + self.dropout(sublayer(self.norm(x)))

# 测试代码
input_tensor = torch.randn(32, 512, 64)  # 输入张量大小为(batch_size, seq_len, embedding_dim)
sublayer = nn.Linear(64, 64)  # 子层(注意力层或前馈神经网络)
sublayer_connection = SublayerConnection(64, 0.1)  # 创建子层连接实例
output_tensor = sublayer_connection(input_tensor, sublayer)  # 前向传播
print(output_tensor.shape)

在这个代码示例中,我们定义了一个名为 SublayerConnection 的子层连接类,它包含了一个层归一化层和一个Dropout层。在前向传播方法中,我们首先对输入张量进行层归一化操作,然后将归一化后的结果传递给子层进行处理,最后通过残差连接将输入张量和子层的输出相加得到最终的输出张量。

在测试代码中,我们创建了一个输入张量 input_tensor,大小为 (32, 512, 64),表示一个批次大小为32、序列长度为512、特征维度为64的输入。然后,我们创建了一个线性层 sublayer 作为子层,并创建了一个子层连接实例 sublayer_connection。最后,我们对输入张量进行前向传播,得到输出张量 output_tensor,并打印输出张量的形状。

  def forward(self, x, sublayer):
        # 残差连接
        return x + self.dropout(sublayer(self.norm(x)))

在这个 forward 方法中,我们对输入 x 进行了如下处理:

  1. self.norm(x):首先,输入 x 经过层归一化处理,得到了归一化后的输出。这一步确保了每个神经元的输出具有相似的均值和方差。

  2. sublayer(self.norm(x)):然后,归一化后的输出作为子层 sublayer 的输入,经过子层处理,得到了子层的输出。

  3. self.dropout(sublayer(self.norm(x))):为了防止过拟合,我们对子层的输出进行了dropout操作,得到了dropout后的输出。

  4. x + self.dropout(sublayer(self.norm(x))):最后,我们将原始输入 x 与dropout后的输出相加,得到了残差连接的结果。由于dropout后的输出已经经过了子层的处理,因此这个操作实际上相当于将原始输入与子层的输出进行了相加,从而实现了残差连接。

残差连接的核心思想是将原始输入直接与子层的输出相加,使得模型在学习时可以更加轻松地学习到残差(或者说误差),从而加速模型的训练和收敛。在这个代码中,我们通过 x + self.dropout(sublayer(self.norm(x))) 这一行代码实现了这一思想,因此这个操作被称为残差连接。

六、Encoder-Decoder Architecture(编码器-解码器结构)

编码器-解码器结构是一种常见的神经网络架构,广泛应用于序列到序列(sequence-to-sequence)的任务,例如机器翻译、文本摘要等。在这种架构中,输入序列经过编码器进行编码,然后解码器使用编码器生成的表示来生成目标序列。

让我们以机器翻译任务为例来详细解释编码器-解码器结构:

  1. 编码器(Encoder):编码器负责处理输入序列,并将其转换成一个高维度的表示。在Transformer中,编码器通常由多个相同的编码器层(Encoder Layers)堆叠而成。每个编码器层包含一个自注意力层和一个前馈神经网络层,用于捕捉输入序列的特征表示。编码器的输出是输入序列的表示。

  2. 解码器(Decoder):解码器使用编码器生成的表示来生成目标序列。解码器也由多个相同的解码器层(Decoder Layers)堆叠而成,每个解码器层也包含一个自注意力层、一个编码器-解码器注意力层和一个前馈神经网络层。解码器的自注意力层用于捕捉目标序列的内部结构,编码器-解码器注意力层用于将编码器生成的表示与目标序列进行对齐,前馈神经网络层用于生成下一个单词的概率分布。

在训练过程中,编码器和解码器是分开训练的。首先,编码器将输入序列编码成表示,然后解码器根据这个表示生成目标序列。在推理过程中,编码器和解码器是一起使用的。编码器将输入序列编码成表示,然后解码器根据这个表示生成目标序列。

下面是一个简单的Python代码示例,演示了如何使用PyTorch实现一个简单的编码器-解码器结构:

import torch
import torch.nn as nn

class EncoderDecoder(nn.Module):
    def __init__(self, encoder, decoder):
        super(EncoderDecoder, self).__init__()
        self.encoder = encoder  # 编码器
        self.decoder = decoder  # 解码器

    def forward(self, src, tgt):
        # 编码器处理输入序列
        encoder_output = self.encoder(src)
        # 解码器使用编码器的输出生成目标序列
        decoder_output = self.decoder(tgt, encoder_output)
        return decoder_output

# 测试代码
encoder = nn.Linear(512, 256)  # 编码器
decoder = nn.Linear(256, 512)  # 解码器
encoder_decoder = EncoderDecoder(encoder, decoder)  # 创建编码器-解码器实例
src_tensor = torch.randn(32, 10, 512)  # 输入张量大小为(batch_size, seq_len, input_dim)
tgt_tensor = torch.randn(32, 20, 256)  # 目标张量大小为(batch_size, seq_len, output_dim)
output_tensor = encoder_decoder(src_tensor, tgt_tensor)  # 前向传播
print(output_tensor.shape)

在这个代码示例中,我们定义了一个名为 EncoderDecoder 的编码器-解码器类,它包含一个编码器和一个解码器。在前向传播方法中,我们首先将输入序列 src 传递给编码器,然后将编码器的输出传递给解码器,最后得到解码器的输出。在测试代码中,我们创建了一个编码器和一个解码器,然后创建了一个编码器-解码器实例,并对输入张量进行前向传播,得到输出张量,并打印输出张量的形状。

七、Transformer整体架构

在这里插入图片描述
这幅图展示了Transformer模型的主要架构,包括以下几个关键模块:

  • 输入嵌入(Input Embedding):将输入序列转换为向量序列。
  • 位置编码(Positional Encoding):给向量序列添加位置信息,以保持序列顺序。
  • 多头注意力(Multi-Head Attention):允许模型在处理每个位置的输入时同时考虑序列中的所有位置。
  • 前馈神经网络(Feed Forward):对每个位置的向量进行独立的相同变换。
  • 掩蔽多头注意力(Masked Multi-Head Attention):确保解码器在生成下一个输出时只能看到之前的输出。
  • 线性层和Softmax激活函数:将解码器的输出转换为最终的输出概率。

每个编码器和解码器层都包含一个“加法 & 归一化(Add & Norm)”步骤,以稳定训练过程并加速收敛。整个模型由Nx个这样的编码器和解码器层堆叠而成。这种架构使得Transformer模型能够有效地处理长距离依赖关系,并在各种序列到序列的任务中取得了显著的成果。

Source: Conversation with Bing, 31/03/2024
(1) . https://aitechtogether.com/article/44936.html.
(2) . https://blog.csdn.net/zgpeace/article/details/126635650.
(3) . https://velog.io/@sobing/%EB%94%A5%EB%9F%AC%EB%8B%9DNLPTransformerAttention-Is-All-You-Need.
(4) . https://www.johngo689.com/270648/.
(5) . https://www.yacinemahdid.com/are-ai-becoming-sentient/.
(6) https://medium.com/@inboxparas1/image-captioning-using-transformers-73c76c83a7b4. https://medium.com/@inboxparas1/image-captioning-using-transformers-73c76c83a7b4.
(7) https://www.linkedin.com/pulse/llm-transformer-architecture-shivasish-mahapatra-kj9qf. https://www.linkedin.com/pulse/llm-transformer-architecture-shivasish-mahapatra-kj9qf.

  • 25
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值