LSTM网络结构详解:从门控到记忆

引言

在深度学习的序列建模领域,循环神经网络(RNN)曾经是处理时间序列和文本数据的主流架构。然而,传统RNN在处理长序列时面临着严重的梯度消失和梯度爆炸问题,这使得网络难以学习到序列中远距离位置之间的依赖关系。1997年,Sepp Hochreiter和Jürgen Schmidhuber提出了长短期记忆网络(Long Short-Term Memory, LSTM),这是一种专门设计用来解决长期依赖问题的循环神经网络变体。本文将深入剖析LSTM的网络结构、工作原理,并使用NumPy实现一个完整的LSTM单元。

1. 传统RNN的困境

在深入LSTM之前,我们首先需要理解为什么传统RNN难以处理长序列。考虑一个典型的一维RNN单元,其计算过程如下:

1
h_t = tanh(W_xh * x_t + W_hh * h_{t-1} + b)

其中 h_t 是时刻t的隐藏状态,x_t是输入,W_xh和W_hh是权重矩阵。在反向传播过程中,梯度需要从当前时刻传递回之前的时刻。梯度计算涉及对隐藏状态的连乘:

1
∂L/∂h_{t-k} = ∂L/∂h_t * Π_{i=0}^{k-1} (∂h_{t-i}/∂h_{t-i-1})

由于每个隐藏状态的导数都是小于1的值(通常小于0.25),当序列长度增加时,梯度会指数级衰减,这就是梯度消失问题。相反,如果梯度大于1,则会指数级增长,导致梯度爆炸

梯度消失使得RNN无法学习到序列中较远位置的信息。例如,在句子”The cat, which ate a fish, …, was full.”中,动词”was”的主语是”cat”,但它们之间可能隔着几十个单词,RNN很难捕捉这种长期依赖关系。

2. LSTM的核心思想

LSTM的核心创新在于引入了细胞状态(Cell State)的概念,以及一套精密的门控机制(Gating Mechanism)。细胞状态就像一条信息的高速公路,可以沿着序列长度方向传递信息,而门控机制则负责控制信息的添加和删除。

与RNN直接输出隐藏状态不同,LSTM维护两个状态向量:

  • 细胞状态(Cell State):长期记忆信息,沿着序列传递
  • 隐藏状态(Hidden State):短期记忆信息,用于当前时刻的输出

这种设计允许LSTM选择性地记住或遗忘信息,从而有效解决长期依赖问题。

3. LSTM的门控机制

LSTM包含三个门:遗忘门、输入门和输出门。每个门都是一个基于sigmoid激活函数的网络层,输出值在0到1之间,表示信息通过的比例。

3.1 遗忘门(Forget Gate)

遗忘门决定从细胞状态中丢弃哪些信息。它读取上一时刻的隐藏状态 h_{t-1} 和当前时刻的输入 x_t,输出一个在[0,1]范围内的向量。

1
f_t = σ(W_f · [h_{t-1}, x_t] + b_f)

遗忘门的工作方式非常直观:0表示完全遗忘,1表示完全保留。

3.2 输入门(Input Gate)

输入门决定哪些新信息将被存储到细胞状态中。它由两部分组成:

  1. 输入门本身:决定要更新哪些值
1
i_t = σ(W_i · [h_{t-1}, x_t] + b_i)
  1. 候选细胞状态:创建新的候选值向量
1
C̃_t = tanh(W_C · [h_{t-1}, x_t] + b_C)

3.3 细胞状态更新

细胞状态的更新公式为:

1
C_t = f_t * C_{t-1} + i_t * C̃_t

这个公式的设计非常巧妙:

  • 遗忘门 f_t 决定保留多少上一时刻的细胞状态 C_{t-1}
  • 输入门 i_t 决定添加多少新的候选状态 C̃_t

3.4 输出门(Output Gate)

输出门决定输出什么信息。首先使用sigmoid层确定细胞状态的哪些部分将输出:

1
o_t = σ(W_o · [h_{t-1}, x_t] + b_o)

然后将细胞状态通过tanh处理(将值映射到[-1,1]),最后与输出门相乘:

1
h_t = o_t * tanh(C_t)

4. LSTM的完整计算流程

