从RNN到LSTM(包含代码实现)

本文最后更新于 2025年2月19日 晚上

一,RNN

  • 红色:输入层$(X_i)$
  • 绿色:隐藏层$(h_i)$
  • 蓝色:输出层$(Y_i)$

1. 内核公式

  • 核心思想:每个时间步(time step)接收当前输入和前一时刻的隐藏状态,生成当前输出和新的隐藏状态。
  • 核心公式:看似是每个进行计算,其实还是基于矩阵的。

$$
h_t = \sigma(W_{xh} x_t + W_{hh} h_{t-1} + b_h)
$$

$$ y_t = W_{hy} h_t + b_y $$
  • $ h_t $: 当前时刻的隐藏状态
  • $ x_t $: 当前时刻的输入
  • $ y_t $: 当前时刻的输出
  • $ W_{xh}, W_{hh}, W_{hy} $: 权重矩阵
  • $ \sigma $: 激活函数(如tanh或ReLU)

在RNN中,权重矩阵的格式(即维度)是由输入维度、隐藏状态维度和输出维度共同决定的。以下是具体说明。


2. 权重矩阵的格式(维度)

假设:

  • 输入向量 $ x_t $ 的维度为 $ d $(即 $ x_t \in \mathbb{R}^d $)。
  • 隐藏状态 $ h_t $ 的维度为 $ D $(即 $ h_t \in \mathbb{R}^D $)。
  • 输出向量 $ y_t $ 的维度为 $ k $(即 $ y_t \in \mathbb{R}^k $)。

那么,权重矩阵的格式如下:

(1) 输入到隐藏层的权重矩阵 $ W_{xh} $

  • 维度:$ D \times d $
  • 作用:将输入 $ x_t $ 从 $ d $ 维映射到隐藏层的 $ D $ 维空间。
  • 公式
    $$
    W_{xh} \in \mathbb{R}^{D \times d}, \quad W_{xh} x_t \in \mathbb{R}^D
    $$

(2) 隐藏层到隐藏层的权重矩阵 $ W_{hh} $

  • 维度:$ D \times D $
  • 作用:将前一步的隐藏状态 $ h_{t-1} $ 映射到当前隐藏状态 $ h_t $ 的空间。
  • 公式
    $$
    W_{hh} \in \mathbb{R}^{D \times D}, \quad W_{hh} h_{t-1} \in \mathbb{R}^D
    $$

(3) 隐藏层到输出层的权重矩阵 $ W_{hy} $

  • 维度:$ k \times D $
  • 作用:将隐藏状态 $ h_t $ 从 $ D $ 维映射到输出 $ y_t $ 的 $ k $ 维空间。
  • 公式
    $$
    W_{hy} \in \mathbb{R}^{k \times D}, \quad W_{hy} h_t \in \mathbb{R}^k
    $$

(4) 偏置向量 $ b_h $ 和 $ b_y $

  • **隐藏层偏置 $ b_h $**:维度为 $ D $(即 $ b_h \in \mathbb{R}^D $)。
  • **输出层偏置 $ b_y $**:维度为 $ k $(即 $ b_y \in \mathbb{R}^k $)。

3. 具体示例

假设:

  • 输入维度 $ d = 3 $(例如一个3维的词向量)。
  • 隐藏层维度 $ D = 5 $。
  • 输出维度 $ k = 2 $(例如二分类任务)。

那么,权重矩阵的格式为:

  1. $ W_{xh} \in \mathbb{R}^{5 \times 3} $
  2. $ W_{hh} \in \mathbb{R}^{5 \times 5} $
  3. $ W_{hy} \in \mathbb{R}^{2 \times 5} $
  4. $ b_h \in \mathbb{R}^5 $
  5. $ b_y \in \mathbb{R}^2 $

4. 权重矩阵的初始化

