【大模型】Transformer知识点详解

一、位置编码(Positional Encoding)

【参考博客】:AIGC大模型八股整理(1):Transformer中的位置编码

1.1 位置编码的设计理念

因为Transformer的基础结构——自注意力(Self-Attention)机制本身并不具备处理序列中元素位置的能力,位置编码(Positional Encoding)的设计目的是为了使模型能够理解单词的位置信息。 我们从几个重要方面来详细探讨位置编码的设计、原理和意义。位置编码的加入是为了让模型能够利用单词的位置信息。通过给每个单词添加一个独特的编码,进而表征每个单词在句子中的位置。

1. 无状态的自注意力:由于自注意力机制在处理输入序列时,并不考虑元素的顺序,它需要某种机制来加入这种序列信息。位置编码正是为了补充这一点。
2. 位置信息的编码:位置编码通过向每个输入元素的嵌入向量中加入一些与位置相关的信息来实现。这种方式让模型能够利用元素的位置信息。
3. 保持模型的灵活性:位置编码允许Transformer保持其并行处理序列的能力,因为它不需要像递归神经网络(RNNs)那样,顺序地处理序列中的每个元素。

1.2 位置编码的细节理解

对于位置编码向量中的每个维度,使用正弦和余弦函数的交替来生成位置编码。

对于位置 pos(即单词在句子中的位置)和维度 i,位置编码PE(pos,2i) 和 PE(pos,2i+1) 的计算方式分别是:

在这里插入图片描述
这里:

  • pos 表示单词在句子中的位置。
  • i 表示位置编码向量中的维度索引。
  • d 表示位置编码向量的总维度,也即是模型的维度。

通过使用正弦和余弦函数的不同频率,每个位置的编码都会唯一。

1.3 举个例子

让我们通过一个包含8个字的示例句子来详细解释Transformer中的位置编码过程。为了简化解释,我们假设模型的维度为6。

  • 句子:假设为“我们正在学习位置编码”,共8个字。
  • 模型维度:6
  • 位置:从0到7(代表句子中每个字的位置)
  • 维度索引i:由于总维度为6,我们考虑的i范围为0到2(每个i计算两个值,所以总共6个值)

位置编码的计算使用Transformer原始论文中的位置编码公式:

在这里插入图片描述
对于位置 pos=0(句子中的第一个字):位置编码向量为 [0,1,0,1,0,1]

对于位置为1的位置编码:
在这里插入图片描述
最终的位置编码如下:[0.8414709848078965,0.5403023058681398,0.046399223464731285,0.9989229760406304,0.0021544330233656045,0.9999976792064809]

在Transformer的位置编码公式中,使用这样的形式有几个原因和好处,它们共同构成了设计这种编码方式的基础理念:

为什么使用这种形式?

1. 周期性和波长变化:通过使用正弦和余弦函数,每个位置编码可以包含周期性信息,这是因为这些三角函数天生就具有周期性特征。此外,随着维度 (i) 的增加,周期(或波长)以指数方式增长。这意味着模型可以在不同的频率上学习位置信息,从高频到低频。
2. 连续性和平滑性:正弦和余弦函数在整个定义域内都是平滑且连续的,这为模型提供了平滑变化的位置信号,有助于模型更好地学习和泛化位置信息。
3. 相对位置信息:这种编码方式不仅能够让模型捕捉到绝对位置信息,还因为正弦和余弦函数的特性,使得模型能够从编码中提取出词汇之间的相对位置信息。即使在序列很长时,模型也能够通过位置编码的相对变化理解单词之间的相对距离。

这些都是怎么来的?

  • 基数选择(为什么是10000而不是其他数字):10000 的基数是经验性选择的,目的是为了在模型的嵌入维度 d 范围内提供一个合适的周期变化范围。这个数值足够大,可以确保即使在较高的维度索引 i处,周期也足够长,避免了所有位置编码快速重复的问题。
  • 指数下降:指数形式确保了随着维度 i 的增加,影响因子逐渐减小,这样在较低的维度中,模型可以捕获更精细的位置信息,而在较高的维度中,模型则捕获更全局的位置信息。

1.4 位置编码的优点

使用位置编码(Positional Encoding)在Transformer模型及其变种中具有多个优点和好处,这些特性共同促进了模型在处理序列数据方面的出色表现。以下是使用位置编码的主要优点:

1. 使模型具有顺序感知能力
Transformer架构本身是**基于自注意力机制的,这意味着它处理输入的方式本质上是无序的。**通过引入位置编码,每个输入单元(如单词)都被赋予了其在序列中位置的信息,从而使得模型能够理解和利用序列的顺序。这对于大多数自然语言处理任务至关重要,因为词序通常携带着关键的语法和语义信息。

2. 保持模型的并行化能力
与传统的序列处理模型(如RNN或LSTM)不同,位置编码允许Transformer在处理序列数据时仍然能够进行高效的并行计算。因为位置编码是提前计算并添加到输入嵌入中的,这样整个序列可以作为一个整体在模型中一次性处理,而不需要像RNN那样逐步处理,从而大幅提高了训练和推理的效率。

3. 捕获长距离依赖
位置编码通过为模型提供关于序列中元素位置的明确信息,帮助模型学习到序列中距离较远的元素之间的关系。这对于理解复杂的句子结构和含义,尤其是那些需要跨越长距离的语境理解的任务,是非常有用的。

4. 灵活性和可扩展性
位置编码的设计具有很高的灵活性和可扩展性,能够适应不同长度的输入序列。通过调整位置编码的生成方式,模型可以轻松处理变长的输入,这使得Transformer模型能够应用于广泛的序列长度变化较大的任务中。

5. 促进模型学习相对位置信息
位置编码(特别是通过正弦和余弦函数生成的编码)能够使模型不仅学习到序列中元素的绝对位置信息,还能学习到元素之间的相对位置信息。这种相对位置信息对于很多任务来说是非常重要的,例如在语言模型中理解“宾语”通常跟在“主语”和“谓语”之后。

1.5 一些常见的问题和解释

