21
2025
03
01:25:54

【LLM|BLOG】挑战极限!一次搞懂Transformer Encoder的所有秘密!

1. Encoder 整体架构图

 【LLM|BLOG】挑战极限!一次搞懂Transformer Encoder的所有秘密!
Encoder 架构图

2. 什么是Encoder

编码器是负责通过自注意力和前馈神经网络(FNN)处理输入标记以生成上下文感知表示的部分。

它是理解 NLP 模型中序列的强大动力。

 【LLM|BLOG】挑战极限!一次搞懂Transformer Encoder的所有秘密!
Encoder

2.1 输入Embedding

编码器的第一步是将每个输入单词嵌入到大小为 512维度的向量中。 此嵌入过程仅发生在最底部的编码器中。 将其视为将单词翻译成模型可以理解的语言.

 【LLM|BLOG】挑战极限!一次搞懂Transformer Encoder的所有秘密!
输入词进行Embedding

1. 分词与输入准备

在进行嵌入之前,输入文本需要首先被分割成离散的tokens(标记)。分词的方式可以有多种:

  • 词级别:将整个单词作为标记。

  • 子词级别:将单词拆分为有意义的较小单位(例如前缀、词根、后缀),通常使用像 BPE(Byte-Pair Encoding)这样的算法。

  • 字符级别:将每个字符作为标记。

在图片中,西班牙语短语 "De quién es" 被分割成三个标记:Dequién 和 es

2. 嵌入层的实现

分词之后,每个 token 通过嵌入层(Embedding Layer)映射到一个固定大小的向量。这个嵌入层通常以查找表的形式实现,表中的每个 token 都与一个固定大小的向量相关联,向量的维度(例如 512 维)是预先定义的。

PyTorch 实现示例:

import torchimport torch.nn as nn# 1. 定义词汇表,将词语映射到唯一的 ID# 这个词汇表根据具体任务和数据集定义vocab = {
    "De": 5,
    "quién": 45,
    "es": 789,
    "[PAD]": 0,   # 填充标记 (Padding) 用于对齐不同长度的句子
    "[UNK]": 1,   # 未知标记 (Unknown) 用于处理词汇表中没有的词}# 2. 输入的西班牙语句子sentence = "De quién es"# 3. 简单的分词过程:将句子分割成单词(标记)# 这里直接使用 Python 的 split 方法按空格分割tokens = sentence.split()  # ['De', 'quién', 'es']# 4. 将分词后的单词映射到词汇表中的 token IDs# 使用词汇表将每个 token 映射为 ID,如果词不在词汇表中,则使用 "[UNK]" 的 IDtoken_ids = [vocab.get(token, vocab["[UNK]"]) for token in tokens]# 输出 Token IDs,检查转换是否正确print("Token IDs:", token_ids)  # 输出示例: [5, 45, 789]# 5. 将 token ID 转换为 PyTorch 张量(用于后续的嵌入操作)# 注意:需要将 token ID 转换为二维张量 (batch_size, sequence_length)input_tokens = torch.tensor([token_ids])  # 形状 (1, 3),批次大小为 1,序列长度为 3# 6. 定义嵌入层# vocab_size 定义为词汇表大小(假设词汇表最多 10000 个 token),embedding_dim 是嵌入的维度embedding_layer = nn.Embedding(num_embeddings=10000, embedding_dim=512)# 7. 生成嵌入向量# 通过嵌入层将 token ID 转换为 512 维的嵌入向量embedded_input = embedding_layer(input_tokens)# 输出嵌入向量的形状和内容,形状应为 (batch_size, sequence_length, embedding_dim)print("嵌入向量形状:", embedded_input.shape)  # 输出示例: torch.Size([1, 3, 512])print("嵌入向量:", embedded_input)

 【LLM|BLOG】挑战极限!一次搞懂Transformer Encoder的所有秘密!
Embedding 后输出

1、定义词汇表:

手动定义了一个简单的词汇表 vocab,将西班牙语中的 De、quién 和 es 映射到唯一的整数 ID(分别为 5、45 和 789)。另外还包括了 [PAD] 和 [UNK] 两个特殊标记,分别用于填充和处理未知的词语。

2、输入句子和分词:

输入的句子是 "De quién es",我们使用 Python 的 split() 方法将句子分割为单词列表 ['De', 'quién', 'es']。

3、将 token 转换为 token IDs:

使用词汇表中的映射关系,将每个单词转换为对应的 token ID。如果某个单词不在词汇表中,默认返回 [UNK](ID 为 1)。生成的 token IDs 为 [5, 45, 789]。

4、将 token IDs 转换为张量:

PyTorch 中的嵌入层期望输入的 token 是张量形式。因此我们将 token IDs 列表转换为一个二维张量。这里假设批次大小为 1,因此张量的形状是 (1, 3),表示一个包含 3 个 token 的输入序列。