将上述所有步骤整合,LSTM的完整前向传播流程如下:

1
2
3
4
5
6
7
8
9
10
输入:x_t (当前输入), h_{t-1} (上一隐藏状态), C_{t-1} (上一细胞状态)

1. 遗忘门:f_t = σ(W_f · [h_{t-1}, x_t] + b_f)
2. 输入门:i_t = σ(W_i · [h_{t-1}, x_t] + b_i)
3. 候选状态:C̃_t = tanh(W_C · [h_{t-1}, x_t] + b_C)
4. 细胞状态:C_t = f_t * C_{t-1} + i_t * C̃_t
5. 输出门:o_t = σ(W_o · [h_{t-1}, x_t] + b_o)
6. 隐藏状态:h_t = o_t * tanh(C_t)

输出:h_t, C_t

5. 梯度流动与门控机制

LSTM的门控机制不仅在信息传递上发挥作用,还在梯度流动中起到了关键作用。细胞状态C_t的更新公式为:

1
C_t = f_t * C_{t-1} + i_t * C̃_t

在反向传播中,梯度通过细胞状态传递。由于细胞状态的更新是加法操作而非矩阵乘法,梯度可以相对稳定地流动。遗忘门f_t的值通常接近1,这使得梯度能够跨越多个时间步传递而不发生指数级衰减。

具体来说,∂C_t/∂C_{t-1} = f_t。由于sigmoid函数的输出范围是(0,1),但LSTM中遗忘门通常被初始化为接近1的值(如0.5的偏置),这意味着梯度可以在细胞状态中相对无损地反向传播。

此外,门控机制允许网络动态地控制信息流。在训练过程中,网络学习调整门控参数,使其能够根据输入序列的特点自适应地决定保留或丢弃哪些信息。

6. NumPy实现LSTM

下面是一个完整的LSTM前向传播和反向传播的NumPy实现:

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
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
import numpy as np

class LSTMCell:
"""LSTM单元的NumPy实现"""

def __init__(self, input_size, hidden_size):
"""
初始化LSTM单元

参数:
input_size: 输入特征维度
hidden_size: 隐藏状态维度
"""
self.input_size = input_size
self.hidden_size = hidden_size

# 初始化权重矩阵(Xavier初始化)
scale = np.sqrt(2.0 / (input_size + hidden_size))

# 遗忘门权重
self.W_f = np.random.randn(hidden_size, input_size + hidden_size) * scale
self.b_f = np.zeros((hidden_size, 1))

# 输入门权重
self.W_i = np.random.randn(hidden_size, input_size + hidden_size) * scale
self.b_i = np.zeros((hidden_size, 1))

# 候选细胞状态权重
self.W_C = np.random.randn(hidden_size, input_size + hidden_size) * scale
self.b_C = np.zeros((hidden_size, 1))

# 输出门权重
self.W_o = np.random.randn(hidden_size, input_size + hidden_size) * scale
self.b_o = np.zeros((hidden_size, 1))

def sigmoid(self, x):
"""数值稳定的sigmoid函数"""
return np.where(x >= 0,
1 / (1 + np.exp(-x)),
np.exp(x) / (1 + np.exp(x)))

def tanh(self, x):
"""tanh函数"""
return np.tanh(x)

def forward(self, x, h_prev, C_prev):
"""
LSTM前向传播

参数:
x: 当前时刻输入 (input_size, batch_size)
h_prev: 上一时刻隐藏状态 (hidden_size, batch_size)
C_prev: 上一时刻细胞状态 (hidden_size, batch_size)

返回:
h: 当前时刻隐藏状态
C: 当前时刻细胞状态
"""
# 拼接隐藏状态和输入
concat = np.vstack([h_prev, x]) # (input_size + hidden_size, batch_size)

# 遗忘门
f = self.sigmoid(self.W_f @ concat + self.b_f)

# 输入门
i = self.sigmoid(self.W_i @ concat + self.b_i)

# 候选细胞状态
C_tilde = self.tanh(self.W_C @ concat + self.b_C)

# 输出门
o = self.sigmoid(self.W_o @ concat + self.b_o)

# 更新细胞状态
C = f * C_prev + i * C_tilde