在实际训练中,权重矩阵通常通过以下方式初始化:

  • 随机初始化:从均匀分布或正态分布中随机采样。
  • Xavier初始化:适用于Sigmoid或Tanh激活函数,初始化范围为 $ \left[-\sqrt{\frac{6}{d + D}}, \sqrt{\frac{6}{d + D}}\right] $。
  • He初始化:适用于ReLU激活函数,初始化范围为 $ \left[-\sqrt{\frac{6}{d}}, \sqrt{\frac{6}{d}}\right] $。

5. 总结

  • 权重矩阵的格式由输入维度 $ d $、隐藏层维度 $ D $ 和输出维度 $ k $ 决定。
  • $ W_{xh} $:$ D \times d $
  • $ W_{hh} $:$ D \times D $
  • $ W_{hy} $:$ k \times D $
  • 偏置向量:$ b_h \in \mathbb{R}^D $,$ b_y \in \mathbb{R}^k $

通过这种设计,RNN能够灵活地处理不同维度的输入、隐藏状态和输出,同时确保时间步 $ t $ 的计算流程一致。

二,LSTM

传统RNN
LSTM的改进

LSTM(Long Short-Term Memory,长短期记忆网络) 是一种特殊的循环神经网络(RNN),专门设计用来解决传统RNN在处理长序列数据时的梯度消失长程依赖问题。LSTM通过引入门控机制记忆单元,能够有效地捕捉序列数据中的长期依赖关系,广泛应用于自然语言处理、语音识别、时间序列预测等领域。


1. LSTM的核心思想

LSTM的核心是记忆单元(Cell State)门控机制。记忆单元负责保存长期信息,而门控机制(输入门、遗忘门、输出门)则控制信息的流动,决定哪些信息需要保留、哪些需要丢弃。


2. LSTM的结构


$ \odot $ 表示逐元素相乘(Hadamard积),LSTM的每个时间步包含以下关键组件:

(1) 记忆单元(Cell State)

  • 记忆单元 $ C_t $ 是LSTM的核心,负责保存长期信息。
  • 它在时间步之间传递,并通过门控机制更新。

(2) 遗忘门(Forget Gate)

  • 决定从记忆单元中丢弃哪些信息。
  • 公式:
    $$
    f_t = \sigma(W_f \cdot [h_{t-1}, x_t] + b_f)
    $$
    • $ f_t $ 是遗忘门的输出(范围在0到1之间)。
    • $ W_f $ 是权重矩阵,$ b_f $ 是偏置。
    • $ \sigma $ 是Sigmoid激活函数。

(3) 输入门(Input Gate)

  • 决定哪些新信息需要存储到记忆单元中。
  • 公式:
    $$
    i_t = \sigma(W_i \cdot [h_{t-1}, x_t] + b_i)
    $$ $$ g_t = \tanh(W_C \cdot [h_{t-1}, x_t] + b_C) $$
    • $ i_t $ 是输入门的输出。
    • $ g_t $ 是候选记忆单元值。

(4) 更新记忆单元

  • 结合遗忘门和输入门的信息,更新记忆单元:
    $$
    C_t = f_t \odot C_{t-1} + i_t \odot g_t
    $$
    • $ \odot $ 表示逐元素相乘(Hadamard积)。

(5) 输出门(Output Gate)

  • 决定从记忆单元中输出哪些信息到隐藏状态。
  • 公式:
    $$
    o_t = \sigma(W_o \cdot [h_{t-1}, x_t] + b_o)
    $$ $$ h_t = o_t \odot \tanh(C_t) $$
    • $ o_t $ 是输出门的输出。
    • $ h_t $ 是当前时间步的隐藏状态。

3. LSTM的工作流程

  1. 遗忘门决定从记忆单元中丢弃哪些信息。
  2. 输入门决定将哪些新信息存储到记忆单元中。
  3. 更新记忆单元,结合遗忘门和输入门的信息。
  4. 输出门决定从记忆单元中输出哪些信息到隐藏状态。
  5. 隐藏状态 $ h_t $ 作为当前时间步的输出,并传递到下一个时间步。