嵌入矩阵是一张查找表,模型通过该表将 token ID 转换为相应的向量:每一行对应词汇表中的一个 token。

每一列代表嵌入空间中的一个特征(例如词语的语法或语义属性)。嵌入矩阵在训练开始时是随机初始化的,但随着训练过程的进行,它会根据任务优化,以捕捉 token 之间的语义和上下文关系。

5、定义嵌入层:

嵌入层的定义需要两个参数:num_embeddings 表示词汇表的大小(这里假设最多有 10000 个 token),embedding_dim 表示每个 token 嵌入向量的维度(这里设为 512)。

嵌入层的维度是固定的(例如 512 维),这确保了无论输入序列有多长,模型都能统一处理每个 token。无论某个 token 的出现频率高低,它的表示都会被映射到这个固定的向量空间中。这种维度的大小(例如 512)是模型的一个超参数,可以根据任务的复杂性进行调整。

6、生成嵌入向量:

我们通过 embedding_layer(input_tokens) 将 token IDs 传入嵌入层,生成对应的嵌入向量。嵌入向量的输出形状为 (1, 3, 512),即 1 个句子(批次大小为 1),3 个 token,每个 token 的嵌入向量是 512 维

嵌入原因:

  • 语义相似性:相似的 token 会在嵌入空间中靠得更近,捕捉它们的语法或语义相似性。

  • 效率:相比于 one-hot 编码,嵌入层通过将稀疏的高维数据转换为密集的低维表示,极大提升了计算效率。

7、输出嵌入向量:

打印了嵌入向量的形状和内容,输出的向量为 [1, 3, 512],表示模型生成了 3 个 token 的 512 维嵌入

2.2 位置编码(position-encoding)

由于 Transformer 模型没有内置的序列化机制(不像 RNN 那样逐步处理输入序列),因此它使用 位置编码(Positional Encoding) 来捕捉输入序列中每个 token 的位置信息。位置编码通过将词嵌入向量和位置信息相加,使模型能够感知输入序列中每个 token 的位置关系。该位置编码的生成使用了一组正弦和余弦函数。

 【LLM|BLOG】挑战极限!一次搞懂Transformer Encoder的所有秘密!
position-encoding

位置编码的主要特点:

  1. 引入序列顺序信息:位置编码为每个 token 加入了它在序列中的位置信息,使模型能够感知词与词之间的相对顺序。

  2. 正弦和余弦函数:使用正弦和余弦函数来生成位置编码,这些函数的不同频率允许模型识别远距离和近距离的依赖关系。

  3. 与词向量相加:位置编码是与词嵌入向量逐元素相加的,不改变嵌入向量的维度。

1. Positional Encoding 的公式


 【LLM|BLOG】挑战极限!一次搞懂Transformer Encoder的所有秘密!

其中,pos 表示输入 token 的位置,i 是维度索引, 奇数时候用cos, 偶数用sin。通过正弦和余弦函数,我们能够为每个位置生成独特的编码,并为模型提供足够的顺序信息。

pytorch 代码实现

import torch
import torch.nn as nn
import math


# 1. 定义词汇表,将词语映射到唯一的 ID
# 这个词汇表根据具体任务和数据集定义
vocab = {
    "De": 5,
    "quién": 45,
    "es": 789,
    "[PAD]": 0,   # 填充标记 (Padding) 用于对齐不同长度的句子
    "[UNK]": 1,   # 未知标记 (Unknown) 用于处理词汇表中没有的词
}

# 2. 输入的西班牙语句子
sentence = "De quién es"

# 3. 简单的分词过程:将句子分割成单词(标记)
# 这里直接使用 Python 的 split 方法按空格分割
tokens = sentence.split()  # ['De', 'quién', 'es']

# 4. 将分词后的单词映射到词汇表中的 token IDs
# 使用词汇表将每个 token 映射为 ID,如果词不在词汇表中,则使用 "[UNK]" 的 ID
token_ids = [vocab.get(token, vocab["[UNK]"]) for token in tokens]

# 输出 Token IDs,检查转换是否正确
print("Token IDs:", token_ids)  # 输出示例: [5, 45, 789]

# 5. 将 token ID 转换为 PyTorch 张量(用于后续的嵌入操作)
# 注意:需要将 token ID 转换为二维张量 (batch_size, sequence_length)
input_tokens = torch.tensor([token_ids])  # 形状 (1, 3),批次大小为 1,序列长度为 3

# 6. 定义嵌入层
# vocab_size 定义为词汇表大小(假设词汇表最多 10000 个 token),embedding_dim 是嵌入的维度
embedding_layer = nn.Embedding(num_embeddings=10000, embedding_dim=512)

# 7. 生成嵌入向量
# 通过嵌入层将 token ID 转换为 512 维的嵌入向量
embedded_input = embedding_layer(input_tokens)

