LLM-1 transformer

transformer结构

几个疑问

1.梯度消失或者爆炸是什么?
梯度消失问题会导致网络无法更新权重,而梯度爆炸问题会导致权重更新过大,使训练不稳定。能举出例子吗?

2.掩码多头注意力怎么实现,具体怎么用?掩码是什么?
3.什么是过拟合,dropout为什么有用?
4.为什么在嵌入向量中每个元素乘以\(\sqrt{d_{\text{model}}}\)后再加上位置向量?
5.为什么基于transformer的解码器中的多头交叉注意力模块中的kv值来自编码器?
通过将编码器的输出作为 K 和 V,解码器可以在生成每个输出词时,关注输入序列的不同部分。这样,解码器能够根据编码器提供的上下文信息,生成与输入序列相关的输出,否则要编码器干什么吃的,问题和答案之间要具有一定的联系?

4个模块

  • 注意力层:采用多头注意力机制,并行计算多个独立的注意力机制,从多维度捕捉输入的序列信息。传统的循环神经网络的主要特点是通过隐藏状态(hidden state)在时间步之间传递信息。常见的变种包括长短期记忆(LSTM)和门控循环单元(GRU),适合处理短序列。
  • 位置感知前馈网络层:
  • 残差连接
  • 层归一化
    image
    各个模块的具体功能和实现方法

嵌入表示层

单词向量+位置编码向量->输入
词嵌入(word embedding)可采用如下两种方法:预训练词嵌入:如Word2Vec、GloVe等,这些方法在大量文本上训练得到固定的词向量。随机初始化:在模型训练时随机初始化词向量,并通过反向传播学习。
位置编码:为每个位置都计算一个编码。
\(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)\)

点击查看代码
import torch
import torch.nn as nn
import math
from torch.autograd import Variable


class Embedder(nn.Module):#继承
    def __init__(self, vocab_size, d_model):
        super().__init__()
        self.d_model = d_model
        self.embed = nn.Embedding(vocab_size, d_model)#nn.Embedding 内部会随机初始化一个权重矩阵,形状为 [vocab_size, d_model]。每个索引对应的行就是该索引的嵌入向量。
    def forward(self, x):#转换成嵌入向量
        return self.embed(x)
#max_seq_len表示每个句子最多有多少单词,d_model是每个单词用向量的表示

'''1.1.1 嵌入表示层'''
class PositionalEncoder(nn.Module):
    def __init__(self, d_model, max_seq_len=80, dropout=0.1):
        super().__init__()
        self.d_model = d_model
        self.dropout = nn.Dropout(dropout)
        # 根据pos和i创建一个常量PE矩阵
        pe = torch.zeros(max_seq_len, d_model)
        for pos in range(max_seq_len):#pos是单词中的位置,i是单词内部嵌入向量的索引
            for i in range(0, d_model, 2):
                pe[pos, i] = math.sin(pos / (10000 ** (i / d_model)))
                if i + 1 < d_model:
                    pe[pos, i + 1] = math.cos(pos / (10000 ** (i / d_model)))
        
        pe = pe.unsqueeze(0)#在第一维(批次维度)增加一个维度,使得位置编码的形状变为 (1, max_seq_len, d_model)。
        self.register_buffer('pe', pe)#将位置编码矩阵注册为一个缓冲区,使得它在模型保存和加载时被正确处理,而不会被视为一个可训练的参数。

    def forward(self, x):
        # 使得单词嵌入表示相对大一些
        x = x * math.sqrt(self.d_model)
        # 增加位置常量到单词嵌入表示中
        seq_len = x.size(1)
        x = x + Variable(self.pe[:, :seq_len, :], requires_grad=False)#使用 Variable 并设置 requires_grad=False,确保位置编码不会参与梯度计算。
        return self.dropout(x)
#self.pe[:, :seq_len, :]::表示选择所有的批次(在这里只有一个批次,因为 pe 在第一维的大小为1)。[:seq_len, :]:表示选择从位置 0 到 seq_len-1 的所有位置编码,seq_len 是输入嵌入 x 的序列长度。

注意力层

为了对单词之间的关系进行有效建模,引入注意力机制。引入自注意力机制涉及的三个元素:查询 \(q_i\)(Query)、键 s\(k_i\)(Key)和值 \(v_i\)(Value)。通过计算查询 q 与每个键 k 的相似度,然后归一化为权重,最后根据这些权重加权求和值 v。
为什么还要多一个v呢?
直接将qk的计算过程转换为概率分布来用?
$
\text{score}(q, k_i) = q \cdot k_i
$

\( \text{attention_weights} = \text{Softmax}(\text{score}(q, k)) \)