4.生成输出 $ y_t $ 的公式

  1. 线性变换
    $$
    y_t = W_{hy} \cdot h_t + b_y
    $$

    • $ W_{hy} \in \mathbb{R}^{k \times D} $: 输出层权重矩阵($ k $ 是输出维度,$ D $ 是隐藏状态维度)。
    • $ b_y \in \mathbb{R}^k $: 输出层偏置。
    • $ h_t \in \mathbb{R}^D $: 当前时间步的隐藏状态。
  2. 激活函数(根据任务选择)

    • 分类任务(Softmax)
      $$
      y_t = \text{Softmax}(W_{hy} \cdot h_t + b_y)
      $$
    • 二分类任务(Sigmoid)
      $$
      y_t = \sigma(W_{hy} \cdot h_t + b_y)
      $$
    • 回归任务(线性激活)
      $$
      y_t = W_{hy} \cdot h_t + b_y
      $$

三,pytorch实现LSTM

  • 因为RNN的效果和LSTM相差较大,因此这里只放LSTM的,将网络结构定义改变即可实现RNN。

  • 词汇表和词嵌入映射均下载在本地。

  • 数据为评论+类别,任务是10分类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import Dataset, DataLoader
import pickle
import numpy as np
from tqdm import tqdm

# 加载 npz 文件
npz = np.load("C:\\Users\jimes\PycharmProjects\课程\PyTorch框架(2022重录)\第七章:LSTM文本分类实战\\text\THUCNews\data\embedding_Tencent.npz")
#print(len(npz['embeddings'])) # 输出文件中包含的数组名称
# 打开文件并读取词汇表
with open("C:\\Users\\jimes\\PycharmProjects\\课程\\PyTorch框架(2022重录)\\第七章:LSTM文本分类实战\\text\THUCNews\data\\vocab.pkl", "rb") as file:
vocab = pickle.load(file)

# 数据预加载
train_dataset = []
with open('./data/train.txt','r', encoding="utf-8") as file:
while True:
line = file.readline() # 使用readline()逐行读取
if not line: # 如果读到文件末尾,line为空字符串,退出循环
break
columns = line.strip().split("\t") # 使用strip()去除首尾空白字符,然后按Tab分割
train_dataset.append(columns)
test_dataset = []
with open('./data/dev.txt','r', encoding="utf-8") as file:
while True:
line = file.readline() # 使用readline()逐行读取
if not line: # 如果读到文件末尾,line为空字符串,退出循环
break
columns = line.strip().split("\t") # 使用strip()去除首尾空白字符,然后按Tab分割
test_dataset.append(columns)

for i in range(len(train_dataset)):
ind1 = []
for j in range(25): # 文字最大长度
try:
ind1.append(vocab[train_dataset[i][0][j]])
except KeyError:
ind1.append(vocab['<UNK>'])
except IndexError:
ind1.append(vocab['<PAD>'])
train_dataset[i][0] = ind1

for i in range(len(test_dataset)):
ind2 = []
for j in range(25): # 文字最大长度
try:
ind2.append(vocab[test_dataset[i][0][j]])
except KeyError:
ind2.append(vocab['<UNK>'])
except IndexError:
ind2.append(vocab['<PAD>'])
test_dataset[i][0] = ind2

class CustomDataset(Dataset):
def __init__(self, data):
"""
初始化数据集
data: 原始数据集,格式为 [[features, label], ...]
"""
self.data = data
def __len__(self):
#返回数据集的大小
return len(self.data)
def __getitem__(self, idx):
"""
根据索引返回一个样本及其标签
idx: 样本索引
return: (features, label)
"""
features, label = self.data[idx]
# 将 features 转换为 Tensor
features = torch.tensor(features, dtype=torch.long)
# 将标签转换为 Tensor(假设标签是整数)
label = torch.tensor(int(label), dtype=torch.long)
return features, label
train_dataset = CustomDataset(train_dataset)
test_dataset = CustomDataset(test_dataset)
# 创建DataLoader
train_loader = DataLoader(
train_dataset,
batch_size=8,
shuffle=False)
test_loader = DataLoader(
test_dataset,
batch_size=8,
shuffle=False)

