目录
- 一、前期准备工作
- 学习如何读AI论文
- 读Transformer原始论文
- 用Pytorch从头实现Transformer
- 二、我的完整代码实现
- 1.导入库
- 2.基本组件
- 创建词嵌入
- 位置嵌入
- 自注意力
- 3.编码器
- 4.解码器
- 5.完整架构
- 6.简单测试一下代码
- 创建模型和准备简单的训练数据
- 训练一次(前向传播)
- 推理
- 三、bug修复
- 位置编码部分,发现参考代码的两处bug
- 四、背景知识补充
- 残差连接(Residual Connection)
- 层归一化(Layer Normalization)
- 1. **归一化的对象**
- 2. **适用场景**
- 总结
- 图解
- 自回归(Autoregression,AR)
- 注意力机制(Attention Mechanism)
- Self-attention(自注意力机制)
- Self-attention的基本步骤
- Self-attention的基本步骤图
- Self-attention 的优势
- Self-attention在Transformer中的应用
- 点积(Dot-Product)
- 全连接层(Fully Connected Layer,简称 FC 层)
- 多头注意力(Multi-Head Attention)
- 多头注意力的基本步骤
- 图解
- 多头注意力的优势
- 多头注意力的公式总结
- 多头注意力的实际应用
- Position-wise Feed-Forward Networks(位置前馈网络,简称 FFN)
- 为什么 Transformer 需要 Position-wise Feed-Forward Networks?
- Position-wise Feed-Forward Networks 的工作原理
- L2范数(L2 norm)
- L2范数的作用和应用
- L2范数与其他范数的区别
- 总结
- Positional Encoding(位置编码)
- 为什么需要 Positional Encoding?
- Positional Encoding 的原理
- 解释
- 加法操作
- Positional Encoding 的性质
- 总结
一、前期准备工作
学习如何读AI论文
看了B站李沐老师的下面几个视频
如何读论文【论文精读·1】
9年后重读深度学习奠基作之一:AlexNet【论文精读·2】
AlexNet论文逐段精读【论文精读】
读Transformer原始论文
我是看B站李沐老师的Transformer论文精读视频,结合原作论文和chatgpt来学习的。chatgpt用于查一些背景知识。
原作论文作为参考,自己读当然OK,但是如果有大神帮你解读,对于小白来说,站在巨人的肩膀上效率可能更高,比如大神会提到经典论文中哪些部分是现在还常用的,哪些部分已经过时或者不重要。
李沐老师的视频:Transformer论文逐段精读【论文精读】
原作论文:Attention Is All You Need
用Pytorch从头实现Transformer
去kaggle或者github上找一份不错的范例代码来参考学习
我参考的代码kaggle地址:Transformer from scratch using pytorch
二、我的完整代码实现
1.kaggle上有我的完整代码,这里贴一份是为了方便那些不会科学上网打不开kaggle的读者。
2.代码有不懂的地方,可以看下背景知识补充部分
我的kaggle代码地址
先看一下论文中完整的transformer架构图:
1.导入库
import torch.nn as nn
import torch
import torch.nn.functional as F
import math,copy,re
import warnings
import pandas as pd
import numpy as np
import seaborn as sns
import torchtext
import matplotlib.pyplot as plt
warnings.simplefilter("ignore")
print(torch.__version__)
2.基本组件
创建词嵌入
- 这个
Embedding
类用于将词汇索引转换为相应的嵌入向量,以便输入到神经网络中。 nn.Embedding
层通过查表的方式,将每个词汇的索引映射到一个固定维度的嵌入向量上。
class Embedding(nn.Module):
def __init__(self, vocab_size, embed_dim):
"""
Args:
vocab_size: 词汇表大小
embed_dim: 嵌入维度
"""
super(Embedding, self).__init__()
self.embed = nn.Embedding(vocab_size, embed_dim)
def forward(self, x):
"""
Args:
x: 输入向量,通常是一个包含词汇索引的张量(整数表示词汇在词汇表中的位置)
out: 嵌入向量
"""
out = self.embed(x)
return out
位置嵌入
这个 PositionalEmbedding 模块通过对每个位置进行正弦和余弦编码,为每个输入序列的嵌入向量提供了位置信息,使模型能够感知序列中词汇的顺序。
class PositionalEmbedding(nn.Module):
def __init__(self,max_seq_len,embed_model_dim):
"""
Args:
max_seq_len: 输入序列的最大长度
embed_model_dim: 嵌入的维度
"""
super(PositionalEmbedding, self).__init__()
self.embed_dim = embed_model_dim
# 生成位置嵌入矩阵
# 为每个位置计算对应的正弦和余弦编码
pe = torch.zeros(max_seq_len,self.embed_dim)
for pos in range(max_seq_len):
for i in range(0,self.embed_dim,2):
# i = 0,2,4,6 ...
pe[pos, i] = np.sin(pos/(10000**(i/self.embed_dim)))
if i+1 < self.embed_dim:
pe[pos, i+1] = np.cos(pos/(10000**(i/self.embed_dim)))
# 扩展维度以适应批量输入
# 使用unsqueeze(0)在第一个维度上增加一个维度,使pe的形状变为(1, max_seq_len, embed_dim)。
pe = pe.unsqueeze(0)
# 将 pe 注册为 buffer,防止它在训练过程中被更新
# 缓冲区的作用:在模型保存和加载时,缓冲区会一同保存,但不会作为模型参数参与训练。
self.register_buffer('pe', pe)
def forward(self, x):
"""
Args:
x: 输入的词嵌入张量,形状通常为(batch_size, seq_len, embed_dim)
Returns:
x: 输出
"""
# 使输入嵌入向量的值相对较大
x = x * math.sqrt(self.embed_dim)
# 获取输入序列的长度
seq_len = x.size(1)
# 提取对应长度的位置嵌入,将位置嵌入与词嵌入相加,融合位置信息
x = x + torch.autograd.Variable(self.pe[:, :seq_len, :], requires_grad=False)
return x
自注意力
这个 MultiHeadAttention
类实现了多头注意力机制(Multi-Head Attention),是 Transformer 中的一个重要组件。多头注意力机制允许模型通过多个注意力头(heads)从不同的子空间中关注不同的部分。
论文中多头注意力机制的图
class MultiHeadAttention(nn.Module):
def __init__(self, embed_dim=512, n_heads=8):
"""
Args:
embed_dim: 嵌入向量的维度
n_heads: 注意力头的数量
"""
super(MultiHeadAttention, self).__init__()
self.embed_dim = embed_dim # 512维嵌入向量
self.n_heads = n_heads # 8个注意力头
self.single_head_dim = int(self.embed_dim / self.n_heads) # 每个注意力头的维度 = 512 / 8 = 64
# 定义线性变换矩阵,用于生成 Query, Key 和 Value 矩阵
self.query_matrix = nn.Linear(self.single_head_dim, self.single_head_dim, bias=False)
self.key_matrix = nn.Linear(self.single_head_dim, self.single_head_dim, bias=False)
self.value_matrix = nn.Linear(self.single_head_dim, self.single_head_dim, bias=False)
# 输出线性层,作用是将多头注意力的输出拼接并变换回原始嵌入维度
self.out = nn.Linear(self.n_heads * self.single_head_dim, self.embed_dim)
def forward(self, key, query, value, mask=None):
"""
Args:
key: Key 向量
query: Query 向量
value: Value 向量
mask: 用于屏蔽不需要计算注意力的部分
Returns:
output: 多头注意力机制的输出
"""
batch_size = key.size(0)
seq_length = key.size(1)
seq_length_query = query.size(1)
# 重塑 key, query 和 value 的维度,使其适应多头注意力的格式
# key、query、value 的输入维度为 (batch_size, seq_length, embed_dim),例如 (32, 10, 512) 表示 32 个序列,每个序列长度为 10,每个词的嵌入维度为 512。
# 使用view()而不是reshape()的理由:view() 不会创建新张量,只是修改现有张量的元数据(假设内存布局是连续的),因此速度快,内存开销低。
key = key.view(batch_size, seq_length, self.n_heads, self.single_head_dim) # (32, 10, 8, 64)
query = query.view(batch_size, seq_length_query, self.n_heads, self.single_head_dim)
value = value.view(batch_size, seq_length, self.n_heads, self.single_head_dim)
# 线性变换得到 Q, K, V 矩阵
k = self.key_matrix(key) # (32, 10, 8, 64)
q = self.query_matrix(query) # (32, 10, 8, 64)
v = self.value_matrix(value) # (32, 10, 8, 64)
# 交换维度,适应矩阵乘法的形状
q = q.transpose(1, 2) # (32, 8, 10, 64)
k = k.transpose(1, 2) # (32, 8, 10, 64)
v = v.transpose(1, 2) # (32, 8, 10, 64)
# 计算注意力分数
k_adjusted = k.transpose(-1, -2) # 调整 Key 矩阵以便于与 Query 相乘 (32, 8, 64, 10)
product = torch.matmul(q, k_adjusted) # 矩阵乘法得到注意力分数 (32, 8, 10, 10)
# 如果提供了 mask,则屏蔽某些位置的注意力分数
if mask is not None:
product = product.masked_fill(mask == 0, float("-1e20"))
# 对 Key 的维度进行缩放,缓解梯度消失问题
product = product / math.sqrt(self.single_head_dim) # 缩放因子 √64
# 计算注意力权重
scores = F.softmax(product, dim=-1) # (32, 8, 10, 10)
# 将注意力权重与 Value 相乘,得到加权后的输出
scores = torch.matmul(scores, v) # (32, 8, 10, 64)
# 将多头的输出拼接起来
concat = scores.transpose(1, 2).contiguous().view(batch_size, seq_length_query, self.single_head_dim * self.n_heads) # (32, 10, 512)
# 通过线性层输出
output = self.out(concat) # (32, 10, 512)
return output
3.编码器
论文中编码器的图
TransformerBlock
是 Transformer 编码器的基本单元,它由多头注意力机制、残差连接、归一化层、前馈神经网络和 dropout 组成。
class TransformerBlock(nn.Module):
def __init__(self, embed_dim, expansion_factor=4, n_heads=8):
super(TransformerBlock, self).__init__()
"""
Args:
embed_dim: 嵌入向量的维度
expansion_factor: 前馈网络的扩展因子,用于扩展中间层的维度
n_heads: 注意力头的数量
"""
self.attention = MultiHeadAttention(embed_dim, n_heads) # 多头注意力机制
self.norm1 = nn.LayerNorm(embed_dim) # 第一个归一化层(LayerNorm)
self.norm2 = nn.LayerNorm(embed_dim) # 第二个归一化层
self.feed_forward = nn.Sequential( # 前馈神经网络部分
nn.Linear(embed_dim, expansion_factor * embed_dim), # 线性层,维度从 embed_dim 扩展到 expansion_factor * embed_dim
nn.ReLU(), # 激活函数,引入非线性
nn.Linear(expansion_factor * embed_dim, embed_dim) # 再次将维度缩小到原始的 embed_dim
)
self.dropout1 = nn.Dropout(0.2) # 第一个 dropout,用于防止过拟合
self.dropout2 = nn.Dropout(0.2) # 第二个 dropout
def forward(self, key, query, value):
"""
Args:
key: Key 向量
query: Query 向量
value: Value 向量
Returns:
norm2_out: 经过 Transformer Block 的输出
"""
attention_out = self.attention(key, query, value) # 计算多头注意力输出
attention_residual_out = attention_out + value # 残差连接,添加 Value 向量
norm1_out = self.dropout1(self.norm1(attention_residual_out)) # 归一化 + dropout
feed_fwd_out = self.feed_forward(norm1_out) # 前馈网络计算
feed_fwd_residual_out = feed_fwd_out + norm1_out # 残差连接
norm2_out = self.dropout2(self.norm2(feed_fwd_residual_out)) # 归一化 + dropout
return norm2_out
TransformerEncoder
类由多个 TransformerBlock
组成,它负责对输入序列进行编码。输入经过词嵌入层和位置编码层后,进入多个 TransformerBlock
层进行处理。
class TransformerEncoder(nn.Module):
"""
Transformer 编码器
Args:
seq_len : 输入序列的长度
vocab_size: 词汇表大小
embed_dim: 嵌入向量的维度
num_layers: 编码器的层数
expansion_factor: 前馈网络中的扩展因子
n_heads: 多头注意力机制中的头数
Returns:
out: 编码器的输出
"""
def __init__(self, seq_len, vocab_size, embed_dim, num_layers=2, expansion_factor=4, n_heads=8):
super(TransformerEncoder, self).__init__()
self.embedding_layer = Embedding(vocab_size, embed_dim) # 词嵌入层
self.positional_encoder = PositionalEmbedding(seq_len, embed_dim) # 位置编码
# 多层 Transformer Block
self.layers = nn.ModuleList([TransformerBlock(embed_dim, expansion_factor, n_heads) for i in range(num_layers)])
def forward(self, x):
embed_out = self.embedding_layer(x) # 词嵌入
out = self.positional_encoder(embed_out) # 加上位置编码
for layer in self.layers:
out = layer(out, out, out) # 通过多个 TransformerBlock
return out # 输出维度为 (batch_size, seq_len, embed_dim)
4.解码器
论文中解码器的图
这个 DecoderBlock
类是 Transformer 解码器(Decoder)中的一个基本模块,主要用于处理输入的解码序列,同时结合编码器(Encoder)输出的信息。它包含了多头自注意力机制、前馈神经网络和残差连接等部分。
class DecoderBlock(nn.Module):
def __init__(self, embed_dim, expansion_factor=4, n_heads=8):
super(DecoderBlock, self).__init__()
"""
Args:
embed_dim: 嵌入向量的维度
expansion_factor: 前馈网络中的扩展因子
n_heads: 多头注意力头的数量
"""
self.attention = MultiHeadAttention(embed_dim, n_heads=n_heads) # 解码器中的自注意力机制
self.norm = nn.LayerNorm(embed_dim) # 归一化层,用于稳定网络训练
self.dropout = nn.Dropout(0.2) # dropout,用于防止过拟合
self.transformer_block = TransformerBlock(embed_dim, expansion_factor, n_heads) # 解码器中的 transformer 模块
def forward(self, key, query, x, mask):
"""
Args:
key: Key 向量,通常来自编码器的输出
query: Query 向量,来自解码序列
x: 输入解码序列
mask: 遮掩掩码,用于屏蔽未来信息
Returns:
out: 解码器块的输出
"""
# 对解码器输入进行自注意力计算,传入 mask 防止查看未来信息
attention = self.attention(x, x, x, mask=mask) # 输出大小为 (batch_size, seq_len, embed_dim)
# 残差连接,并通过归一化和 dropout
value = self.dropout(self.norm(attention + x))
# 将处理后的 `value` 传入 TransformerBlock,与来自编码器的 `key` 和 `query` 进行进一步处理
out = self.transformer_block(key, query, value)
return out
这个 TransformerDecoder
类是 Transformer 解码器的完整实现,它由词嵌入层、位置编码层、多个解码块(DecoderBlock
),以及最终的全连接层组成。解码器的主要作用是生成输出序列,例如机器翻译中的目标语言序列。
class TransformerDecoder(nn.Module):
# 初始化方法定义了解码器的主要组件,包括嵌入层、位置编码层、多个 DecoderBlock 层和一个全连接层。
def __init__(self, target_vocab_size, embed_dim, seq_len, num_layers=2, expansion_factor=4, n_heads=8):
super(TransformerDecoder, self).__init__()
"""
Args:
target_vocab_size: 目标词汇表的大小
embed_dim: 嵌入向量的维度
seq_len : 输入序列的长度
num_layers: 解码器层的数量
expansion_factor: 前馈网络中的扩展因子
n_heads: 多头注意力头的数量
"""
# 词嵌入层
self.word_embedding = nn.Embedding(target_vocab_size, embed_dim)
# 位置编码层
self.position_embedding = PositionalEmbedding(seq_len, embed_dim)
# 多个解码层(DecoderBlock)
self.layers = nn.ModuleList(
[
DecoderBlock(embed_dim, expansion_factor=expansion_factor, n_heads=n_heads)
for _ in range(num_layers)
]
)
# 输出层,全连接层将嵌入维度转换为词汇表大小
self.fc_out = nn.Linear(embed_dim, target_vocab_size)
# dropout 用于防止过拟合
self.dropout = nn.Dropout(0.2)
# 前向传播过程中,输入序列首先经过嵌入层和位置编码层,然后通过多个 DecoderBlock 进行处理,最后通过全连接层输出每个位置上词汇的概率分布。
def forward(self, x, enc_out, mask):
"""
Args:
x: 来自目标序列的输入向量(目标词汇的嵌入)
enc_out: 来自编码器的输出
mask: 自注意力机制的掩码,用于防止模型看到未来信息
Returns:
out: 解码器的输出向量
"""
# 词嵌入 + 位置编码
x = self.word_embedding(x) # 词嵌入 (batch_size, seq_len, embed_dim) -> (32, 10, 512)
x = self.position_embedding(x) # 位置编码 (32, 10, 512)
x = self.dropout(x) # 加入 dropout,防止过拟合
# 多层解码器块
for layer in self.layers:
x = layer(enc_out, x, enc_out, mask) # 在每层解码器中,将编码器的输出 `enc_out` 和解码器的 `x` 进行结合
# 全连接层,生成词汇表大小的输出概率分布
out = F.softmax(self.fc_out(x), dim=-1) # (batch_size, seq_len, target_vocab_size)
return out
5.完整架构
最后,我们将组装所有子模块并创建整个 Transformer 架构。
这个 Transformer
类实现了一个完整的 Transformer 模型,它由编码器(TransformerEncoder
)和解码器(TransformerDecoder
)组成,用于处理序列到序列的任务,比如机器翻译。模型的前向传播和推理过程都得到了实现。
class Transformer(nn.Module):
# 在初始化方法中,定义了编码器、解码器以及它们的超参数,如嵌入维度、词汇表大小、序列长度等。
def __init__(self, embed_dim, src_vocab_size, target_vocab_size, seq_length, num_layers=2, expansion_factor=4, n_heads=8):
super(Transformer, self).__init__()
"""
Args:
embed_dim: 嵌入向量的维度
src_vocab_size: 源语言的词汇表大小
target_vocab_size: 目标语言的词汇表大小
seq_length: 输入序列的长度
num_layers: 编码器和解码器的层数
expansion_factor: 前馈网络中的扩展因子
n_heads: 多头注意力头的数量
"""
self.target_vocab_size = target_vocab_size
# 初始化 Transformer 编码器
self.encoder = TransformerEncoder(
seq_length, src_vocab_size, embed_dim, num_layers=num_layers, expansion_factor=expansion_factor, n_heads=n_heads
)
# 初始化 Transformer 解码器
self.decoder = TransformerDecoder(
target_vocab_size, embed_dim, seq_length, num_layers=num_layers, expansion_factor=expansion_factor, n_heads=n_heads
)
# 这是一个用于生成目标序列掩码(`mask`)的函数。解码器在生成序列时不能看到未来的信息,因此需要掩码来屏蔽未来的词。
def make_trg_mask(self, trg):
"""
Args:
trg: 目标序列
Returns:
trg_mask: 目标序列的掩码
"""
batch_size, trg_len = trg.shape
# 返回一个下三角矩阵,其中上三角部分被屏蔽
trg_mask = torch.tril(torch.ones((trg_len, trg_len))).expand(
batch_size, 1, trg_len, trg_len
)
return trg_mask
# `decode` 函数用于推理过程中的解码,它逐步生成序列中的下一个词
def decode(self, src, trg):
"""
推理过程中逐步生成序列
Args:
src: 编码器的输入
trg: 解码器的输入
Returns:
out_labels: 生成的序列标签
"""
trg_mask = self.make_trg_mask(trg) # 生成解码器掩码
enc_out = self.encoder(src) # 通过编码器处理源序列
out_labels = [] # 保存生成的输出序列
batch_size, seq_len = src.shape[0], src.shape[1]
out = trg # 目标序列的初始输入
for i in range(seq_len):
# 逐步通过解码器
out = self.decoder(out, enc_out, trg_mask) # (batch_size, seq_len, vocab_size)
# 取出最后一个时间步的输出
out = out[:, -1, :] # (batch_size, vocab_size)
# 获取输出的最大概率词汇索引
out = out.argmax(-1) # (batch_size,)
# 保存当前步的预测结果
out_labels.append(out.item())
# 将预测出的词添加到输入序列中,继续下一个时间步的预测
out = torch.unsqueeze(out, axis=0)
return out_labels # 返回生成的序列
# `forward` 函数用于训练阶段,处理完整的源序列和目标序列。
def forward(self, src, trg):
"""
Args:
src: 编码器的输入
trg: 解码器的输入
Returns:
outputs: 解码器的输出,包含每个词汇的概率分布
"""
trg_mask = self.make_trg_mask(trg) # 生成目标序列掩码
enc_out = self.encoder(src) # 通过编码器处理源序列
outputs = self.decoder(trg, enc_out, trg_mask) # 通过解码器处理目标序列
return outputs # 返回目标词汇的概率分布
6.简单测试一下代码
创建模型和准备简单的训练数据
创建了一个 Transformer
模型,并准备了源序列 (src
) 和目标序列 (target
) 作为输入
# 输入的参数
src_vocab_size = 11
target_vocab_size = 11
num_layers = 6
seq_length= 12
# 训练数据
# 模拟的源序列和目标序列,包含sos和eos tokens
# `src` 和 `target` 序列中都包括特殊的开始符(`sos`,用 0 表示)和结束符(`eos`,用 1 表示)。
src = torch.tensor([[0, 2, 5, 6, 4, 3, 9, 5, 2, 9, 10, 1],
[0, 2, 8, 7, 3, 4, 5, 6, 7, 2, 10, 1]])
target = torch.tensor([[0, 1, 7, 4, 3, 5, 9, 2, 8, 10, 9, 1],
[0, 1, 5, 6, 2, 4, 7, 6, 2, 8, 10, 1]])
# 打印输入的形状
print(src.shape, target.shape)
# 创建Transformer模型
model = Transformer(embed_dim=512,
src_vocab_size=src_vocab_size,
target_vocab_size=target_vocab_size,
seq_length=seq_length,
num_layers=num_layers,
expansion_factor=4,
n_heads=8)
# 打印模型以确认其结构
print(model)
训练一次(前向传播)
通过源序列 src
和目标序列 target
进行一次前向传播的训练。
这个 out
是解码器对每个时间步的词汇预测。它的形状通常是 (batch_size, seq_len, target_vocab_size)
。其中每个时间步都会生成一个词汇表大小的概率分布。
out = model(src, target)
print(out.shape)
print(out)
输出为
torch.Size([2, 12, 11])
out是一个3维的概率分布,比较长就不贴了,就像下面这样:
tensor([[[0.0306, 0.0768, 0.1866, 0.0364, 0.0384, 0.0591, 0.0850, 0.1164,0.1425, 0.1635, 0.0647],…]]]
推理
在推理过程中,模型会通过编码器处理 src
,然后在每个时间步上通过解码器逐步生成下一个词。out
是模型生成的目标序列,包含生成的词汇索引。
# 创建模型
model = Transformer(embed_dim=512, src_vocab_size=src_vocab_size,
target_vocab_size=target_vocab_size, seq_length=seq_length,
num_layers=num_layers, expansion_factor=4, n_heads=8)
# 源序列 src (1 个句子,长度为 12)
src = torch.tensor([[0, 2, 5, 6, 4, 3, 9, 5, 2, 9, 10, 1]])
# 目标序列的初始部分 trg,初始为 <sos> token (0)
trg = torch.tensor([[0]])
# 打印输入的形状
print("Source shape:", src.shape) # (1, 12)
print("Target shape (initial):", trg.shape) # (1, 1)
# 调用模型的推理函数进行解码
out = model.decode(src, trg)
# 打印输出结果
print("Generated output:", out)
结果如下:
Source shape: torch.Size([1, 12])
Target shape (initial): torch.Size([1, 1])
Generated output: [9, 1, 1, 1, 1, 1, 1, 1, 1, 10, 1, 1]
三、bug修复
位置编码部分,发现参考代码的两处bug
原代码:
pe[pos, i] = math.sin(pos / (10000 ** ((2 * i)/self.embed_dim)))
pe[pos, i + 1] = math.cos(pos / (10000 ** ((2 * (i + 1))/self.embed_dim)))
参考论文的数学公式应该修改如下:
pe[pos, i] = np.sin(pos/(10000**(i/self.embed_dim)))
if i+1 < self.embed_dim:
pe[pos, i+1] = np.cos(pos/(10000**(i/self.embed_dim)))
原代码:
self.pe[:,:seq_len]
因为 self.pe
的维度是 (1, max_seq_len, embed_dim)
,那么 self.pe[:, :seq_len]
是错误的,因为它忽略了第三个维度(嵌入维度)。应该修改如下:
self.pe[:, :seq_len, :]
四、背景知识补充
残差连接(Residual Connection)
残差连接(Residual Connection) 是一种神经网络架构设计,它允许输入信息直接跳过某些层,并与这些层的输出相加。具体公式为:
y
=
F
(
x
)
+
x
y = F(x) + x
y=F(x)+x
其中,
x
x
x 是输入,
F
(
x
)
F(x)
F(x) 是经过若干层的非线性变换后的输出,
y
y
y 是最终的输出。
残差连接的引入是为了缓解深层神经网络中的梯度消失和梯度爆炸问题,确保信息能够在深层网络中顺利传播,使得模型在增加层数时仍然能够有效训练,避免性能下降。
图解:
图片来源:https://en.wikipedia.org/wiki/Residual_neural_network
层归一化(Layer Normalization)
批量归一化(Batch Normalization)和层归一化(Layer Normalization)都是加速神经网络训练并提高模型稳定性的正则化技术,但它们在操作方式和适用场景上有所不同。以下是它们之间的主要区别:
1. 归一化的对象
-
批量归一化(Batch Normalization, BN):
- 归一化的对象是同一神经元在不同样本上的激活值。
- 具体来说,BN 在整个批次(batch)的样本中,对每个神经元的输出值进行归一化。
- 例如,如果批次大小是 32,那么对某个特定神经元来说,它的32个输出会被一起归一化。
-
层归一化(Layer Normalization, LN):
- 归一化的对象是同一个样本在同一层的所有神经元的激活值。
- LN 对每个样本在该层所有神经元的输出进行归一化,适用于小批次或者单样本的情况,不依赖批次大小。
2. 适用场景
-
批量归一化:
- 通常用于卷积神经网络(CNN)和全连接层(Dense Layer)中,在图像分类等任务中表现良好。
- 需要较大的批次(batch size),以确保对每一层的归一化效果足够好。
- 在训练和推理阶段有区别:训练时计算当前批次的均值和方差;推理时使用在训练过程中累积的均值和方差。
-
层归一化:
- 更适合于序列模型(如RNN、LSTM、Transformer),特别是在自然语言处理(NLP)任务中,因为这类模型处理的是时间序列数据,批次可能较小,甚至可能批次大小为1。
- 不依赖于批次大小,在训练和推理阶段操作相同,这对批次大小变化敏感的任务非常有用。
总结
归一化方式 | 归一化对象 | 适用场景 | 依赖批次大小 | 训练与推理的差异 |
---|---|---|---|---|
批量归一化 | 同一神经元在不同样本上的激活值 | 卷积神经网络、全连接层,适合大批次 | 是 | 有 |
层归一化 | 同一层的所有神经元的激活值 | RNN、LSTM、Transformer等序列模型,适合小批次 | 否 | 无 |
图解
图片来源:https://medium.com/@bhavtoshrath.umn/batch-normalization-vs-layer-normalization-c76bb3cbf388
自回归(Autoregression,AR)
自回归(Autoregression,AR) 是一种时间序列模型,用于通过历史数据预测未来值。自回归模型假设当前的时间序列值是过去一段时间内观测值的线性组合。它常用于预测金融市场、经济指标、气象数据等具有时间依赖性的序列。
下图是用自回归来预测时间序列(飞机乘客数)
图片来源:https://towardsdatascience.com/how-to-forecast-time-series-using-autoregression-1d45db71683
注意力机制(Attention Mechanism)
注意力机制最早在机器翻译任务中被引入,它的核心思想是:在处理一个输入序列(例如一句话)时,不是对每个输入都平等地看待,而是根据任务的需要,动态地决定哪些部分更重要。
具体来说,假设我们在做机器翻译任务,翻译一段英文句子到中文。传统模型会把整个句子编码成一个固定的向量,然后生成翻译的句子。但对于长句子,固定的向量可能无法有效保留所有信息,导致翻译质量下降。
注意力机制则不一样。它允许模型在生成每个输出词时,根据当前的输出词需求,有选择性地“关注”输入句子的不同部分。比如,当翻译一个句子中的主语时,模型会自动关注与主语相关的词,而忽略其他无关的词。
这种机制可以通过一个权重矩阵实现,权重决定了输入的哪些部分被更多地“关注”。权重由模型学习得来,表示模型认为输入序列中哪些词对于当前输出最重要。
简而言之,注意力机制让模型在处理信息时,像人一样,有能力灵活地聚焦在关键信息上,而不是被全部信息平均分散精力,从而提高模型的表现。
示例图:
图片来源:https://www.scaler.com/topics/deep-learning/attention-mechanism-deep-learning/
Self-attention(自注意力机制)
Self-attention(自注意力机制)是一种深度学习模型中用于计算输入序列内部各元素之间依赖关系的机制。它允许模型在处理序列数据(如句子)时,动态关注序列中各个元素(如词)与其他元素的关系。这种机制在模型处理长距离依赖或捕捉全局信息时非常有效,尤其在Transformer模型中至关重要。
Self-attention的基本步骤
假设我们有一个长度为 n n n 的输入序列,每个元素(如词)表示为一个向量。
- 输入序列表示:每个输入向量
x
i
x_i
xi 会使用三个不同的权重矩阵
W
Q
,
W
K
,
W
V
W^Q, W^K, W^V
WQ,WK,WV 将其分别映射成三个新的向量,分别是:
- Query 向量: q i = W Q x i q_i = W^Q x_i qi=WQxi
- Key 向量: k i = W K x i k_i = W^K x_i ki=WKxi
- Value 向量: v i = W V x i v_i = W^V x_i vi=WVxi
其中 W Q , W K , W V W^Q, W^K, W^V WQ,WK,WV 都是可训练学习的参数矩阵。
- 计算相似性(权重):对于序列中的每个元素,计算它的 Query 向量与其他所有元素的 Key 向量的点积,衡量它们的相似性。这就是注意力分数(Attention Score)。具体计算如下:
Attention Score i j = q i ⋅ k j \text{Attention Score}_{ij} = q_i \cdot k_j Attention Scoreij=qi⋅kj
- 归一化:将所有的注意力分数通过Softmax函数归一化,得到该元素与其他元素的注意力权重。这样保证所有权重的和为1,方便后续计算:
Attention Weight i j = exp ( Attention Score i j ) ∑ j exp ( Attention Score i j ) \text{Attention Weight}_{ij} = \frac{\exp(\text{Attention Score}_{ij})}{\sum_{j} \exp(\text{Attention Score}_{ij})} Attention Weightij=∑jexp(Attention Scoreij)exp(Attention Scoreij)
- 加权求和:用每个元素的注意力权重去加权相应的 Value 向量,得到最终的输出表示。即对于每个输入 x i x_i xi,它的输出是所有 v j v_j vj 向量的加权和:
Output i = ∑ j Attention Weight i j ⋅ v j \text{Output}_i = \sum_{j} \text{Attention Weight}_{ij} \cdot v_j Outputi=j∑Attention Weightij⋅vj
Self-attention的基本步骤图
图片来源:https://sebastianraschka.com/blog/2023/self-attention-from-scratch.html
Self-attention 的优势
- 捕捉长距离依赖:相比于卷积层或循环神经网络,Self-attention 可以直接建模序列中任意位置元素的关系,不管它们距离多远。
- 并行计算:由于Self-attention 不依赖序列的顺序,因此可以并行计算,从而提升计算效率。
- 全局感知:每个元素在生成输出时,能够参考整个输入序列的信息,而不仅仅是局部信息。
Self-attention在Transformer中的应用
Transformer模型使用Self-attention机制来处理输入序列(如句子),特别是在编码器和解码器中广泛使用。通过多头注意力机制(Multi-head Attention),模型可以从多个角度关注不同的特征,使模型更加灵活和强大。
总的来说,Self-attention 为序列中的每个元素提供了灵活且全局的上下文信息,是NLP任务(如机器翻译、文本生成等)中非常重要的工具。
点积(Dot-Product)
Dot-Product(点积)是线性代数中的一个基本运算,主要用于向量之间的操作。点积的结果是一个标量值,它反映了两个向量之间的相似度或关联性。
对于两个维度相同的向量 a = [ a 1 , a 2 , … , a n ] \mathbf{a} = [a_1, a_2, \dots, a_n] a=[a1,a2,…,an] 和 b = [ b 1 , b 2 , … , b n ] \mathbf{b} = [b_1, b_2, \dots, b_n] b=[b1,b2,…,bn],它们的点积可以表示为:
a ⋅ b = a 1 b 1 + a 2 b 2 + ⋯ + a n b n = ∑ i = 1 n a i b i \mathbf{a} \cdot \mathbf{b} = a_1 b_1 + a_2 b_2 + \dots + a_n b_n = \sum_{i=1}^{n} a_i b_i a⋅b=a1b1+a2b2+⋯+anbn=i=1∑naibi
通俗解释:点积就是将两个向量的每个对应元素相乘,然后把这些乘积加在一起,最终得到一个数值。
图解:
点积可以理解为向量的投影
图片来源:https://www.cuemath.com/algebra/dot-product/
应用场景:
- 向量相似度:在深度学习的注意力机制中,点积常被用来计算两个向量(如词向量)之间的相似度。点积越大,表示这两个向量越“相似”。
- 投影:点积也可以理解为一个向量在另一个向量方向上的投影,反映了两个向量的方向关系。
在注意力机制(如Transformer架构)中,Scaled Dot-Product Attention 计算的是 Query 向量和 Key 向量之间的点积,用来衡量输入序列中不同部分的相关性或重要性。
全连接层(Fully Connected Layer,简称 FC 层)
全连接层(Fully Connected Layer,简称 FC 层)是神经网络中的一种基本层,通常用于最后的输出或分类任务。它的特点是:每个输入节点都与下一层的每个输出节点完全连接,因此被称为“全连接”。
假设你有一个输入向量 x = [ x 1 , x 2 , … , x n ] \mathbf{x} = [x_1, x_2, \dots, x_n] x=[x1,x2,…,xn],全连接层通过一个权重矩阵 W \mathbf{W} W 和一个偏置向量 b \mathbf{b} b 将输入映射到输出。输出向量 y \mathbf{y} y 可以表示为:
y = W x + b \mathbf{y} = \mathbf{W} \mathbf{x} + \mathbf{b} y=Wx+b
其中:
- W \mathbf{W} W 是权重矩阵,每个元素表示从输入节点到输出节点的连接强度。
- b \mathbf{b} b 是偏置向量,帮助调整输出。
- x \mathbf{x} x 是输入向量, y \mathbf{y} y 是输出向量。
通俗解释:
全连接层就像一个“黑盒子”,它接收输入(向量),然后通过每个输入与每个输出之间的加权连接,将输入信息转化为一个新的输出。这个过程类似于对输入进行线性组合,再通过激活函数处理得到最终结果。
图解:
图片来源:https://indiantechwarrior.com/fully-connected-layers-in-convolutional-neural-networks/
应用场景:
- 分类任务:在图像分类、自然语言处理等任务的最后一层,通常会用全连接层将中间特征映射到分类标签。
- 回归任务:全连接层也可以用于预测连续值,比如房价预测等。
在深度学习网络的早期阶段,全连接层通常位于网络的末端,负责整合之前层提取的特征,最终进行预测或分类。
多头注意力(Multi-Head Attention)
多头注意力(Multi-Head Attention) 是 Transformer 模型中的一个核心机制,它扩展了 Self-Attention 的功能,通过引入多个“头”来同时从不同的子空间中提取信息。这样做的好处是,模型可以从不同的角度、不同的特征层次上理解序列中的依赖关系,使得模型更具表达能力和灵活性。
我们可以把子空间理解为一组特征的组合,每个子空间会强调输入序列中的某些特定的特征或关系。对于每个输入序列(比如句子中的词向量),模型会通过不同的线性变换矩阵,将输入映射到不同的子空间中。这些子空间的不同在于它们关注的重点信息不同,从而使得每个注意力头能够捕捉到不同的上下文关系或语义特征。
多头注意力的基本步骤
-
线性变换:对于每个输入向量 x i x_i xi,我们首先通过线性变换生成 Query ( Q Q Q)、Key ( K K K)、Value ( V V V) 向量,这一步与单头注意力类似。假设我们使用 h h h 个注意力头,则每个头都会生成一组独立的 Q Q Q, K K K, V V V 向量。
每个头的 Q h Q_h Qh、 K h K_h Kh 和 V h V_h Vh 的维度通常比原始向量要小,以减少计算量。
-
独立计算每个头的注意力:对于每个头,单独进行 Self-Attention 计算。具体来说,对于第 h h h 个头,计算 Query 和 Key 的点积,再通过 Softmax 归一化,得到注意力权重,然后用这些权重加权 Value 向量,计算出当前头的输出。
对第 h h h 个头,计算方式为:
Attention h ( Q h , K h , V h ) = softmax ( Q h K h T d k ) V h \text{Attention}_h(Q_h, K_h, V_h) = \text{softmax}\left(\frac{Q_h K_h^T}{\sqrt{d_k}}\right) V_h Attentionh(Qh,Kh,Vh)=softmax(dkQhKhT)Vh
其中 d k d_k dk 是 Key 向量的维度,用来缩放点积结果,避免数值过大。
-
拼接头的输出:每个头都会产生一个独立的输出向量,我们将所有 h h h 个头的输出拼接起来,形成一个新的向量。这个步骤可以让不同的注意力头捕捉到输入序列中不同方面的信息。
拼接后的向量为:
Concat ( Attention 1 , Attention 2 , … , Attention h ) \text{Concat}( \text{Attention}_1, \text{Attention}_2, \dots, \text{Attention}_h ) Concat(Attention1,Attention2,…,Attentionh)
-
线性变换:最后,将拼接后的向量通过一个线性变换,得到最终的输出。这个线性变换的作用是将拼接后的多头输出映射回到与输入相同的维度,以便后续层继续处理。
图解
图片来源:Attention Is All You Need
多头注意力的优势
-
多角度信息提取:单个头只能关注输入序列中的某一方面或某个层次的特征,而多头注意力可以并行地从不同子空间中捕捉序列中的不同依赖关系。比如,一个头可能关注局部上下文,另一个头可能关注更远距离的依赖关系。
-
提高模型的学习能力:通过多头注意力,模型能够从多个角度学习不同的语义或模式,使得模型具备更强的泛化能力。
-
并行计算:多个注意力头可以并行计算,从而提高训练和推理的效率。
多头注意力的公式总结
假设有 h h h 个注意力头,输入向量 X X X,多头注意力的计算可以总结为:
MultiHead ( Q , K , V ) = Concat ( head 1 , … , head h ) W O \text{MultiHead}(Q, K, V) = \text{Concat}(\text{head}_1, \dots, \text{head}_h) W^O MultiHead(Q,K,V)=Concat(head1,…,headh)WO
其中,每个 head i \text{head}_i headi 表示第 i i i 个头的计算:
head i = Attention ( Q W i Q , K W i K , V W i V ) \text{head}_i = \text{Attention}(QW_i^Q, KW_i^K, VW_i^V) headi=Attention(QWiQ,KWiK,VWiV)
W i Q W_i^Q WiQ, W i K W_i^K WiK, W i V W_i^V WiV 是每个头的线性变换权重矩阵, W O W^O WO 是拼接后的线性变换权重矩阵。
多头注意力的实际应用
在 Transformer 模型中,多头注意力被广泛应用于编码器和解码器结构中,尤其在自然语言处理任务(如机器翻译、文本生成等)中效果显著。它通过同时捕捉不同层次、不同角度的依赖关系,使得模型可以更好地理解序列中的复杂信息结构。
Position-wise Feed-Forward Networks(位置前馈网络,简称 FFN)
Position-wise Feed-Forward Networks(位置前馈网络,简称 FFN) 是 Transformer 架构中的一个重要组成部分,它位于每个注意力层(Self-Attention 层)之后,主要用于对每个位置的表示进行进一步的非线性变换,从而提升模型的表达能力。
为什么 Transformer 需要 Position-wise Feed-Forward Networks?
Transformer 的核心之一是 Self-Attention,它能够捕捉输入序列中各个词之间的依赖关系。不过,Self-Attention 是一种 线性操作,虽然可以捕捉序列中的全局关系,但缺少对输入进行 非线性变换 的能力,而深度神经网络中的非线性特征是非常关键的,因为它能帮助模型学习更复杂的表示和关系。
为了弥补这一不足,Position-wise Feed-Forward Networks 被加入到 Transformer 中。它通过对每个位置的向量单独应用非线性变换,使得每个位置的表示更具丰富性和灵活性。
Position-wise Feed-Forward Networks 的工作原理
每个位置的 FFN 是独立的,即对每个输入位置的向量单独进行操作,不同位置之间没有共享权重。具体计算过程如下:
- 对每个位置的向量 x i x_i xi,首先通过一个线性变换(矩阵乘法),然后通过一个非线性激活函数(通常是 ReLU)。
- 再通过一个第二个线性变换,最后得到该位置的输出。
公式表示如下:
FFN ( x i ) = W 2 ⋅ ReLU ( W 1 ⋅ x i + b 1 ) + b 2 \text{FFN}(x_i) = W_2 \cdot \text{ReLU}(W_1 \cdot x_i + b_1) + b_2 FFN(xi)=W2⋅ReLU(W1⋅xi+b1)+b2
其中:
- W 1 W_1 W1 和 W 2 W_2 W2 是两个不同的权重矩阵,分别用于线性变换。
- b 1 b_1 b1 和 b 2 b_2 b2 是偏置项。
- ReLU \text{ReLU} ReLU 是激活函数,增加非线性能力。
这个操作独立应用于每个输入序列位置的向量,所以叫做 Position-wise,因为每个位置的处理方式相同但独立。
L2范数(L2 norm)
L2范数(L2 norm) 是向量或矩阵的一种度量方式,它表示向量的“长度”或“欧几里得距离”。在机器学习和深度学习中,L2范数常用于正则化、梯度计算和度量向量相似性等任务。
L2范数有时也被称为欧几里得范数,定义为向量中所有元素的平方和的平方根。对于一个向量 x = [ x 1 , x 2 , … , x n ] \mathbf{x} = [x_1, x_2, \dots, x_n] x=[x1,x2,…,xn],其 L2范数定义为:
∥ x ∥ 2 = x 1 2 + x 2 2 + ⋯ + x n 2 = ∑ i = 1 n x i 2 \|\mathbf{x}\|_2 = \sqrt{x_1^2 + x_2^2 + \dots + x_n^2} = \sqrt{\sum_{i=1}^{n} x_i^2} ∥x∥2=x12+x22+⋯+xn2=i=1∑nxi2
L2范数的作用和应用
-
度量向量的长度:L2范数衡量向量在空间中的“长度”或“大小”。它可以用来评估向量的规模或者在不同空间中的距离。
-
向量正则化:L2范数在机器学习中的常见应用是L2正则化(也叫 Ridge 正则化)。它通过在损失函数中加入 L2 范数惩罚项,防止模型过拟合。正则化项形式为:
Regularization Term = λ ∥ w ∥ 2 2 = λ ∑ i = 1 n w i 2 \text{Regularization Term} = \lambda \|\mathbf{w}\|_2^2 = \lambda \sum_{i=1}^{n} w_i^2 Regularization Term=λ∥w∥22=λi=1∑nwi2
其中 λ \lambda λ 是正则化系数, w \mathbf{w} w 是模型的权重向量。L2正则化惩罚大权重值,鼓励模型找到更平滑、泛化能力更强的解。
-
梯度更新中的归一化:在深度学习中,向量有时会被归一化为单位长度(即 L2 范数为 1)。这种 L2归一化 有助于消除不同特征之间尺度差异对梯度更新的影响。
-
距离度量:L2范数可以用来衡量两个向量之间的欧几里得距离。在神经网络中,特别是词向量、特征向量或嵌入向量的比较中,L2范数用于计算相似度或距离。
L2范数与其他范数的区别
-
L1范数:L1范数是向量元素绝对值的和,它更适合用于稀疏表示(如L1正则化)。L2范数则倾向于平滑化权重,抑制过大的权重值。
对于向量 x = [ x 1 , x 2 , … , x n ] \mathbf{x} = [x_1, x_2, \dots, x_n] x=[x1,x2,…,xn],L1范数定义为:
∥ x ∥ 1 = ∣ x 1 ∣ + ∣ x 2 ∣ + ⋯ + ∣ x n ∣ \|\mathbf{x}\|_1 = |x_1| + |x_2| + \dots + |x_n| ∥x∥1=∣x1∣+∣x2∣+⋯+∣xn∣
-
L∞范数:L∞范数是向量中元素的最大绝对值,它更多用于极值问题的度量,而 L2 范数则强调向量整体的长度。
总结
L2范数 是一种衡量向量大小的常用工具,广泛应用于机器学习的正则化、距离计算、梯度归一化等任务中。它通过计算向量元素平方和的平方根,提供了对向量规模的度量,帮助模型更好地处理数值规模、正则化和相似性评估等问题。
Positional Encoding(位置编码)
Positional Encoding(位置编码) 是 Transformer 模型中的一个关键机制,用于在模型中引入位置信息。因为 Transformer 模型的结构不同于循环神经网络(RNN)或卷积神经网络(CNN),它没有序列顺序感知能力,即模型并不知道输入的序列中词与词之间的相对位置。为了让模型能够理解输入序列中词的顺序和相对位置信息,位置编码被引入。
为什么需要 Positional Encoding?
Transformer 依赖于 Self-Attention(自注意力机制),而自注意力机制并不保留输入序列的位置信息,它仅根据输入序列中的词语间的相似性来决定哪些词要被“关注”。这意味着模型在处理输入时,缺少序列顺序的感知。
与 RNN 不同,RNN 是依次处理输入序列的,因此天然保留了顺序信息。而 Transformer 模型是并行处理整个输入序列,所以为了弥补这一不足,需要为每个词引入位置信息,即通过 Positional Encoding 来告诉模型每个词在序列中的位置。
Positional Encoding 的原理
Positional Encoding 将位置信息嵌入到词向量中,使得模型能够识别序列中的位置信息。这些位置编码是一些加到词嵌入上的向量。每个输入向量被加上一个与其位置相关的编码向量,最终输入模型的向量不仅包含了词语的语义信息,还包含了它在序列中的位置。
具体来说,位置编码是通过特定的数学函数生成的,公式如下:
对于序列中的第 p o s pos pos 个位置,第 i i i 维的 Positional Encoding 定义为:
P E ( p o s , 2 i ) = sin ( p o s 1000 0 2 i / d m o d e l ) PE_{(pos, 2i)} = \sin\left(\frac{pos}{10000^{2i / d_{model}}}\right) PE(pos,2i)=sin(100002i/dmodelpos)
P E ( p o s , 2 i + 1 ) = cos ( p o s 1000 0 2 i / d m o d e l ) PE_{(pos, 2i+1)} = \cos\left(\frac{pos}{10000^{2i / d_{model}}}\right) PE(pos,2i+1)=cos(100002i/dmodelpos)
其中:
- p o s pos pos 表示输入序列中的位置。
- i i i 表示向量的维度索引。
- d m o d e l d_{model} dmodel 是词嵌入的维度。
解释
-
正弦和余弦函数:位置编码的值基于正弦(sine)和余弦(cosine)函数。这些函数的周期性质使得不同位置的编码产生不同的向量,同时也让位置编码保留了一定的相对位置关系。例如,靠近的词语在向量空间中的位置编码也会比较相近,反映了词之间的相对距离。
-
指数缩放:公式中的 1000 0 2 i / d m o d e l 10000^{2i / d_{model}} 100002i/dmodel 是一个指数缩放因子,确保不同维度上的编码具有不同的频率,使得不同维度能够捕捉到不同粒度的位置信息。
加法操作
在 Transformer 中,词嵌入向量与位置编码相加,形成模型的最终输入。这样做的目的是在保持原有词嵌入语义信息的基础上,赋予每个词语其位置信息,使得模型可以既理解每个词的含义,也能知道它们在句子中的顺序。
Positional Encoding 的性质
-
序列长度无关:位置编码公式与序列的长度无关,因此模型可以处理任意长度的输入序列。
-
捕捉相对位置:正弦和余弦函数的周期性帮助模型在较大范围内捕捉相对位置信息。模型不仅能了解词语在句子中的绝对位置,还能通过编码向量之间的差异感知词与词之间的相对距离。
-
通用性:由于位置编码通过固定的函数生成,无需额外学习参数,因此位置编码在不同任务和数据上都可以适用。
总结
Positional Encoding 通过为每个输入词加上基于其位置的编码,赋予了 Transformer 模型位置信息,使其能够理解序列中的顺序。它利用正弦和余弦函数生成的周期性编码,在保证模型并行计算的同时,能够有效捕捉到输入词序列中的相对和绝对位置关系。