1. 为什么Transformer需要位置编码?
答案:Transformer模型基于自注意力机制,这意味着它本身不像循环神经网络(RNN)那样自然地处理序列的顺序信息。位置编码被引入是为了让模型能够理解单词在序列中的位置,从而能够处理基于顺序的语义信息,如语法结构和词序依赖。

2. Transformer位置编码的具体实现方式是什么?
答案:Transformer模型中的位置编码通过对每个位置生成一个唯一的向量来实现,这个向量是通过正弦和余弦函数的线性组合计算得到的。每个维度的位置编码是这些函数的函数值,其频率随着维度的增加而呈指数级下降,使得每个位置的编码是独一无二的。

3. 位置编码为什么使用正弦和余弦函数?
答案:正弦和余弦函数被用于位置编码因为它们具有周期性,这使得模型能够更容易地学习和推理关于序列长度和元素位置的信息。此外,它们可以帮助模型捕捉到相对位置信息,因为正弦和余弦函数的值可以通过叠加和差分运算来编码元素间的相对距离。

4. 位置编码是否参与模型训练?
答案:位置编码不是通过训练学习得到的;它们是预先计算好的,并直接加到输入嵌入向量上。这意味着位置编码是静态的,不会在模型训练过程中更新。

5. 位置编码如何加到输入嵌入上?
答案:位置编码通过简单的元素级加法操作直接加到每个输入嵌入向量上。这种方法保持了操作的简单性和模型的并行处理能力,同时允许模型在处理词义信息的同时考虑位置信息。

为什么位置编码可以直接进行相加,位置编码可以直接加到输入嵌入向量上的原因,主要基于模型设计和数学上的考量。这种设计选择背后的逻辑是优雅而有效的,具体包括以下几个方面:

  1. 线性叠加的简单性和有效性
    简单性:直接将位置编码加到输入嵌入向量上是一种非常直接且简单的方法,可以在不增加模型复杂性的前提下引入位置信息。这种方法不需要任何额外的模型参数或复杂的操作,是一种低成本的解决方案。
    有效性:尽管简单,这种方法被证明在实践中非常有效。它允许模型同时考虑到单词的语义信息(来自词嵌入)和位置信息(来自位置编码),而这两种信息的线性组合足以让模型进行有效的学习和推理。

  2. 保持嵌入空间的一致性
    通过将位置编码加到词嵌入上,模型的输入仍然保持在同一个嵌入空间内。这意味着加入位置信息不会改变嵌入的本质特性,如维度。同时,这种方式允许模型在学习过程中自然地平衡词语的语义内容和它们在序列中的位置,而不需要通过复杂的机制区分这两种类型的信息。

  3. 促进梯度流动和信息融合
    直接相加的方式有助于在前向传播和反向传播过程中保持梯度的流动,因为这种操作 不引入任何非线性或复杂的参数化函数,从而有利于模型的训练和优化。 此外,这种线性叠加也促进了位置信息和语义信息的融合,使得模型能够更好地利用这两方面的信息进行决策。

  4. 允许模型自动学习对位置和语义信息的权衡
    将位置编码和词嵌入向量直接相加后,**模型可以通过训练过程自动学习如何最有效地结合这两种信息,而不需要人为地设计复杂的机制来分别处理它们。**这种自动学习的能力使得Transformer模型极具灵活性,能够适应各种不同的任务和数据集。

  5. 位置编码的维度如何确定?
    答案:位置编码的维度与模型的嵌入维度相同。这是为了确保位置编码可以直接与输入嵌入向量相加,而不需要任何额外的转换或缩放操作。

  6. 如何处理超过预定义最大序列长度的输入?
    答案:在实践中,如果输入序列的长度超过了位置编码的最大预定义长度,一种常见的做法是截断输入序列或者使用循环方式来重复使用位置编码。但这种情况很少发生,因为大多数情况下,模型的最大序列长度足够大,能够覆盖绝大多数实际应用场景。

  7. 是否有替代位置编码的方法?
    答案:是的,除了最初的正弦余弦位置编码外,还有一些研究提出了可学习的位置编码,即通过训练过程自动学习位置信息。此外,有些变体模型采用了相对位置编码,能够更直接地表达序列中元素间的相对位置关系。

  8. 位置编码对模型性能的影响有多大?
    答案:位置编码对于模型处理基于顺序的任务(如文本生成、机器翻译等)至关重要。没有位置编码,模型将无法理解词序,从而严重影响其理解和生成文本的能力。实验证明,引入位置编码显著提高了模型在各种自然语言处理任务上的表现。

  9. 位置编码在模型的哪个部分被加入?
    答案:位置编码在模型的输入阶段被加入,具体来说,就是在单词被转换为嵌入向量之后、进入第一个自注意力层之前。这样设计是为了让自注意力机制和后续的Transformer结构能够在处理信息时考虑到位置信息。

1.6 代码实现(PositionalEncoder 类)

import torch
import torch.nn as nn
import math

class PositionalEncoder(nn.Module):
    def __init__(self, d_model, max_seq_len=5000):
        super(PositionalEncoder, self).__init__()
        self.d_model = d_model

        # 创建一个足够长的位置编码矩阵
        pe = torch.zeros(max_seq_len, d_model)
        for pos in range(max_seq_len):
            for i in range(0, d_model, 2):
                pe[pos, i] = math.sin(pos / (10000 ** ((2 * i)/d_model)))
                pe[pos, i + 1] = math.cos(pos / (10000 ** ((2 * (i + 1))/d_model)))

        pe = pe.unsqueeze(0) # 增加一个批次维度,方便后续的广播操作
        self.register_buffer('pe', pe)

    def forward(self, x):
        # x: [Batch size, Sequence length, d_model]
        # 添加位置编码到输入嵌入向量中。不训练位置编码,只是简单地加到输入向量上。
        x = x + self.pe[:, :x.size(1)]
        return x

