RNN的基本结构与前向传播
一、RNN的基本结构
1.1 基本单元结构
循环神经网络的基本单元(RNN Cell)由以下部分组成:
- 输入门:接收当前时间步的输入数据
- 隐藏状态:存储之前的信息,作为网络的"记忆"
- 输出门:产生当前时间步的输出
- 循环连接:将隐藏状态的输出反馈到输入,形成循环
+------------------------+
| |
v |
+---------+---------+ +---------+---------+
| | | |
| 输入 x(t) |--->| 隐藏状态 h(t) |---> 输出 y(t)
| | | |
+-------------------+ +-------------------+1.2 展开的RNN结构
为了更好地理解RNN的工作过程,我们可以将其按时间步展开:
t=1 t=2 t=3
+---------+ +---------+ +---------+
| | | | | |
| x(1) |-----> | x(2) |-----> | x(3) |
| | | | | |
+---------+ +---------+ +---------+
| | |
v v v
+---------+ +---------+ +---------+
| | | | | |
| h(1) |<----->| h(2) |<----->| h(3) |
| | | | | |
+---------+ +---------+ +---------+
| | |
v v v
+---------+ +---------+ +---------+
| | | | | |
| y(1) | | y(2) | | y(3) |
| | | | | |
+---------+ +---------+ +---------+在展开的结构中,我们可以看到:
- 每个时间步都有一个输入 x(t)
- 每个时间步都有一个隐藏状态 h(t)
- 每个时间步都有一个输出 y(t)
- 隐藏状态 h(t) 不仅取决于当前输入 x(t) ,还取决于上一个时间步的隐藏状态 h(t-1)
1.3 RNN的参数
RNN包含以下参数:
- **输入权重 W_{xh} **:连接输入 x(t) 到隐藏状态 h(t)
- **循环权重 W_{hh} **:连接上一个隐藏状态 h(t-1) 到当前隐藏状态 h(t)
- **隐藏偏置 b_h **:隐藏状态的偏置项
- **输出权重 W_{hy} **:连接隐藏状态 h(t) 到输出 y(t)
- **输出偏置 b_y **:输出的偏置项
这些参数在所有时间步中共享,这是RNN的一个重要特点。
二、RNN的数学原理
2.1 隐藏状态的计算
RNN的核心是隐藏状态的计算,它综合了当前输入和之前的隐藏状态:
h(t) = anh(W_{xh} dot x(t) + W_{hh} dot h(t-1) + b_h)
其中:
anh 是激活函数,用于引入非线性- x(t) 是当前时间步的输入
- h(t-1) 是上一个时间步的隐藏状态
- W_{xh}, W_{hh}, b_h 是模型参数
2.2 输出的计算
当前时间步的输出由隐藏状态计算得到:
y(t) = W_{hy} dot h(t) + b_y
对于分类任务,通常会在输出层后添加softmax激活函数:
at{y}(t) = ext{softmax}(y(t))
2.3 初始隐藏状态
在处理序列的第一个元素时,需要一个初始隐藏状态 h(0) 。通常,我们将其初始化为全零向量:
h(0) = 0
三、RNN的前向传播过程
3.1 前向传播步骤
RNN的前向传播过程按时间步依次进行:
- 初始化:设置初始隐藏状态 h(0) = 0
- 时间步 t=1:
- 输入 x(1)
- 计算隐藏状态 h(1) = anh(W_{xh} dot x(1) + W_{hh} dot h(0) + b_h)
- 计算输出 y(1) = W_{hy} dot h(1) + b_y
- 时间步 t=2:
- 输入 x(2)
- 计算隐藏状态 h(2) = anh(W_{xh} dot x(2) + W_{hh} dot h(1) + b_h)
- 计算输出 y(2) = W_{hy} dot h(2) + b_y
- 重复:直到处理完整个序列的所有时间步
- 返回:所有时间步的输出和最终的隐藏状态
3.2 前向传播示例
假设我们有一个简单的RNN,参数如下:
- W_{xh} = [[0.1, 0.2], [0.3, 0.4]]
- W_{hh} = [[0.5, 0.6], [0.7, 0.8]]
- b_h = [0.1, 0.2]
- W_{hy} = [[0.9, 0.8], [0.7, 0.6]]
- b_y = [0.1, 0.2]
输入序列为:
- x(1) = [1.0, 2.0]
- x(2) = [3.0, 4.0]
计算过程:
初始化: h(0) = [0, 0]
时间步 t=1:
- W_{xh} dot x(1) = [[0.1, 0.2], [0.3, 0.4]] dot [1.0, 2.0] = [0.5, 1.1]
- W_{hh} dot h(0) = [[0.5, 0.6], [0.7, 0.8]] dot [0, 0] = [0, 0]
- h(1) = anh([0.5, 1.1] + [0, 0] + [0.1, 0.2]) = anh([0.6, 1.3]) ≈ [0.537, 0.861]
- y(1) = [[0.9, 0.8], [0.7, 0.6]] dot [0.537, 0.861] + [0.1, 0.2] ≈ [1.33, 1.09]
时间步 t=2:
- W_{xh} dot x(2) = [[0.1, 0.2], [0.3, 0.4]] dot [3.0, 4.0] = [1.1, 2.5]
- W_{hh} dot h(1) = [[0.5, 0.6], [0.7, 0.8]] dot [0.537, 0.861] ≈ [0.775, 1.056]
- h(2) = anh([1.1, 2.5] + [0.775, 1.056] + [0.1, 0.2]) = anh([1.975, 3.756]) ≈ [0.964, 0.999]
- y(2) = [[0.9, 0.8], [0.7, 0.6]] dot [0.964, 0.999] + [0.1, 0.2] ≈ [1.87, 1.47]
3.3 向量化计算
为了提高计算效率,实际实现中通常使用向量化计算,同时处理整个批次的序列:
- 输入:形状为 (batch_size, seq_length, input_size)
- 隐藏状态:形状为 (num_layers, batch_size, hidden_size)
- 输出:形状为 (batch_size, seq_length, output_size)
向量化计算大大提高了RNN的训练和推理速度。
四、不同类型的RNN结构
4.1 一对一结构
输入是单个向量,输出也是单个向量,类似于传统的前馈神经网络。这种结构通常用于不需要考虑序列关系的任务。
+---------+ +---------+ +---------+
| | | | | |
| 输入 x | ----> | 隐藏层h | ----> | 输出 y |
| | | | | |
+---------+ +---------+ +---------+4.2 一对多结构
输入是单个向量,输出是一个序列。这种结构常用于生成任务,如图像描述生成。
时间步 t=1 时间步 t=2 时间步 t=3
+---------+ +---------+ +---------+ +---------+
| | | | | | | |
| 输入 x | ----> | 隐藏层h1| ----> | 隐藏层h2| ----> | 隐藏层h3|
| | | | | | | |
+---------+ +---------+ +---------+ +---------+
| | |
v v v
+---------+ +---------+ +---------+
| | | | | |
| 输出 y1 | | 输出 y2 | | 输出 y3 |
| | | | | |
+---------+ +---------+ +---------+4.3 多对一结构
输入是一个序列,输出是单个向量。这种结构常用于序列分类任务,如情感分析。
时间步 t=1 时间步 t=2 时间步 t=3 +---------+
+---------+ +---------+ +---------+ | |
| | | | | | | 输出 y |
| 输入 x1 | ----> | 输入 x2 | ----> | 输入 x3 | ----> | |
| | | | | | +---------+
+---------+ +---------+ +---------+
| | |
v v v
+---------+ +---------+ +---------+
| | | | | |
| 隐藏层h1|<----->| 隐藏层h2|<----->| 隐藏层h3|
| | | | | |
+---------+ +---------+ +---------+4.4 多对多结构(同步)
输入是一个序列,输出也是一个序列,且输入和输出的长度相同。这种结构常用于序列标注任务,如命名实体识别。
时间步 t=1 时间步 t=2 时间步 t=3
+---------+ +---------+ +---------+
| | | | | |
| 输入 x1 | ----> | 输入 x2 | ----> | 输入 x3 |
| | | | | |
+---------+ +---------+ +---------+
| | |
v v v
+---------+ +---------+ +---------+
| | | | | |
| 隐藏层h1|<----->| 隐藏层h2|<----->| 隐藏层h3|
| | | | | |
+---------+ +---------+ +---------+
| | |
v v v
+---------+ +---------+ +---------+
| | | | | |
| 输出 y1 | | 输出 y2 | | 输出 y3 |
| | | | | |
+---------+ +---------+ +---------+4.5 多对多结构(异步)
输入是一个序列,输出也是一个序列,但输入和输出的长度可以不同。这种结构常用于机器翻译等任务,通常采用编码器-解码器架构。
编码器部分(处理输入序列):
时间步 t=1 时间步 t=2 时间步 t=3
+---------+ +---------+ +---------+
| | | | | |
| 输入 x1 | ----> | 输入 x2 | ----> | 输入 x3 |
| | | | | |
+---------+ +---------+ +---------+
| | |
v v v
+---------+ +---------+ +---------+
| | | | | |
| 隐藏层h1|<----->| 隐藏层h2|<----->| 隐藏层h3|
| | | | | |
+---------+ +---------+ +---------+
|
v
解码器部分(生成输出序列):
时间步 t=1 时间步 t=2 时间步 t=3
+---------+ +---------+ +---------+
| | | | | |
| 输入 y0 | ----> | 输入 y1 | ----> | 输入 y2 |
| | | | | |
+---------+ +---------+ +---------+
| | |
v v v
+---------+ +---------+ +---------+
| | | | | |
| 隐藏层d1|<----->| 隐藏层d2|<----->| 隐藏层d3|
| | | | | |
+---------+ +---------+ +---------+
| | |
v v v
+---------+ +---------+ +---------+
| | | | | |
| 输出 y1 | | 输出 y2 | | 输出 y3 |
| | | | | |
+---------+ +---------+ +---------+五、RNN的参数计算
5.1 参数数量计算
RNN的参数数量取决于输入维度、隐藏状态维度和输出维度:
- 输入到隐藏的权重: W_{xh} 的形状为 (hidden_size, input_size)
- 隐藏到隐藏的权重: W_{hh} 的形状为 (hidden_size, hidden_size)
- 隐藏层偏置: b_h 的形状为 (hidden_size, 1)
- 隐藏到输出的权重: W_{hy} 的形状为 (output_size, hidden_size)
- 输出层偏置: b_y 的形状为 (output_size, 1)
总参数数量为:
ext{参数数量} = hiddensize imes (inputsize + hiddensize + 1) + outputsize imes (hiddensize + 1) 5.2 参数共享
RNN的一个重要特点是参数共享:
- 时间步共享:所有时间步使用相同的参数集
- 参数效率:减少了模型的参数量,提高了泛化能力
- 模式学习:有助于学习序列中的通用模式
六、RNN的代码实现
6.1 使用Python实现基本的RNN前向传播
import numpy as np
def rnn_forward(x, h_prev, Wxh, Whh, Why, bh, by):
"""
RNN前向传播
x: 输入序列,形状为 (seq_length, input_size)
h_prev: 初始隐藏状态,形状为 (hidden_size,)
Wxh: 输入到隐藏的权重,形状为 (hidden_size, input_size)
Whh: 隐藏到隐藏的权重,形状为 (hidden_size, hidden_size)
Why: 隐藏到输出的权重,形状为 (output_size, hidden_size)
bh: 隐藏层偏置,形状为 (hidden_size,)
by: 输出层偏置,形状为 (output_size,)
"""
seq_length, input_size = x.shape
hidden_size = Wxh.shape[0]
output_size = Why.shape[0]
# 存储隐藏状态和输出
hs = np.zeros((seq_length, hidden_size))
ys = np.zeros((seq_length, output_size))
h = h_prev
for t in range(seq_length):
# 获取当前时间步的输入
x_t = x[t]
# 计算隐藏状态
h = np.tanh(np.dot(Wxh, x_t) + np.dot(Whh, h) + bh)
# 计算输出
y = np.dot(Why, h) + by
# 存储结果
hs[t] = h
ys[t] = y
return ys, hs, h
# 示例使用
if __name__ == "__main__":
# 定义参数
seq_length = 3
input_size = 2
hidden_size = 2
output_size = 2
# 初始化参数
Wxh = np.random.randn(hidden_size, input_size) * 0.01
Whh = np.random.randn(hidden_size, hidden_size) * 0.01
Why = np.random.randn(output_size, hidden_size) * 0.01
bh = np.zeros(hidden_size)
by = np.zeros(output_size)
# 生成输入序列
x = np.random.randn(seq_length, input_size)
# 初始化隐藏状态
h_prev = np.zeros(hidden_size)
# 前向传播
ys, hs, h_final = rnn_forward(x, h_prev, Wxh, Whh, Why, bh, by)
print("输入序列:")
print(x)
print("\n隐藏状态序列:")
print(hs)
print("\n输出序列:")
print(ys)
print("\n最终隐藏状态:")
print(h_final)6.2 使用PyTorch实现RNN
import torch
import torch.nn as nn
class SimpleRNN(nn.Module):
def __init__(self, input_size, hidden_size, output_size):
super(SimpleRNN, self).__init__()
self.hidden_size = hidden_size
# 定义RNN的线性层
self.Wxh = nn.Linear(input_size, hidden_size)
self.Whh = nn.Linear(hidden_size, hidden_size)
self.Why = nn.Linear(hidden_size, output_size)
def forward(self, x, h_prev=None):
"""
前向传播
x: 输入序列,形状为 (batch_size, seq_length, input_size)
h_prev: 初始隐藏状态,形状为 (batch_size, hidden_size)
"""
batch_size, seq_length, input_size = x.size()
# 初始化隐藏状态
if h_prev is None:
h_prev = torch.zeros(batch_size, self.hidden_size).to(x.device)
# 存储隐藏状态和输出
hs = []
ys = []
h = h_prev
for t in range(seq_length):
# 获取当前时间步的输入
x_t = x[:, t, :]
# 计算隐藏状态
h = torch.tanh(self.Wxh(x_t) + self.Whh(h))
# 计算输出
y = self.Why(h)
# 存储结果
hs.append(h.unsqueeze(1))
ys.append(y.unsqueeze(1))
# 拼接结果
hs = torch.cat(hs, dim=1)
ys = torch.cat(ys, dim=1)
return ys, hs, h
# 示例使用
if __name__ == "__main__":
# 定义参数
batch_size = 2
seq_length = 3
input_size = 2
hidden_size = 2
output_size = 2
# 创建模型
model = SimpleRNN(input_size, hidden_size, output_size)
# 生成输入
x = torch.randn(batch_size, seq_length, input_size)
# 前向传播
ys, hs, h_final = model(x)
print("输入形状:", x.shape)
print("输出形状:", ys.shape)
print("隐藏状态形状:", hs.shape)
print("最终隐藏状态形状:", h_final.shape)
# 使用PyTorch内置的RNN
print("\n使用PyTorch内置的RNN:")
rnn = nn.RNN(input_size, hidden_size, batch_first=True)
output, hn = rnn(x)
print("内置RNN输出形状:", output.shape)
print("内置RNN最终隐藏状态形状:", hn.shape)6.3 使用TensorFlow实现RNN
import tensorflow as tf
class SimpleRNN(tf.keras.Model):
def __init__(self, hidden_size, output_size):
super(SimpleRNN, self).__init__()
self.hidden_size = hidden_size
self.rnn_cell = tf.keras.layers.SimpleRNNCell(hidden_size)
self.dense = tf.keras.layers.Dense(output_size)
def call(self, inputs, initial_state=None):
# 输入形状: (batch_size, seq_length, input_size)
batch_size, seq_length, input_size = inputs.shape
if initial_state is None:
initial_state = tf.zeros((batch_size, self.hidden_size))
# 存储隐藏状态和输出
hidden_states = []
outputs = []
state = initial_state
for t in range(seq_length):
# 获取当前时间步的输入
x_t = inputs[:, t, :]
# 计算隐藏状态
output, state = self.rnn_cell(x_t, state)
# 计算最终输出
final_output = self.dense(output)
# 存储结果
hidden_states.append(state)
outputs.append(final_output)
# 拼接结果
hidden_states = tf.stack(hidden_states, axis=1)
outputs = tf.stack(outputs, axis=1)
return outputs, hidden_states, state
# 示例使用
if __name__ == "__main__":
# 定义参数
batch_size = 2
seq_length = 3
input_size = 2
hidden_size = 2
output_size = 2
# 创建模型
model = SimpleRNN(hidden_size, output_size)
# 生成输入
inputs = tf.random.normal((batch_size, seq_length, input_size))
# 前向传播
outputs, hidden_states, final_state = model(inputs)
print("输入形状:", inputs.shape)
print("输出形状:", outputs.shape)
print("隐藏状态形状:", hidden_states.shape)
print("最终隐藏状态形状:", final_state.shape)
# 使用TensorFlow内置的RNN
print("\n使用TensorFlow内置的RNN:")
rnn_layer = tf.keras.layers.SimpleRNN(hidden_size, return_sequences=True, return_state=True)
output, state = rnn_layer(inputs)
print("内置RNN输出形状:", output.shape)
print("内置RNN最终隐藏状态形状:", state.shape)七、RNN的实际应用示例
7.1 字符级语言模型
字符级语言模型使用RNN预测下一个字符,给定前几个字符:
import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim
# 准备数据
text = "hello world"
chars = list(set(text))
char_to_idx = {char: idx for idx, char in enumerate(chars)}
idx_to_char = {idx: char for idx, char in enumerate(chars)}
# 超参数
seq_length = 3
hidden_size = 10
learning_rate = 0.01
epochs = 1000
# 准备训练数据
data = [char_to_idx[char] for char in text]
x_data = []
y_data = []
for i in range(len(data) - seq_length):
x_seq = data[i:i+seq_length]
y_seq = data[i+1:i+seq_length+1]
x_data.append(x_seq)
y_data.append(y_seq)
x_data = torch.tensor(x_data, dtype=torch.long)
y_data = torch.tensor(y_data, dtype=torch.long)
# 转换为one-hot编码
def one_hot_encode(x, num_classes):
batch_size, seq_length = x.shape
one_hot = torch.zeros(batch_size, seq_length, num_classes)
for i in range(batch_size):
for j in range(seq_length):
one_hot[i, j, x[i, j]] = 1
return one_hot
x_one_hot = one_hot_encode(x_data, len(chars))
# 定义模型
class CharRNN(nn.Module):
def __init__(self, input_size, hidden_size, output_size):
super(CharRNN, self).__init__()
self.hidden_size = hidden_size
self.rnn = nn.RNN(input_size, hidden_size, batch_first=True)
self.fc = nn.Linear(hidden_size, output_size)
def forward(self, x, h_prev=None):
out, h = self.rnn(x, h_prev)
out = self.fc(out)
return out, h
# 创建模型
model = CharRNN(len(chars), hidden_size, len(chars))
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=learning_rate)
# 训练模型
for epoch in range(epochs):
optimizer.zero_grad()
output, _ = model(x_one_hot)
loss = criterion(output.view(-1, len(chars)), y_data.view(-1))
loss.backward()
optimizer.step()
if (epoch + 1) % 100 == 0:
print(f"Epoch: {epoch+1}, Loss: {loss.item():.4f}")
# 生成文本
def generate_text(model, start_seq, length=10):
model.eval()
with torch.no_grad():
# 初始化输入
input_seq = torch.tensor([[char_to_idx[char] for char in start_seq]], dtype=torch.long)
input_one_hot = one_hot_encode(input_seq, len(chars))
# 初始化隐藏状态
h = None
# 生成文本
generated = start_seq
for _ in range(length):
output, h = model(input_one_hot, h)
# 预测下一个字符
prob = torch.softmax(output[:, -1, :], dim=1)
next_char_idx = torch.multinomial(prob, 1).item()
next_char = idx_to_char[next_char_idx]
# 添加到生成的文本中
generated += next_char
# 更新输入
input_seq = torch.tensor([[next_char_idx]], dtype=torch.long)
input_one_hot = one_hot_encode(input_seq, len(chars))
return generated
# 测试生成文本
print("\n生成文本:")
print(generate_text(model, "hel", length=10))7.2 简单的情感分析
使用RNN进行简单的情感分析,判断句子的情感倾向:
import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim
# 准备数据
sentences = ["I love this movie", "This film is great", "I hate this film", "This movie is terrible"]
sentiments = [1, 1, 0, 0] # 1: positive, 0: negative
# 构建词汇表
words = set()
for sentence in sentences:
words.update(sentence.lower().split())
word_to_idx = {word: idx for idx, word in enumerate(words)}
# 超参数
embedding_dim = 5
hidden_size = 10
learning_rate = 0.01
epochs = 1000
# 准备训练数据
x_data = []
y_data = []
for sentence, sentiment in zip(sentences, sentiments):
word_indices = [word_to_idx[word.lower()] for word in sentence.split()]
x_data.append(word_indices)
y_data.append(sentiment)
# 填充序列到相同长度
max_length = max(len(seq) for seq in x_data)
x_padded = []
for seq in x_data:
padded = seq + [0] * (max_length - len(seq))
x_padded.append(padded)
x_data = torch.tensor(x_padded, dtype=torch.long)
y_data = torch.tensor(y_data, dtype=torch.float32)
# 定义模型
class SentimentRNN(nn.Module):
def __init__(self, vocab_size, embedding_dim, hidden_size, output_size):
super(SentimentRNN, self).__init__()
self.embedding = nn.Embedding(vocab_size, embedding_dim)
self.rnn = nn.RNN(embedding_dim, hidden_size, batch_first=True)
self.fc = nn.Linear(hidden_size, output_size)
self.sigmoid = nn.Sigmoid()
def forward(self, x):
# 嵌入层
embedded = self.embedding(x)
# RNN层
out, h = self.rnn(embedded)
# 使用最后一个时间步的隐藏状态
out = self.fc(h.squeeze(0))
out = self.sigmoid(out)
return out
# 创建模型
model = SentimentRNN(len(words), embedding_dim, hidden_size, 1)
criterion = nn.BCELoss()
optimizer = optim.Adam(model.parameters(), lr=learning_rate)
# 训练模型
for epoch in range(epochs):
optimizer.zero_grad()
output = model(x_data)
loss = criterion(output.squeeze(), y_data)
loss.backward()
optimizer.step()
if (epoch + 1) % 100 == 0:
# 计算准确率
predictions = (output.squeeze() > 0.5).float()
accuracy = (predictions == y_data).float().mean()
print(f"Epoch: {epoch+1}, Loss: {loss.item():.4f}, Accuracy: {accuracy.item():.4f}")
# 测试模型
test_sentences = ["I love this film", "This movie is terrible"]
print("\n测试结果:")
for sentence in test_sentences:
word_indices = [word_to_idx[word.lower()] for word in sentence.split()]
padded = word_indices + [0] * (max_length - len(word_indices))
input_tensor = torch.tensor([padded], dtype=torch.long)
output = model(input_tensor)
sentiment = "positive" if output.item() > 0.5 else "negative"
print(f"Sentence: {sentence}, Sentiment: {sentiment}")八、总结与思考
通过本教程的学习,我们详细了解了RNN的基本结构和前向传播过程:
- 基本结构:RNN由输入门、隐藏状态、输出门和循环连接组成,通过隐藏状态存储之前的信息
- 数学原理:隐藏状态的计算综合了当前输入和之前的隐藏状态,输出由隐藏状态计算得到
- 前向传播:按时间步依次计算,将隐藏状态的输出反馈到输入,形成循环
- 不同结构:根据输入和输出的形式,RNN可以分为一对一、一对多、多对一和多对多等结构
- 代码实现:我们使用Python、PyTorch和TensorFlow实现了RNN的前向传播
- 实际应用:通过字符级语言模型和情感分析的示例,展示了RNN的应用
思考问题
- RNN中的循环连接有什么作用?它如何帮助RNN处理序列数据?
- 为什么RNN在所有时间步共享参数?这有什么优势和劣势?
- 在RNN的前向传播中,隐藏状态的计算为什么使用tanh激活函数?可以使用其他激活函数吗?
- 对于不同长度的序列,RNN是如何处理的?
- 你认为RNN最适合处理哪些类型的任务?为什么?