在前面的章节中,我们讨论了SimpleRNN层并指出了它的局限性,特别是梯度消失问题,这使得网络难以学习长序列中的依赖关系。为了解决此问题,研究人员开发了更精巧的循环架构。长短期记忆(LSTM)网络就是一种非常成功且广泛使用的网络。LSTM引入了一种机制,可以明确地管理信息随时间流动,使其能够选择性地记忆或遗忘信息。这是通过一个门控系统实现的,该系统控制着一个专门的细胞状态($c_t$),该状态就像一条贯穿整个序列的传送带,以最少处理传输信息。LSTM单元结构一个LSTM单元处理当前时间步的输入($x_t$)以及前一时间步的隐藏状态($h_{t-1}$)。与SimpleRNN不同,它使用三个主要的门,并同时更新隐藏状态($h_t$)和细胞状态($c_t$)。遗忘门($f_t$): 决定要从细胞状态中丢弃哪些信息。它查看$h_{t-1}$和$x_t$,对细胞状态$c_{t-1}$中的每个数字输出一个0到1之间的值。1表示“完全保留此信息”,而0表示“完全丢弃此信息”。它通常使用sigmoid激活函数。 $$f_t = \sigma(W_f \cdot [h_{t-1}, x_t] + b_f)$$输入门($i_t$): 决定我们将要存储到细胞状态中的新信息。这包含两个部分:一个sigmoid层($i_t$)决定要更新哪些值。一个tanh层($\tilde{c}t$)创建一个新的候选值向量,以添加到状态中。 $$i_t = \sigma(W_i \cdot [h{t-1}, x_t] + b_i)$$ $$\tilde{c}t = \tanh(W_C \cdot [h{t-1}, x_t] + b_C)$$细胞状态更新: 旧细胞状态$c_{t-1}$被更新为新细胞状态$c_t$。遗忘门$f_t$乘以旧状态,输入门$i_t$乘以候选值$\tilde{c}t$。 $$c_t = f_t * c{t-1} + i_t * \tilde{c}_t$$输出门($o_t$): 决定细胞状态的哪一部分将作为输出。一个sigmoid层($o_t$)决定细胞状态的哪些部分要输出。细胞状态通过tanh函数(将值推到-1到1之间),并乘以sigmoid门的输出。这个过滤后的版本成为新的隐藏状态$h_t$。 $$o_t = \sigma(W_o \cdot [h_{t-1}, x_t] + b_o)$$ $$h_t = o_t * \tanh(c_t)$$隐藏状态$h_t$是当前时间步LSTM单元的输出。细胞状态和门的结合使得LSTM能够处理比SimpleRNN长得多的序列并保持相关信息,从而缓解了梯度消失问题。digraph G { rankdir=TB; node [shape=box, style="rounded,filled", fillcolor="#e9ecef"]; splines=ortho; edge [arrowhead=vee]; {rank=same; x_t [label="输入 (x_t)", fillcolor="#a5d8ff"]; h_t_minus_1 [label="前一隐藏状态 (h_{t-1})", fillcolor="#ffec99"]; c_t_minus_1 [label="前一细胞状态 (c_{t-1})", fillcolor="#ffd8a8"];} {rank=same; h_t [label="隐藏状态 (h_t)", fillcolor="#ffec99"]; c_t [label="细胞状态 (c_t)", fillcolor="#ffd8a8"];} Forget [label="遗忘门 (σ)", shape=ellipse, fillcolor="#fcc2d7"]; Input_Sig [label="输入门 (σ)", shape=ellipse, fillcolor="#b2f2bb"]; Input_Tanh [label="候选值 (tanh)", shape=ellipse, fillcolor="#bac8ff"]; Output [label="输出门 (σ)", shape=ellipse, fillcolor="#d0bfff"]; Tanh_Ct [label="tanh", shape=ellipse, fillcolor="#bac8ff"]; Point_Mul_1 [label="*", shape=circle, fillcolor="#ced4da", width=0.3, height=0.3, fixedsize=true]; Point_Mul_2 [label="*", shape=circle, fillcolor="#ced4da", width=0.3, height=0.3, fixedsize=true]; Point_Add [label="+", shape=circle, fillcolor="#ced4da", width=0.3, height=0.3, fixedsize=true]; Point_Mul_3 [label="*", shape=circle, fillcolor="#ced4da", width=0.3, height=0.3, fixedsize=true]; x_t -> Forget; x_t -> Input_Sig; x_t -> Input_Tanh; x_t -> Output; h_t_minus_1 -> Forget; h_t_minus_1 -> Input_Sig; h_t_minus_1 -> Input_Tanh; h_t_minus_1 -> Output; c_t_minus_1 -> Point_Mul_1 [label=""]; Forget -> Point_Mul_1; Input_Sig -> Point_Mul_2; Input_Tanh -> Point_Mul_2; Point_Mul_1 -> Point_Add; Point_Mul_2 -> Point_Add; Point_Add -> c_t [label=""]; c_t -> Tanh_Ct; Output -> Point_Mul_3; Tanh_Ct -> Point_Mul_3; Point_Mul_3 -> h_t [label=""]; c_t -> c_t [style=invis]; h_t -> h_t [style=invis]; }一个LSTM单元的图示,呈现了信息如何通过遗忘门、输入门和输出门流动,并联系着细胞状态和隐藏状态。在Keras中实现LSTM在Keras中使用LSTM简单直接,这多亏了keras.layers.LSTM层。它的功能类似于SimpleRNN,但融入了上面描述的更复杂的内部逻辑。import keras from keras import layers # 定义一个包含64个单元的LSTM层 # 假设输入形状为 (批量大小, 时间步, 特征) # 例如,(32, 10, 8) 表示32个序列,每个序列10个时间步,每个时间步8个特征 lstm_layer = layers.LSTM(units=64) # 您可以将其添加到Sequential模型中: model = keras.Sequential([ # 第一层需要输入形状 layers.Input(shape=(None, 8)), # (时间步, 特征) - None允许变长的序列 layers.LSTM(units=64, return_sequences=True), # 返回完整的序列输出 layers.LSTM(units=32), # 只返回最后一个输出 layers.Dense(units=10) # 示例最终分类层 ]) model.summary()keras.layers.LSTM的重要参数:units:这是输出空间的维度,也对应着隐藏状态$h_t$和细胞状态$c_t$的维度。这是一个必需的参数。activation:应用于候选细胞状态($\tilde{c}_t$)和最终隐藏状态输出计算($h_t$)的激活函数。默认是'tanh'。recurrent_activation:用于三个门(遗忘、输入、输出)的激活函数。默认是'sigmoid'。return_sequences:一个布尔值。如果为False(默认),该层只返回输入序列中最后一个时间步的隐藏状态($h_T$)。当LSTM层是Dense层之前的最终循环层,用于序列分类等任务时,这非常适合。如果为True,该层返回每个时间步的隐藏状态($h_1, h_2, ..., h_T$)。这在堆叠LSTM层(以便下一个LSTM层接收序列作为输入)或在需要每个时间步都有输出的序列到序列任务中是必需的。input_shape:与其他Keras层一样,您需要为模型中的第一层指定输入的形状。对于循环层,这通常是(时间步, 特征)。如果您的序列长度可变,可以将时间步维度设置为None。默认情况下,当在兼容的GPU上运行时,LSTM层使用优化的CuDNN核,从而大幅加快训练速度。与SimpleRNN相比,LSTM层由于其内部门控机制,每个时间步涉及更多计算。然而,正是这种复杂性使其能够有效学习长距离依赖关系,从而使其成为许多序列建模任务的强大工具。在本章后面的实践部分,您将实现一个用于文本分类的LSTM模型。