代码详解

  1. 初始化(init)方法:这个方法首先调用super函数来初始化nn.Module,然后设置模型的维度(d_model)。max_seq_len参数是序列的最大长度,用于预先计算和存储位置编码。
  2. 位置编码矩阵的计算:在初始化方法中,创建了一个零矩阵pe,其大小为max_seq_len行和d_model列,分别对应最大序列长度和模型维度。然后,通过遍历每个位置和每个维度,使用前面提到的正弦和余弦函数计算位置编码。这个计算遵循我们之前讨论的公式。
  3. 增加维度:通过unsqueeze方法,给位置编码矩阵增加一个批次维度(在第0位),这样在之后的操作中可以利用广播机制来简化计算。
  4. forward方法:在模型的前向传播过程中,forward方法接收输入的嵌入向量x,它的维度是[Batch size, Sequence length, d_model]。然后,将存储在self.pe中的位置编码加到这个嵌入向量上。这里,位置编码不是模型的训练参数,而是直接加到输入上的固定值。
  5. 广播机制:在加法操作中,位置编码self.pe[:, :x.size(1)]根据输入序列的长度自动调整,这是通过PyTorch的广播机制实现的,确保每个输入序列元素都加上了相应的位置编码。
  • 旋转位置编码RoPE
  • 其他位置编码方法和长文本生成

二、Transformer中的注意力层

【参考博客】:AIGC大模型八股整理(2):Transformer中的注意力层

2.1 什么是注意力层

Transformer 模型中的注意力层提供了处理序列数据的强大能力,自注意力机制允许输入序列的每个位置都能接收到来自序列中其他所有位置的信息,这种机制可以被视为输入序列内部的全连接层。Transformer模型通过使用多头注意力机制来增强模型的能力。简单来说,多头注意力就是 并行运行多个自注意力机制,每个机制关注输入的不同部分。通过这种方式,模型可以在不同的表示子空间中学习到输入之间的不同的依赖关系。 在实际操作中,每个头的输出会被拼接起来,并通过一个线性层来整合信息。

2.2 自注意力机制的计算过程

自注意力机制的核心思想是,对于序列中的每个元素(比如一个单词),计算它与序列中其他所有元素的关联权重,然后用这些权重的加权和来更新每个元素的表示。

给定一个序列的输入表示( X = [ x 1 , x 2 , . . . , x n ] X = [x_1, x_2, ..., x_n] X=[x1,x2,...,xn]),其中 x i x_i xi是序列中第 i i i个元素的表示,自注意力机制可以通过以下步骤计算序列的输出表示:
在这里插入图片描述

  • 查询(Query)反映了当前处理的元素(或位置)对其他元素的“询问”或关注点。 它是判断其他元素与当前元素关系的基准。
  • 键(Key):代表每个元素提供的信息或特征,用于响应Query的“询问”。Query和Key之间的匹配程度决定了各元素对当前元素的重要性。
  • 值(Value):一旦Key被Query“选中”,相应的Value就会用来计算最终的输出。Value承载了元素的实际信息,当其对应的Key与Query匹配时,这些信息将被用来生成输出。

在上述自注意力层的代码实现中,查询(Q)、键(K)和值(V)是通过对输入向量应用三个不同的线性变换(即全连接层)来生成的。这些线性变换分别由self.values、self.keys、和self.queries实现。每个变换都不包含偏置项(bias=False),这是一个设计选择,旨在减少模型的参数数量并防止过拟合,尽管在实际应用中,是否包含偏置项可以根据具体任务进行调整。

  • 注意力分数(Attention Score):衡量一个元素对另一个元素的影响或重要性。高分数意味着更大的相关性。

  • 注意力权重(Attention Weights):通过softmax归一化后的分数,表示在更新当前元素表示时,其他元素的相对贡献度。

  • 输出表示:考虑了整个序列上下文信息的元素新表示。通过聚合整个序列的信息,每个元素的表示现在包含了与其最相关的全局信息,从而提高了模型捕捉长距离依赖和理解序列内部结构的能力

  • 输入维度:输入的values、keys和queries的维度通常是 [batch_size, seq_len, embed_size],其中batch_size是批量大小,seq_len是序列长度,embed_size 是嵌入向量的维度。

  • 输出维度:自注意力层的输出维度为 [batch_size, seq_len, embed_size]。这是因为尽管进行了多头处理,最后的输出是通过将所有头的结果拼接起来,再经过一个线性层调整为原始的embed_size 维度。

2.3 多头注意力机制

多头注意力机制是Transformer模型的一个核心创新,它通过并行执行多个注意力过程来增强模型聚合上下文信息的能力。这一机制使得模型能够在不同的表示子空间中捕捉到更加丰富和细微的信息。接下来,我将从原理和代码层面详细解释为什么要引入多头注意力机制以及它是如何工作的。

为什么引入多头注意力

  • 捕获多种关系:在自然语言处理中,单词或句子之间可能存在多种不同类型的关系。多头注意力机制通过将输入分散到不同的头(即不同的表示空间)中处理,可以并行地捕捉这些不同的关系,从而获得更丰富的上下文信息。
  • 增加模型的表达能力:通过并行地在多个子空间中学习信息,多头注意力机制可以让模型有更大的灵活性和表达能力,从而更好地理解和处理复杂的输入序列。
  • 提高学习效率:多头注意力机制可以让模型在不同的头中学习到更多样化的特征,这有助于提高学习效率和模型性能。

增强聚合上下文信息的能力

  • 不同头关注不同信息:在多头注意力中,每个头可以通过不同的权重矩阵将输入映射到不同的子空间,这样每个头就可以关注输入中的不同特征或信息。例如,某些头可能专注于捕捉语法结构,而其他头可能专注于语义关系。
  • 信息融合:通过将所有头的输出向量合并(通常是拼接后通过一个线性变换),模型可以综合不同头捕获的信息,从而获得一个全面的上下文表示。

2.4 代码层面

下面是多头注意力机制的一个简化的PyTorch代码实现示例,它展示了如何从代码层面实现多头注意力机制。

import torch
import torch.nn as nn
import torch.nn.functional as F