\(\text{output} = \sum_{i} \text{attention_weights}_i \cdot v_i \)

\( \text{output} = \sum_{i} \text{Softmax}\left(\frac{q \cdot k_i}{\sqrt{d_k}}\right) \cdot v_i \)
引入\(\sqrt{d_k}\)缩小相似度得分,为了防止过大值的梯度爆炸。(因为前面嵌入层的时候每个x都放大了\(\sqrt{d_k}\),而qkv的计算又是x的方,所以除一个)
多头注意力:将多个子空间的注意力层线性组合出来的值。
于是,通过把 Embedding 向量线性变幻成 8 个 1/8 的向量再分别去做 Attention 机制运算,这其实在本质上并不会耽误每个 token 的语义表达,而只是细分出了不同的语义子空间,即不同类型的细分语义逻辑而已,Attention 机制运算起来将更细腻精准、更有针对性。

点击查看代码
#
'''1.1.2 注意力层'''
class MultiHeadAttention(nn.Module):
    def __init__(self, heads, d_model, dropout=0.1):
        super().__init__()

        self.d_model = d_model
        self.d_k = d_model // heads#每个头的维度
        self.h = heads#表示要分成多少个不同的注意力头

        self.q_linear = nn.Linear(d_model, d_model)#转换成向量
        self.k_linear = nn.Linear(d_model, d_model)
        self.v_linear = nn.Linear(d_model, d_model)

        self.dropout = nn.Dropout(dropout)
        self.out = nn.Linear(d_model, d_model)#将多个头合并成d_model的维度,然后输出

    def attention(self, q, k, v, d_k, mask=None, dropout=None):
		#q k v 的形状是(batch_size, num_heads, seq_len, d_k)
		#scores形状是(batch_size, num_heads, seq_len, seq_len)
        scores = torch.matmul(q, k.transpose(-2, -1)) / math.sqrt(d_k)#此处转秩是因为做矩阵乘法要维度匹配

        # 掩盖那些为了补全长度而增加的单元,使其通过Softmax计算后为0
        if mask is not None:
            mask = mask.unsqueeze(1)#在第几个维度,插入一个新的维度
            scores = scores.masked_fill(mask == 0, -1e9)

        scores = F.softmax(scores, dim=-1)

        if dropout is not None:
            scores = dropout(scores)

        output = torch.matmul(scores, v)#此处不用转秩是因为,维度已经匹配
        return output

    def forward(self, q, k, v, mask=None):
        bs = q.size(0)

        # 利用线性计算划分成h个头
        k = self.k_linear(k).view(bs, -1, self.h, self.d_k)
        q = self.q_linear(q).view(bs, -1, self.h, self.d_k)
        v = self.v_linear(v).view(bs, -1, self.h, self.d_k)

        # 矩阵转置
        k = k.transpose(1, 2)
        q = q.transpose(1, 2)
        v = v.transpose(1, 2)

        # 计算attention
        scores = self.attention(q, k, v, self.d_k, mask, self.dropout)

        # 连接多个头并输入最后的线性层
        concat = scores.transpose(1, 2).contiguous().view(bs, -1, self.d_model)

        output = self.out(concat)

        return output

前馈层

前馈层以自注意力层作为输入,通过带有ReLU激化函数的两层全连接网络对输入进行非线性变换。
\(\text{output} = W_2 \cdot \text{ReLU}(W_1 \cdot x + b_1) + b_2\)

点击查看代码
'''1.1.3 前馈层'''
class FeedForward(nn.Module):
    def __init__(self, d_model, d_ff=2048, dropout=0.1):
        super().__init__()

        # d_ff 默认设为 2048
        self.linear_1 = nn.Linear(d_model, d_ff)#创建一个线性层,将输入特征从 d_model 维度映射到 d_ff 维度。
        self.dropout = nn.Dropout(dropout)
        self.linear_2 = nn.Linear(d_ff, d_model)#创建另一个线性层,将隐藏层的输出从 d_ff 维度映射回 d_model 维度。

    def forward(self, x):
        x = self.dropout(F.relu(self.linear_1(x)))
        x = self.linear_2(x)
        return x

残差连接与层归一化

残差连接主要是指使用一条直连通道直接将对应子层的输入连接到输出,避免在优化过程中因网络过深而产生潜在的梯度消失问题。
层归一化为了将每一层的输入输出稳定在合理的范围。层归一化技术可以有效地缓解优化过程中潜在的不稳定、收敛速度慢等问题。

