大模型学习笔记(一)—— transformer

写在前面,一定要看懂self attention的代码实现,注意矩阵乘是谁@谁,矩阵乘不可以变换位置!!!

Attention的出现

由于翻译任务往往不是1 vs 1的翻译,因此输入与输出不等长,所以出现了encoder-decoder的形式:

RNN具有短期记忆的问题,随着输入序列的增加,以前的输入的影响会越来越小。

传统encoder-decoder的缺陷

在传统的Encoder-Decoder结构中,Encoder处理一个输入序列,把它压缩成一个固定长度的向量c(也叫 context vector,上下文向量),然后这个c被Decoder用来逐步生成输出序列。比如:

  • 输入序列:I am a student(长度为4)。
  • Encoder输出的最终隐藏状态h4就是c
  • Decoder使用这个c作为起点,开始翻译成另一个语言的句子。

不管输入序列有多长(比如一个 20 个词的长句子),最终都被压缩成一个固定长度的向量c,就会造成信息的丢失。

Attention如何解决这个问题?

Attention机制通过在每个时间输入不同的c来解决这个问题,也就是说不再把全部信息压缩成一个c,Decoder每生成一个词时,可以动态地访问Encoder输出的所有隐藏状态(也就是输入序列每一步的编码),并根据当前生成内容“选择性关注”原始输入的不同部分。

对于decoder而言,它获取的隐状态输入就需要有着重点,也就是"注意"到,哪里的信息更重要。因此encoder输出的隐状态,就需要对每一个输入有选择,这个选择就是注意力,本质上是给Encoder输出的每一个隐状态h_t一个权重(α),那么context vector (当前时刻) = ∑ (attention weight α_i × encoder output h_i)

那么,接下来有了第二个问题:权重怎么计算?

权重的计算方式

对于Decoder而言,每一个位置都需要有自己的注意力,且每一个位置的注意力肯定是不一样的,这就推断出:每一个Decoder时间步的权重是根据当前时间步的隐状态和Encoder的隐状态一起计算出来的。计算注意力权重:

αₜᵢ = score(s_t, hᵢ)        # i 从 1 到 n
aₜ = softmax([αₜ₁, αₜ₂, ..., αₜₙ])  # 权重向量
c_t = Σᵢ (aₜᵢ · hᵢ)

其中:score是注意力打分函数,transformer中最常用的就是:

Attention机制

相比较于原来的RNN结构,Encoder的结构并没有改变,只是每一个时间步都输出了隐状态。attention是Decoder时,添加了注意力机制,计算了context vector与上一个时间步的输出一起做拼接,来预测当前时间步的输出。

  • Encoder的工作:

    h_t = RNN(h_{t-1}, x_t)
    x_t 是第 t 个位置的输入词(嵌入)
    h_{t-1} 是上一个时间步的隐藏状态
    
  • Decoder的工作:

    s_t = RNN(s_{t-1}, y_{t-1}, c_t)
    y_{t-1} 是上一个时间步的输出词的嵌入(注意是目标序列的词)
    s_{t-1} 是上一个隐藏状态
    c_t 是当前时间步的上下文向量
    

QKV是什么?

上述基于RNN的attention没有显式的说明KQV这样的概念,因为KQV本身就是transformer之后才有的概念,但是看transformer里KQV的职能,仍然可以把基于encoder-decoder的attention划分到KVQ结构里:

角色 对应 来自哪里
Query (Q) Decoder 当前时间步的隐藏状态 s_t Decoder
Key (K) Encoder 所有时间步的隐藏状态 h₁...h_Tx Encoder
Value (V) Encoder 所有时间步的隐藏状态 h₁...h_Tx Encoder

Attention是一种子结构,它的作用是:让当前的Query去从一堆Key-Value对中选择相关的Value,作为“上下文”提供给 Decoder 来生成输出。

在基于RNN的attention中,Query就是s_t,用来和Key也就是h₁...h_Tx生成score,这个score让它判断Value的重要性,获取当前时间步的输出。不过,这里也可以看出来,基于RNN的encoder和decoder结构的缺陷:必须按照顺序一个个预测输出,时间成本过高。为了解决这个问题,transformer使用Attention替代时间序列依赖。

Self Attention

在Encoder-Decoder框架中,Query是Decoder的隐状态,因为输入Source和输出Target内容是不一样的,任务站在输出的角度去“注意”输入内容的重要性,但是对于文本生成类任务而言,在后续对话中,模型应该关注的是自己之前生成的对话内容,不是最原始的问题,因此Self Attention相比较于Attention,最大的改变就是Query=Key=Value=X * w_{i} i ∈ {k, q, v},也就是说KQV都是经过输入X演变而来的。