class MultiHeadAttention(nn.Module):
    def __init__(self, embed_size, heads):
        super(MultiHeadAttention, self).__init__()
        self.embed_size = embed_size
        self.heads = heads
        self.head_dim = embed_size // heads

        assert self.head_dim * heads == embed_size, "Embed size needs to be divisible by heads"

        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)
        self.fc_out = nn.Linear(heads * self.head_dim, embed_size)

    def forward(self, values, keys, queries, mask=None):
        N = queries.shape[0]
        value_len, key_len, query_len = values.shape[1], keys.shape[1], queries.shape[1]

        # 分割输入为多个头
        values = values.reshape(N, value_len, self.heads, self.head_dim)
        keys = keys.reshape(N, key_len, self.heads, self.head_dim)
        queries = queries.reshape(N, query_len, self.heads, self.head_dim)

        values = self.values(values)
        keys = self.keys(keys)
        queries = self.queries(queries)

        # 注意力机制的实现部分
        attention = torch.einsum("nqhd,nkhd->nhqk", [queries, keys])
        if mask is not None:
            attention = attention.masked_fill(mask == 0, float("-inf"))
        attention = torch.softmax(attention / (self.embed_size ** (1/2)), dim=-1)

        out = torch.einsum("nhql,nlhd->nqhd", [attention, values]).reshape(
            N, query_len, self.heads * self.head_dim
        )

        out = self.fc_out(out)
        return out

代码解释

  • 初始化:在构造函数中,定义了模型的基础结构,包括将输入维度分割成多个头,并为每个头的Q、K、V分别初始化线性变换。
  • 前向传播:
    • 输入分割:首先将输入的values、keys和queries分割为多个头处理的形式,这样每个头可以独立地处理输入的一部分。
    • 生成Q、K、V:通过对每个头的输入应用相应的线性变换生成Q、K、V。
    • 计算注意力:计算查询和键的点积,应用softmax获取注意力权重,然后用这些权重对值进行加权求和。
    • 合并输出:将所有头的输出合并,然后通过一个额外的全连接层(fc_out)生成最终的输出。

2.5 为什么注意力机制如此有效?

自注意力机制提供了几个关键优势:

  • 灵活的长距离依赖学习:在自然语言处理等序列任务中,元素之间的关系往往跨越长距离。传统的序列模型(如RNNs和CNNs)在处理这种长距离依赖时面临挑战。Transformer不需要逐步处理序列或通过固定大小的窗口来捕获局部特征,注意力层通过直接计算序列内任意两点之间的关系,使模型能够轻松捕获这种依赖,从而提高了处理复杂语言结构的能力。
  • 并行化处理:与需要逐步处理序列的RNN不同,注意力层可以同时处理序列中的所有元素。这种并行能力显著提高了模型的训练效率,特别是在处理长序列时。通过并行计算注意力分数,Transformer模型可以快速学习序列中的依赖关系,这对于大规模数据集和复杂任务尤其重要。
  • 动态注意力分配:通过在多头注意力机制中并行运行多个注意力“头”,模型可以在不同的表示子空间中捕捉输入之间的不同关系。这种多样性允许模型从多个角度学习输入数据,从而提高了模型的表达能力和泛化能力。自注意力机制能够根据输入序列的不同动态调整注意力的分配,这使得模型更加灵活和强大。

2.6 举个例子

例子一:

假设我们有一个简单的句子:“I like natural language processing”,我们想要通过多头注意力机制来处理这个句子。为了简化,假设我们将每个词编码成一个8维的向量,使用2个头进行注意力计算。

输入句子

  • 句子:“I like natural language processing”
  • 假设每个单词都被嵌入到一个8维向量中。
  • 假设使用2个注意力头(heads=2),因此每个头处理的子空间维度是4(因为8维被分为两部分)。

步骤1:输入嵌入

  • 输入维度:5个单词,每个单词8维向量,所以输入矩阵的维度是5,8。

步骤2:分割为多头

  • 对于每个头,我们将8维分成2组,每组4维,因此每个头会处理5,4维的数据。

步骤3:计算Q, K, V矩阵

  • 每个头对其5,4的输入分别计算Q, K, V矩阵。假设没有降维,则每个的Q, K, V矩阵的维度都是5,4。

步骤4:点积和缩放

  • 对于每个头,计算Q和K的点积,结果是每个头的注意力得分矩阵,维度是5,5,因为我们是对每个单词的Q与所有单词的K进行点积。
  • 然后,点积结果除以dk进行缩放,其中dk=4是每个头的维度。

步骤5:应用softmax和计算注意力得分

  • 应用softmax到每个头的5,5注意力得分上,得分不变,维度仍是5,5。
  • 使用这个得分矩阵对每个头的V矩阵(5,4)进行加权求和,输出的维度为每个头的5,4。

步骤6:拼接和线性变换

  • 将两个头的输出拼接回5,8的矩阵。
  • 最后,通过一个线性变换层(通常是全连接层)将拼接后的矩阵转换回原始的嵌入维度5,8,作为多头注意力层的输出。

例子二:

假设我们处理的句子是:“Hello, world”,并假设经过词嵌入层后,每个词被表示为一个4维向量。我们将使用2个头(heads)进行多头注意力计算。

句子和初始化参数

  • 句子: “Hello, world”
  • 词嵌入维度: 4
  • 头数(heads): 2
  • 因此,每个头处理的子维度(head_dim): 4 / 2 = 2

词嵌入

  • 输入句子的词嵌入表示:假设维度为[2, 4](2个单词,每个单词4维)。

2.7 多头注意力计算过程

在这里插入图片描述