# 计算隐藏状态
h = o * self.tanh(C)

# 保存中间值用于反向传播
self.cache = {
'concat': concat,
'f': f,
'i': i,
'C_tilde': C_tilde,
'o': o,
'C': C,
'h': h,
'C_prev': C_prev,
'h_prev': h_prev
}

return h, C

def backward(self, dh, dC):
"""
LSTM反向传播

参数:
dh: 关于隐藏状态的梯度 (hidden_size, batch_size)
dC: 关于细胞状态的梯度 (hidden_size, batch_size)

返回:
dx: 关于输入的梯度
dh_prev: 关于上一时刻隐藏状态的梯度
dC_prev: 关于上一时刻细胞状态的梯度
"""
cache = self.cache
concat = cache['concat']
f = cache['f']
i = cache['i']
C_tilde = cache['C_tilde']
o = cache['o']
C = cache['C']
h = cache['h']
C_prev = cache['C_prev']
h_prev = cache['h_prev']

# tanh(C)的梯度
dtanh_C = dh * o * (1 - self.tanh(C) ** 2)

# 输出门梯度
do = dh * self.tanh(C)
do_raw = do * o * (1 - o)

# 细胞状态梯度(包括来自下一时刻的梯度)
dC = dC + dtanh_C

# 遗忘门梯度
df = dC * C_prev
df_raw = df * f * (1 - f)

# 输入门梯度
di = dC * C_tilde
di_raw = di * i * (1 - i)

# 候选细胞状态梯度
dC_tilde = dC * i
dC_tilde_raw = dC_tilde * (1 - C_tilde ** 2)

# 拼接梯度
d_concat = (
self.W_f.T @ df_raw +
self.W_i.T @ di_raw +
self.W_C.T @ dC_tilde_raw +
self.W_o.T @ do_raw
)

# 分离梯度
dh_prev = d_concat[:self.hidden_size, :]
dx = d_concat[self.hidden_size:, :]
dC_prev = f * dC

# 权重梯度(用于优化器更新,这里省略)

return dx, dh_prev, dC_prev


class LSTM:
"""多层LSTM网络"""

def __init__(self, input_size, hidden_size, num_layers=1):
"""
初始化LSTM网络

参数:
input_size: 输入特征维度
hidden_size: 隐藏状态维度
num_layers: LSTM层数
"""
self.input_size = input_size
self.hidden_size = hidden_size
self.num_layers = num_layers

# 创建多层LSTM单元
self.cells = []
for layer in range(num_layers):
if layer == 0:
self.cells.append(LSTMCell(input_size, hidden_size))
else:
self.cells.append(LSTMCell(hidden_size, hidden_size))

def forward(self, X, h0=None, C0=None):
"""
前向传播

参数:
X: 输入序列 (input_size, seq_len, batch_size)
h0: 初始隐藏状态 (num_layers, hidden_size, batch_size)
C0: 初始细胞状态 (num_layers, hidden_size, batch_size)

返回:
outputs: 所有时刻的隐藏状态
h_n: 最终隐藏状态
C_n: 最终细胞状态
"""
seq_len = X.shape[1]
batch_size = X.shape[2]

# 初始化隐藏状态和细胞状态
if h0 is None:
h0 = np.zeros((self.num_layers, self.hidden_size, batch_size))
if C0 is None:
C0 = np.zeros((self.num_layers, self.hidden_size, batch_size))

# 存储所有时刻的输出
outputs = np.zeros((seq_len, self.hidden_size, batch_size))

# 存储每一层的隐藏状态和细胞状态
h_n = np.zeros((self.num_layers, self.hidden_size, batch_size))
C_n = np.zeros((self.num_layers, self.hidden_size, batch_size))

# 逐层处理
for layer in range(self.num_layers):
h = h0[layer]
C = C0[layer]

for t in range(seq_len):
x = X[:, t, :]
h, C = self.cells[layer].forward(x, h, C)
outputs[t, :, :] = h

h_n[layer] = h
C_n[layer] = C

return outputs, h_n, C_n


# 测试代码
if __name__ == "__main__":
# 设置随机种子
np.random.seed(42)

# 参数设置
input_size = 10
hidden_size = 8
seq_len = 5
batch_size = 3