点击查看代码
'''1.1.4 残差连接与层归一化'''
class Norm(nn.Module):
    def __init__(self, d_model, eps=1e-6):
        super().__init__()

        self.size = d_model

        # 层归一化包含两个可学习参数
        self.alpha = nn.Parameter(torch.ones(self.size))
        self.bias = nn.Parameter(torch.zeros(self.size))

        self.eps = eps  # 避免除零

    def forward(self, x):
        mean = x.mean(dim=-1, keepdim=True)#特征维度上求均值,哪种特征做均值
        std = x.std(dim=-1, keepdim=True)#标准差
        norm = self.alpha * (x - mean) / (std + self.eps) + self.bias
        return norm

编码器与解码器结构

image

编码器 如图所示

解码器

  • 增加了掩码多头注意力,解码器端则负责生成目标语言序列,这一生成过程是自回归的,即对于每一个单词的生成过程,仅有当前单词之前的目标语言序列是可以被观测的,因此这一额外增加的掩码是用来掩盖后续的文本信息的,以防模型在训练阶段直接看到后续的文本序列,进而无法得到有效的训练。
  • 额外增加了多头交叉注意力模块,同时接收编码器的输出和当前Transformer块前一个掩码注意力层的输出。查询是通过解码器前一层的输出进行投影的,而键和值是使用编码器的输出进行投影的。它的作用是在翻译的过程中,为了生成合理的目标语言序列,观测待翻译的源语言序列是什么。

解码器端以自回归的方式生成目标语言文本,即在每个时间步 t,根据编码器端输出的源语言文本表示,以及前t − 1 个时刻生成的目标语言文本,生成当前时刻的目标语言单词。
编码器(Encoder)
编码器的主要任务是将输入序列转换为一个上下文丰富的表示。Transformer 编码器由多个相同的层堆叠而成,每层的结构包括以下几个部分:

  1. 输入嵌入(Input Embedding)
    输入序列首先被转换为固定维度的向量表示(通常使用词嵌入)。
    还会加入位置编码(Positional Encoding)来保留序列中每个单词的位置信息,因为 Transformer 本身是无序列信息的。

  2. 多头自注意力机制(Multi-Head Self-Attention)
    自注意力机制:允许模型在处理输入序列的某个词时,关注序列中的其他词。这使得模型能够捕捉词与词之间的关系。
    多头机制:通过多个注意力头并行计算不同的注意力分布,使得模型能够从多个子空间中提取信息。

  3. 残差连接与层归一化(Residual Connection and Layer Normalization)
    在每个自注意力层的输出与输入之间添加残差连接,帮助训练更深的网络。
    之后进行层归一化,改善训练的稳定性。

  4. 前馈神经网络(Feed-Forward Neural Network)
    每个编码器层还包含一个前馈神经网络,对每个位置的输出进行非线性变换。
    这个前馈网络通常由两个线性变换和一个激活函数(如 ReLU)组成。

  5. 输出
    编码器的最终输出是一个上下文丰富的表示,供后续的解码器使用。
    解码器(Decoder)
    解码器的主要任务是生成目标序列,通常在机器翻译任务中,这个目标序列是翻译后的文本。解码器的结构与编码器相似,但有一些关键的区别。

  6. 输出嵌入(Output Embedding)
    解码器的输入是目标序列的嵌入表示,同样加入位置编码。

  7. 多头自注意力机制(Masked Multi-Head Self-Attention)
    掩蔽机制:在解码器中,自注意力机制是掩蔽的,确保每个位置只能关注到它之前的位置,避免信息泄露(即,生成时不能使用未来的信息)。

  8. 编码器-解码器注意力(Encoder-Decoder Attention)
    解码器中的每层还包含一个注意力机制,用于关注编码器的输出。这使得解码器能够利用编码器生成的上下文信息。

  9. 残差连接与层归一化
    和编码器相同,解码器中也使用残差连接和层归一化,帮助模型稳定训练。

  10. 前馈神经网络
    和编码器一样,解码器也包含一个前馈神经网络,用于进一步处理信息。

  11. 输出层
    最后,解码器的输出会通过一个线性层和 softmax 函数,生成下一个词的概率分布。模型通过选择概率最高的词,逐步生成目标序列。
    总结
    编码器:将输入序列转换为上下文丰富的表示,使用多头自注意力和前馈神经网络来捕捉序列中的信息。
    解码器:根据编码器的输出和自身的历史输出生成目标序列,使用掩蔽自注意力和编码器-解码器注意力来确保生成的连贯性。
    这种结构使得 Transformer 能够有效地捕捉长距离依赖关系,并且在许多自然语言处理任务中表现出色。
    让我们通过一个具体的例子来说明 Transformer 中编码器和解码器的工作流程。假设我们正在进行英语到法语的翻译任务。