2.8 一些常见的问题

  1. 什么是注意力机制?
    注意力机制是一种使模型能够聚焦于输入信息的重要部分的技术,尤其在处理序列数据时,能够帮助模型捕捉到长距离依赖关系。它通过为不同的输入部分分配不同的权重来实现,这些权重表示了各部分对于任务的重要性。

  2. 注意力机制在自然语言处理中的应用有哪些?
    在NLP中,注意力机制被广泛应用于机器翻译、文本摘要、情感分析、问答系统等任务,通过提高模型对关键信息的关注度,来提升任务的性能。

  3. 什么是自注意力机制?
    自注意力(Self-Attention),又称内部注意力,是注意力机制的一种形式,它允许输入序列的每个位置直接与序列中的其他所有位置交互并计算自身的表示。

  4. 请解释多头注意力机制的原理。
    多头注意力机制通过并行地执行多个注意力计算(称为“头”),每个头学习序列的不同方面,然后将这些头的输出合并。这样能够使模型在不同的表示子空间中捕捉到更丰富的信息。

  5. Transformer模型中的注意力机制有何特点?
    Transformer完全依赖于注意力机制,摒弃了传统的循环和卷积结构。它通过自注意力来捕捉序列内的依赖关系,通过多头注意力来提取不同层面的特征,从而有效地处理序列数据。

  6. 为什么Transformer需要位置编码?
    由于Transformer模型中缺乏循环结构,自身不具备处理序列顺序信息的能力,位置编码被引入以提供序列中各元素的位置信息,使模型能够利用这一信息进行有效的学习。

  7. 点积注意力与加性注意力有何不同?
    点积注意力通过计算查询和键的点积来得到注意力权重,而加性注意力则是先将查询和键连接或组合后,通过一个前馈网络来计算注意力权重。点积注意力计算更高效,而加性注意力在处理维度不同时更灵活。

  8. 注意力权重是如何计算的?
    注意力权重通常通过计算查询(Query)和键(Key)之间的相似度得到,然后通过softmax函数进行归一化,使得所有权重的和为1。

  9. 为什么注意力机制能处理长距离依赖问题?
    注意力机制通过直接计算序列中任意两个元素之间的关系,而不是依赖于序列的逐步处理,因此能够有效捕捉长距离依赖,避免了信息在长距离传递过程中的衰减。

  10. 缩放点积注意力是什么?为什么要进行缩放?
    缩放点积注意力是点积注意力的一个变体,它通过除以键向量维度的平方根来缩放点积的结果。这样做是为了控制点积之后值的范围,防止因为维度较大导致的梯度消失问题。

  11. 注意力机制如何并行处理?
    注意力机制可以同时计算序列中所有位置的输出,而不需要像RNN那样逐步计算,这使得注意力机制能够利用现代硬件的并行计算能力,显著提高计算效率。

  12. 如何解释注意力机制增加了模型的可解释性?
    注意力权重直观地表示了模型在做出决策时对输入序列不同部分的关注程度,通过分析这些权重,我们可以了解模型的决策依据,从而提高模型的可解释性。

  13. 注意力机制有哪些变体?
    注意力机制的变体包括但不限于自注意力、多头注意力、双向注意力(Bi-Directional Attention)、层次注意力(Hierarchical Attention)等,这些变体针对不同的应用场景和需求进行了优化。

  14. 注意力机制在图像处理中的应用有哪些?
    在图像处理领域,注意力机制被用于图像分类、目标检测、图像分割等任务中,通过聚焦于图像的关键区域来提升模型性能。

  15. 如何在自己的模型中集成注意力机制?
    集成注意力机制通常涉及定义注意力层或模块,并将其嵌入到模型的适当位置。具体步骤包括选择合适的注意力类型(如自注意力、多头注意力等),计算注意力权重,并根据这些权重对输入进行加权汇总。实现时还需考虑如何整合位置信息(通过位置编码)以及如何调整模型结构以适应注意力机制带来的变化。

16.为什么要用缩放因子根号d?
为了方式过大的匹配分数在后续Softmax计算中导致的提督爆炸以及收敛效率差的问题,需要处以稳定因子稳定优化。

在Transformer模型的自注意力机制中,使用缩放因子(\sqrt{d_k})(其中(d_k)是键(Key)向量的维度)来缩放查询(Query)和键(Key)的点积,这是一个关键的设计选择。下面将解释为什么采用这种方法以及如果不采用可能会导致的后果。

为什么使用缩放因子 d k \sqrt{d_k} dk

  • 点积的大小问题:当计算Query和Key之间的点积时,结果的大小随着(d_k)的增加而线性增长。这意味着,当(d_k)较大时,点积会变得非常大。
  • 激活函数的梯度问题:点积结果的增大会导致在应用softmax函数时产生梯度消失或梯度爆炸的问题。具体来说,当点积结果很大时,softmax函数的输出会接近0或1,导致梯度非常小,这使得在训练过程中难以有效地更新权重;在极端情况下,也可能导致数值稳定性问题。
  • 缩放的目的:通过引入缩放因子(\sqrt{d_k}),可以控制点积的规模,使得softmax函数的梯度更加稳定。这有助于保持梯度在一个合理的范围内,避免梯度消失或爆炸的问题,从而有利于模型的训练和收敛。

缩放因子的作用举例
假设(d_k = 64),而Query和Key的每个元素都从标准正态分布中随机抽取。不使用缩放因子时,点积的期望大小为64,标准差也为64,这会导致softmax的输入非常大,进而导致输出分布非常极端,大部分概率集中在少数几个值上,其他值几乎被忽略。这种极端的概率分布会导致梯度非常小,难以通过梯度下降有效地训练模型。
引入缩放因子(\sqrt{64} = 8)后,点积被缩放到更合理的范围,softmax函数的输入变得更加平滑,输出分布也更加平均,每个值都有合理的概率被选中,梯度也更加稳定,有利于模型的学习和优化。

如果不使用缩放因子可能导致的后果

  • 梯度消失或爆炸:如前所述,不使用缩放因子可能会导致梯度消失或爆炸,这会严重影响模型的训练效率和效果。
  • 学习效率降低:由于梯度不稳定,模型可能需要更多的训练周期才能收敛,甚至可能无法收敛到一个好的解。
  • 性能下降:即使模型最终收敛,性能也可能不如使用缩放因子的情况,因为模型可能无法有效地捕获输入序列中的复杂依赖关系。
    总之,引入缩放因子(\sqrt{d_k})是Transformer设计中的一个重要特性,它有助于保持模型的数值稳定性,优化训练过程,从而提高模型的性能和泛化能力。
  1. 为什么要引入多头注意力机制?
    为了进一步增强自注意力机制聚合上下文信息的能力,在不同的子空间分别计算不同的上下文相关单词序列,通过线性变化综合不同子空间中的表示并得到最后的输出

2.9 关于注意力层的改进

