assignment_3_transformer

Transformer

pk5NGOx.png

输入部分

pk50vOs.jpg

输入向量化(input embedding)

  • 嵌入算法将每个输入词转换为向量,编码器接收向量列表作为输入。它通过将这些向量传递到“自我注意”层来处理此列表,然后传递到前馈神经网络,然后将输出向上发送到下一个编码器。

pk5DY2F.jpg

位置编码(position embedding)

位置编码的数学表达

对于每个位置 \(p\) 的编码,可以使用以下公式生成:

\(\begin{aligned}PE(pos,2i)&=sin(\frac{pos}{10000^{\frac{2i}{d_{model}}}})\\\\PE(pos,2i+1)&=cos(\frac{pos}{10000^{\frac{2i}{d_{model}}}})\end{aligned}\)

其中: * \(PE\) 是位置编码矩阵。 * \(pos\)是词在序列中的位置。 * \(i\) 是维度索引。 * \(d_model\) 是模型的维度。

位置编码的基本原理

位置编码通常添加到词嵌入(Word Embedding)之上,确保模型能够利用每个单词在序列中的位置信息。位置编码通常使用正弦和余弦函数的组合来实现,这样可以保证编码的平滑性和周期性。

位置编码的作用

位置编码使得Transformer模型能够:

  • 捕捉长距离依赖关系,即使在很长的序列中也能保持有效。
  • 理解序列中单词的相对位置,这对于语言的语义理解至关重要

位置编码的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

class PositionalEncoding(nn.Module):
def __init__(self, d_model, dropout, max_len=5000):
"""
执行论文的PE方程:
PE(pos,2i) = sin(pos/10000^(2i/d_model))
PE(pos,2i+1) = cos(pos/10000^(2i/d_model))
由于Transformer不包含递归和卷积,为了让模型利用序列的顺序,必须注入一些关于标记在序列中的相对或绝对位置的信息。
为此,在编码器和解码器堆栈底部的输入嵌入中添加了“位置编码”,作者使用不同频率的正弦和余弦函数实现。
:param d_model: (int) embedding后词向量的维度
:param dropout: (int) 丢弃机制
:param max_len: (int) 最大长度
"""
super(PositionalEncoding, self).__init__()
self.dropout = nn.Dropout(p=dropout)
# 在对数空间计算一次位置编码.
pe = torch.zeros(max_len, d_model)
position = torch.arange(0, max_len).unsqueeze(1)
div_term = torch.exp(torch.arange(0, d_model, 2) * -(math.log(10000.0) / d_model))
pe[:, 0::2] = torch.sin(position * div_term)
pe[:, 1::2] = torch.cos(position * div_term)
pe = pe.unsqueeze(0)
self.register_buffer('pe', pe)

def forward(self, x):
x = x + Variable(self.pe[:, :x.size(1)], requires_grad=False)
return self.dropout(x)

编码层(Encoder layer)

注意力机制:Attention

  • 注意力机制(Attention Mechanism)是深度学习领域中一种重要的模型结构,它模仿了人类的注意力行为。 它的名字来源于人类的注意力,指的人能够有意识和主动地关注某个对象。

  • 人的注意力是可以自下而上,也可以绑定一个任务至上而下。 如我们看下一张图片的,注意力一般会优先集中在桌面和柜子的物体上,这是自下而上的,但如果我们有饮食的目的,注意力会进一步聚焦到红框所展示的食物饮料上,这是自上而下的。而深度学习中的attention模仿的是后者。

pk5NYm6.jpg

  • 从这个例子可以引申出attention中比较抽象的三个定义,我们以饮食为目的,想找寻“食物”,这里“食物”就可以看作query,图片各个区域的像素就是value,它们有自己对应的物品key。注意力机制就是优先关注key与query更加相似的value,比如在这个场景下,就是图片中以水果、蛋糕为代表的下午茶部分,而且最终提取出的信息(attention value)着重来自于这几个关键信息的加权。

在人类的注意力行为中,我们可以有意识地关注某个对象,这种注意力可以是自下而上的,也可以是自上而下的。例如,在观看一张图片时,我们的注意力可能会首先集中在图片中的某些显著物体上(自下而上),而如果我们有特定的目标,比如寻找食物,我们的注意力就会进一步聚焦到与食物相关的物体上(自上而下)。