# 词嵌入二维列表
embedding_matrix = torch.tensor(npz['embeddings'], dtype=torch.float)
# 检查词嵌入的维度
vocab_size, embedding_dim = embedding_matrix.shape
print(f"词汇表大小: {vocab_size}, 词嵌入维度: {embedding_dim}")

# 定义RNN模型
class SimpleLSTM(nn.Module):
def __init__(self, vocab_size, embedding_dim, hidden_dim, output_dim, num_layers, pad_idx, embedding_matrix=None):
"""
初始化 RNN 网络
vocab_size: 词汇表大小
embedding_dim: 词嵌入维度
hidden_dim: 隐藏层维度
output_dim: 输出维度(类别数)
pad_idx: <PAD> 的索引
embedding_matrix: 预训练的词嵌入矩阵(可选)
"""
super(SimpleLSTM, self).__init__()
# 词嵌入层
self.embedding = nn.Embedding(vocab_size, embedding_dim, padding_idx=pad_idx)
if embedding_matrix is not None:
self.embedding.weight = nn.Parameter(embedding_matrix, requires_grad=False) # 使用预训练的词嵌入
# LSTM 层
self.hidden_size = hidden_dim
self.num_layers = num_layers
self.lstm = nn.LSTM(embedding_dim, hidden_dim, num_layers, batch_first=True)
# 全连接层
self.fc = nn.Linear(hidden_dim, output_dim)
# 激活函数
self.softmax = nn.LogSoftmax(dim=1)
def forward(self, x):
"""
前向传播
x: 输入的序列数据,形状为 [batch_size, seq_len]
return: 输出的类别概率
"""
# 将输入索引转换为嵌入向量
x = self.embedding(x) # (batch_size, sequence_length) -> (batch_size, sequence_length, embed_size)
# 初始化隐藏状态和细胞状态
h0 = torch.zeros(self.num_layers, x.size(0), self.hidden_size).to(x.device)
c0 = torch.zeros(self.num_layers, x.size(0), self.hidden_size).to(x.device)
# 前向传播LSTM
_, (hidden, _) = self.lstm(x, (h0, c0)) # hidden: (batch_size, sequence_length, hidden_size)
# 使用 LSTM 的最后一个隐藏状态
hidden = hidden[-1] # [batch_size, hidden_dim]
# 全连接层
output = self.fc(hidden) # [batch_size, output_dim]
# 激活函数
return self.softmax(output)

# 超参数
vocab_size = 4762 # 词汇表大小
embedding_dim = 200 # 词嵌入维度
hidden_dim = 128 # 隐藏层维度
num_layers = 2 # LSTM的层数
output_dim = 10 # 输出类别数
pad_idx = 4761 # <PAD> 的索引
#embedding = nn.Embedding.from_pretrained(embedding_matrix, freeze=True) # 词嵌入不变
# 初始化模型
model = SimpleLSTM(vocab_size, embedding_dim, hidden_dim, output_dim,num_layers, pad_idx, embedding_matrix)

# 定义损失函数和优化器
criterion = nn.NLLLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)

# 训练模型
num_epochs = 1
for epoch in tqdm(range(num_epochs)):
model.train()
total_loss = 0
for inputs, labels in train_loader:
optimizer.zero_grad()
outputs = model(inputs) # 实际对应forward函数
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
total_loss += loss.item()
print(f"Epoch [{epoch+1}/{num_epochs}], Loss: {total_loss/len(train_loader):.4f}")

# 测试网络
model.eval()
correct = 0
total = 0
with torch.no_grad():
for inputs, labels in test_loader:
outputs = model(inputs)
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()

print(f'Accuracy of the network on the 10000 test: {100 * correct / total:.2f}%')

# 保存模型
torch.save(model.state_dict(), "LSTM_model.pth")

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
import torch
import torch.nn as nn
import pickle
import numpy as np