2.10 代码讲解

import torch
import torch.nn as nn
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

        assert (
            self.head_dim * heads == embed_size
        ), "Embedding size needs to be divisible by heads"

        # 初始化权重矩阵
        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)
        self.fc_out = nn.Linear(heads * self.head_dim, embed_size)

    def forward(self, values, keys, queries, mask=None):
        N = queries.shape[0]
        value_len, key_len, query_len = values.shape[1], keys.shape[1], queries.shape[1]

        # 分割输入为多个头
        values = values.reshape(N, value_len, self.heads, self.head_dim)
        keys = keys.reshape(N, key_len, self.heads, self.head_dim)
        queries = queries.reshape(N, query_len, self.heads, self.head_dim)

        values = self.values(values)
        keys = self.keys(keys)
        queries = self.queries(queries)

        # 计算注意力分数
        energy = torch.einsum("nqhd,nkhd->nhqk", [queries, keys])
        if mask is not None:
            energy = energy.masked_fill(mask == 0, float("-1e20"))

        attention = torch.softmax(energy / (self.embed_size ** (1/2)), dim=3)

        # 应用注意力权重到值上
        out = torch.einsum("nhql,nlhd->nqhd", [attention, values]).reshape(
            N, query_len, self.heads * self.head_dim
        )

        # 最后通过一个全连接层
        out = self.fc_out(out)

        return out

代码解释

  • 初始化部分:在__init__方法中,定义了模型的基本结构。这包括输入向量的维度(embed_size),多头注意力的头数(heads),以及每个头处理的维度大小(head_dim)。此外,还初始化了四个线性层来转换输入数据为QKV向量和最终的输出。
  • 前向传播:forward方法接收输入数据(values、keys、queries)和可选的掩码(mask)。
    • 首先,将输入数据重塑为多个头的格式,以便并行处理。
    • 然后,通过三个线性层生成QKV向量。
    • 使用torch.einsum计算Query和Key之间的点积,得到注意力分数(energy)。如果提供了mask,则使用它来遮盖某些不应被考虑的值。
    • 对注意力分数应用softmax函数,得到最终的注意力权重(attention)。
    • 再次使用torch.einsum将注意力权重应用于Value向量,生成加权的输出。
    • 最后,将所有头的输出拼接起来,并通过一个线性层产生最终的输出结果。

三、前馈层FFN

【参考博客】:AIGC大模型八股整理(3):Transformer中的前馈层和激活函数

3.1 前馈层

Transformer模型的前馈层对于每个位置的词向量独立地进行操作,这意味着 对于输入序列中的每个位置,前馈层都会应用相同的线性变换,但它们并不共享参数。 这种设计使得Transformer能够在保持较低复杂度的同时,增加模型的深度和表达能力。在Transformer中,前馈层由两个线性变换组成,这两个变换之间有一个ReLU激活函数。具体来说,一个前馈层可以表示为以下形式:
F F N ( x ) = m a x ( 0 , x ∗ w 1 + b 1 ) w 2 + b 2 FFN(x) = max(0, x *w_1 + b_1)w_2 + b_2 FFN(x)=max(0,xw1+b1)w2+b2

这里,x是前一层的输出, W 1 W_1 W1 W 2 W_2 W2是权重矩阵, b 1 b_1 b1 b 2 b_2 b2是偏置项。第一个线性变换 x ∗ W 1 + b 1 ) x*W_1 + b_1) xW1+b1) 将输入映射到一个较高维度的空间(通常称为“扩展”),接着应用ReLU激活函数,最后一个线性变换 m a x ( 0 , x W 1 + b 1 ) W 2 + b 2 max(0, xW_1 + b_1)W_2 + b_2 max(0,xW1+b1)W2+b2 将数据映射回原始维度。这种“扩展”和“收缩”的设计目的是让网络能够捕捉更复杂的特征,并提高模型的表达能力。

import torch
import torch.nn as nn

class FeedForward(nn.Module):
    def __init__(self, input_dim, ff_dim):
        super(FeedForward, self).__init__()
        self.linear1 = nn.Linear(input_dim, ff_dim)
        self.relu = nn.ReLU()
        self.linear2 = nn.Linear(ff_dim, input_dim)

    def forward(self, x):
        x = self.relu(self.linear1(x))
        x = self.linear2(x)
        return x

在这个例子中,FeedForward类定义了一个前馈层,其中input_dim是输入和输出的维度,ff_dim是第一个线性层映射到的高维空间的维度。这个类首先使用一个线性层将输入映射到一个较高的维度,然后应用ReLU激活函数,最后通过另一个线性层将其映射回原始的维度。这种设计模仿了原始Transformer模型中前馈层的结构。ff_dim 是前馈网络中间层的维度。在Transformer模型中,这个维度通常远大 于input_dim,目的是增加模型的表达能力。常见的做法是将ff_dim设置为input_dim的4倍,比如如果input_dim是512,那么ff_dim 可能是2048。这样的设置可以让前馈网络在保持输入和输出维度不变的同时,通过增加中间层的维度来增加模型的容量和非线性。

前馈层在Transformer架构中的作用不可小觑。尽管注意力层能够捕捉序列中的长距离依赖关系,但前馈层为模型引入了必要的非线性,使模型能够学习复杂的函数映射。 此外,前馈层也增加了模型的深度,有助于提高模型的表达能力和泛化能力。

3.2 FFN的工作原理

  • 参数共享: 在Transformer的每个编码器或解码器层中,FFN对序列中的每个位置独立地应用相同的线性变换(即权重和偏置)。这意味着无论序列中的哪个位置,对于给定的层,都是使用相同的FFN参数进行处理。这种设计有助于模型掌握如何将某种计算普遍应用于不同的输入位置,而不是对每个位置学习完全不同的变换。
  • 位置独立的操作: “对所有位置的表示进行相同的操作”这句话的意思是,FFN在处理序列时不考虑输入元素之间的位置关系。每个元素(或称为“位置”的表示)被独立地送入同一个FFN中进行处理,且该处理对所有元素是一样的。这与自注意力层(Self-Attention Layer)形成对比,后者的目的就是捕获这些元素之间的相互作用。