在深度学习中,注意力机制主要模仿的是自上而下的过程。具体来说,注意力机制涉及到以下几个关键概念:

  • Query(查询):这是我们想要关注的目标或对象,比如在寻找食物的场景中,"食物"就是Query。
  • Value(值):这是输入数据中的元素,它们各自拥有对应的信息。在图片中,Value可以是图片的各个区域的像素。
  • Key(键):与Value相对应,每个Value都有一个Key,它们用来标识Value。

注意力机制的核心思想是,系统会优先关注那些与Query更加相似的Value。这种相似度是通过计算Query和Key之间的相似度来确定的,通常使用向量之间的点积来衡量。两个向量的方向越相近,夹角越小,它们的相似度就越大。这个相似度可以用来作为Value的权重,通过加权求和的方式,生成一个新的输出向量,这个输出向量可以看作是Query在当前场景下更具体的指代。 #### attention 的计算 * 向量内积可以解释为key向量在query向量方向的投影距离与query向量长度的乘积,这在很大程度上反义了向量相似度。因为两个向量越相关投影就会越长,完全不相关时夹角为90度,对应内积为0。 * 除以$ \(以消除计算内积时的求和对方差的扩大效应,其中\)d_k$ 为维度 * 归一。Softmax的公式为 $ (x_{i})= $ 可以将向量中各元素变为和为1的权重,实现对value向量的加权求和。 * 乘以V矩阵,此矩阵由 \(m\) 个长度为\(d_k\)的value向量组成\(m*d_k\)的矩阵,作为attention的最终输出。

注意力机制的数学表达可以通过以下公式来描述:

$ (Q, K, V)=() V $

注意力机制在自然语言处理、图像处理、机器翻译等多个领域都有广泛的应用,它能够使模型更加灵活地处理序列数据,提高模型的性能和泛化能力。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

def attention(query, key, value, mask=None, dropout=None):
"""
计算'Scaled Dot Product Attention',输入由维度为dk的query,key以及维度为dv的values组​​成
# 注意力函数可以描述为将一个query和一组key对映射到一个output,其中query、keys、values和output都是向量。
# 输出为values的加权总和,其中分配给每个值的权重由query与相应key的相关性函数计算。
# 计算key和query的点积,将每个key除以√dk,并应用 softmax 函数来获得值的权重。
# 在实践中,同时计算一组query的注意力函数,打包成一个矩阵Q
:param query: (Tensor)query矩阵
:param key: (Tensor)key矩阵
:param value: (Tensor)value矩阵
:param mask:
:param dropout: (int)丢弃机制
:return:
"""
d_k = query.size(-1)
scores = torch.matmul(query, key.transpose(-2, -1)) \
/ math.sqrt(d_k)
if mask is not None:
scores = scores.masked_fill(mask == 0, -1e9)
p_attn = F.softmax(scores, dim=-1)
if dropout is not None:
p_attn = dropout(p_attn)
return torch.matmul(p_attn, value), p_attn

自注意力机制:Self-Attention

在自注意力机制(\(self-attention\))中,\(Q、K、V\)全都来自原始文本\(X\),之前会先乘以一个可训练的矩阵作为缓冲,将此映射到不同的空间提升模型的拟合能力。

换句话说,对于一段文本,我们以续写此文本为目的,关注每一个词在整段语境下的\(attention value\),我们以下面一段话为例模拟一下自然语言大模型的的学习过程:

你是信的开头,诗的内容,童话的结尾。你是理所当然的奇迹,你是月色真美。你是圣诞老人送给我好孩子的礼物,你是三千美丽世界里,我的一瓢水。”

在自然语言处理的大型模型中,最小的语义单元被称为token。例如,"结尾"这个词可以被视为一个token,它会被赋予一个特定的数值标记,并转化为一个查询向量。在之前的讨论中,我们了解到注意力机制的输出向量可以代表这个场景下query的具体含义。如果模型识别出"结尾"是用来修饰另一个token"童话"的,那么它们之间就会因为相似性而建立联系,"童话"的词向量会以一定的权重被加到query"结尾"上,使得"结尾"的词向量从原本的中性含义向正面含义倾斜。因为童话的结局通常都是美好的,所以"结尾"的词向量在这种语境下会带有积极的色彩。

然而,大型模型的分析并没有就此停止。模型可以再次检查这些经过调整的词向量,并进行新一轮的注意力计算。在这个过程中,模型可能发现原本中性的词汇如"开头"、"内容"、"结尾"现在都带有了正面的色彩,它们之间的相似度因此增加,模型通过这种新的注意力机制赋予了它们一种排比的修辞效果。