# 加载 npz 文件
npz = np.load("C:\\Users\jimes\PycharmProjects\课程\PyTorch框架(2022重录)\第七章:LSTM文本分类实战\\text\THUCNews\data\embedding_Tencent.npz")
#print(len(npz['embeddings'])) # 输出文件中包含的数组名称
# 打开文件并读取词汇表
with open("C:\\Users\\jimes\\PycharmProjects\\课程\\PyTorch框架(2022重录)\\第七章:LSTM文本分类实战\\text\THUCNews\data\\vocab.pkl", "rb") as file:
vocab = pickle.load(file)

class SimpleLSTM(nn.Module):
def __init__(self, vocab_size, embedding_dim, hidden_dim, output_dim, num_layers, pad_idx, embedding_matrix=None):
"""
初始化 RNN 网络
vocab_size: 词汇表大小
embedding_dim: 词嵌入维度
hidden_dim: 隐藏层维度
output_dim: 输出维度(类别数)
pad_idx: <PAD> 的索引
embedding_matrix: 预训练的词嵌入矩阵(可选)
"""
super(SimpleLSTM, self).__init__()
# 词嵌入层
self.embedding = nn.Embedding(vocab_size, embedding_dim, padding_idx=pad_idx)
if embedding_matrix is not None:
self.embedding.weight = nn.Parameter(embedding_matrix, requires_grad=False) # 使用预训练的词嵌入
# LSTM 层
self.hidden_size = hidden_dim
self.num_layers = num_layers
self.lstm = nn.LSTM(embedding_dim, hidden_dim, num_layers, batch_first=True)
# 全连接层
self.fc = nn.Linear(hidden_dim, output_dim)
# 激活函数
self.softmax = nn.LogSoftmax(dim=1)
def forward(self, x):
"""
前向传播
x: 输入的序列数据,形状为 [batch_size, seq_len]
return: 输出的类别概率
"""
# 将输入索引转换为嵌入向量
x = self.embedding(x) # (batch_size, sequence_length) -> (batch_size, sequence_length, embed_size)
# 初始化隐藏状态和细胞状态
h0 = torch.zeros(self.num_layers, x.size(0), self.hidden_size).to(x.device)
c0 = torch.zeros(self.num_layers, x.size(0), self.hidden_size).to(x.device)
# 前向传播LSTM
_, (hidden, _) = self.lstm(x, (h0, c0)) # hidden: (batch_size, sequence_length, hidden_size)
# 使用 LSTM 的最后一个隐藏状态
hidden = hidden[-1] # [batch_size, hidden_dim]
# 全连接层
output = self.fc(hidden) # [batch_size, output_dim]
# 激活函数
return self.softmax(output)

# 超参数
vocab_size = 4762 # 词汇表大小
embedding_dim = 200 # 词嵌入维度
hidden_dim = 128 # RNN 隐藏层维度
num_layers = 2 # RNN层数
output_dim = 10 # 输出类别数
pad_idx = 4761 # <PAD> 的索引
# 词嵌入二维列表
embedding_matrix = torch.tensor(npz['embeddings'], dtype=torch.float)
# 检查词嵌入的维度
vocab_size, embedding_dim = embedding_matrix.shape
print(f"词汇表大小: {vocab_size}, 词嵌入维度: {embedding_dim}")
# 加载模型参数
model = SimpleLSTM(vocab_size, embedding_dim, hidden_dim, output_dim, num_layers, pad_idx, embedding_matrix)
model.load_state_dict(torch.load('LSTM_model.pth'))
# 将模型设置为评估模式
model.eval()
xx = '在教育界会有一些新的教育方式'
categories = [
"finance",
"realty",
"stocks",
"education",
"science",
"society",
"politics",
"sports",
"game",
"entertainment"]
x = []
for j in range(25):
try:
x.append(vocab[xx[j]])
except KeyError:
x.append(vocab['<UNK>'])
except IndexError:
x.append(vocab['<PAD>'])
# 预测
with torch.no_grad():
output = model(torch.tensor(x).unsqueeze(0))
_, predicted = torch.max(output, 1)
print(f"Predicted class: {categories[predicted.item()]}")

从RNN到LSTM(包含代码实现)
https://jimes.cn/2025/02/08/RNN/
作者
Jimes
发布于
2025年2月8日
许可协议