3.3 参数不共享的含义

当我们谈论到“不共享参数”时,实际上是在不同的FFN之间进行比较,即Transformer模型中 不同编码器或解码器层之间的FFN是不共享参数的。 每一层的FFN都有其独立的参数集,这使得每一层能够学习到不同层次的特征表示。

3.4 为什么这样设计

  • 模型灵活性: 这种设计增加了模型的灵活性,允许每一层学习不同的表示能力,这对于处理复杂的语言模型和其他序列转换任务非常重要。
  • 增加非线性: FFN通过两次线性变换和一个非线性激活函数的组合为模型引入了必要的非线性,使得模型能够学习更加复杂和抽象的数据表示。
  • 并行计算: 由于FFN对每个位置独立操作,这种处理方式非常适合并行计算,大大提高了训练和推理的效率。

总的来说,FFN在Transformer架构中通过对每个位置应用相同的操作(使用共享的参数集)增加了模型的非线性和复杂度,而在不同层之间不共享参数则提供了更大的学习能力和灵活性。

3.5 一些常见的问题

  1. Transformer模型中前馈层的作用是什么?
    解答: 前馈层(FFN)在Transformer模型中的作用是对每个位置的词向量进行独立的非线性变换。尽管注意力机制能够捕捉序列中的全局依赖,但前馈层通过增加模型的深度和复杂度,为模型引入必要的非线性,从而增强模型的表达能力。每个编码器和解码器层都包含一个FFN,它对所有位置的表示进行相同的操作,但并不共享参数。

  2. Transformer的前馈层通常包含哪些组件?
    解答: Transformer的前馈层通常由两个线性变换组成,它们之间插入一个非线性激活函数,通常是ReLU(Rectified Linear Unit)。此外,前馈层后通常会跟一个Dropout层以减少过拟合,以及Layer Normalization层以稳定训练过程。

  3. 为什么Transformer模型中的前馈层使用了ReLU作为激活函数?
    解答: ReLU(Rectified Linear Unit)被用作激活函数,因为它能够引入非线性,同时保持计算的简单性和效率。ReLU能够解决梯度消失问题(对于正输入,其导数为1),加快训练速度,并有助于稀疏激活,这些都是提升模型性能的重要因素。

  4. Transformer前馈层的输入和输出维度是否相同?
    解答: 是的,Transformer模型中前馈层的输入和输出维度相同。这设计是为了确保每个编码器或解码器层的输入和输出可以通过残差连接相加,这有助于解决深层网络中的梯度消失问题。

  5. 前馈层中间层(隐藏层)的维度通常如何选择?
    解答: 前馈层中间层的维度通常大于输入/输出层的维度。一个常见的实践是将中间层的维度设置为输入维度的4倍。例如,如果输入维度为512,则中间层的维度可能为2048。这种扩展有助于增加模型的容量,使其能够捕捉更复杂的特征。

  6. 为什么Transformer的前馈层对序列中的每个位置独立操作?
    解答: 前馈层对序列中每个位置独立操作,因为这允许模型对每个位置的表示进行独立的非线性变换,而不受序列中其他位置的直接影响。这种设计与Transformer的自注意力机制相辅相成,后者负责捕捉序列内的依赖关系。

  7. 在Transformer模型中,前馈层后面是否有残差连接?
    解答: 是的,在Transformer模型中,前馈层后面有残差连接。**前馈层的输出会与进入前馈层之前的输入进行加和,然后通常会进行层归一化。**这种残差连接有助于缓解深层网络中的梯度消失问题,使得更深的网络能够有效训练。
    前馈层的潜在挑战包括参数调优和计算资源管理。由于前馈层的中间维度通常远大于输入输出维度,这会显著增加模型的参数数量和计算负担。此外,选择合适的激活函数、防止过拟合(通过Dropout等技术),以及维持训练过程的稳定性(通过层归一化等)也是训练中需要考虑的问题。

四、ReLU激活函数

【参考博客】:AIGC大模型八股整理(3):Transformer中的前馈层和激活函数

激活函数在神经网络中扮演着至关重要的角色,它们 帮助网络捕捉输入数据中的非线性关系 。没有激活函数,不管神经网络有多少层,它最终只能表示线性关系,这大大限制了网络模型的表达能力和复杂性。激活函数的选择对神经网络的性能有显著影响。

4.1 ReLU激活函数(Rectified Linear Unit)

ReLU函数的公式很简单:f(x) = max(0, x)这意味着如果输入是正数,则输出该正数;如果输入是负数,则输出0。这样的特性让ReLU函数在激活时能够保持神经网络中的稀疏性,从而使网络学习得更加高效。

ReLU函数的优点包括:

  • 非线性性质:尽管看起来很简单,但ReLU提供了一种非线性映射,这是神经网络能够捕捉复杂模式的关键。
  • 计算效率:ReLU及其梯度的计算都非常简单,这对于加速训练过程非常有利。
  • 稀疏激活性:在给定的时间内,网络中只有少部分神经元被激活,这可以导致更加稀疏的网络,有助于减少过拟合的风险。

但是,ReLU也有其缺点,比如:

  • 死亡ReLU问题:在训练过程中,如果一个神经元的输出始终是负数,则该神经元的梯度为0,这意味着在反向传播过程中它不会接收到任何更新,从而“死亡”(不再对任何数据有所响应)。

4.2 ReLU激活函数的代码实现

ReLU函数的实现非常直接,下面是使用Python代码的示例:

import numpy as np

def relu(x):
    return np.maximum(0, x)

这段代码使用了NumPy库中的maximum函数来比较输入x和0,然后返回两者之间的最大值,这正是ReLU函数的定义。

现在,我们可以简单地测试一下这个函数:

x = np.array([-2, -1, 0, 1, 2])
print(relu(x))

这段代码会对数组x中的每个元素应用ReLU函数,期望的输出应该是[0 0 0 1 2],这展示了ReLU函数将负值映射为0,而保留正值不变的特性。

4.3为什么ReLU可以帮助网络捕捉输入数据中的非线性关系