# 输出嵌入向量的形状和内容,形状应为 (batch_size, sequence_length, embedding_dim)
print("嵌入向量形状:", embedded_input.shape)  # 输出示例: torch.Size([1, 3, 512])
print("嵌入向量:", embedded_input)
class PositionalEncoding(nn.Module):
    def __init__(self, d_model, max_len=5000):
        super(PositionalEncoding, self).__init__()
        
        # 创建一个 (max_len, d_model) 的矩阵,保存所有位置编码
        pe = torch.zeros(max_len, d_model)
        position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)
        
        # 定义 div_term 用于计算正弦和余弦
        div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model))
        
        # 计算正弦和余弦部分
        pe[:, 0::2] = torch.sin(position * div_term)  # 偶数维度使用正弦函数
        pe[:, 1::2] = torch.cos(position * div_term)  # 奇数维度使用余弦函数
        
        # 在 batch 维度增加一维
        pe = pe.unsqueeze(0).transpose(0, 1)
        
        # 将 pe 存储为不可训练的参数
        self.register_buffer('pe', pe)
    
    def forward(self, x):
        # 将位置编码加到输入的嵌入向量上
        x = x + self.pe[:x.size(0), :]
        return x

# 假设输入嵌入的维度为 512,最大序列长度为 5000
d_model = 512
pos_encoder = PositionalEncoding(d_model)
# 添加位置编码到嵌入向量
embedded_with_positional = pos_encoder(embedded_input)
print("向量矩阵值和位置编码相加的结果:", embedded_with_positional)
print("嵌入加上位置编码后的输出:", embedded_with_positional.shape)

 【LLM|BLOG】挑战极限!一次搞懂Transformer Encoder的所有秘密!
输出结果
 【LLM|BLOG】挑战极限!一次搞懂Transformer Encoder的所有秘密!

2.3 编码层堆叠

通过这个过程,编码器的每个层会依次处理输入序列,使用多头自注意力机制来理解词与词之间的关系,通过前馈神经网络来增强特征,并结合残差连接归一化保证网络的深度训练稳定性。最终的输出是每个词语在上下文中的高度上下文感知的表示,准备传递到解码器或用于其他任务。

 【LLM|BLOG】挑战极限!一次搞懂Transformer Encoder的所有秘密!
Encoder 层


2.1: - -

2.4 multi-Head Self-Attention(多头注意力)

多头自注意力机制(Multi-Headed Self-Attention)是 Transformer 模型中最核心的部分,它可以让模型在输入序列的不同部分之间计算关联性。这个机制的工作原理是:通过使用多个查询(Query, Q)键(Key, K)和值(Value, V)向量来为输入序列中的每个 token 计算注意力分数,最终模型可以根据这些注意力分数来强调序列中的不同部分。

 【LLM|BLOG】挑战极限!一次搞懂Transformer Encoder的所有秘密!

1、 多头自注意力机制的核心步骤:

1.1 计算查询(Q)、键(K)和值(V)向量

对于输入的每个 token,我们会通过三个线性变换将它转换为查询、键和值向量。这些向量捕捉了每个 token 的不同视角,用于计算注意力权重和最终的注意力结果。

  • Query (Q): 用于表示我们想要关注的信息。

  • Key (K): 用于表示序列中的各个元素将如何与查询进行匹配。

  • Value (V): 是与键配对的内容,用于生成注意力层的输出。

1.2 计算注意力分数(Attention Score)

查询向量和键向量之间的相似度用于计算注意力分数。通常是通过查询向量 QQQ 和键向量 KKK 的点积,然后对点积结果进行缩放,最后应用 softmax 归一化处理得到注意力分数。

注意力分数的公式为:

 【LLM|BLOG】挑战极限!一次搞懂Transformer Encoder的所有秘密!
Self-Attention 公式

其中:

  • Q和 K的点积用于计算相似性。

  • dk 是键向量的维度,用于缩放点积的结果,以防止数值过大。一般是512个维度,8个head ,dk = 512/8 = 64

  • softmax 用于将注意力分数归一化,使其总和为 1。

1.3 多头机制

“多头”的意思是我们不只使用一个查询、键和值向量来计算注意力,而是将这些向量分成多个头(head)。每个头会在不同的子空间上计算注意力,然后将这些头的输出拼接起来,并通过一个线性层融合结果。

多头注意力的优点:

  • 每个头部可以在不同的语义空间中专注于输入序列的不同部分,捕捉到更丰富的信息。

  • 通过并行计算多个注意力层,增强了模型的表现力。

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

