RNN(循环神经网络) 是一种专门设计用来处理序列数据的人工神经网络。它的核心思想是能够“记住”之前处理过的信息,并将其用于当前的计算,这使得它非常适合处理具有时间顺序或上下文依赖关系的数据。
核心概念:循环连接
RNN 与普通的前馈神经网络(如多层感知机)最根本的区别在于它引入了循环连接:
- 输入序列: RNN 接收一个序列作为输入,例如:
- 一个句子(单词序列)
- 一段语音(音频帧序列)
- 股票价格(时间点上的价格序列)
- 视频帧序列
- 隐藏状态: RNN 内部维护一个隐藏状态。这个状态就像是网络的“记忆”,它总结了网络在处理当前输入之前所“看到”的所有历史信息。
- 循环: 在处理序列中的每一个元素时:
- 当前输入 + 前一个隐藏状态: RNN 不仅考虑当前的输入数据,还会结合上一个时间步的隐藏状态。
- 计算新状态和输出: 网络根据当前的输入和前一个隐藏状态,通过激活函数(通常是
tanh
或ReLU
)计算出新的隐藏状态。 - 产生输出(可选): 这个新的隐藏状态可以用来产生当前时间步的输出(例如,预测序列的下一个元素、对当前元素进行分类等)。
- 传递状态: 这个新的隐藏状态被传递到下一个时间步,作为处理下一个输入元素的“记忆”基础。
- 共享权重: RNN 在处理序列的每个时间步时,使用的是同一套网络参数(权重和偏置)。这意味着它用相同的规则(相同的函数)来处理序列中的每一个元素。
为什么需要 RNN?
传统的前馈神经网络在处理序列数据时存在明显缺陷:
- 固定输入大小: 它们要求输入数据具有固定长度(例如,固定数量的像素或特征)。序列数据(如不同长度的句子)很难直接适配。
- 忽略顺序/上下文: 它们独立处理每个输入,没有机制来记住或利用之前输入的信息。对于序列数据,顺序和上下文往往至关重要(例如,句子中单词的含义依赖于前面的单词)。
RNN 的优势(擅长处理的任务)
RNN 的“记忆”特性使其特别擅长以下任务:
- 自然语言处理:
- 语言建模(预测下一个单词)
- 机器翻译(输入源语言序列,输出目标语言序列)
- 文本生成(写文章、诗歌、代码)
- 情感分析(理解句子情感)
- 命名实体识别(识别文本中的人名、地名等)
- 语音识别(音频信号序列转文本)
- 时间序列预测:
- 股票价格预测
- 天气预测
- 销售预测
- 序列标注:
- 词性标注(为句子中的每个单词标注词性)
- 语音分割
- 其他序列任务:
- 视频分析(理解视频帧序列)
- 音乐生成(生成音符序列)
- 聊天机器人(对话是轮次的序列)
RNN 的挑战(经典问题)
标准的 RNN(通常称为 Vanilla RNN)在处理长序列时会遇到一些著名的困难:
- 梯度消失问题: 这是最主要的问题。在通过时间反向传播训练网络时,梯度(用于更新权重的误差信号)会随着时间步的推移而呈指数级衰减。当序列很长时,早期的输入对最终输出的影响梯度会变得非常小,导致网络很难学习到长距离的依赖关系(即序列开头的信息对序列结尾的影响)。
- 梯度爆炸问题: 相对少见但也会发生,梯度变得非常大,导致训练不稳定。
- 有限的记忆容量: 隐藏状态的大小固定,难以记住非常久远的信息。
- 计算效率: 由于其顺序处理的特性(必须一个时间步接一个时间步地计算),难以并行化,训练速度可能较慢。
改进的 RNN 结构
为了解决标准 RNN 的问题(尤其是梯度消失问题),研究者开发了更强大的变体:
- LSTM: 长短期记忆网络。引入了“门”机制(输入门、遗忘门、输出门)和一个额外的“细胞状态”,可以更精细地控制信息的保留、遗忘和输出,特别擅长学习长期依赖关系。
- GRU: 门控循环单元。LSTM 的一个简化版本,只有两个门(更新门、重置门),计算效率更高,在很多时候能达到与 LSTM 相当甚至更好的效果。
总结:
RNN 是一种具有内部循环连接的网络,允许信息在序列处理过程中持续存在(即拥有“记忆”)。这种结构使其成为处理序列数据(文本、语音、时间序列等)的自然选择。尽管标准 RNN 存在梯度消失等限制其处理长序列能力的问题,但其强大的变体 LSTM 和 GRU 已被证明在广泛的序列建模任务中极其成功,是深度学习和人工智能领域的一项基础性技术。当你看到机器翻译、智能对话或文本生成时,背后很可能就有 RNN 或其变体(LSTM/GRU)在工作。
RNN运行原理图:
尝试(代码实现)
import torch
import torch.nn as nn
# 设置随机种子保证可重复性
torch.manual_seed(42)
# =====================
# 1. 准备模拟数据
# =====================
# 创建一个简单的序列数据集:输入序列和对应的目标值
# 输入序列: [0,1,2] -> 目标: 3; [1,2,3] -> 目标: 4 等
seq_length = 3 # 输入序列长度
data_size = 100 # 数据集大小
# 生成特征数据 (100个样本,每个样本是长度为3的序列)
X = torch.stack([torch.arange(i, i + seq_length) for i in range(data_size)]).float()
# 生成目标数据 (每个序列的下一个数字)
y = torch.stack([torch.tensor([i + seq_length]) for i in range(data_size)]).float()
print("输入数据形状:", X.shape) # torch.Size([100, 3])
print("目标数据形状:", y.shape) # torch.Size([100, 1])
# =====================
# 2. 定义RNN模型
# =====================
class SimpleRNN(nn.Module):
def __init__(self, input_size, hidden_size, output_size):
super(SimpleRNN, self).__init__()
# 定义RNN层参数
self.hidden_size = hidden_size
# RNN层: (input_size, hidden_size)
# batch_first=True 表示输入数据的格式为 (batch, seq_len, features) self.rnn = nn.RNN(
input_size=input_size,
hidden_size=hidden_size,
batch_first=True # 输入/输出张量的第一个维度是batch
)
# 全连接输出层
self.fc = nn.Linear(hidden_size, output_size)
def forward(self, x):
# 初始化隐藏状态 (num_layers, batch_size, hidden_size) # 这里只有一层RNN,所以num_layers=1
h0 = torch.zeros(1, x.size(0), self.hidden_size)
# RNN前向传播
# out: 所有时间步的隐藏状态 (batch, seq_len, hidden_size) # hn: 最后一个时间步的隐藏状态 (1, batch, hidden_size) out, hn = self.rnn(x.unsqueeze(2), h0)
# 注意: x.unsqueeze(2) 将形状从 [batch, seq_len] -> [batch, seq_len, 1] # 因为RNN期望每个时间步有特征维度
# 只取最后一个时间步的输出 (许多序列任务只关心最后输出)
last_output = out[:, -1, :]
# 通过全连接层
output = self.fc(last_output)
return output
# =====================
# 3. 初始化模型和训练设置
# =====================
# 模型参数
input_size = 1 # 每个时间步的输入特征维度 (这里我们输入的是单个数字)
hidden_size = 32 # RNN隐藏层大小
output_size = 1 # 输出维度 (预测单个数字)
# 创建模型实例
model = SimpleRNN(input_size, hidden_size, output_size)
# 损失函数和优化器
criterion = nn.MSELoss() # 均方误差损失 (回归任务)
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)
# =====================
# 4. 训练循环
# =====================
num_epochs = 100
for epoch in range(num_epochs):
# 前向传播
outputs = model(X)
loss = criterion(outputs, y)
# 反向传播和优化
optimizer.zero_grad() # 清除历史梯度
loss.backward() # 反向传播计算梯度
optimizer.step() # 更新参数
# 每10轮打印一次损失
if (epoch + 1) % 10 == 0:
print(f'Epoch [{epoch + 1}/{num_epochs}], Loss: {loss.item():.4f}')
# =====================
# 5. 测试模型
# =====================
# 创建测试序列
test_seq = torch.tensor([7, 8, 9]).float().unsqueeze(0) # 添加batch维度
print("\n测试序列:", test_seq.squeeze().tolist())
# 预测下一个数字
with torch.no_grad(): # 禁用梯度计算
prediction = model(test_seq)
print(f"预测下一个数字: {prediction.item():.1f} (期望值: 10.0)")
代码解释:
"""
关键组件说明:
1. nn.RNN 层:
- input_size: 每个时间步输入的特征维度
- hidden_size: RNN隐藏状态的大小
- batch_first: 输入/输出张量格式为 (batch, seq_len, features)
2. 隐藏状态 (hidden state): - 存储序列的历史信息
- 初始化为全零张量 (形状: [num_layers, batch_size, hidden_size])
- 在序列处理过程中不断更新
1. 前向传播流程:
- 输入形状: [batch_size, seq_len] -> 需要扩展为 [batch_size, seq_len, input_size] - RNN输出两个结果:
out: 所有时间步的隐藏状态 [batch, seq_len, hidden_size] hn: 最后一个时间步的隐藏状态 [num_layers, batch, hidden_size] - 我们通常只关心最后一个时间步的输出 (out[:, -1, :]) - 将最后输出传入全连接层得到预测结果
4. 训练过程:
- 使用均方误差(MSE)作为回归任务的损失函数
- Adam优化器更新权重
- 循环多个epoch使模型学习序列模式
"""
运行结果:
输入数据形状: torch.Size([100, 3])
目标数据形状: torch.Size([100, 1])
Epoch [10/100], Loss: 3239.7668
Epoch [20/100], Loss: 2960.7173
Epoch [30/100], Loss: 2692.1509
Epoch [40/100], Loss: 2433.7822
Epoch [50/100], Loss: 2194.8665
Epoch [60/100], Loss: 1978.9081
Epoch [70/100], Loss: 1784.7439
Epoch [80/100], Loss: 1610.4106
Epoch [90/100], Loss: 1454.0194
Epoch [100/100], Loss: 1313.7197
测试序列: [7.0, 8.0, 9.0]
预测下一个数字: 10.1 (期望值: 10.0)
**改变一:修改hidden_size
hidden_size=8
输入数据形状: torch.Size([100, 3])
目标数据形状: torch.Size([100, 1])
Epoch [10/100], Loss: 3465.6765
Epoch [20/100], Loss: 3378.4478
Epoch [30/100], Loss: 3299.8362
Epoch [40/100], Loss: 3214.9880
Epoch [50/100], Loss: 3129.9934
Epoch [60/100], Loss: 3046.5991
Epoch [70/100], Loss: 2965.3425
Epoch [80/100], Loss: 2886.3469
Epoch [90/100], Loss: 2809.4622
Epoch [100/100], Loss: 2734.4731
测试序列: [7.0, 8.0, 9.0]
预测下一个数字: 8.5 (期望值: 10.0)
hidden_size=16
输入数据形状: torch.Size([100, 3])
目标数据形状: torch.Size([100, 1])
Epoch [10/100], Loss: 3403.8201
Epoch [20/100], Loss: 3222.7463
Epoch [30/100], Loss: 3082.8389
Epoch [40/100], Loss: 2934.8223
Epoch [50/100], Loss: 2788.8025
Epoch [60/100], Loss: 2648.1326
Epoch [70/100], Loss: 2514.1960
Epoch [80/100], Loss: 2387.1860
Epoch [90/100], Loss: 2266.7705
Epoch [100/100], Loss: 2152.6624
测试序列: [7.0, 8.0, 9.0]
预测下一个数字: 9.9 (期望值: 10.0)
hidden_size=64
输入数据形状: torch.Size([100, 3])
目标数据形状: torch.Size([100, 1])
Epoch [10/100], Loss: 2981.0894
Epoch [20/100], Loss: 2482.7971
Epoch [30/100], Loss: 2024.7319
Epoch [40/100], Loss: 1634.8406
Epoch [50/100], Loss: 1318.6863
Epoch [60/100], Loss: 1065.7627
Epoch [70/100], Loss: 864.2839
Epoch [80/100], Loss: 703.8886
Epoch [90/100], Loss: 575.9725
Epoch [100/100], Loss: 473.6202
测试序列: [7.0, 8.0, 9.0]
预测下一个数字: 9.9 (期望值: 10.0)
#### hidden_size=128
输入数据形状: torch.Size([100, 3])
目标数据形状: torch.Size([100, 1])
Epoch [10/100], Loss: 2416.3115
Epoch [20/100], Loss: 1606.8107
Epoch [30/100], Loss: 1012.2352
Epoch [40/100], Loss: 632.2427
Epoch [50/100], Loss: 399.0681
Epoch [60/100], Loss: 257.7469
Epoch [70/100], Loss: 171.4580
Epoch [80/100], Loss: 117.6267
Epoch [90/100], Loss: 83.0710
Epoch [100/100], Loss: 60.3334
测试序列: [7.0, 8.0, 9.0]
预测下一个数字: 10.0 (期望值: 10.0)
hidden_size=256
输入数据形状: torch.Size([100, 3])
目标数据形状: torch.Size([100, 1])
Epoch [10/100], Loss: 1700.6941
Epoch [20/100], Loss: 687.7560
Epoch [30/100], Loss: 225.4265
Epoch [40/100], Loss: 75.9410
Epoch [50/100], Loss: 28.6406
Epoch [60/100], Loss: 12.8711
Epoch [70/100], Loss: 6.8923
Epoch [80/100], Loss: 4.2395
Epoch [90/100], Loss: 2.8668
Epoch [100/100], Loss: 6.7286
测试序列: [7.0, 8.0, 9.0]
预测下一个数字: 10.8 (期望值: 10.0)
改个脚本来自动测试一下:
import torch
import torch.nn as nn
from xformers.benchmarks.benchmark_sddmm import results
# 设置随机种子保证可重复性
torch.manual_seed(42)
# =====================
# 1. 准备模拟数据
# =====================
# 创建一个简单的序列数据集:输入序列和对应的目标值
# 输入序列: [0,1,2] -> 目标: 3; [1,2,3] -> 目标: 4 等
seq_length = 3 # 输入序列长度
data_size = 100 # 数据集大小
# 生成特征数据 (100个样本,每个样本是长度为3的序列)
X = torch.stack([torch.arange(i, i + seq_length) for i in range(data_size)]).float()
# 生成目标数据 (每个序列的下一个数字)
y = torch.stack([torch.tensor([i + seq_length]) for i in range(data_size)]).float()
print("输入数据形状:", X.shape) # torch.Size([100, 3])
print("目标数据形状:", y.shape) # torch.Size([100, 1])
# =====================
# 2. 定义RNN模型
# =====================
class SimpleRNN(nn.Module):
def __init__(self, input_size, hidden_size, output_size):
super(SimpleRNN, self).__init__()
# 定义RNN层参数
self.hidden_size = hidden_size
# RNN层: (input_size, hidden_size)
# batch_first=True 表示输入数据的格式为 (batch, seq_len, features) self.rnn = nn.RNN(
input_size=input_size,
hidden_size=hidden_size,
batch_first=True # 输入/输出张量的第一个维度是batch
)
# 全连接输出层
self.fc = nn.Linear(hidden_size, output_size)
def forward(self, x):
# 初始化隐藏状态 (num_layers, batch_size, hidden_size) # 这里只有一层RNN,所以num_layers=1
h0 = torch.zeros(1, x.size(0), self.hidden_size)
# RNN前向传播
# out: 所有时间步的隐藏状态 (batch, seq_len, hidden_size) # hn: 最后一个时间步的隐藏状态 (1, batch, hidden_size) out, hn = self.rnn(x.unsqueeze(2), h0)
# 注意: x.unsqueeze(2) 将形状从 [batch, seq_len] -> [batch, seq_len, 1] # 因为RNN期望每个时间步有特征维度
# 只取最后一个时间步的输出 (许多序列任务只关心最后输出)
last_output = out[:, -1, :]
# 通过全连接层
output = self.fc(last_output)
return output
# =====================
# 3. 初始化模型和训练设置
# =====================
# 模型参数
input_size = 1 # 每个时间步的输入特征维度 (这里我们输入的是单个数字)
hidden_size = [8, 16, 32, 64, 128, 256] # RNN隐藏层大小
output_size = 1 # 输出维度 (预测单个数字)
results = {}
# 创建模型实例
for hs in hidden_size:
model = SimpleRNN(input_size, hs, output_size)
# 损失函数和优化器
criterion = nn.MSELoss() # 均方误差损失 (回归任务)
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)
# =====================
# 4. 训练循环
# =====================
num_epochs = 100
for epoch in range(num_epochs):
# 前向传播
outputs = model(X)
loss = criterion(outputs, y)
# 反向传播和优化
optimizer.zero_grad() # 清除历史梯度
loss.backward() # 反向传播计算梯度
optimizer.step() # 更新参数
# 每10轮打印一次损失
if (epoch + 1) % 10 == 0:
print(f'Epoch [{epoch + 1}/{num_epochs}], Loss: {loss.item():.4f}')
# =====================
# 5. 测试模型
# =====================
# 创建测试序列
test_seq = torch.tensor([7, 8, 9]).float().unsqueeze(0) # 添加batch维度
print("\n测试序列:", test_seq.squeeze().tolist())
# 预测下一个数字
with torch.no_grad(): # 禁用梯度计算
prediction = model(test_seq)
# print(f"预测下一个数字: {prediction.item():.1f} (期望值: 10.0)")
results[hs] = {
"final_loss": loss.item(),
"prediction": prediction.item()
}
# 打印结果
for size, res in results.items():
print(f"hidden_size: {size:<3}, final_loss: {res['final_loss']:.5f}, prediction: {res['prediction']:.2f} (期望值=10)")
结果:
测试序列: [7.0, 8.0, 9.0]
hidden_size: 8 , final_loss: 2734.47314, prediction: 8.52 (期望值=10)
hidden_size: 16 , final_loss: 2143.95459, prediction: 9.99 (期望值=10)
hidden_size: 32 , final_loss: 1293.59521, prediction: 9.95 (期望值=10)
hidden_size: 64 , final_loss: 448.14648, prediction: 9.90 (期望值=10)
hidden_size: 128, final_loss: 60.28671, prediction: 9.60 (期望值=10)
hidden_size: 256, final_loss: 2.17465, prediction: 10.08 (期望值=10)
这个损失率…,有点离谱啊。
尝试解决:归一化
import torch
import torch.nn as nn
from xformers.benchmarks.benchmark_sddmm import results
# 设置随机种子保证可重复性
torch.manual_seed(42)
# =====================
# 1. 准备模拟数据
# =====================
# 创建一个简单的序列数据集:输入序列和对应的目标值
# 输入序列: [0,1,2] -> 目标: 3; [1,2,3] -> 目标: 4 等
seq_length = 3 # 输入序列长度
data_size = 100 # 数据集大小
# 生成特征数据 (100个样本,每个样本是长度为3的序列)
X_raw = torch.stack([torch.arange(i, i + seq_length) for i in range(data_size)]).float()
# 生成目标数据 (每个序列的下一个数字)
y_raw = torch.stack([torch.tensor([i + seq_length]) for i in range(data_size)]).float()
# print("输入数据形状:", X.shape) # torch.Size([100, 3])
# print("目标数据形状:", y.shape) # torch.Size([100, 1])
# 数据归一化
def normalize(tensor, mean=None, std=None):
if mean is None or std is None:
mean = tensor.mean()
std = tensor.std()
return (tensor - mean) / std, mean, std
# 归一化数据,并保存归一化参数
X, X_mean, X_std = normalize(X_raw)
y, y_mean, y_std = normalize(y_raw)
print(f"归一化后输入范围: {X.min().item():.4f} to {X.max().item():.4f}")
print(f"归一化后目标范围: {y.min().item():.4f} to {y.max().item():.4f}")
# =====================
# 2. 定义RNN模型
# =====================
class SimpleRNN(nn.Module):
def __init__(self, input_size, hidden_size, output_size):
super(SimpleRNN, self).__init__()
# 定义RNN层参数
self.hidden_size = hidden_size
# RNN层: (input_size, hidden_size)
# batch_first=True 表示输入数据的格式为 (batch, seq_len, features) self.rnn = nn.RNN(
input_size=input_size,
hidden_size=hidden_size,
batch_first=True # 输入/输出张量的第一个维度是batch
)
# 全连接输出层
self.fc = nn.Linear(hidden_size, output_size)
def forward(self, x):
# 初始化隐藏状态 (num_layers, batch_size, hidden_size) # 这里只有一层RNN,所以num_layers=1
h0 = torch.zeros(1, x.size(0), self.hidden_size)
# RNN前向传播
# out: 所有时间步的隐藏状态 (batch, seq_len, hidden_size) # hn: 最后一个时间步的隐藏状态 (1, batch, hidden_size) out, hn = self.rnn(x.unsqueeze(2), h0)
# 注意: x.unsqueeze(2) 将形状从 [batch, seq_len] -> [batch, seq_len, 1] # 因为RNN期望每个时间步有特征维度
# 只取最后一个时间步的输出 (许多序列任务只关心最后输出)
last_output = out[:, -1, :]
# 通过全连接层
output = self.fc(last_output)
return output
# =====================
# 3. 初始化模型和训练设置
# =====================
# 模型参数
input_size = 1 # 每个时间步的输入特征维度 (这里我们输入的是单个数字)
hidden_size = [8, 16, 32, 64, 128, 256] # RNN隐藏层大小
output_size = 1 # 输出维度 (预测单个数字)
results = {}
# 创建模型实例
for hs in hidden_size:
model = SimpleRNN(input_size, hs, output_size)
# 损失函数和优化器
criterion = nn.MSELoss() # 均方误差损失 (回归任务)
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)
# =====================
# 4. 训练循环
# =====================
num_epochs = 100
for epoch in range(num_epochs):
# 前向传播
outputs = model(X)
loss = criterion(outputs, y)
# 反向传播和优化
optimizer.zero_grad() # 清除历史梯度
loss.backward() # 反向传播计算梯度
# 梯度裁剪(防止梯度爆炸)
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
optimizer.step() # 更新参数
# 每10轮打印一次损失
if (epoch + 1) % 10 == 0:
print(f'Epoch [{epoch + 1}/{num_epochs}], Loss: {loss.item():.4f}')
# =====================
# 5. 测试模型
# =====================
# 创建测试序列
test_seq = torch.tensor([7, 8, 9]).float()
test_seq_norm = (test_seq - X_mean) / X_std
test_seq_norm = test_seq_norm.unsqueeze(0) # 添加batch维度
# print("\n测试序列:", test_seq.squeeze().tolist())
# 预测下一个数字
with torch.no_grad(): # 禁用梯度计算
prediction_norm = model(test_seq_norm)
# print(f"预测下一个数字: {prediction.item():.1f} (期望值: 10.0)")
# 将预测结果反归一化到原始空间
prediction = prediction_norm * y_std + y_mean
results[hs] = {
"final_loss": loss.item(),
"prediction": prediction.item()
}
# 打印结果
for size, res in results.items():
print(f"hidden_size: {size:<3}, final_loss: {res['final_loss']:.5f}, prediction: {res['prediction']:.2f} (期望值=10)")
运行结果:
hidden_size: 8 , final_loss: 0.00042, prediction: 10.35 (期望值=10)
hidden_size: 16 , final_loss: 0.00073, prediction: 10.73 (期望值=10)
hidden_size: 32 , final_loss: 0.00129, prediction: 10.18 (期望值=10)
hidden_size: 64 , final_loss: 0.00069, prediction: 10.54 (期望值=10)
hidden_size: 128, final_loss: 0.00040, prediction: 10.63 (期望值=10)
hidden_size: 256, final_loss: 0.01118, prediction: 8.55 (期望值=10)
结论
hidden_size
(隐藏层大小)是RNN中最关键的超参数之一,它直接影响模型的表达能力、学习能力和最终结果。让我们深入分析它与结果的关系:
1. 模型容量与表达能力
- 较小的hidden_size (如4-16):
- ✅ 训练更快,内存占用小
- ❌ 模型容量低,只能学习简单模式
- ➡️ 适合简单序列(如等差数列)
- 较大的hidden_size (如64-256):
- ✅ 能学习复杂模式和非线性关系
- ❌ 需要更多数据和训练时间
- ❌ 可能过拟合(记忆训练数据但泛化差)
- ➡️ 适合真实世界数据(如自然语言)
2. 实际建议(针对初学者)
- 起始点:从
hidden_size=32
开始(良好平衡点) - 调整策略:
- 如果欠拟合(训练损失高)→ 增大hidden_size
- 如果过拟合(训练损失低但测试差)→ 减小hidden_size或添加正则化