ReLU(Rectified Linear Unit)激活函数之所以能帮助神经网络捕捉输入数据中的非线性关系,是因为它引入了一种简单而有效的非线性变换,这对于深度学习模型的学习和表达能力至关重要。

  • 非线性引入
    在没有激活函数的情况下,不管神经网络有多少层,每一层的输出都是输入的线性组合,这意味着整个网络仍然是输入的线性函数。这种线性模型在处理复杂问题时能力有限,因为现实世界中的很多数据和关系是非线性的。这个简单的函数对输入进行非线性变换:如果输入是正数,输出就是输入本身;如果输入是负数,输出就是0。这种非线性变换使得网络能够学习和模拟复杂的、非线性的函数关系。

  • 稀疏激活
    ReLU激活函数的另一个关键特性是它可以产生稀疏的激活。**在给定的输入下,只有部分神经元的输出是非零的,这是因为负值输入被映射为0。**这种稀疏性有几个好处:

    • 计算效率:在任何时刻,只有一部分神经元需要进行计算,这可以减少资源消耗。
    • 减少过拟合:稀疏性可以被视为一种正则化形式,它限制了模型的复杂度,从而有助于减少过拟合的风险。
  • 梯度消失问题的缓解
    在深度神经网络中,梯度消失是一个常见问题,它会阻碍网络的训练。梯度消失主要是由于在反向传播过程中连续乘以小于1的数导致的。由于ReLU在正区间的梯度为1,这意味着它不会在正区间内降低梯度的大小,从而有助于缓解深层网络中的梯度消失问题。

  • 加速收敛
    与其他激活函数相比,ReLU可以加速神经网络的训练。因为它的线性、非饱和形式允许梯度通过网络传播得更远,这有助于快速收敛到较低的训练误差。

4.4为什么一定要用ReLU

在Transformer模型的前馈层中选择ReLU(Rectified Linear Unit)作为激活函数而非其他激活函数,主要基于ReLU的几个显著优势。这些优势不仅体现在计算效率上,也涉及模型训练和性能方面。以下是对ReLU及其与其他激活函数比较的详细分析:

  1. 非饱和性(Non-saturating)
    ReLU函数定义为f(x) = max(0, x)\,对于正输入,其导数为1,这意味着在正区间内,ReLU不会进入饱和状态。与此相反,传统的激活函数如sigmoid和tanh在其输入远离0时会饱和(导数接近0),导致梯度消失问题,这在深度网络中尤为突出。ReLU的非饱和性质有助于缓解梯度消失问题,使得深层网络的训练变得更加可行。

  2. 计算简单
    ReLU的计算非常简单,只需要对输入进行阈值处理。相比之下,sigmoid和tanh等激活函数涉及更复杂的指数运算,这在计算上更为昂贵。ReLU的简单性不仅提高了计算效率,还有助于降低训练深度模型时的计算成本。

  3. 稀疏激活
    ReLU能够产生稀疏激活,因为它将所有负值输入都设置为0。这与sigmoid和tanh不同,后者即使在输入为负值时也会产生非零的输出。稀疏激活有助于减少神经元间的依赖关系,提高模型的泛化能力,并降低过拟合风险。此外,稀疏性还可以提高计算效率,因为不需要对所有神经元进行计算。

  4. 加速训练
    多项研究表明,使用ReLU可以加速深度网络的训练。这部分原因是由于其**非饱和性质,以及能够提供更强的梯度,从而加快权重的调整速度。**相比之下,使用sigmoid或tanh等饱和激活函数的网络通常训练更慢。

  5. 缓解梯度消失问题
    正如前面提到的,ReLU可以有效缓解梯度消失问题,这使得训练深层网络变得更加容易。相比之下,sigmoid和tanh激活函数在输入绝对值较大时容易饱和,导致梯度接近0,从而阻碍了深层网络中误差信号的有效传播。

4.5 FFN+ReLU代码详解

在Transformer架构中,每个编码器和解码器层都包含一个前馈层。这个前馈层对每个位置的词向量独立处理,这意味着它对序列中的每个元素应用相同的操作,但是对序列中的每个元素都独立处理。前馈层的主要目的是增加模型的非线性能力。没有这种非线性,模型将无法学习复杂的数据表示。让我们通过一段代码详细探讨Transformer模型中前馈层(Feed-Forward Network, FFN)的作用和实现方式。

PyTorch代码示例
下面是一个使用PyTorch实现Transformer中前馈层的例子。这个例子展示了如何定义前馈层,并且揭示了其在整个Transformer架构中的作用。

import torch
import torch.nn as nn

class PositionwiseFeedForward(nn.Module):
    def __init__(self, d_model, d_ff, dropout=0.1):
        super(PositionwiseFeedForward, self).__init__()
        # 第一个线性层
        self.linear1 = nn.Linear(d_model, d_ff)
        # ReLU激活函数
        self.relu = nn.ReLU()
        # 第二个线性层
        self.linear2 = nn.Linear(d_ff, d_model)
        # Dropout层
        self.dropout = nn.Dropout(dropout)

    def forward(self, x):
        x = self.linear1(x)
        x = self.relu(x)
        x = self.dropout(x)  # 应用dropout减少过拟合
        x = self.linear2(x)
        return x

在这个实现中:

  • d_model是输入和输出向量的维度,它与模型中所有其他层的输出维度保持一致。
  • d_ff是第一个线性层将输入映射到的内部维度,这个维度通常大于d_model。这个“扩展”操作增加了模型的容量和复杂度,让模型能够捕捉更复杂的特征。
  • dropout是一个正则化技术,通过在训练过程中随机“丢弃”一部分的特征,它帮助减少模型在训练数据上的过拟合。

工作流程:

  1. 输入映射(扩展):第一个线性层将每个位置的输入向量映射到一个较高的维度。
  2. 非线性激活:ReLU激活函数为模型引入非线性,使模型能够学习复杂的函数映射。
  3. Dropout正则化:在ReLU之后应用dropout,以防止模型在训练过程中过拟合。
  4. 输出映射(收缩):第二个线性层将高维空间中的表示映射回原始的维度。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值