PyTorch深度学习框架60天进阶学习计划第14天:循环神经网络进阶
PyTorch深度学习框架60天进阶学习计划第14天:
循环神经网络进阶
在深度学习处理序列数据时,循环神经网络(RNN)家族的模型扮演着至关重要的角色。今天,我们将深入探讨循环神经网络的进阶内容,包括BiLSTM的工作机制、注意力机制的数学原理,以及Transformer编码层的实现。
目录
-
BiLSTM的双向信息流机制
- LSTM回顾
- BiLSTM架构解析
- 时序特征融合策略
- BiLSTM实现与案例
-
注意力机制原理
- 注意力机制基础
- 缩放点积注意力的数学推导
- 注意力机制的变体
-
Transformer编码层实现
- 多头注意力机制
- nn.MultiheadAttention使用详解
- 位置编码的正弦函数实现
- 完整Transformer编码器实现
-
实战案例:文本分类任务
- 数据准备
- 模型构建
- 训练与评估
BiLSTM的双向信息流机制
LSTM回顾
在深入BiLSTM之前,让我们简要回顾LSTM(长短期记忆网络)的核心组件。LSTM通过三个门控单元来控制信息流:
- 遗忘门(Forget Gate):决定丢弃哪些信息
- 输入门(Input Gate):决定更新哪些信息
- 输出门(Output Gate):决定输出哪些信息
LSTM的数学表达式如下:
- 遗忘门: f t = σ ( W f ⋅ [ h t − 1 , x t ] + b f ) f_t = \sigma(W_f \cdot [h_{t-1}, x_t] + b_f) ft=σ(Wf⋅[ht−1,xt]+bf)
- 输入门: i t = σ ( W i ⋅ [ h t − 1 , x t ] + b i ) i_t = \sigma(W_i \cdot [h_{t-1}, x_t] + b_i) it=σ(Wi⋅[ht−1,xt]+bi)
- 候选记忆单元: C ~ t = tanh ( W C ⋅ [ h t − 1 , x t ] + b C ) \tilde{C}_t = \tanh(W_C \cdot [h_{t-1}, x_t] + b_C) C~t=tanh(WC⋅[ht−1,xt]+bC)
- 记忆单元更新: C t = f t ∗ C t − 1 + i t ∗ C ~ t C_t = f_t * C_{t-1} + i_t * \tilde{C}_t Ct=ft∗Ct−1+it∗C~t
- 输出门: o t = σ ( W o ⋅ [ h t − 1 , x t ] + b o ) o_t = \sigma(W_o \cdot [h_{t-1}, x_t] + b_o) ot=σ(Wo⋅[ht−1,xt]+bo)
- 隐藏状态: h t = o t ∗ tanh ( C t ) h_t = o_t * \tanh(C_t) ht=ot∗tanh(Ct)
其中, σ \sigma σ是sigmoid激活函数, ∗ * ∗表示元素级乘法。
BiLSTM架构解析
BiLSTM(双向长短期记忆网络)扩展了标准LSTM,使其能够同时从两个方向捕获序列信息:前向和后向。这使模型能够获取每个时间步的完整上下文信息。
BiLSTM的核心思想是:
- 一个LSTM层按正向处理序列(从左到右)
- 另一个LSTM层按反向处理序列(从右到左)
- 两个方向的输出在每个时间步合并
BiLSTM的关键优势在于能够捕获依赖于未来上下文的复杂模式,这在处理自然语言等序列数据时尤为重要。
时序特征融合策略
在BiLSTM中,来自两个方向的隐藏状态需要进行融合。常见的融合策略包括:
-
连接(Concatenation):将前向和后向的隐藏状态简单连接
h t = [ h t forward ; h t backward ] h_t = [h_t^{\text{forward}}; h_t^{\text{backward}}] ht=[htforward;htbackward] -
求和(Summation):对前向和后向的隐藏状态进行元素级求和
h t = h t forward + h t backward h_t = h_t^{\text{forward}} + h_t^{\text{backward}} ht=htforward+htbackward -
平均(Averaging):对前向和后向的隐藏状态求平均
h t = h t forward + h t backward 2 h_t = \frac{h_t^{\text{forward}} + h_t^{\text{backward}}}{2} ht=2htforward+htbackward -
加权组合(Weighted Combination):使用学习的权重组合两个方向的输出
h t = W ⋅ [ h t forward ; h t backward ] + b h_t = W \cdot [h_t^{\text{forward}}; h_t^{\text{backward}}] + b ht=W⋅[htforward;htbackward]+b
在实践中,连接是最常用的方法,因为它保留了两个方向的完整信息。
BiLSTM实现与案例
以下是PyTorch中实现BiLSTM的完整示例:
import torch
import torch.nn as nn
class BiLSTMModel(nn.Module):
def __init__(self, input_size, hidden_size, num_layers, num_classes):
super(BiLSTMModel, self).__init__()
self.hidden_size = hidden_size
self.num_layers = num_layers
self.lstm = nn.LSTM(input_size, hidden_size, num_layers,
batch_first=True, bidirectional=True)
# 注意输出维度是hidden_size*2,因为有两个方向
self.fc = nn.Linear(hidden_size*2, num_classes)
def forward(self, x):
# 初始化隐藏状态和记忆单元
# h0和c0的形状: [num_layers * num_directions, batch_size, hidden_size]
h0 = torch.zeros(self.num_layers*2, x.size(0), self.hidden_size).to(x.device)
c0 = torch.zeros(self.num_layers*2, x.size(0), self.hidden_size).to(x.device)
# 前向传播LSTM
# out形状: [batch_size, seq_length, hidden_size*2]
out, _ = self.lstm(x, (h0, c0))
# 解码最后一个时间步的隐藏状态
# out[:, -1, :] 形状: [batch_size, hidden_size*2]
out = self.fc(out[:, -1, :])
return out
# 创建模型示例
input_size = 28 # 输入特征维度
hidden_size = 128 # 隐藏状态维度
num_layers = 2 # LSTM层数
num_classes = 10 # 分类数
model = BiLSTMModel(input_size, hidden_size, num_layers, num_classes)
print(model)
# 假设我们有一个序列输入,形状为[batch_size, seq_length, input_size]
sample_input = torch.randn(32, 100, input_size)
output = model(sample_input)
print(f"输出形状: {output.shape}") # 应该是[32, 10]
让我们来分析上述代码的关键部分:
- BiLSTM的实现仅需将LSTM的
bidirectional
参数设置为True
- 双向LSTM的隐藏状态需要初始化为原来的两倍(
num_layers*2
) - 输出特征的维度是隐藏状态维度的两倍(
hidden_size*2
),因为它包含两个方向的信息
注意力机制原理
注意力机制基础
注意力机制允许模型专注于输入序列的特定部分,类似于人类阅读文本时的注意力集中。注意力机制已成为处理序列数据的强大工具,特别是在Transformer架构中。
注意力机制的核心思想是:
- 计算查询(Query)与一组键(Key)之间的相似度
- 使用这些相似度对值(Value)进行加权求和
注意力函数可以被描述为将查询和一组键值对映射到输出:
Attention
(
Q
,
K
,
V
)
=
softmax
(
Q
K
T
d
k
)
V
\text{Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V
Attention(Q,K,V)=softmax(dkQKT)V
缩放点积注意力的数学推导
缩放点积注意力(Scaled Dot-Product Attention)是Transformer中使用的注意力机制。让我们详细推导其数学表达式:
-
计算查询与键的相似度:对于查询矩阵 Q Q Q和键矩阵 K K K,我们计算它们的点积 Q K T QK^T QKT,得到相似度矩阵。
-
缩放:为了防止点积的值过大(导致softmax函数的梯度变得很小),我们将点积除以 d k \sqrt{d_k} dk,其中 d k d_k dk是键的维度。这种缩放使得方差保持为1,使梯度更稳定。
-
应用softmax:使用softmax函数将缩放后的点积转换为概率分布。
-
加权值:使用这些概率对值矩阵 V V V进行加权求和。
完整的数学表达式如下:
Attention ( Q , K , V ) = softmax ( Q K T d k ) V \text{Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V Attention(Q,K,V)=softmax(dkQKT)V
其中:
- Q Q Q 是查询矩阵,形状为 ( n q , d k ) (n_q, d_k) (nq,dk)
- K K K 是键矩阵,形状为 ( n k , d k ) (n_k, d_k) (nk,dk)
- V V V 是值矩阵,形状为 ( n k , d v ) (n_k, d_v) (nk,dv)
- d k d_k dk 是键的维度
- 输出形状为 ( n q , d v ) (n_q, d_v) (nq,dv)
让我们使用PyTorch实现一个简单的缩放点积注意力:
import torch
import torch.nn as nn
import torch.nn.functional as F
import math
def scaled_dot_product_attention(query, key, value, mask=None):
"""
计算缩放点积注意力
参数:
query: 形状为 (..., seq_len_q, depth)
key: 形状为 (..., seq_len_k, depth)
value: 形状为 (..., seq_len_k, depth_v)
mask: 形状为 (..., seq_len_q, seq_len_k) 的可选掩码
返回:
output: 形状为 (..., seq_len_q, depth_v) 的注意力输出
attention_weights: 形状为 (..., seq_len_q, seq_len_k) 的注意力权重
"""
# 获取维度
d_k = query.size(-1)
# 计算缩放点积
matmul_qk = torch.matmul(query, key.transpose(-2, -1)) # (..., seq_len_q, seq_len_k)
scaled_attention_logits = matmul_qk / math.sqrt(d_k)
# 如果提供了掩码,将对应位置的logits设为很大的负值
if mask is not None:
scaled_attention_logits += (mask * -1e9)
# 应用softmax
attention_weights = F.softmax(scaled_attention_logits, dim=-1) # (..., seq_len_q, seq_len_k)
# 计算输出
output = torch.matmul(attention_weights, value) # (..., seq_len_q, depth_v)
return output, attention_weights
注意力机制的变体
除了缩放点积注意力外,还有几种常见的注意力变体:
-
加性注意力(Additive Attention):使用单层前馈网络计算注意力分数
score ( Q , K ) = v T tanh ( W 1 Q + W 2 K ) \text{score}(Q, K) = v^T \tanh(W_1 Q + W_2 K) score(Q,K)=vTtanh(W1Q+W2K) -
乘性注意力(Multiplicative Attention):简单的点积注意力,没有缩放
score ( Q , K ) = Q K T \text{score}(Q, K) = QK^T score(Q,K)=QKT -
位置加权注意力(Location-Based Attention):考虑位置信息的注意力
score ( Q , K ) = W T tanh ( W Q Q + W K K + W L L ) \text{score}(Q, K) = W^T \text{tanh}(W_Q Q + W_K K + W_L L) score(Q,K)=WTtanh(WQQ+WKK+WLL) -
自注意力(Self-Attention):查询、键和值都来自同一序列
SelfAttention ( X ) = Attention ( X W Q , X W K , X W V ) \text{SelfAttention}(X) = \text{Attention}(XW_Q, XW_K, XW_V) SelfAttention(X)=Attention(XWQ,XWK,XWV)
Transformer编码层实现
多头注意力机制
多头注意力机制(Multi-Head Attention)通过运行多个注意力"头"并连接结果,允许模型同时关注来自不同位置和不同表示子空间的信息。
多头注意力的数学表达式:
MultiHead ( Q , K , V ) = Concat ( head 1 , head 2 , . . . , head h ) W O \text{MultiHead}(Q, K, V) = \text{Concat}(\text{head}_1, \text{head}_2, ..., \text{head}_h)W^O MultiHead(Q,K,V)=Concat(head1,head2,...,headh)WO
其中每个头定义为:
head i = Attention ( Q W i Q , K W i K , V W i V ) \text{head}_i = \text{Attention}(QW^Q_i, KW^K_i, VW^V_i) headi=Attention(QWiQ,KWiK,VWiV)
- W i Q W^Q_i WiQ, W i K W^K_i WiK, W i V W^V_i WiV 是投影矩阵
- W O W^O WO 是输出投影矩阵
nn.MultiheadAttention使用详解
PyTorch提供了内置的nn.MultiheadAttention
模块,使实现多头注意力变得简单。以下是其使用详解:
import torch
import torch.nn as nn
# 创建多头注意力模块
embed_dim = 512 # 嵌入维度
num_heads = 8 # 注意力头数(必须能整除embed_dim)
multihead_attn = nn.MultiheadAttention(embed_dim, num_heads)
# 准备输入
batch_size = 32
seq_length = 100
query = torch.rand(seq_length, batch_size, embed_dim) # 注意顺序是(seq_len, batch, embed_dim)
key = torch.rand(seq_length, batch_size, embed_dim)
value = torch.rand(seq_length, batch_size, embed_dim)
# 前向传播
attn_output, attn_weights = multihead_attn(query, key, value)
print(f"注意力输出形状: {attn_output.shape}") # 应为 [seq_length, batch_size, embed_dim]
print(f"注意力权重形状: {attn_weights.shape}") # 应为 [batch_size, seq_length, seq_length]
nn.MultiheadAttention
的关键参数:
embed_dim
:模型的维度num_heads
:注意力头的数量dropout
:应用于注意力权重的dropout率(默认为0.0)bias
:是否使用偏置(默认为True)add_bias_kv
:是否向键和值序列添加偏置(默认为False)add_zero_attn
:是否向注意力计算添加零注意力(默认为False)kdim
:键的维度(默认为None,在这种情况下等于embed_dim)vdim
:值的维度(默认为None,在这种情况下等于embed_dim)
位置编码的正弦函数实现
在Transformer中,由于自注意力缺乏序列顺序的内在理解,我们需要添加位置信息。位置编码使用正弦和余弦函数,使模型能够学习相对和绝对位置:
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(pos / 10000^{2i/d_{model}})
PE(pos,2i)=sin(pos/100002i/dmodel)
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(pos / 10000^{2i/d_{model}})
PE(pos,2i+1)=cos(pos/100002i/dmodel)
其中, p o s pos pos是位置, i i i是维度索引, d m o d e l d_{model} dmodel是模型的维度。
以下是PyTorch中位置编码的实现:
import torch
import math
def positional_encoding(seq_len, d_model):
"""
创建正弦位置编码
参数:
seq_len: 序列长度
d_model: 模型维度
返回:
pos_encoding: 形状为 [seq_len, d_model] 的位置编码
"""
# 创建位置索引
position = torch.arange(seq_len, dtype=torch.float).unsqueeze(1)
# 创建维度索引,步长为2(偶数位置用sin,奇数位置用cos)
div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model))
# 创建位置编码矩阵
pe = torch.zeros(seq_len, d_model)
# 填充位置编码
pe[:, 0::2] = torch.sin(position * div_term) # 偶数列
pe[:, 1::2] = torch.cos(position * div_term) # 奇数列
return pe
# 测试位置编码
seq_len = 100
d_model = 512
pe = positional_encoding(seq_len, d_model)
# 可视化位置编码(如果在Jupyter notebook中)
import matplotlib.pyplot as plt
plt.figure(figsize=(15, 8))
plt.imshow(pe.numpy())
plt.xlabel('Embedding Dimension')
plt.ylabel('Position')
plt.colorbar()
plt.title('Sinusoidal Positional Encoding')
plt.show()
正弦位置编码的关键特性:
- 每个位置有唯一的编码
- 不同位置之间的相对距离可以被模型学习
- 可以扩展到未见过的序列长度
- 具有周期性,可以捕获序列的周期模式
完整Transformer编码器实现
现在,让我们实现一个完整的Transformer编码器层,包括多头自注意力、前馈网络、层归一化和残差连接:
import torch
import torch.nn as nn
import math
class PositionalEncoding(nn.Module):
def __init__(self, d_model, max_len=5000):
super(PositionalEncoding, self).__init__()
# 创建位置编码矩阵
pe = torch.zeros(max_len, d_model)
position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)
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)
pe = pe.unsqueeze(0).transpose(0, 1) # 形状: [max_len, 1, d_model]
# 注册为不需要梯度的缓冲区
self.register_buffer('pe', pe)
def forward(self, x):
# x 形状: [seq_len, batch_size, embed_dim]
return x + self.pe[:x.size(0), :]
class TransformerEncoderLayer(nn.Module):
def __init__(self, d_model, nhead, dim_feedforward=2048, dropout=0.1):
super(TransformerEncoderLayer, self).__init__()
# 多头自注意力
self.self_attn = nn.MultiheadAttention(d_model, nhead, dropout=dropout)
# 前馈网络
self.linear1 = nn.Linear(d_model, dim_feedforward)
self.dropout = nn.Dropout(dropout)
self.linear2 = nn.Linear(dim_feedforward, d_model)
# 层归一化
self.norm1 = nn.LayerNorm(d_model)
self.norm2 = nn.LayerNorm(d_model)
# dropout
self.dropout1 = nn.Dropout(dropout)
self.dropout2 = nn.Dropout(dropout)
# 激活函数
self.activation = nn.ReLU()
def forward(self, src, src_mask=None, src_key_padding_mask=None):
# 多头自注意力 + 残差连接 + 层归一化
src2, _ = self.self_attn(src, src, src, attn_mask=src_mask,
key_padding_mask=src_key_padding_mask)
src = src + self.dropout1(src2)
src = self.norm1(src)
# 前馈网络 + 残差连接 + 层归一化
src2 = self.linear2(self.dropout(self.activation(self.linear1(src))))
src = src + self.dropout2(src2)
src = self.norm2(src)
return src
class TransformerEncoder(nn.Module):
def __init__(self, d_model, nhead, num_layers, dim_feedforward=2048, dropout=0.1):
super(TransformerEncoder, self).__init__()
# 位置编码
self.pos_encoder = PositionalEncoding(d_model)
# 编码器层
encoder_layers = nn.TransformerEncoderLayer(d_model, nhead,
dim_feedforward, dropout)
self.transformer_encoder = nn.TransformerEncoder(encoder_layers, num_layers)
self.d_model = d_model
def forward(self, src, src_mask=None, src_key_padding_mask=None):
# src 形状: [seq_len, batch_size, embed_dim]
# 添加位置编码
src = self.pos_encoder(src)
# 通过Transformer编码器
output = self.transformer_encoder(src, src_mask, src_key_padding_mask)
return output
使用自定义Transformer编码器:
# 模型参数
d_model = 512 # 模型维度
nhead = 8 # 头数量
num_layers = 6 # 编码器层数
dim_feedforward = 2048 # 前馈网络维度
dropout = 0.1 # dropout率
# 创建模型
encoder = TransformerEncoder(d_model, nhead, num_layers, dim_feedforward, dropout)
# 准备输入
seq_len = 100
batch_size = 32
src = torch.rand(seq_len, batch_size, d_model)
# 前向传播
output = encoder(src)
print(f"编码器输出形状: {output.shape}") # 应为 [seq_len, batch_size, d_model]
实战案例:文本分类任务
让我们应用所学知识,使用BiLSTM和Transformer编码器实现文本分类任务。
数据准备
我们将使用torchtext加载和处理IMDB电影评论数据集:
from torchtext.legacy import data
from torchtext.legacy import datasets
import torch
import torch.nn as nn
import torch.optim as optim
import random
import numpy as np
# 设置随机种子
SEED = 1234
random.seed(SEED)
np.random.seed(SEED)
torch.manual_seed(SEED)
torch.backends.cudnn.deterministic = True
# 定义字段
TEXT = data.Field(tokenize='spacy', tokenizer_language='en_core_web_sm')
LABEL = data.LabelField(dtype=torch.float)
# 加载IMDB数据集
train_data, test_data = datasets.IMDB.splits(TEXT, LABEL)
# 拆分训练集为训练和验证
train_data, valid_data = train_data.split(random_state=random.seed(SEED))
# 构建词汇表
MAX_VOCAB_SIZE = 25000
TEXT.build_vocab(train_data, max_size=MAX_VOCAB_SIZE, vectors='glove.6B.100d')
LABEL.build_vocab(train_data)
# 创建迭代器
BATCH_SIZE = 64
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
train_iterator, valid_iterator, test_iterator = data.BucketIterator.splits(
(train_data, valid_data, test_data),
batch_size=BATCH_SIZE,
device=device,
sort_key=lambda x: len(x.text),
sort_within_batch=True
)
模型构建
BiLSTM模型
class BiLSTMClassifier(nn.Module):
def __init__(self, vocab_size, embedding_dim, hidden_dim, output_dim, n_layers,
bidirectional, dropout, pad_idx):
super(BiLSTMClassifier, self).__init__()
# 嵌入层
self.embedding = nn.Embedding(vocab_size, embedding_dim, padding_idx=pad_idx)
# LSTM层
self.lstm = nn.LSTM(embedding_dim, hidden_dim, n_layers,
bidirectional=bidirectional, dropout=dropout,
batch_first=True)
# 全连接层
self.fc = nn.Linear(hidden_dim * 2 if bidirectional else hidden_dim, output_dim)
# Dropout
self.dropout = nn.Dropout(dropout)
def forward(self, text):
# text: [batch_size, seq_len]
# 嵌入
embedded = self.embedding(text) # [batch_size, seq_len, embedding_dim]
# LSTM
outputs, (hidden, _) = self.lstm(embedded) # outputs: [batch_size, seq_len, hidden_dim * num_directions]
# 如果是双向,连接两个方向的最后隐藏状态
if self.lstm.bidirectional:
hidden = torch.cat((hidden[-2,:,:], hidden[-1,:,:]), dim=1) # [batch_size, hidden_dim * 2]
else:
hidden = hidden[-1,:,:] # [batch_size, hidden_dim]
# Dropout和全连接
return self.fc(self.dropout(hidden))
# 创建BiLSTM模型
vocab_size = len(TEXT.vocab)
embedding_dim = 100
hidden_dim = 256
output_dim = 1 # 二分类
n_layers = 2
bidirectional = True
dropout = 0.5
pad_idx = TEXT.vocab.stoi[TEXT.pad_token]
bilstm_model = BiLSTMClassifier(vocab_size, embedding_dim, hidden_dim, output_dim,
n_layers, bidirectional, dropout, pad_idx)
# 使用预训练词向量
bilstm_model.embedding.weight.data.copy_(TEXT.vocab.vectors)
# 将padding设为0
bilstm_model.embedding.weight.data[pad_idx] = torch.zeros(embedding_dim)
bilstm_model = bilstm_model.to(device)
Transformer模型
class TransformerClassifier(nn.Module):
def __init__(self, vocab_size, embedding_dim, hidden_dim, output_dim, n_heads,
n_layers, dropout, pad_idx):
super(TransformerClassifier, self).__init__()
# 嵌入层
self.embedding = nn.Embedding(vocab_size, embedding_dim, padding_idx=pad_idx)
# 位置编码
self.pos_encoder = PositionalEncoding(embedding_dim)
# Transformer编码器
encoder_layers = nn.TransformerEncoderLayer(embedding_dim, n_heads, hidden_dim, dropout)
self.transformer_encoder = nn.TransformerEncoder(encoder_layers, n_layers)
# 全连接层
self.fc = nn.Linear(embedding_dim, output_dim)
# Dropout
self.dropout = nn.Dropout(dropout)
# pad_idx
self.pad_idx = pad_idx
## 实战案例:文本分类任务(续)
### 模型构建(续)
#### Transformer模型(续)
```python
def make_src_mask(self, src):
# 创建掩码,将padding标记为True,非padding标记为False
src_mask = (src == self.pad_idx).transpose(0, 1)
return src_mask
def forward(self, text):
# text形状: [batch_size, seq_len]
# 创建掩码
src_mask = self.make_src_mask(text)
# 嵌入和位置编码
embedded = self.embedding(text).transpose(0, 1) # [seq_len, batch_size, embedding_dim]
embedded = self.pos_encoder(embedded)
# Transformer编码器
encoded = self.transformer_encoder(embedded, src_key_padding_mask=src_mask)
# 获取序列的平均表示
encoded = encoded.transpose(0, 1) # [batch_size, seq_len, embedding_dim]
mask = (text != self.pad_idx).unsqueeze(-1).float() # [batch_size, seq_len, 1]
encoded = (encoded * mask).sum(dim=1) / mask.sum(dim=1) # [batch_size, embedding_dim]
# 全连接层
return self.fc(self.dropout(encoded))
# 创建Transformer模型
embedding_dim = 100
hidden_dim = 256
output_dim = 1 # 二分类
n_heads = 8
n_layers = 2
dropout = 0.5
transformer_model = TransformerClassifier(vocab_size, embedding_dim, hidden_dim, output_dim,
n_heads, n_layers, dropout, pad_idx)
# 使用预训练词向量
transformer_model.embedding.weight.data.copy_(TEXT.vocab.vectors)
# 将padding设为0
transformer_model.embedding.weight.data[pad_idx] = torch.zeros(embedding_dim)
transformer_model = transformer_model.to(device)
训练与评估
接下来,让我们定义训练和评估函数,并训练我们的模型:
# 二元交叉熵损失函数
criterion = nn.BCEWithLogitsLoss()
# 优化器
optimizer_bilstm = optim.Adam(bilstm_model.parameters())
optimizer_transformer = optim.Adam(transformer_model.parameters())
# 计算准确率
def binary_accuracy(preds, y):
"""
计算二分类准确率
"""
# 四舍五入预测值
rounded_preds = torch.round(torch.sigmoid(preds))
correct = (rounded_preds == y).float()
acc = correct.sum() / len(correct)
return acc
# 训练函数
def train(model, iterator, optimizer, criterion):
"""
训练一个epoch
"""
epoch_loss = 0
epoch_acc = 0
model.train()
for batch in iterator:
# 清零梯度
optimizer.zero_grad()
# 前向传播
predictions = model(batch.text).squeeze(1)
# 计算损失和准确率
loss = criterion(predictions, batch.label)
acc = binary_accuracy(predictions, batch.label)
# 反向传播
loss.backward()
# 更新参数
optimizer.step()
epoch_loss += loss.item()
epoch_acc += acc.item()
return epoch_loss / len(iterator), epoch_acc / len(iterator)
# 评估函数
def evaluate(model, iterator, criterion):
"""
评估模型
"""
epoch_loss = 0
epoch_acc = 0
model.eval()
with torch.no_grad():
for batch in iterator:
# 前向传播
predictions = model(batch.text).squeeze(1)
# 计算损失和准确率
loss = criterion(predictions, batch.label)
acc = binary_accuracy(predictions, batch.label)
epoch_loss += loss.item()
epoch_acc += acc.item()
return epoch_loss / len(iterator), epoch_acc / len(iterator)
# 训练模型
def train_model(model, train_iterator, valid_iterator, optimizer, criterion, n_epochs=5):
"""
训练模型多个epoch
"""
best_valid_loss = float('inf')
for epoch in range(n_epochs):
# 训练和评估
train_loss, train_acc = train(model, train_iterator, optimizer, criterion)
valid_loss, valid_acc = evaluate(model, valid_iterator, criterion)
# 如果验证损失更低,保存模型
if valid_loss < best_valid_loss:
best_valid_loss = valid_loss
torch.save(model.state_dict(), 'best-model.pt')
print(f'Epoch: {epoch+1:02}')
print(f'\tTrain Loss: {train_loss:.3f} | Train Acc: {train_acc*100:.2f}%')
print(f'\tValid Loss: {valid_loss:.3f} | Valid Acc: {valid_acc*100:.2f}%')
为方便比较,我们将训练两个模型:BiLSTM和Transformer:
N_EPOCHS = 5
print("===== 训练BiLSTM模型 =====")
train_model(bilstm_model, train_iterator, valid_iterator, optimizer_bilstm, criterion, N_EPOCHS)
print("\n===== 训练Transformer模型 =====")
train_model(transformer_model, train_iterator, valid_iterator, optimizer_transformer, criterion, N_EPOCHS)
最后,让我们在测试集上评估我们的最佳模型:
# 加载最佳BiLSTM模型
bilstm_model.load_state_dict(torch.load('best-model.pt'))
# 在测试集上评估
test_loss, test_acc = evaluate(bilstm_model, test_iterator, criterion)
print(f'BiLSTM测试结果 - 损失: {test_loss:.3f} | 准确率: {test_acc*100:.2f}%')
# 加载最佳Transformer模型
transformer_model.load_state_dict(torch.load('best-model.pt'))
# 在测试集上评估
test_loss, test_acc = evaluate(transformer_model, test_iterator, criterion)
print(f'Transformer测试结果 - 损失: {test_loss:.3f} | 准确率: {test_acc*100:.2f}%')
进阶分析与比较
现在我们已经实现了BiLSTM和Transformer模型,让我们深入分析它们之间的差异和性能比较。
BiLSTM vs Transformer 对比
特性 | BiLSTM | Transformer |
---|---|---|
序列处理 | 顺序处理(前向和后向) | 并行处理(自注意力) |
长程依赖 | 有限(受LSTM记忆限制) | 更强(直接注意力连接) |
计算效率 | 较低(顺序依赖) | 较高(可并行化) |
参数量 | 少 | 多 |
训练速度 | 慢 | 快 |
短文本性能 | 很好 | 好 |
长文本性能 | 较弱 | 很好 |
优化技巧
-
BiLSTM优化:
- 使用梯度裁剪防止梯度爆炸
- 使用残差连接帮助梯度流动
- 尝试不同的特征融合策略
-
Transformer优化:
- 使用学习率预热调度
- 调整注意力头数和层数
- 使用更大的批大小
可视化分析
让我们编写一个函数来可视化注意力权重,帮助我们理解模型如何处理序列数据:
import matplotlib.pyplot as plt
import seaborn as sns
def visualize_attention(model, text, tokenizer):
"""
可视化Transformer的注意力权重
"""
model.eval()
# 分词
tokens = tokenizer(text)
# 转换为索引
indexed = [TEXT.vocab.stoi[t] for t in tokens]
tensor = torch.LongTensor(indexed).unsqueeze(0).to(device)
# 前向传播,保存注意力权重
with torch.no_grad():
output = model(tensor)
# 获取最后一个注意力层的权重
# 注:这需要修改模型以返回注意力权重
attention_weights = model.get_attention_weights(tensor) # 假设函数
# 可视化
plt.figure(figsize=(10, 8))
sns.heatmap(attention_weights[0].cpu().numpy(),
xticklabels=tokens,
yticklabels=tokens,
cmap='viridis')
plt.title('Attention Weights')
plt.xlabel('Key')
plt.ylabel('Query')
plt.show()
BiLSTM的双向信息流机制详解
让我们更深入地探讨BiLSTM中的双向信息流机制,这是模型理解上下文的关键。
在BiLSTM中,序列数据从两个方向处理:
- 前向LSTM层(从左到右)捕获当前位置之前的上下文
- 后向LSTM层(从右到左)捕获当前位置之后的上下文
信息流详解
对于输入序列 X = [ x 1 , x 2 , . . . , x T ] X = [x_1, x_2, ..., x_T] X=[x1,x2,...,xT],BiLSTM计算:
前向LSTM:
h
→
t
=
LSTM
(
x
t
,
h
→
t
−
1
)
\overrightarrow{h}_t = \text{LSTM}(x_t, \overrightarrow{h}_{t-1})
ht=LSTM(xt,ht−1)
后向LSTM:
h
←
t
=
LSTM
(
x
t
,
h
←
t
+
1
)
\overleftarrow{h}_t = \text{LSTM}(x_t, \overleftarrow{h}_{t+1})
ht=LSTM(xt,ht+1)
组合隐藏状态:
h
t
=
[
h
→
t
;
h
←
t
]
h_t = [\overrightarrow{h}_t; \overleftarrow{h}_t]
ht=[ht;ht]
其中 [ ; ] [;] [;] 表示连接操作。
门控网络的时序特征融合
LSTM的门控机制是其处理长序列的关键。每个LSTM单元包含三个门:
- 遗忘门:控制从上一时间步的记忆单元中保留多少信息
- 输入门:控制当前输入信息的影响程度
- 输出门:控制当前记忆单元到隐藏状态的信息流动
这些门的协同工作允许LSTM有选择地更新和传递信息。在BiLSTM中,这种选择性双向信息流使得模型能够有效融合整个序列的上下文。
缩放点积注意力的数学原理深入分析
缩放点积注意力是Transformer的核心组件。让我们深入分析其数学原理和为什么它如此有效。
为什么需要缩放?
当输入序列很长或嵌入维度很大时,点积的方差也会增大。这导致softmax函数在大值区域的梯度变得极小,影响训练效果。
假设查询和键的分量是均值为0、方差为1的独立随机变量,那么它们的点积的方差将是 d k d_k dk,其中 d k d_k dk 是键的维度。
通过除以 d k \sqrt{d_k} dk,我们将点积的方差标准化为1,使得softmax的输入具有更合理的方差,从而获得更稳定的梯度。
注意力机制的信息瓶颈理论
从信息论角度看,注意力机制可以被视为一种软信息瓶颈,它压缩输入序列的信息,只保留与当前查询相关的部分。
在缩放点积注意力中,查询 Q Q Q 可以看作是"问题",键 K K K 是确定相关性的"索引",而值 V V V 是实际的"信息"。通过计算 Q Q Q 和 K K K 的相似度,并用它来加权 V V V,注意力机制有效地过滤并聚合了最相关的信息。
总结与展望
在本课中,我们深入探讨了循环神经网络的进阶内容,包括:
- BiLSTM的双向信息流机制,它允许模型同时捕获序列中的过去和未来信息
- 注意力机制的数学原理,特别是缩放点积注意力的工作原理
- Transformer编码器的实现,包括多头注意力和位置编码
- 实际应用案例,展示了如何将这些技术应用于文本分类任务
这些高级技术在自然语言处理、时间序列分析和其他序列数据处理任务中都有广泛应用。随着研究的进展,我们看到了从RNN到Transformer的演变,后者因其并行性和长距离依赖建模能力而成为当前深度学习的主流架构。
清华大学全三版的《DeepSeek教程》完整的文档需要的朋友,关注我私信:deepseek 即可获得。
怎么样今天的内容还满意吗?再次感谢朋友们的观看,关注GZH:凡人的AI工具箱,回复666,送您价值199的AI大礼包。最后,祝您早日实现财务自由,还请给个赞,谢谢!