经过多轮的迭代计算,模型最终识别出这些排比句共同隐含的主题——美好的爱情。基于这一发现,模型接着生成了下一个token,以延续这个主题。

多头注意力机制:MultiHeadAttention

一组 \(Q,K,V\) 得到了一组当前词的特征表达,类似卷积神经网络中的filter,提取更多种特征

每一个词向量有多组\(Q_i,K_i,V_i\),分别计算并拼接

  • multi-headed结果:不同的注意力结果,得到的特征向量表达也不相同
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43

    class MultiHeadedAttention(nn.Module):
    def __init__(self, h, d_model, dropout=0.1):
    """
    计算多头注意力机制,其实就是将'Scaled Dot Product Attention'重复h次
    # 两个最常用的注意力函数是加法注意力 (cite)和点积(乘法)注意力。点积注意力与我们的算法相同
    # 多头注意力允许模型共同关注来自不同位置的不同表示子空间的信息。
    :param h: (int)重复次数
    :param d_model: (int) embedding后词向量维度
    :param dropout: (int)丢弃机制
    """
    super(MultiHeadedAttention, self).__init__()
    assert d_model % h == 0
    # 假设 d_v 总是等于 d_k
    self.d_k = d_model // h
    self.h = h
    self.linears = clones(nn.Linear(d_model, d_model), 4)
    self.attn = None
    self.dropout = nn.Dropout(p=dropout)

    def forward(self, query, key, value, mask=None):
    """
    前向传播,对应论文多头注意力那个图
    :param query: (Tensor)query矩阵
    :param key: (Tensor)key矩阵
    :param value: (Tensor)value矩阵
    :param mask:
    :return:
    """
    if mask is not None:
    mask = mask.unsqueeze(1)
    nbatches = query.size(0)

    # 1) 做线性变换获取query, key, value,模型维度(d_model) 为 h x d_k
    query, key, value = [l(x).view(nbatches, -1, self.h, self.d_k).transpose(1, 2)
    for l, x in zip(self.linears, (query, key, value))]

    # 2) 对每个batch所有向量施加注意力.
    x, self.attn = attention(query, key, value, mask=mask, dropout=self.dropout)

    # 3) 使用view函数和全连接层实现"Concat".
    x = x.transpose(1, 2).contiguous().view(nbatches, -1, self.h * self.d_k)
    return self.linears[-1](x)
  • 每个位置上的单词都会经过一个自注意力过程。然后,它们会分别经过一个前馈神经网络——完全相同的网络,每个向量都会分别流过它。

求和与归一化(Add & Norm)

Add和Norm层是由Add和Norm两部分构成的计算公式如下:

\[LayerNorm(X+MultiHeadAttention(X))\]

\[LayerNorm(X+FeedForward(X))\]

  • X表示多头或者前馈神经网络的输入,MultiHeadAttention(X)和FeedForward(X)表示输出(输出与输入X维度是一样的,所以 可以相加)。
  • X+MultiHeadAttention(X)是一种残差连接,通常用于解决多层网络梯度消失和梯度爆炸的问题,可以让网格只关注当前差异的 部分,在ResNet中经常用到,公式简单表达就是:

\[输出=输入十F(输入)\]

pk5UlE8.png

Feedforward前馈神经网络

前馈神经网络层结构相对比较简单,是一个两层的全连接层,第一层的激活函数为Relu,第二层不使用激活函数,公式如下

\[output = max(0,X*W_1 + b_1)*W_2 + b_2\]

pk56nJ0.png

解码层(Decoder Layer)

decode部分和encode部分相似,但是也存在一些区别 * decode侧有两个multi--head attention层 * decode的第一个多头采用了mask操作,而第二个多头的k,v矩阵使用的是encode侧编码信息矩阵c(encode的输出)计算, 而q则是第一个decode侧的多头的输出计算 * 最后包含一个softmax层计算下一个翻译单词的概率(假设是翻译任务)

第一个多头注意力(带有mask)

  • 流程就是:首先输入作为decode侧的开始标志预测第一个单词,得到I后,然后将""作为下一次预测的输入,以此类推
  • 具体操作其实是加了一个与输入矩阵形状相同的ask矩阵,这样就可以把每一次计算当前时间后的信息盖住;
  • 然后的操作就是和之前的自注意力一样,通过输入矩阵×计算得到q,k,v矩阵然后计算q和k转置的乘积,但是再softmax之 前需要乘以mask矩阵,如下图所示
  • 得到的mask QKAT进行softmax就会发现单词0那一行1234的位置都是0,因为我们设置的是负无穷映射后就成0了,再去 与矩阵V得到矩阵Z