示例:英语到法语翻译
输入句子:"The cat is on the mat."

目标句子:"Le chat est sur le tapis."

编码器部分
输入嵌入:
输入句子中的每个单词被转换为向量(词嵌入),并加上位置编码,以保留单词在句子中的位置信息。
例如:
The → [0.1, 0.2, ...]
cat → [0.3, 0.1, ...]
is → [0.0, 0.4, ...]
on → [0.2, 0.3, ...]
the → [0.1, 0.2, ...]
mat → [0.5, 0.0, ...]
多头自注意力机制:
编码器计算每个单词对其他单词的注意力权重,例如,在处理 cat 时,可能会关注 the 和 mat,因为这些词与 cat 有关。
前馈神经网络:
对每个单词的表示进行非线性变换,提取更复杂的特征。
输出:
编码器输出一个上下文向量序列,表示整个输入句子的含义。
解码器部分
输出嵌入:
解码器的输入是目标句子的嵌入,通常从一个特殊的开始符号(如 )开始。
例如:
→ [0.2, 0.1, ...](对应于法语句子开始)
掩蔽自注意力机制:
解码器在生成过程中仅关注当前和之前的词。例如,当生成 Le 时,只能关注
编码器-解码器注意力:
解码器的每一层会关注编码器的输出,以获取有关输入句子的上下文信息。例如,Le 可能会关注 cat 和 is,以理解如何翻译。
前馈神经网络:
对解码器的输出进行非线性变换。
输出层:
最终,解码器生成每个词的概率分布,选择概率最高的词。在输出 Le 后,解码器接着生成 chat,然后是 est,依此类推,直到生成整个目标句子 Le chat est sur le tapis.。
过程总结
编码器:通过处理输入句子,提取上下文信息,并生成一个上下文向量序列。
解码器:使用编码器的输出和自身生成的词来逐步生成目标句子,确保翻译的连贯性和准确性。

编码器代码

点击查看代码
'''1.1.5 编码器和解码器结构'''
class EncoderLayer(nn.Module):
    def __init__(self, d_model, heads, dropout=0.1):
        super().__init__()
        self.norm_1 = Norm(d_model)
        self.norm_2 = Norm(d_model)
        self.attn = MultiHeadAttention(heads, d_model, dropout=dropout)
        self.ff = FeedForward(d_model, dropout=dropout)
        self.dropout_1 = nn.Dropout(dropout)
        self.dropout_2 = nn.Dropout(dropout)

    def forward(self, x, mask):
        attn_output = self.attn(x, x, x, mask)
        attn_output = self.dropout_1(attn_output)
        x = x + attn_output
        x = self.norm_1(x)
        ff_output = self.ff(x)
        ff_output = self.dropout_2(ff_output)
        x = x + ff_output
        x = self.norm_2(x)
        return x

解码器代码

点击查看代码
class DecoderLayer(nn.Module):
    def __init__(self, d_model, heads, dropout=0.1):
        super().__init__()
        self.norm_1 = Norm(d_model)
        self.norm_2 = Norm(d_model)
        self.norm_3 = Norm(d_model)

        self.dropout_1 = nn.Dropout(dropout)
        self.dropout_2 = nn.Dropout(dropout)
        self.dropout_3 = nn.Dropout(dropout)

        self.attn_1 = MultiHeadAttention(heads, d_model, dropout=dropout)
        self.attn_2 = MultiHeadAttention(heads, d_model, dropout=dropout)
        self.ff = FeedForward(d_model, dropout=dropout)

    def forward(self, x, e_outputs, src_mask, trg_mask):#trg代表目标序列,是解码器要生成的序列
        attn_output_1 = self.attn_1(x, x, x, trg_mask)#此处是调用了MultiHeadAttention中的forward方法,不显性使用。这是因为 PyTorch 的 nn.Module 类已经重载了 __call__ 方法,使得调用模型实例时会自动调用 forward 方法。
        attn_output_1 = self.dropout_1(attn_output_1)
        x = x + attn_output_1
        x = self.norm_1(x)
        attn_output_2 = self.attn_2(x, e_outputs, e_outputs, src_mask)
        attn_output_2 = self.dropout_2(attn_output_2)
        x = x + attn_output_2
        x = self.norm_2(x)

        ff_output = self.ff(x)
        ff_output = self.dropout_3(ff_output)
        x = x + ff_output
        x = self.norm_3(x)

        return x

基于 Transformer 的编码器和解码器结构整体实现的参考代码如下:

点击查看代码

posted @ 2025-03-16 16:00  keepsoft123  阅读(47)  评论(0)    收藏  举报