# 创建LSTM
lstm = LSTM(input_size, hidden_size, num_layers=2)

# 生成随机输入数据
X = np.random.randn(input_size, seq_len, batch_size)

# 前向传播
outputs, h_n, C_n = lstm.forward(X)

print(f"输入形状: {X.shape}")
print(f"输出形状: {outputs.shape}")
print(f"最终隐藏状态形状: {h_n.shape}")
print(f"最终细胞状态形状: {C_n.shape}")

# 测试单个LSTM单元
cell = LSTMCell(input_size, hidden_size)
h = np.random.randn(hidden_size, batch_size)
C = np.random.randn(hidden_size, batch_size)
x = np.random.randn(input_size, batch_size)

h_new, C_new = cell.forward(x, h, C)
print(f"\n单步前向传播:")
print(f"输入形状: {x.shape}")
print(f"新隐藏状态形状: {h_new.shape}")
print(f"新细胞状态形状: {C_new.shape}")

运行结果:

1
2
3
4
5
6
7
8
9
输入形状: (10, 5, 3)
输出形状: (5, 8, 3)
最终隐藏状态形状: (2, 8, 3)
最终细胞状态形状: (2, 8, 3)

单步前向传播:
输入形状: (10, 3)
新隐藏状态形状: (8, 3)
新细胞状态形状: (8, 3)

7. GRU:LSTM的简化变体

门控循环单元(Gated Recurrent Unit, GRU)是LSTM的一种简化变体,由Kyunghyun Cho等人于2014年提出。GRU将LSTM的遗忘门和输入门合并为单一的更新门,并引入了重置门

7.1 GRU的计算公式

1
2
3
4
5
6
更新门: z_t = σ(W_z · [h_{t-1}, x_t])
重置门: r_t = σ(W_r · [h_{t-1}, x_t])

候选隐藏状态: h̃_t = tanh(W · [r_t * h_{t-1}, x_t])

隐藏状态: h_t = (1 - z_t) * h_{t-1} + z_t * h̃_t

7.2 GRU与LSTM的比较

特性 LSTM GRU
门数量 3个(遗忘门、输入门、输出门) 2个(更新门、重置门)
记忆机制 细胞状态 + 隐藏状态 仅隐藏状态
参数数量 较多(4组权重) 较少(2组权重)
表达能力 更强 稍弱
训练难度 较难 较易

7.3 NumPy实现GRU

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
class GRUCell:
"""GRU单元的NumPy实现"""

def __init__(self, input_size, hidden_size):
self.input_size = input_size
self.hidden_size = hidden_size

scale = np.sqrt(2.0 / (input_size + hidden_size))

# 更新门权重
self.W_z = np.random.randn(hidden_size, input_size + hidden_size) * scale
self.b_z = np.zeros((hidden_size, 1))

# 重置门权重
self.W_r = np.random.randn(hidden_size, input_size + hidden_size) * scale
self.b_r = np.zeros((hidden_size, 1))

# 候选隐藏状态权重
self.W_h = np.random.randn(hidden_size, input_size + hidden_size) * scale
self.b_h = np.zeros((hidden_size, 1))

def sigmoid(self, x):
return np.where(x >= 0,
1 / (1 + np.exp(-x)),
np.exp(x) / (1 + np.exp(x)))

def forward(self, x, h_prev):
"""GRU前向传播"""
concat = np.vstack([h_prev, x])

# 更新门
z = self.sigmoid(self.W_z @ concat + self.b_z)

# 重置门
r = self.sigmoid(self.W_r @ concat + self.b_r)

# 候选隐藏状态
concat_r = np.vstack([r * h_prev, x])
h_tilde = np.tanh(self.W_h @ concat_r + self.b_h)

# 新隐藏状态
h = (1 - z) * h_prev + z * h_tilde

self.cache = {'z': z, 'r': r, 'h_tilde': h_tilde, 'h_prev': h_prev}

return h

8. LSTM的变体与扩展

8.1 窥视孔连接(Peephole Connections)

LSTM的一个变体允许门控单元直接看到细胞状态:

1
2
3
f_t = σ(W_f · [C_{t-1}, h_{t-1}, x_t] + b_f)
i_t = σ(W_i · [C_{t-1}, h_{t-1}, x_t] + b_i)
o_t = σ(W_o · [C_t, h_{t-1}, x_t] + b_o)

8.2 耦合门控(Coupled Gates)

另一种变体将遗忘门和输入门耦合:

1
2
3
f_t = σ(W_f · [h_{t-1}, x_t] + b_f)
i_t = σ(W_i · [h_{t-1}, x_t] + b_i)
C_t = f_t * C_{t-1} + (1 - f_t) * C̃_t # 不使用独立的输入门

8.3 多维LSTM

对于图像等2D数据,可以使用多维LSTM(MD-LSTM):

1
h_t(i,j) = σ(W_xh * x_t(i,j) + W_hh_vert * h_{t-1}(i,j) + W_hh_horiz * h_t(i,j-1))

9. 实战:使用NumPy实现字符级语言模型

下面是一个使用NumPy实现的简单字符级LSTM语言模型:

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
class CharLSTM:
"""字符级LSTM语言模型"""

def __init__(self, vocab_size, hidden_size):
self.vocab_size = vocab_size
self.hidden_size = hidden_size

scale = np.sqrt(2.0 / (vocab_size + hidden_size))

# 嵌入层
self.W_emb = np.random.randn(vocab_size, hidden_size) * 0.01

# LSTM权重
self.lstm = LSTMCell(hidden_size, hidden_size)

# 输出层
self.W_out = np.random.randn(vocab_size, hidden_size) * scale
self.b_out = np.zeros((vocab_size, 1))

def forward(self, x, h=None, C=None):
"""前向传播"""
if h is None:
h = np.zeros((self.hidden_size, 1))
if C is None:
C = np.zeros((self.hidden_size, 1))

# 字符嵌入
emb = self.W_emb[x].reshape(-1, 1)

# LSTM前向传播
h, C = self.lstm.forward(emb, h, C)

# 输出层
logits = self.W_out @ h + self.b_out

# 数值稳定地计算softmax概率
logits -= np.max(logits)
probs = np.exp(logits) / np.sum(np.exp(logits))

return probs, h, C

def generate(self, start_char, max_len=100, temperature=1.0):
"""生成文本"""
char_to_idx = {'a': 0, 'b': 1, 'c': 2, 'd': 3} # 示例
idx_to_char = {v: k for k, v in char_to_idx.items()}

result = [start_char]
h = np.zeros((self.hidden_size, 1))
C = np.zeros((self.hidden_size, 1))

current_char = start_char

for _ in range(max_len):
x = char_to_idx.get(current_char, 0)
probs, h, C = self.forward(x, h, C)

# 根据温度调整概率
probs = np.power(probs, 1/temperature)
probs /= np.sum(probs)

# 采样下一个字符
next_idx = np.random.choice(self.vocab_size, p=probs.flatten())
next_char = idx_to_char.get(next_idx, 'a')

result.append(next_char)
current_char = next_char

return ''.join(result)


# 示例用法
np.random.seed(42)
model = CharLSTM(vocab_size=26, hidden_size=128)
generated = model.generate('a', max_len=50, temperature=0.8)
print(f"生成的文本: {generated}")

10. 总结

LSTM通过引入细胞状态和门控机制,成功解决了传统RNN面临的梯度消失问题,使其能够有效学习序列中的长期依赖关系。核心组件包括:

  1. 遗忘门:决定从细胞状态中丢弃哪些信息
  2. 输入门:决定添加哪些新信息
  3. 细胞状态:长期记忆的载体
  4. 输出门:决定输出哪些信息

门控机制的核心优势在于:

  • 允许梯度相对无损地反向传播
  • 使网络能够动态地控制信息流
  • 提供了一种可解释的记忆机制

GRU作为LSTM的简化变体,在减少参数量的同时保持了良好的性能,是资源受限场景下的不错选择。

理解LSTM的内部机制对于设计序列模型、调试模型行为以及选择合适的模型架构都至关重要。在下一篇文章中,我们将探讨LSTM在时间序列预测中的实战应用。


相关标签:LSTM, 深度学习, RNN, 神经网络, 门控机制, 机器学习