pk5aJIO.png

pk5atiD.png

pk5aNJe.png

此处我有一个一直没看懂的疑问,为啥要加mask,我查阅了很多文章都是说为了让模型预测第i+1的时候只能使用+1前的信息, 但是场景类似于完型填空,因为self attention是使用的上下文信息是全文的,所以它是提前知道信息的,所以需要加mask,但是 比如这个翻译,是怎么提前看到的呢?搞了半天才明白一个重点,就是上面讲述的decode侧的输入,train的时候decode侧输入 的是正确答案,所以要mask

第二个多头注意力

  • 这个多头的区别就是输入,自注意力的k,v矩阵使用的encode侧输出的编码信息矩阵c,第一个多头提供的输出计算q,(train的时候)此处的q就是加了掩码的正确信息,而kv是原始信息,好处就是q这边的每一个信息都可以利用上encode侧的所有信息,这 些信息是不需要mask的 ### softmax预测输出 pk5aBLt.png
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76

class EncoderDecoder(nn.Module):
def __init__(self, encoder, decoder, src_embed, tgt_embed, generator):
"""
标准的encoder-decoder架构
:param encoder: (nn.Module) transformer编码器模型
:param decoder: (nn.Module) transformer解码器模型
:param src_embed: (nn.Module) 输入词向量(embedding层)
:param tgt_embed: (nn.Module) 目标词向量(embedding层)
:param generator: (nn.Module) 生成器,实现transformer的decoder最后的linear+softmax
"""
super(EncoderDecoder, self).__init__()
self.encoder = encoder
self.decoder = decoder
self.src_embed = src_embed
self.tgt_embed = tgt_embed
self.generator = generator

def forward(self, src, tgt, src_mask, tgt_mask):
"""
喂入和处理masked src和目标序列.
decode的输入是encode输出,目标词向量
:param src: (Tensor) 输入词向量
:param tgt: (Tensor) 输出词向量
:param src_mask:
:param tgt_mask:
:return: (nn.Module) 整个transformer模型
"""
return self.decode(self.encode(src, src_mask), src_mask, tgt, tgt_mask)

def encode(self, src, src_mask):
return self.encoder(self.src_embed(src), src_mask)

def decode(self, memory, src_mask, tgt, tgt_mask):
return self.decoder(self.tgt_embed(tgt), memory, src_mask, tgt_mask)


class Generator(nn.Module):
"""定义标准的linear + softmax生成器."""
def __init__(self, d_model, vocab):
super(Generator, self).__init__()
self.proj = nn.Linear(d_model, vocab)

def forward(self, x):
return F.log_softmax(self.proj(x), dim=-1)


def make_model(src_vocab, tgt_vocab, N=6, d_model=512, d_ff=2048, h=8, dropout=0.1):
"""
构建整体模型
:param src_vocab: (int) 输入词向量尺寸
:param tgt_vocab: (int) 输出词向量尺寸
:param N: (int, default=6) 编解码层的重复次数
:param d_model: (int, default=512) embedding后词向量维度
:param d_ff: (int, default=2048) 编解码器内层维度
:param h: (int, default=8) 'Scaled Dot Product Attention',使用的次数
:param dropout: (int, default=0.1) 丢弃机制,正则化的一种方式,默认为0.1
:return: (nn.Module) 整个transformer模型
"""
c = copy.deepcopy
attn = MultiHeadedAttention(h, d_model)
ff = PositionwiseFeedForward(d_model, d_ff, dropout)
position = PositionalEncoding(d_model, dropout)
model = EncoderDecoder(
Encoder(EncoderLayer(d_model, c(attn), c(ff), dropout), N),
Decoder(DecoderLayer(d_model, c(attn), c(attn), c(ff), dropout), N),
nn.Sequential(Embeddings(d_model, src_vocab), c(position)),
nn.Sequential(Embeddings(d_model, tgt_vocab), c(position)),
Generator(d_model, tgt_vocab))

# 下面这部分非常重要,模型参数使用Xavier初始化方式,基本思想是输入和输出的方差相同,包括前向传播和后向传播
for p in model.parameters():
if p.dim() > 1:
nn.init.xavier_uniform(p)
return model


assignment_3_transformer
https://fu-jingqi.github.io/2024/07/13/assignment-3-transformer/
作者
coderfjq
发布于
2024年7月13日
许可协议