class MultiHeadAttention(nn.Module):
    def __init__(self, d_model, num_heads):
        super(MultiHeadAttention, self).__init__()
        self.num_heads = num_heads
        self.d_model = d_model
        
        # 线性层用于将输入数据映射到 Q, K, V 空间
        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.out = nn.Linear(d_model, d_model)

    def split_heads(self, x, batch_size):
        """ 将输入 x 拆分为 num_heads 个头部 """
        return x.view(batch_size, -1, self.num_heads, self.d_model // self.num_heads).transpose(1, 2)
    
    def forward(self, queries, keys, values):
        batch_size = queries.size(0)

        # 计算 Q, K, V 向量
        Q = self.q_linear(queries)
        K = self.k_linear(keys)
        V = self.v_linear(values)

        # 将 Q, K, V 拆分为多个头部
        Q = self.split_heads(Q, batch_size)  # 形状: [batch_size, num_heads, seq_len, depth]
        K = self.split_heads(K, batch_size)
        V = self.split_heads(V, batch_size)
        
        # 计算缩放的点积注意力
        scores = torch.matmul(Q, K.transpose(-2, -1)) / torch.sqrt(torch.tensor(self.d_model // self.num_heads, dtype=torch.float32))
        attention_weights = F.softmax(scores, dim=-1)
        
        # 使用注意力权重对值向量加权
        attention_output = torch.matmul(attention_weights, V)
        
        # 将多头的结果拼接起来
        attention_output = attention_output.transpose(1, 2).contiguous().view(batch_size, -1, self.d_model)
        
        # 通过线性层生成最终输出
        output = self.out(attention_output)
        return output
  • q_lineark_linear 和 v_linear:这些是线性层,用于分别将输入序列映射到查询、键和值的向量空间。

  • split_heads:由于我们有多个注意力头,每个注意力头只会处理嵌入维度的一部分,因此我们需要将输入拆分成多个头部。每个头部处理不同的子空间。

  • 点积计算注意力分数:对于每个查询向量 Q,通过点积计算它与每个键向量 K 的相似度。然后将这个相似度分数除以 dk\sqrt{d_k}dk 进行缩放(防止数值过大)。

  • softmax 归一化:对点积结果应用 softmax 函数,将相似度转换为权重,表示该查询向量应该在多大程度上关注序列中的每个词。

  • 多头拼接:将所有头部的输出拼接在一起,并通过线性层进行映射,得到最终的注意力输出。

4、为什么需要多头注意力?

  • 捕捉不同层次的关系:每个注意力头可以捕捉到序列中的不同类型的关系,有些头可能专注于词与词之间的局部关系,而另一些头则专注于长距离的依赖关系。

  • 提升表现力:通过多个头的并行计算,模型可以在不同的特征空间中捕捉到更丰富的信息。

5、多头自注意力的输出是什么?

多头自注意力的输出是经过每个注意力头的结果拼接并线性变换后的向量。这个输出将作为下一层编码器的输入,或者在解码器中被用作生成序列的基础。

2.5 (矩阵乘法)

在 Transformer 模型中的 多头自注意力机制(Self-Attention Mechanism)中,查询(Query, Q)和键(Key, K)之间的 矩阵乘法 是一个至关重要的步骤。这个步骤通过矩阵乘法来计算序列中每个词与其他词之间的关联分数,帮助模型理解输入序列中的依赖关系。

 【LLM|BLOG】挑战极限!一次搞懂Transformer Encoder的所有秘密!
Q * K 矩阵乘法

1、查询(Q)和键(K)的矩阵乘法

在自注意力机制中,给定输入序列中的每个 token,我们会为它计算一个查询向量和一个键向量。查询向量代表该词的“问题”,键向量则是这个词的“回答”。我们通过计算查询向量和键向量的点积来得到每个词与序列中其他词的相关性分数。

公式:

 【LLM|BLOG】挑战极限!一次搞懂Transformer Encoder的所有秘密!
Q K 矩阵相乘

解释

  • 矩阵乘法会给出每个查询和每个键之间的相似度得分。通过这种方式,模型能够知道句子中的每个词和其他所有词的关联性。

  • 结果矩阵中的每一行表示一个词与序列中所有其他词的相似度分数。

2、举例

例如,对于输入句子 “De quién es”,我们将计算如下矩阵乘法:

  • 查询矩阵 Q:代表 "De"、"quién"、"es" 三个词的查询向量。

  • 键矩阵 K:代表 "De"、"quién"、"es" 的键向量。

矩阵乘法的结果是一个 3x3 的矩阵,其中每个元素 (i,j)(i, j)(i,j) 表示句子中的第 i个词与第 j个词的相似度。

3、 缩放点积注意力(Scaled Dot-Product Attention)

 【LLM|BLOG】挑战极限!一次搞懂Transformer Encoder的所有秘密!

import torch
import torch.nn.functional as F

# 模拟输入的查询 (Q) 和键 (K) 向量
# 假设我们有一个序列长度为 3,每个词的嵌入维度为 4
Q = torch.tensor([[0.1, 0.2, 0.3, 0.4],   # "De" 的查询向量
                  [0.5, 0.6, 0.7, 0.8],   # "quién" 的查询向量
                  [0.9, 1.0, 1.1, 1.2]])  # "es" 的查询向量

K = torch.tensor([[0.1, 0.2, 0.3, 0.4],   # "De" 的键向量
                  [0.5, 0.6, 0.7, 0.8],   # "quién" 的键向量
                  [0.9, 1.0, 1.1, 1.2]])  # "es" 的键向量

# 查询矩阵和键矩阵的点积
attention_scores = torch.matmul(Q, K.T)

# 缩放步骤
d_k = Q.size(-1)  # 获取查询和键向量的维度(这里是 4)
scaled_attention_scores = attention_scores / torch.sqrt(torch.tensor(d_k, dtype=torch.float32))

# 使用 softmax 归一化注意力分数
attention_weights = F.softmax(scaled_attention_scores, dim=-1)

print("Attention Scores:\n", attention_scores)
print("Scaled Attention Scores:\n", scaled_attention_scores)
print("Attention Weights (Softmax):\n", attention_weights)

 【LLM|BLOG】挑战极限!一次搞懂Transformer Encoder的所有秘密!

  • attention_scores:这是未缩放的查询和键的点积结果。该矩阵表示输入序列中每个词与其他词的关联性。

  • scaled_attention_scores:通过将点积结果除以 dk\sqrt{d_k}dk 进行缩放,避免过大的数值影响训练。

  • attention_weights:通过 softmax 将缩放后的分数归一化,得到每个词与其他词的注意力权重。

  • 矩阵乘法:查询向量 Q 和键向量 K 的点积会为每个词和其他词生成一个关联分数。对于输入序列中的每个词,它可以了解到自己和其他词之间的相似度,从而决定应该关注哪些词。

  • 缩放:将这些分数除以 dk 开根号,保证数值范围适中,防止 softmax 的输出过于极端。

  • Softmax:将注意力分数转化为概率分布,表示模型在计算上下文时需要关注其他词的程度。

2.6 scaling the attention scores(缩放注意力分数)

在 Transformer 模型的多头自注意力机制中,缩放点积注意力(Scaled Dot-Product Attention)是非常关键的一步。由于查询(Query, Q)和键(Key, K)向量的点积可能导致数值过大,从而影响梯度的稳定性,我们需要对点积结果进行缩放处理。这就是在公式中引入 开根号 dk 的原因,其中 dk 是键和查询向量的维度。

 【LLM|BLOG】挑战极限!一次搞懂Transformer Encoder的所有秘密!

2.7 applying softmax

在 Transformer 模型的多头自注意力机制中,Softmax 函数 用于将注意力分数转换为概率分布,从而为每个词分配一个权重。这一步将确保所有的注意力分数总和为 1,使得模型能够根据权重更合理地对其他词进行加权。通过归一化后的注意力权重,Transformer 模型能够更加精确地捕捉输入序列中的词与词之间的关联,从而提升模型的表现力。

 【LLM|BLOG】挑战极限!一次搞懂Transformer Encoder的所有秘密!

1. Softmax 函数的作用

 【LLM|BLOG】挑战极限!一次搞懂Transformer Encoder的所有秘密!

在注意力机制中的作用:

  1. 将注意力分数转换为权重:Softmax 函数会将高分数放大,而低分数压缩,从而使得模型专注于最重要的词语。

  2. 归一化:Softmax 输出的权重在所有词之间进行归一化,使得它们的总和为 1。

  3. 计算注意力权重:这些归一化后的权重会用于对值向量(Value, V)进行加权平均,从而生成最终的输出。

2、使用 Softmax 的原因

Softmax 的应用非常重要,因为它使得注意力分数可以直接被解读为概率,从而:

  1. 高分放大,低分压缩:Softmax 放大了高分的影响,并减小了低分的作用,这意味着模型会更加关注那些与当前词语相关性强的词。

  2. 归一化:所有的权重总和为 1,表示模型在处理每个词时不会有偏差,并确保每个词语都有一定的注意力。

Softmax 在 PyTorch 中的实现非常简单,可以通过 torch.nn.functional.softmax 直接实现。

示例代码:

import torch
import torch.nn.functional as F

# 模拟计算后的注意力分数 (缩放后)
# 这是点积和缩放后得到的分数矩阵
scaled_attention_scores = torch.tensor([[0.15, 0.35, 0.55],
                                        [0.35, 0.75, 1.15],
                                        [0.55, 1.15, 1.75]])

# 在最后一个维度上应用 softmax
attention_weights = F.softmax(scaled_attention_scores, dim=-1)

# 输出 softmax 结果
print("Scaled Attention Scores:\n", scaled_attention_scores)
print("Attention Weights (Softmax):\n", attention_weights)

解释:

  • scaled_attention_scores 是经过缩放后的注意力分数矩阵。

  • F.softmax 将缩放后的分数转化为注意力权重。

  • dim=-1 表示在最后一个维度上进行 softmax 计算,即对每一行的分数进行归一化。

输出示例:

Scaled Attention Scores:
 tensor([[0.1500, 0.3500, 0.5500],
         [0.3500, 0.7500, 1.1500],
         [0.5500, 1.1500, 1.7500]])

Attention Weights (Softmax):
 tensor([[0.2562, 0.3493, 0.3945],
         [0.2562, 0.3493, 0.3945],
         [0.2562, 0.3493, 0.3945]])

2.8 combining softmax results

在 Transformer 的 多头自注意力机制(Multi-Headed Self-Attention)中,最后一步是将计算出的注意力权重值向量(Value Vector, V)相乘,生成最终的输出向量。这一步骤非常重要,因为它根据每个词与其他词的相关性,对不同的值向量进行加权平均,从而将最相关的上下文信息整合到输出中。

 【LLM|BLOG】挑战极限!一次搞懂Transformer Encoder的所有秘密!

1、 过程概述

在自注意力机制中:

  • 我们已经通过查询(Query, Q)和键(Key, K)的点积,计算出了每个词与其他词之间的相似度。

  • 通过 Softmax 对相似度进行了归一化,得到了每个词对于其他词的注意力权重。

  • 最后一步,我们要将这些注意力权重与输入的值向量(Value, V)相乘,生成输出向量。

公式

 【LLM|BLOG】挑战极限!一次搞懂Transformer Encoder的所有秘密!

其中:

  • Attention Weights 是通过 Softmax 计算出的归一化权重矩阵。

  • Value Vectors 是输入序列中每个词的值向量。

  • 最终输出是每个词的值向量的加权平均,权重由注意力权重决定。

2. 具体步骤

2.1 值向量(Value Vectors)

值向量 V 是输入序列中每个词的特征表示。它们通常是通过线性变换从输入词嵌入中获得的。值向量 V 在整个自注意力过程中没有被修改,直到我们将注意力权重应用于它。

2.2 注意力权重与值向量相乘

对于每个词语,我们已经通过 Softmax 获得了一个注意力权重矩阵。我们将每个词的注意力权重与序列中的值向量逐元素相乘,生成加权的值向量。然后,所有加权的值向量会相加,得到最终的输出表示。

2.3 加权平均

通过对值向量进行加权平均,模型能够提取出上下文中与当前词最相关的信息。这就是自注意力机制的核心:让每个词都能从其他词中提取到最有用的信息,并整合到自身的表示中。

2.9 Normalization and Residuals(归一化和残差)

Transformer 的编码器和解码器中,每个子层都使用了残差连接层归一化。残差连接通过直接将输入与输出相加,帮助缓解深层网络中的梯度消失问题,而层归一化则确保输出的分布一致性,保证模型的训练稳定性。通过这两个操作,Transformer 模型能够有效地学习复杂的特征并加速训练过程。

 【LLM|BLOG】挑战极限!一次搞懂Transformer Encoder的所有秘密!


正则化(LayerNorm):

正则化的主要作用是将每个层的输出保持在一个统一的尺度上,确保模型在训练过程中不会因为输入数据分布的变化而出现不稳定的情况。Transformer 中使用的是层归一化(Layer Normalization, LayerNorm),它会对每个隐藏状态的特征进行归一化处理。

残差连接:

残差连接的作用是通过跳过某些层,将输入直接添加到输出上,从而防止深层网络中的梯度消失问题。具体来说,在每个子层(如多头自注意力层和前馈网络层)之后,Transformer 会将子层的输出与输入相加。这有助于模型更容易地学习到有用的表示,并让梯度能够更好地反向传播。

残差连接的好处:

  • 缓解梯度消失问题:在深层网络中,梯度消失是一个常见问题。残差连接通过将输入直接添加到输出中,创建了“捷径”,使得梯度可以更容易地反向传播,缓解梯度消失问题。

  • 加速训练:有了残差连接,模型可以更快地学习到合适的特征表示,因为即使深层网络还没有完全学习到好的表示,输入数据也能直接通过残差路径流到后续层。

正则化的好处:

  • 稳定训练过程:层归一化确保每层输出的分布相对稳定,避免了训练过程中因为输入分布的变化而导致模型训练不稳定。

  • 提高泛化能力:归一化处理有助于模型更好地适应新数据,防止过拟合,提高模型的泛化能力

import torch
import torch.nn as nn

# 假设输入张量和经过多头注意力的输出张量
input_tensor = torch.tensor([[0.1, 0.2, 0.3, 0.4],  # 输入
                             [0.5, 0.6, 0.7, 0.8],
                             [0.9, 1.0, 1.1, 1.2]])

attention_output = torch.tensor([[0.05, 0.10, 0.15, 0.20],  # 多头自注意力的输出
                                 [0.25, 0.30, 0.35, 0.40],
                                 [0.45, 0.50, 0.55, 0.60]])

# 残差连接:将输入和自注意力层的输出相加
residual_output = input_tensor + attention_output

# 定义层归一化
layer_norm = nn.LayerNorm(input_tensor.size(-1))

# 进行归一化处理
normalized_output = layer_norm(residual_output)

# 输出结果
print("Input Tensor:\n", input_tensor)
print("Attention Output:\n", attention_output)
print("Residual Connection Output (Input + Attention Output):\n", residual_output)
print("Normalized Output:\n", normalized_output)




Input Tensor:
 tensor([[0.1000, 0.2000, 0.3000, 0.4000],
         [0.5000, 0.6000, 0.7000, 0.8000],
         [0.9000, 1.0000, 1.1000, 1.2000]])

Attention Output:
 tensor([[0.0500, 0.1000, 0.1500, 0.2000],
         [0.2500, 0.3000, 0.3500, 0.4000],
         [0.4500, 0.5000, 0.5500, 0.6000]])

Residual Connection Output (Input + Attention Output):
 tensor([[0.1500, 0.3000, 0.4500, 0.6000],
         [0.7500, 0.9000, 1.0500, 1.2000],
         [1.3500, 1.5000, 1.6500, 1.8000]])

Normalized Output:
 tensor([[-1.1832, -0.5071,  0.1690,  0.8451],
         [-1.1832, -0.5071,  0.1690,  0.8451],
         [-1.1832, -0.5071,  0.1690,  0.8451]])
  • Input Tensor:输入张量是来自上一层的输入(可能是嵌入层或多头自注意力层的输出)。

  • Attention Output:这是经过多头自注意力机制计算出的输出。

  • Residual Connection Output:通过将输入和自注意力层的输出相加,形成残差连接。

  • Normalized Output:通过层归一化处理,将残差连接后的结果进行归一化,确保每个特征的分布一致。

3.0 - (前馈神经网络)

在 Transformer 模型的编码器和解码器中,每一层都包含一个前馈神经网络(FFN),这是多头自注意力层之后的关键组成部分。前馈网络用于对每个位置的输入进行逐步处理,进一步提取和转换特征。

前馈神经网络在 Transformer 模型中起到了对每个位置的特征进行进一步转换和提取的作用。它通过两个线性变换和非线性激活函数增强了模型的表达能力。相比于多头自注意力层,前馈神经网络对每个词进行独立处理,帮助模型在局部上下文中学习复杂特征。

 【LLM|BLOG】挑战极限!一次搞懂Transformer Encoder的所有秘密!

1、前馈神经网络的组成

前馈神经网络通常由两个全连接层(线性层)组成,在它们之间夹着一个非线性激活函数。具体结构如下:

  • 第一层线性变换:输入被映射到一个更高维度的空间,增加特征的复杂度。

  • ReLU 激活函数:引入非线性,使模型能够学习复杂的特征。

  • 第二层线性变换:将高维空间的特征映射回原始维度。

  • 正则化和残差连接:处理后的输出与输入相加,并通过 LayerNorm 进行归一化。

2、前馈网络的工作流程

2.1 两个线性层(Linear Layers)

前馈网络包含两个线性变换:

  • 第一层:将输入的特征维度 d model(例如 512 维)映射到更高维度 dff(通常是 2048 维)。

  • 第二层:将高维度的特征重新映射回 dmodel(512 维)。

2.2 ReLU 激活函数

在两个线性层之间使用了 ReLU 激活函数,这是一种非线性函数,能够引入非线性能力,使模型可以学习到复杂的特征表示。

2.3 残差连接与正则化

每次前馈神经网络的输出都会通过残差连接与输入相加,并通过 LayerNorm 进行归一化。这一过程确保了深层网络的梯度不会消失,并保持模型稳定。

以下是使用 PyTorch 实现 Transformer 中前馈神经网络的代码,包括两层线性层、ReLU 激活函数、残差连接和 LayerNorm 归一化。

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

class FeedForward(nn.Module):
    def __init__(self, d_model, d_ff):
        super(FeedForward, self).__init__()
        # 第一层线性变换,将 d_model 映射到 d_ff
        self.linear1 = nn.Linear(d_model, d_ff)
        # 第二层线性变换,将 d_ff 映射回 d_model
        self.linear2 = nn.Linear(d_ff, d_model)
        # ReLU 激活函数
        self.relu = nn.ReLU()
        # 层归一化
        self.norm = nn.LayerNorm(d_model)

    def forward(self, x):
        # 残差连接:保存输入
        residual = x
        # 第一层线性变换 + ReLU
        x = self.relu(self.linear1(x))
        # 第二层线性变换
        x = self.linear2(x)
        # 残差连接 + 层归一化
        x = self.norm(x + residual)
        return x

# 参数设置:假设 d_model = 512, d_ff = 2048
d_model = 512
d_ff = 2048
ffn = FeedForward(d_model, d_ff)

# 模拟输入张量,形状为 (batch_size, seq_len, d_model)
input_tensor = torch.randn(32, 10, 512)  # 批次大小为 32,序列长度为 10,特征维度为 512

# 前馈神经网络前向传播
output = ffn(input_tensor)

print("输入张量形状:", input_tensor.shape)
print("输出张量形状:", output.shape)




输入张量形状: torch.Size([32, 10, 512])
输出张量形状: torch.Size([32, 10, 512])
  • 第一层线性层self.linear1 将输入的特征维度从 dmodel(例如 512)映射到一个较高的维度 dff(例如 2048)。

  • ReLU 激活函数self.relu 在两个线性层之间引入非线性能力。

  • 第二层线性层self.linear2 将高维空间的特征重新映射回 dmodeld_{model}dmodel。

  • 残差连接与归一化self.norm 进行层归一化,并将输入加到输出上,以防止梯度消失问题。

3.1 Output

在 Transformer 模型中,经过编码器的输入序列将生成一组上下文丰富的向量,这些向量捕获了输入序列的深层次语义信息。接下来,这些向量将被传递给解码器,解码器将利用这些上下文信息生成最终的输出(如翻译任务中的目标语言序列)

 【LLM|BLOG】挑战极限!一次搞懂Transformer Encoder的所有秘密!

在西班牙语翻译中当输入句子 "De quién es" 被输入到 Transformer 模型的编码器时,经过编码器的各个层后,输出将是一组上下文向量,它们是每个词的深层表示。这些上下文向量不仅包含每个词的语义信息,还捕捉了它们在整个句子中的关系。

1、编码器的处理过程:

假设句子 "De quién es" 经过以下几个步骤:

1.1 词嵌入(Word Embedding)

首先,输入的每个单词 "De""quién", 和 "es" 将被转换为对应的词嵌入向量。这些向量是高维度的(例如 512 维),每个向量代表该单词的基本语义特征。

例如:

"De"     -> [0.1, 0.05, ..., 0.2] (512 维向量)
"quién"  -> [0.4, 0.3, ..., 0.7] (512 维向量)
"es"     -> [0.2, 0.6, ..., 0.5] (512 维向量)

1.2 位置编码(Positional Encoding)

由于 Transformer 模型没有内置的顺序信息,所以会将位置编码与词嵌入相加,为模型提供序列中的位置信息。

1.3 多头自注意力机制(Multi-Head Self-Attention)

每个词的嵌入会经过多头自注意力机制。该机制会计算每个词与其他词之间的相似度(即查询、键、值的点积),从而捕捉到词与词之间的依赖关系。

例如,经过注意力机制后,词 "De" 可能会与 "quién" 和 "es" 之间的依赖关系被捕捉到。

1.4 前馈神经网络(Feed-Forward Network)

每个词的表示会经过一个前馈神经网络,进一步提取特征并转换为更高维的语义表示。

2、 编码器的输出

编码器的最终输出是一组高维度的上下文向量,每个向量对应输入句子中的每个单词。这些向量不仅包含每个单词的语义信息,还捕捉了与其他单词的关系。

假设编码器的输出维度为 512,那么输出的张量形状为 (序列长度, d_model),即:

[
 [0.1, 0.05, ..., 0.2],   # "De" 的上下文向量
 [0.4, 0.3, ..., 0.7],    # "quién" 的上下文向量
 [0.2, 0.6, ..., 0.5]     # "es" 的上下文向量
]
  • 每个词("De"、"quién"、"es")对应的上下文向量是经过多头注意力机制和前馈网络后的结果,它们不仅包含该词的语义信息,还整合了句子中其他词的信息。例如,"De" 的向量会结合了它与 "quién" 和 "es" 的关系。

  • 这些上下文向量被传递给解码器,用于在翻译任务中生成目标语言序列。

3、 上下文向量的作用

在翻译任务中,编码器的输出(上下文向量)将为解码器提供对输入句子深刻理解的依据。解码器会根据这些向量生成目标句子(如目标语言的翻译)。

例如,假设解码器要将 "De quién es" 翻译为 "Whose is it?"

  • 解码器会首先生成 "Whose",然后结合编码器的输出上下文向量,生成与 "quién" 和 "es" 相关的翻译,逐步生成完整的句子。




推荐本站淘宝优惠价购买喜欢的宝贝:

 【LLM|BLOG】挑战极限!一次搞懂Transformer Encoder的所有秘密!

本文链接:https://hqyman.cn/post/9556.html 非本站原创文章欢迎转载,原创文章需保留本站地址!

分享到:
打赏





休息一下~~


« 上一篇 下一篇 »

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

请先 登录 再评论,若不是会员请先 注册

您的IP地址是: