1.Transformer是什么

Transformer模型是由Vaswani等人在2017年提出的一种神经网络架构,专门用于处理序列数据,尤其在自然语言处理(NLP)任务中取得了显著成功。与之前的RNN(递归神经网络)和LSTM(长短期记忆网络)不同,Transformer完全基于注意力机制(Attention Mechanism),摒弃了序列数据的逐步处理方式,从而在并行计算和长距离依赖建模上表现出色。

Transformer的核心组成部分有:

  • 自注意力机制(Self-Attention)

模型能够在处理每个位置的词时,考虑整个输入序列中的其他位置。这样,模型可以捕捉到词与词之间的关系,无论它们在序列中的相对位置如何。

  • 多头注意力(Multi-Head Attention)

将注意力机制分为多个“头”,每个头独立计算注意力权重,最终将它们的输出进行拼接,这样可以从不同的子空间学习信息,提高了模型的表达能力。

  • 位置编码(Positional Encoding)

由于Transformer不依赖于序列的顺序,位置编码被加到输入中,提供序列中每个词的位置信息。这样可以让模型理解词语在序列中的相对位置。

  • 前馈神经网络(Feed-forward Neural Network)

每个Transformer层除了自注意力模块外,还包括一个全连接的前馈神经网络,用来进一步处理信息。

  • 编码器-解码器架构

Transformer采用经典的编码器-解码器结构,编码器负责处理输入序列,解码器生成输出序列。在机器翻译等任务中,编码器将源语言转换为一个固定维度的表示,解码器再将其转换为目标语言。

整个Transformer的结构,就是这张经典的图:

image
image
image
image

优点

  • 并行计算:由于没有递归结构,Transformer可以对输入序列进行全局计算,便于并行化。
  • 长程依赖建模:自注意力机制使得模型能够有效捕捉长距离依赖关系。
  • 灵活性:可以处理不同长度的输入序列,且不受限制。

Transformer已经成为现代NLP的基石,许多强大的模型(如BERT、GPT、T5等)都是基于Transformer架构构建的。

2.源与嵌入层

以自然语言处理任务为例,举一个最简单的例子,现在有一个字符串:

abcdabcd

由于计算机/机器学习模型只能处理数值数据,无法直接理解文本、字符或单词,通常而言,需要将这个字符串映射成一串数字:

abcd[1234]abcd \to \begin{bmatrix} 1&2&3&4\end{bmatrix}

但是,这种简单的映射由于只在大小上做出了区分,没有在空间上的特征,导致其在一些线性模型(例如线性回归,逻辑回归,支持向量机等等)进行计算的时候出现问题,原因是线性模型可能误以为数值大小代表某种顺序或重要性,例如认为0.4比0.1更重要,但这在类别型变量中并不成立。。

为了提取更多的特征并捕捉单词之间的语义关系,可以采用的方式叫嵌入(Embedding),也就是嵌入层发挥的作用:将每个单词表示为一个固定长度的实数向量。

abcd[0.10.20.30.40.50.60.70.80.91.01.11.21.31.41.51.6]abcd\to \begin{bmatrix} 0.1 & 0.2 & 0.3 & 0.4 \\ 0.5 & 0.6 & 0.7 & 0.8 \\ 0.9 & 1.0 & 1.1 & 1.2 \\ 1.3 & 1.4 & 1.5 & 1.6 \\ \end{bmatrix}

代码形式如下:

  • 导入模块
import torch
from torch import nn
  • 创建包含索引的张量(4个索引)
a = torch.tensor([1, 2, 3, 4])
  • 创建嵌入层
ebd =nn.Embedding(5,24)
  • 输出和打印结果
b = ebd(a)
print(b)
# 结果
tensor([[-0.7904,  0.5471, -0.4514, -0.7538,  1.5639,  0.0705,  1.9967,  0.3174,
          0.1046, -0.5370,  1.5536,  0.4291,  2.1933,  1.1922, -1.0455, -0.8296,
          1.7162, -1.9261,  0.3409, -2.2696,  0.2463, -0.8309,  1.2765,  1.2191],
        [ 0.4621,  0.8377,  0.2505, -0.9243,  1.6725,  1.2940, -1.0017,  0.1868,
          0.9620,  0.3331,  0.2290, -0.8497,  1.4719, -2.3361,  0.6665,  0.4182,
         -0.2027, -0.4107,  0.9139, -1.2058, -0.4289, -1.6890, -0.0834,  0.4286],
        [ 1.0918,  0.5407, -0.7166,  0.2917, -0.5545, -0.6827, -1.0542,  1.4444,
          0.2575, -0.1669, -0.3521,  0.7163,  0.7645,  0.8136,  0.0690, -0.2884,
         -1.0883, -0.2865, -0.1383, -0.6299,  0.3096,  0.8552, -0.7034,  2.1753],
        [-0.7510,  0.6825,  0.2270,  1.0130,  0.1493,  1.6222,  0.0598, -0.1233,
          0.3305,  0.0930, -1.0862, -0.3040, -0.2602,  0.4079, -0.5940, -2.3670,
         -0.7736, -1.7522, -0.2198,  0.2601, -0.8661,  1.6565,  0.4271, -0.7885]],
       grad_fn=<EmbeddingBackward0>)

nn.Embedding(5,24)中,设置了num_embeddings=5embedding_dim=24。这两个参数分别定义了索引范围为[0,4],向量维度为24其中所有的输入索引必须在索引范围内。如果a = torch.tensor([1, 2, 3, 5]),就会抛出IndexError

如果输入做出如下改变:

a = torch.tensor([1, 1, 3, 4],[1, 2, 3, 4])

再打印结果:

b = ebd(a)
print(b)
# 结果
tensor([[[-1.1983, -0.3423,  1.2560, -1.2265,  0.1575, -0.6674, -0.2696,
           0.5489,  1.6796, -0.5678, -0.3400,  1.6862, -0.3589, -1.3475,
          -0.1106,  0.2354, -1.3852,  1.4993,  0.8008,  1.0397,  0.8497,
           1.3448,  0.4140, -0.9389],
         [-1.1983, -0.3423,  1.2560, -1.2265,  0.1575, -0.6674, -0.2696,
           0.5489,  1.6796, -0.5678, -0.3400,  1.6862, -0.3589, -1.3475,
          -0.1106,  0.2354, -1.3852,  1.4993,  0.8008,  1.0397,  0.8497,
           1.3448,  0.4140, -0.9389],
         [ 0.1288, -0.3129,  0.2500, -0.5909,  0.0918,  0.9257, -1.2978,
          -1.2873, -0.7122,  0.4469,  0.1014,  0.0917, -1.2800, -0.4082,
           0.7470,  1.2570,  0.0305,  0.0569,  0.3743, -0.7988, -0.3869,
          -1.2933,  1.9395, -1.3572],
         [ 0.6838,  1.4581, -1.3712,  0.8771,  1.0049, -1.3770,  0.1110,
           1.9460,  0.5817,  0.8095,  1.3511, -1.3308,  0.5980,  0.5942,
           0.5007, -2.8064,  0.2287, -1.6572,  1.9331,  1.4488,  0.4328,
          -0.7724,  0.3820, -0.1690]],

        [[-1.1983, -0.3423,  1.2560, -1.2265,  0.1575, -0.6674, -0.2696,
           0.5489,  1.6796, -0.5678, -0.3400,  1.6862, -0.3589, -1.3475,
          -0.1106,  0.2354, -1.3852,  1.4993,  0.8008,  1.0397,  0.8497,
           1.3448,  0.4140, -0.9389],
         [ 0.3517,  0.3673,  0.7477,  0.0468, -0.1705,  0.8861,  0.9255,
           0.6945, -0.8805,  0.3809, -1.6305,  1.2361, -1.3786,  0.8341,
           0.0260,  0.3535, -0.9166, -1.2232, -0.3622, -0.3645, -1.1246,
           0.7363, -1.0496,  0.1185],
...
         [ 0.6838,  1.4581, -1.3712,  0.8771,  1.0049, -1.3770,  0.1110,
           1.9460,  0.5817,  0.8095,  1.3511, -1.3308,  0.5980,  0.5942,
           0.5007, -2.8064,  0.2287, -1.6572,  1.9331,  1.4488,  0.4328,
          -0.7724,  0.3820, -0.1690]]], grad_fn=<EmbeddingBackward0>)

可以看到,输入数据可以是批量的(即batch_size = 2),并且相同输入(无论在不在同一张量)的向量是一致的。此时也可以查看a,b的维度信息:

print(a.shape)
print(b.shape)
# 结果
torch.Size([2, 4])
torch.Size([2, 4, 24])

3.位置编码

Transformer中的位置编码(Positional Encoding)用于为模型提供序列中每个元素的位置信息。由于Transformer模型完全基于自注意力机制(Self-Attention),而自注意力机制本身是排列不变(Permutation Invariant)的,即它无法区分输入序列中元素的顺序。因此,位置编码的引入是为了让模型能够感知输入序列中元素的相对或绝对位置。

在原始Transformer论文中,位置编码是通过正弦和余弦函数生成的,公式如下:

PE(pos,2i)=sin(pos100002idmodel)PE(pos,2i+1)=cos(pos100002idmodel)PE_{(pos, 2i)} = \sin\left(\frac{pos}{10000^{\frac{2i}{d_{\text{model}}}}}\right) \\ PE_{(pos, 2i+1)} = \cos\left(\frac{pos}{10000^{\frac{2i}{d_{\text{model}}}}}\right)

其中:

  • pospos是元素在序列中的位置(从0开始)。
  • ii是维度索引(从0到 dmodel1d_{model}-1)。
  • dmodeld_{model}是模型的嵌入维度

位置编码通常直接加到输入嵌入(Input Embedding)上:

X=Embedding(x)+PositionalEncodingX=Embedding(x)+PositionalEncoding

其中:

  • Embeddinng(x)Embeddinng(x)是输入序列的嵌入表示。
  • PositionalEncodingPositional Encoding是位置编码。

💡 Transformer原论文中的利用正弦余弦位置编码的方式,由于正弦和余弦函数的定义域是无限的,可以轻松扩展到比训练时更长的序列有更好的泛化能力。例如,即使模型在训练时只见过长度为100的序列,它仍然可以处理长度为1000或更长的序列,对任意pospos都适用。
如果使用可学习的位置编码(如nn.Embedding),模型只能学习到训练时见过的位置索引的编码。当测试时的序列长度超过训练时的最大长度时,模型无法生成未见过的位置编码,导致泛化能力下降,但能够根据任务的需求,自动调整位置信息的表示方式,灵活性更高。

由于此处位置编码是随意填的,非常不可靠,因此需要将位置编码也添加一个嵌入层,使其变为一个可以学习的位置编码。

目前a的每一条数据维度是4,因此可以创建如下位置编码。其中,输入序列与位置编码的嵌入向量的维度必须相同(否则无法相加);通过PyTorch的广播机制,位置编码会自动扩展,从而与词嵌入部分相加:

pos = torch.tensor([[0, 1, 2, 3]])
ebd2 = nn.Embedding(4,24)
pos_ebd = ebd2(pos)
print(pos_ebd.shape)
# 输出
torch.Size([1, 4, 24])

可以尝试编写完整的代码:

import torch
from torch import nn

class EBD(nn.Module):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # 词嵌入层,词汇表大小为28,嵌入维度为24
        self.word_ebd = nn.Embedding(28, 24)
        # 位置编码,词汇表大小为12,嵌入维度为24
        self.pos_ebd = nn.Embedding(12, 24)
        # 创建一个形状为(1, 12)的张量,表示位置索引
        self.pos_t = torch.arange(0, 12).reshape(1, 12)

    # X:(batch_size, length)
    def forward(self, X: torch.Tensor):
        # 将输入X通过词嵌入层和位置嵌入层,并将结果相加
        return self.word_ebd(X) + self.pos_ebd(self.pos_t)

if __name__ == "__main__":
    # 创建一个形状为(2, 12)的张量a,所有元素为1,数据类型为long
    a = torch.ones((2, 12)).long()
    # 实例化EBD类
    ebd = EBD()
    # 将张量a传入EBD实例,得到嵌入后的张量
    a = ebd(a)
# 输出
torch.Size([2, 12, 24])

其中:

  • 2代表有2条数据。
  • 12代表数据长度。
  • 24代表数据里每一位的嵌入向量的维度。

至此,从源输入到多头注意力前的部分就描述完整了。

image
image