如上图所示,对于一个输入序列[a_1, a_2,...a_n]而言,分别可以与对应的参数矩阵点乘获得K向量[k_1, k_2,...k_n],Q向量[q_1, q_2,...q_n],V向量[v_1, v_2,...v_n]。对于第一个输入a_1而言,它对应的q_1想要计算注意力,就需要和[k_1, k_2,...k_n]分别计算,然后再进行归一化softmax处理之后,得到a_1对于其他所有输入得attention scoreattention score再与[v_1, v_2,...v_n]点积,就是最终的预测结果,如下图所示:

那么现在清楚了,对于长度为n的序列,生成的attention score的大小是nxn,再与V向量做矩阵乘后,得到长度为n的输出序列。

Self Attention的score的计算方法为:

其中,由于每一个句子做了分词+词嵌入,所以一个词是大小为embed_dim的向量,所以做点乘以后会很大,softmax 会趋于极端(0或1),梯度变得很小(饱和),训练变困难,所以要除以一个scale`。

下面附赠代码实现:

import torch
import torch.nn.functional as F

class SelfAttention(torch.nn.Module):
    def __init__(self, embed_dim):
        super().__init__()
        self.embed_dim = embed_dim
        self.scale = embed_dim ** 0.5

        # W^Q, W^K, W^V
        self.W_Q = torch.nn.Linear(embed_dim, embed_dim)
        self.W_K = torch.nn.Linear(embed_dim, embed_dim)
        self.W_V = torch.nn.Linear(embed_dim, embed_dim)

    def forward(self, x):
        # x: [batch_size, seq_len, embed_dim]
        Q = self.W_Q(x)  # [batch_size, seq_len, embed_dim]
        K = self.W_K(x)  # same shape
        V = self.W_V(x)  # same shape

        # 计算 Attention Scores: Q @ K^T
        scores = torch.matmul(Q, K.transpose(-2, -1)) / self.scale  # [batch, seq_len, seq_len]

        # 归一化
        weights = F.softmax(scores, dim=-1)  # [batch, seq_len, seq_len]

        # 加权求和 Value
        out = torch.matmul(weights, V)  # [batch, seq_len, embed_dim]
        return out

Multi-head Self-attention

为什么需要Multi-head?

多头注意力机制的核心思想是“将注意力计算分成多个子空间并行计算,每个子空间叫一个头(Head)。”

单头注意力机制的核心问题——单头注意力的表达能力太弱,比如翻译这句话:“The cat that was chased by the dog ran away.”:有的词要关注近邻语法结构(如:the 和 cat);有的词要关注远程依赖(如:cat 和 ran);有的词关注语义上类似的词;有的关注句法结构(比如主语、宾语),单头只能学其中一种关系,不够用

多个头并行学习多种语义、句法关系,从不同上下文解读含义。

Multi-head原理

             ┌──────────────┐
    Input →──► Linear(QKV)  ├─┬──► Head1: Attention(Q₁,K₁,V₁)
             └──────────────┘ │
                              ├──► Head2: Attention(Q₂,K₂,V₂)
                              │
                              ├──► ...
                              │
                              └──► HeadN: Attention(Qₙ,Kₙ,Vₙ)

       Heads拼接 → Concat → Linear → Output

和单头的相比,QKV的向量,变成了QKV矩阵,那么score维度由原来的n x n变成了m x n x n,其中m是指head的数目。最后所有的头的结果拼接后,经过线性层映射为输出。

transformer

位置编码(Positional Encoding)

Transformer 的 Self-Attention 机制是对“序列中所有词做一视同仁的加权平均”,它的核心是这些矩阵计算:

attention_score = softmax(Q @ K.T)
output = attention_score @ V

因此,这一过程根本不会用到token的位置。它只看词之间的相似程度,和它是第几个没关系。为了让模型有位置意识,位置编码(Positional Encoding)就是把每个 token 的位置信息编码成向量,然后加到原始的词向量上,最终的输入 = token 的词向量 + 它的位置向量(positional encoding)

BERT 之后常用Learnable position embedding,把位置也当作一个词,直接用embedding表示,跟token embedding一样参与训练。

transformer的结构

transformer的结构如下图:

transformer仍然采用Encoder和Decoder结构,其中:

  • Encoder:多头自注意力(Self-Attention)+ 前馈网络(Feed Forward)。

    • 多头自注意力:允许模型在同一层中,通过不同的头并行地捕捉各个位置之间的依赖关系,从不同角度来聚合上下文信息,为后续的特征提取提供更丰富的输入表示。

    • 前馈网络:一般由两个线性层和一个激活函数构成。对每个 token 进行更细致的特征提取和非线性变换,增加模型的复杂表达能力?(不明白)教材中提到:

      实验结果表明,增大前馈子层隐状态的维度有利于提 高最终翻译结果的质量,因此,前馈子层隐状态的维度一般比自注意力子层要大。

    • 残差与归一化层:残差避免过程中出现的梯度消失,归一化缓解收敛速度慢等问题。

Q:为什么transformer 里的前馈网络叫位置感知前馈网络?它不是只是两层全连接吗?哪里体现了位置感知?

虽然前馈网络FFN是对每个位置独立处理的,但由于 transformer 中每个 token 有位置信息(Position Encoding),所以这个 FFN 间接地感知了位置,因此称它为位置感知前馈网络。(尴尬笑)

  • Decoder:Masked多头自注意力(用于生成当前词时只能看到前面的词)+ 编码器-解码器注意力(Encoder-Decoder Attention)+ 前馈网络。

    • Transformer的Decoder结构稍微比Encoder复杂一点,因为它需要在生成新词时既考虑已有的生成历史,又能从Encoder的输出中获取上下文信息。

    • Masked Multi-head Self-Attention:输入是目标序列(标签)经过嵌入之后的表示,让Decoder在生成第 t 个词时,只能“看到”前 t−1 个词,防止模型提前“看到”未来词(泄露信息),代码中,score的下三角全部是负无穷。

    • Encoder-Decoder Attention:Decoder 查询 Encoder 的输出,决定当前要生成哪个词。Q 来自 Decoder 的前一个子层(即 Masked Attention 的输出),代表当前要“写”的意图。K, V 来自 Encoder 的输出,代表输入语句的整体信息。

    • Feed Forward Network:对每个位置的向量单独进行非线性转换,增强表示能力,类似于在每个时间步上加一个 MLP。

这里歪一个问题:个人感觉transformer的编码器和解码器是有点反直觉的,人类想知道一个问题的答案,会用问题作为Q,关注答案也就是K和V到底是什么,做出检索,但是transformer的encoder对问题做了自注意力,后面经过位置感知前馈网络,在decoder中做了KV。

GPT对此给出的答案是:

Transformer 中的 Attention 机制逻辑是:Query是我正在思考的东西,Key和Value是我能参考的信息。所以Decoder在生成每一个token的时候,它才是主动思考者,Query自然来Decoder,它看Encoder 的输出,去从中找有用信息,这就是K和V。

Encoder张量大小梳理

为了防止,对过程理解的误区,这里放了整个encoder的过程中,张量大小的变化。

模块 输入 Shape 输出 Shape
输入嵌入 + 位置编码 [B, L][B, L, D] [B, L, D]
Q/K/V 投影 [B, L, D] [B, H, L, D_head]
注意力score [B, H, L, D_head] [B, H, L, L]
score应用到V [B, H, L, L] [B, H, L, D_head]
多头合并输出 [B, L, D] [B, L, D]
前馈网络 [B, L, D][B, L, D_ff][B, L, D] [B, L, D]
最终输出 [B, L, D] [B, L, D]

Q/K/V 投影:D_head * H = D,每一个头的Q/K/V是embedding维度平分的,保证后续经过attention后合并,维度还是[B, L, D]

项目
batch_size B
seq_len(序列长度) L
embedding_dim / d_model(嵌入维度) D(如 512)
num_heads(注意力头数) H(如 8)
d_ff(前馈网络中间维度) D_ff(如 2048)

Attention+RNN 与 transformer

与Attention + RNN的机构相比,transformer不需要循环处理,也就是没有时间步这样的概念,而是采用位置编码,编码时间序列信息,然后一次处理。transformer的处理过程如下:

这里可以看到,encoder和decoder都循环了很多次,但是这种循环与RNN不同,RNN的循环是为了生成下一个输出,而它的循环是为了更好的理解语义。Encoder是为了更好的理解问题,变换出更优秀的词向量(K、V)!而Decoder则是为了更好的回答问题。

参考资料

posted @ 2025-05-31 18:28  ZCry  阅读(179)  评论(0)    收藏  举报