大模型技术:LLM 架构解释,RNN、LSTM 和 GRU

ChatGPT 等大型语言模型 、DALL-E 等 AI 图像生成器以及预测性 AI 模型都在某种程度上依赖于神经网络。

1.什么是神经网络?

神经网络或人工神经网络是一种基于人脑功能模型的计算架构,因此得名“神经网络”。神经网络由称为“节点”的处理单元的集合组成。这些节点相互传递数据,就像大脑中神经元相互传递电脉冲一样。

神经网络用于机器学习,机器学习是指无需明确指令即可学习的一类计算机程序。具体来说,神经网络用于深度学习——一种先进的机器学习类型,可以在无需人工干预的情况下从未标记的数据中得出结论。例如,基于神经网络构建的深度学习模型并提供足够的训练数据,可以识别照片中以前从未见过的项目。

大模型技术:LLM 架构解释,RNN、LSTM 和 GRU

Credits: cloudflare

神经网络使多种类型的人工智能 (AI) 成为可能。 ChatGPT 等大型语言模型 (LLM)、DALL-E 等 AI 图像生成器以及预测性 AI 模型都在某种程度上依赖于神经网络。

1.1 神经网络如何学习?

神经网络的学习(训练)过程是一个迭代过程,通过网络中的每一层向前和向后进行计算,直到损失函数最小化。

整个学习过程可以分为三个主要部分:

  • 前向传播(前向传播)
  • 损失函数的计算
  • 反向传播(Backward pass/Backpropagation)
大模型技术:LLM 架构解释,RNN、LSTM 和 GRU

Learning process of a neural network | Credits: Rukshan Pramoditha

我们将从前向传播开始。

前向传播

神经网络由多个神经元组成(感知器)并且这些神经元被堆叠成层。各层之间的连接通过 参数 (由箭头表示)的网络。参数是 重量偏见.

权重控制每个输入的重要性级别,而偏差则决定神经元激发或激活的难易程度。

首先,我们为权重和偏差分配非零随机值。这就是所谓的 参数初始化网络的。根据这些分配值和输入值,我们在网络的每个神经元中执行以下计算。

  • 神经元线性函数的计算
  • 神经元激活函数的计算

这些计算发生在整个网络中。完成输出层节点的计算后,我们在第一次迭代中得到前向传播部分的最终输出。

大模型技术:LLM 架构解释,RNN、LSTM 和 GRU

Credits: Encord

大模型技术:LLM 架构解释,RNN、LSTM 和 GRU

Credits: iq.opengenus

在前向传播中,计算是根据输入层到输出层 (从左到右)整个网络。

损失函数的计算

前向传播中执行的最终输出称为 预测值。该值应与相应的真实值(真实值)进行比较,以衡量神经网络的性能。这就是 损失函数 (也称为 目标函数 或者 成本函数)开始发挥作用。

在神经网络的背景下, 成本函数损失函数相关,但指的是模型性能评估的不同方面。

损失函数 测量单个示例的误差,并且 成本函数 聚合整个数据集中的此错误以指导训练。

大模型技术:LLM 架构解释,RNN、LSTM 和 GRU

大模型技术:LLM 架构解释,RNN、LSTM 和 GRU

Credits: Mohammed Zeeshan Mulla

损失函数计算一个称为 损失分数预测值和真实值之间。这也称为 错误模型的。损失函数捕获模型在每次迭代中的表现。我们使用损失分数作为反馈信号来更新反向传播部分的参数。

损失函数的理想值为零 (0)。我们的目标是在每次迭代中将损失函数最小化为接近 0,以便模型能够做出更接近真实值的更好预测。

以下是神经网络训练中常用的损失函数的列表。

  • 均方误差 (MSE)— 这用于衡量回归问题的性能。
  • 平均绝对误差 (MAE)— 这用于衡量回归问题的性能。
  • 平均绝对百分比误差— 这用于衡量回归问题的性能。
  • 胡贝尔损失— 这用于衡量回归问题的性能。
  • 二元交叉熵(对数损失)— 用于衡量二元(二类)分类问题的性能。
  • 多类交叉熵/分类交叉熵— 用于衡量多类(多于两类)分类问题的性能。
大模型技术:LLM 架构解释,RNN、LSTM 和 GRU

Credits: Avi Chawla

Keras 中可用损失函数的完整列表可以找到 这里.

向后传播

在第一次迭代中,预测值与地面真实值相距甚远,距离分数会很高。这是因为我们最初为网络参数(权重和偏差)分配了任意值。这些值不是最佳值。因此,我们需要更新这些参数的值以最小化损失函数。更新网络参数的过程称为 参数学习 或者 优化这是使用一个完成的 优化算法 (优化器)实现 反向传播.

优化算法的目标是找到损失函数具有最小值的全局最小值。然而,通过避免所有局部最小值来找到复杂损失函数的全局最小值对于优化算法来说是一个真正的挑战。如果算法停在局部最小值,我们将无法获得损失函数的最小值。因此,我们的模型不会表现良好。

大模型技术:LLM 架构解释,RNN、LSTM 和 GRU

Loss function optimization by finding the global minimum | Credits: Rukshan Pramoditha

以下是神经网络训练中常用优化器的列表。

  • Gradient Descent
  • Stocasticc Gradeint Descent (SGD)
  • Adam
  • Adagrad
  • Adadelta
  • Adamax
  • Nadam
  • Ftrl
  • Root Mean Squared Propagation (RMSProp)
大模型技术:LLM 架构解释,RNN、LSTM 和 GRU

Credits: Springer

在反向传播中, 偏导数 (渐变) 计算损失函数相对于每层模型参数的值。这是通过应用 链式法则 微积分。

损失函数的导数是它的斜率,它为我们提供了更新(更改)模型参数值时需要考虑的方向。

Keras 中的神经网络库提供自动微分功能。这意味着,在定义神经网络架构后,库会自动计算反向传播所需的所有导数。

在反向传播中,计算是根据输出层到输入层 (从右到左)通过网络。

1.2 Epoch、批量大小和迭代

Epochs(时期)

一个Epochs(时期)是指整个数据集仅通过神经网络向前和向后传递一次。

由于一个Epochs(时期)太大而无法一次输入计算机,我们将其分成几个较小的批次。

批次大小

单批次中存在的训练示例总数。

注意: 批次大小和批次数量是两个不同的东西。

但什么是批次?

我们无法立即将整个数据集传递到神经网络中。所以,我们将数据集划分为多个批次、集合或部分。

迭代

迭代是完成一个时期所需的批次数。

注意:批次数等于一个 epoch 的迭代次数。

假设我们有 1000 个要使用的训练示例。我们可以将 1000 个示例的数据集分成 500 个批次,然后需要 2 次迭代才能完成 1 个 epoch,其中批次大小为 500,迭代次数为 2,即 1 个完整的 epoch(下图中的情况 2)。

在神经网络训练期间,我们通常不会在一次迭代中使用所有训练样本(实例/行)。相反,我们指定 批量大小它决定了训练期间要传播(向前和向后)的训练样本的数量。

大模型技术:LLM 架构解释,RNN、LSTM 和 GRU

Credits: Dr. Alvin Ang

1.3 神经网络的类型

神经网络可以拥有的节点数和层数没有限制,并且这些节点几乎可以以任何方式交互。正因为如此,神经网络的类型列表不断扩大。但是,它们大致可以分为以下几类:

  • 浅层神经网络(通常只有一个隐藏层)
  • 深度神经网络 (有多个隐藏层)

浅层神经网络速度快,比深度神经网络需要更少的处理能力,但它们无法执行与深度神经网络一样多的复杂任务。

下面是一个列表 神经网络架构的类型 今天可能会用到:

感知器 神经网络是简单的浅层网络,具有输入层和输出层。

大模型技术:LLM 架构解释,RNN、LSTM 和 GRU

Credits: cloudflare

多层感知器 神经网络增加了感知器网络的复杂性,并包含一个隐藏层。

大模型技术:LLM 架构解释,RNN、LSTM 和 GRU

Credits: cloudflare

前馈神经网络只允许它们的节点将信息传递给前向节点。

大模型技术:LLM 架构解释,RNN、LSTM 和 GRU

Credits: cloudflare

复发性神经网络可以后退,允许某些节点的输出影响前面节点的输入。

大模型技术:LLM 架构解释,RNN、LSTM 和 GRU

Credits: cloudflare

模块化的 神经网络结合两个或多个神经网络以获得输出。

大模型技术:LLM 架构解释,RNN、LSTM 和 GRU

Credits: cloudflare

径向基函数 神经网络节点使用一种称为径向基函数的特定数学函数。

大模型技术:LLM 架构解释,RNN、LSTM 和 GRU

Credits: cloudflare

液态机 神经网络的特点是节点彼此随机连接。

大模型技术:LLM 架构解释,RNN、LSTM 和 GRU

Credits: cloudflare

残差 神经网络允许数据通过称为恒等映射的过程向前跳跃,将早期层的输出与后面层的输出相结合。

大模型技术:LLM 架构解释,RNN、LSTM 和 GRU

Credits: cloudflare

该博客主要关注循环神经网络(RNN)。

2. 循环神经网络(RNN)

循环神经网络(RNN)是一种神经网络架构,专门设计用于处理 顺序数据通过维护先前输入的记忆。这是通过在网络中形成循环的连接来实现的,从而允许信息持续存在。与假设输入彼此独立的传统前馈神经网络不同,RNN 使用其内部状态(记忆)来处理输入序列。这使得它们对于输入顺序很重要的任务特别有用,例如时间序列数据、语言建模或视频序列。

循环细胞是用于处理的神经网络(通常很小) 顺序数据。众所周知,卷积层专门用于处理网格结构值(即图像)。相反, 循环层设计用于处理长序列,无需任何额外的基于序列的设计选择。

我们可以通过将时间步长的输出连接到输入来实现这一点!这就是所谓的序列 展开。通过处理整个序列,我们有了一个考虑序列先前状态的算法。通过这种方式,我们有 记忆的第一个概念 (一个细胞)!我们来看一下:

大模型技术:LLM 架构解释,RNN、LSTM 和 GRU

Simple RNN Cell | Credits : Nikolas Adaloglou

大多数常见的循环细胞也可以处理可变长度的序列。这对于许多应用程序(例如包含不同数量图像的视频)来说非常重要。人们可以将 RNN 单元视为一种常见的 神经网络与 多个时间步长的共享权重。 通过此修改,单元的权重现在可以访问序列的先前状态。

2.1 什么是顺序数据?

顺序数据是具有特定顺序且顺序很重要的信息。序列中的每条数据都与其前后的数据相关,并且此顺序为整个数据提供了上下文和含义。

下面举个例子来说明:

想象一下这样的句子:“敏捷的棕色狐狸跳过了懒惰的狗。”句子中的每个单词都是一段数据。单词的顺序至关重要,因为它决定了句子的含义。 “狐狸棕色快速跳过懒狗”没有多大意义,对吧?

以下是一些其他常见的顺序数据类型:

  • 时间序列数据:这是指随着时间的推移定期收集的数据点。例如股票价格、温度读数或网站流量。数据点的顺序很重要,因为它显示了值如何随时间变化。
  • 自然语言文本: 所有书面语言都是有顺序的。句子或段落中的单词顺序对于传达含义和理解思想之间的关系至关重要。
  • 语音信号: 口语是顺序数据的另一个例子。音素、音节和单词等声音的顺序对于理解口头信息至关重要。

2.2 循环神经网络与前馈神经网络

大模型技术:LLM 架构解释,RNN、LSTM 和 GRU

Credits: v7

前馈人工神经网络允许数据仅沿一个方向流动,即从输入到输出。该网络的架构遵循自上而下的方法并且没有循环 IE。,任何层的输出不会影响同一层。它们主要用于模式识别。

循环神经网络通过使用网络中的反馈循环使信号在两个方向上传播。从早期输入中得出的特征被反馈到网络中,这使它们具有记忆的能力。由于状态不断变化,这些交互网络是动态的,直到达到平衡点。这些网络主要用于时间序列等顺序自相关数据。

2.3 为什么使用RNN?

  1. 传统的人工神经网络 (ANN) 是强大的工具,但它们在处理文本等序列数据时遇到了困难,因为它们需要固定大小的输入。人工神经网络中的每个输入都是独立处理的,这使得它们不适合元素之间的顺序和关系至关重要的任务。
  2. 假设我们使用零填充概念,其中较短的序列在末尾用零填充,以达到批次中最长序列的长度。这些零充当占位符,不携带任何有意义的信息。填充引入了网络需要与实际数据一起处理的不相关的零,从而增加了计算负担。
  3. 而且由于在 ANN 中传递输入时没有顺序,我们会丢失上下文或顺序信息。除此之外,如果任何用户给出的输入长度比我们预期的要大,那么在这种情况下我们无能为力。例如,我们将输入大小设置为 5 个单词,但如果任何用户一次给它 15 个单词,那么在这种情况下我们无法使用 ANN 处理它。

3. RNN 的架构

3.1 Unfolding RNNs in Time

循环神经网络与其他神经网络的不同之处主要在于它们具有内部状态或内存来跟踪它们处理的数据。基本上,RNN 由三个关键组件组成:输入层、一个或多个隐藏层以及输出层。

大模型技术:LLM 架构解释,RNN、LSTM 和 GRU

Feed Forward architecture | Credits: Mark West

大模型技术:LLM 架构解释,RNN、LSTM 和 GRU

A RNN can be viewed as many copies of a Feed Forward ANN executing in a chain | Credits: Mark West

输入层
该层随着时间的推移接收输入序列。与同时处理所有输入的前馈网络不同,RNN 在每个时间步一次处理一个输入。这种顺序处理允许网络保持随时间变化的动态。

我们来表示 X_t 作为时间步的输入 t。该输入一次一步地输入到 RNN 中。

大模型技术:LLM 架构解释,RNN、LSTM 和 GRU

RNN Input | Credits: Cristian Leo

在哪里 n_x​ 是输入层中的单元(神经元)数量。

例如,这就是我们在 Python 中初始化输入层的方式:

self.weights_ih = np.random.randn(input_size, hidden_size) * 0.01

这里 input_size 是输入层的大小(神经元数量)。 hidden_size 是隐藏层的大小。self.weights_ih是连接输入层和隐藏层的权重矩阵,用正态分布的随机值初始化,按 0.01 缩放以保持较小。

隐藏状态
隐藏层在 RNN 中至关重要,因为它们不仅处理当前输入,还保留先前输入的信息。该信息存储在我们所谓的隐藏状态中,并被继续影响未来的处理。这种传递信息的能力赋予了 RNN 记忆能力。

大模型技术:LLM 架构解释,RNN、LSTM 和 GRU

Internal operations in a Hidden Node of an RNN | Credits: Mark West

隐藏状态 h_t​ 在时间步长 t 根据当前输入计算 Xt​和之前的隐藏状态 h_(t−1)​。这表示为:

大模型技术:LLM 架构解释,RNN、LSTM 和 GRU

Hidden State Calculation | Credits: Cristian Leo

在哪里:

  • h_t ​ 是时间步的隐藏状态 t,
  • 是隐藏层的权重矩阵,
  • b_h​ 是隐藏层的偏差向量,
  • f 是非线性激活函数,通常为 tanh⁡tanh 或 ReLU。

让我们将隐藏状态初始设置为零: h = np.zeros((1, self.hidden_size))。这会初始化第一个隐藏状态h带零,为序列中的第一个输入做好准备。

当 RNN 处理序列中的每个输入时,使用当前输入计算新的隐藏状态 x 和之前的隐藏状态 h。这发生在循环内 forward 方法,我们稍后将构建:

for i, x in enumerate(inputs):
    x = x.reshape(1, -1)  # Ensure x is a row vector
    h = np.tanh(np.dot(x, self.weights_ih) + np.dot(h, self.weights_hh) + self.bias_h)
    self.last_hs[i + 1] = h

在循环的每次迭代中,当前输入x转换为行向量,然后乘以输入到隐藏的权重矩阵self.weights_ih.

同时,之前的隐藏状态 h 乘以隐藏层到隐藏层的权重矩阵self.weights_hh。这两个操作的结果与隐藏偏差相加self.bias_h.

然后将总和传递给 np.tanh 函数,它应用非线性变换并产生新的隐藏状态h对于当前时间步长。

这个新的隐藏状态 h 存储在字典中self.last_hs以当前时间步为关键。这使得网络能够“记住”每一步的隐藏状态,这对于训练期间的时间反向传播(BPTT)至关重要。

输出序列
RNN 输出结果的方式非常灵活。他们可以在每个时间步输出(
多对多),在序列末尾产生一个输出(多对一),甚至从单个输入生成一个序列(一对多)。这种灵活性使得 RNN 对于语言建模和时间序列分析等一系列任务非常有用。

大模型技术:LLM 架构解释,RNN、LSTM 和 GRU

Applications of different types of RNNs. | Credits: stanford.edu

每个时间步的输出 奥特​ 可以从隐藏状态计算出来。对于多对多 RNN:

大模型技术:LLM 架构解释,RNN、LSTM 和 GRU

Output Formula | Credits: Cristian Leo

​哪里:

  • O_t​​是时间步的输出 t,
  • V 是输出层的权重矩阵,
  • b_o 是输出层的偏置向量。

对于多对一 RNN,您只需计算最后时间步的输出,而对于一对多 RNN,您将从单个输入开始生成一系列输出。

计算输出 O_t​​​ 如果RNN用于分类任务,通常会通过softmax函数来获取不同类别的概率。

大模型技术:LLM 架构解释,RNN、LSTM 和 GRU

在哪里 P(y_t​ ∣ X_t​, h_(t−1​)) 是输出的概率 yt​ 给定输入 Xt​以及之前的隐藏状态 h_(t−1​)。

从输入到隐藏状态再到输出的操作序列抓住了 RNN 维护和利用时间信息的能力的本质,使它们能够执行涉及序列和时间的复杂任务。

RNN 内部有一个循环,允许信息从模型的后期流回早期阶段。这种循环机制使它们能够处理数据序列:它允许网络的输出影响同一网络处理的后续输入。这种根本区别使得 RNN 能够有效地执行涉及序列和时间序列数据的任务。

大模型技术:LLM 架构解释,RNN、LSTM 和 GRU

Credits: SimpliLearn

3.2 RNN 中的关键操作

大模型技术:LLM 架构解释,RNN、LSTM 和 GRU

Credits: Niklas Donges

了解循环神经网络 (RNN) 的运作方式对于有效使用它们并提高其性能至关重要。让我们分解一下 RNN 中的主要操作:

3.2.1 前向传播

在前向传递中,RNN 一次一步处理数据。对于每个时间步,它将当前输入与先前的隐藏状态相结合以计算新的隐藏状态和输出。该模型使用本质上是循环的特定函数,这意味着每个输出都取决于前面的计算。 sigmoid 或 tanh 等函数通常用于引入非线性,帮助管理信息在隐藏层内的转换方式。

大模型技术:LLM 架构解释,RNN、LSTM 和 GRU

Forward Pass | Credits: Sidharth

数学是这样计算的:

最初,我们设置隐藏状态h到一个零向量。这在数学上表示为:

大模型技术:LLM 架构解释,RNN、LSTM 和 GRU

Hidden States Initialization | Credits: Cristian Leo

或者用 Python 术语来说:

h = np.zeros((1, self.hidden_size))

当我们遍历序列中的每个输入时,我们在时间步计算新的隐藏状态 t,表示为 h_t​,基于之前的隐藏状态h_(t−1)​,当前输入 x_t​,以及相关的权重和偏差:

大模型技术:LLM 架构解释,RNN、LSTM 和 GRU

Hidden States Update Formula | Credits: Cristian Leo

我们可以将 U、W 和 b_h 定义为:

self.weights_ih = np.random.randn(input_size, hidden_size) * 0.01
self.weights_hh = np.random.randn(hidden_size, hidden_size) * 0.01
self.weights_ho = np.random.randn(hidden_size, output_size) * 0.01

这里:

  • Uself.weights_ih,将输入连接到隐藏层的权重矩阵。
  • W self.weights_hh,将一个时间步的隐藏层连接到下一时间步的权重矩阵。
  • b_h​是 self.bias_h,隐藏层的偏置项。
  • tanh 表示双曲正切函数,将非线性引入方程。

这反映了循环 forward 迭代每个输入的方法。

时间步的输出 t,我们称之为 y_t​,然后使用另一组权重和偏差从隐藏状态计算:

大模型技术:LLM 架构解释,RNN、LSTM 和 GRU

Output Formula | Credits: Cristian Leo

​在这种情况下:

  • Vself.weights_ho,从隐藏层到输出层的权重矩阵。
  • b_o​是 self.bias_o,输出层偏置。

代码y = np.dot(h, self.weights_ho) + self.bias_o对应于这个方程,它根据最终时间步的隐藏状态生成输出。

大模型技术:LLM 架构解释,RNN、LSTM 和 GRU

大模型技术:LLM 架构解释,RNN、LSTM 和 GRU

Forward Propagation | Credits: Sachinsoni

3.2.2 Backpropagation Through Time(BPTT)

训练 RNN 涉及一种特殊的反向传播,称为 BPTT。与传统的反向传播不同,BPTT 跨时间延伸——它展开整个数据序列,在每个时间步应用反向传播。该方法计算每个输出的梯度,然后用于调整权重并减少总体损失。然而,BPTT 可能很复杂且占用资源,而且很容易出现梯度消失和爆炸等问题,这可能会干扰网络从较长序列的数据中学习的能力。

大模型技术:LLM 架构解释,RNN、LSTM 和 GRU

Credits: Lakshmi Pallempati

给定 T 个时间步序列并假设一个简单的损失函数 L 在每个时间步 t,例如回归任务的均方误差或分类任务的分类交叉熵,总损失 L_总​ 是每个时间步的损失总和:

大模型技术:LLM 架构解释,RNN、LSTM 和 GRU

Total Loss Formula | Credits: Cristian Leo

​为了更新权重,我们需要计算 L_总​关于权重。对于权重矩阵 U (输入隐藏), W (隐藏到隐藏),以及 V (隐藏到输出),我们有:

大模型技术:LLM 架构解释,RNN、LSTM 和 GRU

Weights Gradients | Credits: Cristian Leo

​​这些梯度是使用链式法则计算的。从最后一个时间步开始向后移动:

大模型技术:LLM 架构解释,RNN、LSTM 和 GRU

Output Chain Rule Formula | Credits: Cristian Leo

在哪里:

  • ​∂L_t/y_t​​ 是损失函数在时间步的导数 t关于输出 y_t​.
  • y_t/V​​ 可以直接计算为隐藏状态 h_t​因为 y_t​ = V_h_t​ + b_o​.

为了 W U,计算涉及网络的循环性质:

大模型技术:LLM 架构解释,RNN、LSTM 和 GRU

Hidden and Initial States Chain Rule Formula | Credits: Cristian Leo

在这里,​LT+1​ / ∂高温+1​ 指timestep处损失的梯度 t+1 相对于隐藏状态 t+1,这又取决于隐藏状态 t。这种递推关系构成了 BPTT 的关键。

3.2.3 权重更新

计算出梯度后,使用随机梯度下降 (SGD) 等优化算法更新权重:

大模型技术:LLM 架构解释,RNN、LSTM 和 GRU

Weight Updates Formulas | Credits: Cristian Leo

​​哪里 η 是学习率。

4. 训练 RNN 的挑战

大模型技术:LLM 架构解释,RNN、LSTM 和 GRU

Credits: Rahul Pandey

4.1 什么是消失梯度?

随着反向传播算法从输出层向下(或向后)进入输入层,梯度通常变得越来越小并接近零,最终使初始层或较低层的权重几乎保持不变。结果,梯度下降永远不会收敛到最优值。这被称为 梯度消失 问题。

4.2 什么是梯度爆炸?

相反,在某些情况下, 渐变随着反向传播算法的进展,它会变得越来越大。这反过来会导致非常大的权重更新并导致梯度下降发散。这被称为 梯度爆炸 问题。

4.3 为什么梯度甚至会消失/爆炸?

某些激活函数,例如逻辑函数(sigmoid),其输入和输出的方差之间存在巨大差异。简而言之,它们将较大的输入空间缩小并转换为位于 [0,1] 范围之间的较小输出空间。

大模型技术:LLM 架构解释,RNN、LSTM 和 GRU

Credits: neptune

观察上面的 Sigmoid 函数图,我们可以看到,对于较大的输入(负或正),它在 0 或 1 处饱和,导数非常接近于零。因此,当反向传播算法进入时,它实际上没有梯度可以在网络中向后传播,并且随着算法向下穿过顶层,存在的任何少量残留梯度都会不断稀释。因此,这不会给较低层留下任何内容。

类似地,在某些情况下,假设分配给网络的初始权重会产生一些较大的损失。现在,梯度会在更新过程中累积并导致非常大的梯度,最终导致网络权重大幅更新并导致网络不稳定。参数有时会变得太大以致于溢出并导致 NaN 值。

大模型技术:LLM 架构解释,RNN、LSTM 和 GRU

4.4 如何知道我们的模型是否存在梯度爆炸/消失问题?

以下是一些迹象,可以表明我们的梯度正在消失和梯度爆炸:

大模型技术:LLM 架构解释,RNN、LSTM 和 GRU

当然,我们既不希望我们的信号爆炸或饱和,也不希望它消失。进行预测时信号需要正确地向前流动,计算梯度时信号需要向后方向正确流动。

5. 处理梯度消失/爆炸

现在我们了解了梯度消失/爆炸问题,我们可以学习一些技术来解决它们。

5.1 正确的权重初始化

研究人员 Xavier Glorot、Antoine Bordes 和 Yoshua Bengio 提出了一种显着缓解这一问题的方法。

为了信号的正确流动,作者认为:

  • 每层输出的方差应等于其输入的方差。
  • 梯度在以相反方向流经层之前和之后应具有相等的方差。

尽管这两个条件不能适用于网络中的任何层,除非该层的输入数量(范宁) 等于层中神经元的数量 (扇出),他们提出了一个经过充分验证的折衷方案,在实践中效果非常好。他们使用以下方程随机初始化网络中每一层的连接权重,通常称为 Xavier 初始化(以作者的名字命名)或 Glorot 初始化(以作者的姓氏命名)。

where  fanavg = ( fanin + fanout ) / 2
  • 带均值的正态分布 0 和方差 σ2 = 1/ 平均
  • 或者之间的均匀分布 -r+r , 和 r = sqrt( 3 / fanavg )

以下是针对不同激活函数的一些更流行的权重初始化策略,它们仅在方差规模和使用 法纳夫 或者 范宁

对于均匀分布,计算 r 为: r = sqrt( 3*σ2)

大模型技术:LLM 架构解释,RNN、LSTM 和 GRU

使用上述初始化策略可以显着加快训练速度并增加梯度下降收敛于较低泛化误差的几率。

等等,但是我们如何将这些策略放入代码中?

放松!我们不需要硬编码任何东西,Keras 会为我们做这件事。

  • Keras 使用 Xavier 的均匀分布初始化策略。
  • 如果我们希望使用与默认策略不同的策略,可以使用 内核初始化器 创建图层时的参数。例如 :
keras.layer.Dense(25, activation = "relu", kernel_initializer="he_normal")

或者

keras.layer.Dense(25, activation = "relu", kernel_initializer="he_uniform")

如果我们希望使用基于的初始化 扇子平均而不是 范宁,我们可以像这样使用 VarianceScaling 初始化器:

he_avg_init = keras.initializers.VarianceScaling(scale=2., mode='fan_avg', distribution='uniform')
keras.layers.Dense(20, activation="sigmoid", kernel_initializer=he_avg_init)

5.2 使用非饱和激活函数

在前面的章节中,在研究 sigmoid 激活函数的性质时,我们观察到它对于较大输入(负或正)的饱和性质是梯度消失和爆炸背后的主要原因,因此不建议使用它在网络的隐藏层中。

因此,为了解决 sigmoid 和 tanh 等激活函数的饱和问题,我们必须使用其他一些非饱和函数,例如 ReLu 及其替代函数。

ReLU(修正线性单元)

大模型技术:LLM 架构解释,RNN、LSTM 和 GRU

Relu(z) = max(0,z)
  • 对于任何负输入,输出 0。
  • 范围:[0,无穷大]

不幸的是,“在某些情况下”,ReLu 函数也不是网络中间层的完美选择。它遇到了一个称为 垂死 ReLus 中一些神经元会消失,这意味着随着训练的进展,它们会继续抛出 0 作为输出。

详细了解垂死的 relus 问题 这里.

ReLU 的一些流行替代函数可以缓解用作网络中间层的激活时梯度消失的问题,包括 LReLU、PReLU、ELU、SELU:

LReLU(Leaky ReLU)

大模型技术:LLM 架构解释,RNN、LSTM 和 GRU

LeakyReLUα(z) = max(αz, z)
  • “泄漏”的量由超参数控制 α,它是 z < 0 时函数的斜率。
  • 较小的泄漏斜率确保由泄漏 Relu 供电的神经元永远不会死亡;尽管他们可能会在长时间的训练阶段陷入昏迷状态,但他们最终总有机会醒来。
  • 该模型还可以训练 α,在训练过程中学习其值。这种变体将 α 视为参数而不是超参数,称为参数泄漏 ReLU (PReLU)。

ELU(指数线性单位)

大模型技术:LLM 架构解释,RNN、LSTM 和 GRU

对于 z < 0,它采用负值,这使得该单元的平均输出更接近 0,从而缓解梯度消失问题

  • 对于 z < 0,梯度不为零。这避免了神经元死亡的问题。
  • 对于 α = 1,函数在各处都是平滑的,这会加速梯度下降,因为它不会在 z=0 左右左右跳动。
  • 该函数的缩放版本( SELU: 缩放ELU )在深度学习中也经常使用。

5.3 批量归一化

使用 He 初始化以及 ReLU 激活函数的任何变体可以显着减少一开始问题消失/爆炸的可能性。但是,这并不能保证该问题在训练过程中不会再次出现。

2015 年,Sergey Ioffe 和 Christian Szegedy 提出了 纸 他们引入了一种称为 批量归一化解决梯度消失/爆炸问题。

以下要点解释了 BN 背后的直觉及其工作原理:

  • 它包括在模型中每个隐藏层的激活函数之前或之后添加一个操作。
  • 此操作简单地对每个输入进行零中心和归一化,然后使用每层两个新的参数向量来缩放和移动结果:一个用于缩放,另一个用于移动。
  • 换句话说,该操作让模型学习每个层输入的最佳规模和平均值。
  • 为了对输入进行归零中心和标准化,算法需要估计每个输入的平均值和标准差。
  • 它通过评估当前小批量输入的平均值和标准差来实现这一点(因此称为“批量归一化”)。
model = keras.models.Sequential([keras.layers.Flatten(input_shape=[28, 28]),keras.layers.BatchNormalization(),keras.layers.Dense(300, activation="relu"),keras.layers.BatchNormalization(),keras.layers.Dense(100, activation="relu"),keras.layers.BatchNormalization(),keras.layers.Dense(10, activation="softmax")])

我们只是在每一层之后添加了批量归一化(数据集:FMNIST)

model.summary()
大模型技术:LLM 架构解释,RNN、LSTM 和 GRU

5.4 梯度裁剪

缓解梯度爆炸问题的另一种流行技术是在反向传播期间修剪梯度,以便它们永远不会超过某个阈值。这称为渐变裁剪。

  • 该优化器会将梯度向量的每个分量裁剪为 –1.0 和 1.0 之间的值。
  • 这意味着我们将把每个可训练参数的损失的所有偏导数限制在 –1.0 和 1.0 之间。
optimizer = keras.optimizers.SGD(clipvalue = 1.0)
  • 阈值是我们可以调整的超参数。
  • 梯度向量的方向可能会因此而改变:例如,让原始梯度向量为 [0.9, 100.0],主要指向第二个轴的方向,但是一旦我们将其剪切某个值,我们就会得到 [0.9, 100.0]。 1.0] 现在指向两个轴之间对角线周围的某个位置。
  • 为了确保即使在裁剪后方向也保持不变,我们应该按规范而不是按值进行裁剪。
optimizer = keras.optimizers.SGD(clipnorm = 1.0)
  • 如果我们选择的阈值小于 ℓ2 范数,我们将裁剪整个梯度。例如,如果clipnorm=1,我们会将向量[0.9, 100.0]裁剪为[0.00899, 0.999995],从而保留其方向。

6. 从头开始​​构建 RNN

对于本次演示,我们将使用 航空旅客数据集,这是托管在 GitHub 上的一个小型开源数据集。

让我们深入研究代码中每个组件的详细信息,以创建有关如何从头开始实现此 RNN 的全面指南!

6.1 定义RNN类

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

class RNN:
    def __init__(self, input_size, hidden_size, output_size, init_method="random"):
        self.weights_ih, self.weights_hh, self.weights_ho = self.initialize_weights(input_size, hidden_size, output_size, init_method)
        self.bias_h = np.zeros((1, hidden_size))
        self.bias_o = np.zeros((1, output_size))
        self.hidden_size = hidden_size

    def initialize_weights(self, input_size, hidden_size, output_size, method):
        if method == "random":
            weights_ih = np.random.randn(input_size, hidden_size) * 0.01
            weights_hh = np.random.randn(hidden_size, hidden_size) * 0.01
            weights_ho = np.random.randn(hidden_size, output_size) * 0.01
        elif method == "xavier":
            weights_ih = np.random.randn(input_size, hidden_size) / np.sqrt(input_size / 2)
            weights_hh = np.random.randn(hidden_size, hidden_size) / np.sqrt(hidden_size / 2)
            weights_ho = np.random.randn(hidden_size, output_size) / np.sqrt(hidden_size / 2)
        elif method == "he":
            weights_ih = np.random.randn(input_size, hidden_size) * np.sqrt(2 / input_size)
            weights_hh = np.random.randn(hidden_size, hidden_size) * np.sqrt(2 / hidden_size)
            weights_ho = np.random.randn(hidden_size, output_size) * np.sqrt(2 / hidden_size)
        else:
            raise ValueError("Invalid initialization method")
        return weights_ih, weights_hh, weights_ho


    def forward(self, inputs):
        h = np.zeros((1, self.hidden_size))
        self.last_inputs = inputs
        self.last_hs = {0: h}

        for i, x in enumerate(inputs):
            x = x.reshape(1, -1)  # Ensure x is a row vector
            h = np.tanh(np.dot(x, self.weights_ih) + np.dot(h, self.weights_hh) + self.bias_h)
            self.last_hs[i + 1] = h

        y = np.dot(h, self.weights_ho) + self.bias_o
        self.last_outputs = y
        return y

    def backprop(self, d_y, learning_rate, clip_value=1):
        n = len(self.last_inputs)

        d_y_pred = (self.last_outputs - d_y) / d_y.size
        d_Whh = np.zeros_like(self.weights_hh)
        d_Wxh = np.zeros_like(self.weights_ih)
        d_Why = np.zeros_like(self.weights_ho)
        d_bh = np.zeros_like(self.bias_h)
        d_by = np.zeros_like(self.bias_o)
        d_h = np.dot(d_y_pred, self.weights_ho.T)

        for t in reversed(range(1, n + 1)):
            d_h_raw = (1 - self.last_hs[t] ** 2) * d_h
            d_bh += d_h_raw
            d_Whh += np.dot(self.last_hs[t - 1].T, d_h_raw)
            d_Wxh += np.dot(self.last_inputs[t - 1].reshape(1, -1).T, d_h_raw)
            d_h = np.dot(d_h_raw, self.weights_hh.T)

        for d in [d_Wxh, d_Whh, d_Why, d_bh, d_by]:
            np.clip(d, -clip_value, clip_value, out=d)
            
        self.weights_ih -= learning_rate * d_Wxh
        self.weights_hh -= learning_rate * d_Whh
        self.weights_ho -= learning_rate * d_Why
        self.bias_h -= learning_rate * d_bh
        self.bias_o -= learning_rate * d_by

这是我们的 RNN 的蓝图。

我们将在此类中定义 RNN 的初始化、前向传播和反向传播。

RNN 初始化

class RNN:
  def __init__(self, input_size, hidden_size, output_size, init_method="random"):
    self.weights_ih, self.weights_hh, self.weights_ho = self.initialize_weights(input_size, hidden_size, output_size, init_method)
    self.bias_h = np.zeros((1, hidden_size))
    self.bias_o = np.zeros((1, output_size))
    self.hidden_size = hidden_size

__init__ 方法使用每层(输入、隐藏、输出)中的神经元数量和权重初始化方法来初始化 RNN。

self.weights_ih, self.weights_hh, self.weights_ho = self.initialize_weights(input_size, hidden_size, output_size, init_method)

这里我们称之为initialize_weights方法根据指定的初始化方法(“random”、“xavier”或“he”)设置权重。每组权重连接网络的不同层: weights_ih 将输入层连接到隐藏层,weights_hh在下一个时间步将隐藏层与其自身连接(捕获 RNN 的“循环”部分),并且 weights_ho 将隐藏层连接到输出层。

self.bias_h = np.zeros((1, hidden_size))
self.bias_o = np.zeros((1, output_size))

偏差被初始化为零向量,这将在训练期间进行调整。隐藏层有一种偏差,输出层有一种偏差。

前向传递法

def forward(self, inputs):
    h = np.zeros((1, self.hidden_size))
    self.last_inputs = inputs
    self.last_hs = {0: h}
  
    for i, x in enumerate(inputs):
        x = x.reshape(1, -1)  # Ensure x is a row vector
        h = np.tanh(np.dot(x, self.weights_ih) + np.dot(h, self.weights_hh) + self.bias_h)
        self.last_hs[i + 1] = h
  
    y = np.dot(h, self.weights_ho) + self.bias_o
    self.last_outputs = y
    return y

forward函数接受一系列输入并通过 RNN 对其进行处理。它在序列长度上循环计算隐藏状态和最终输出。

h = np.zeros((1, self.hidden_size))

这将隐藏状态初始化为零向量。当网络看到更多的输入序列时,该状态将被更新以从输入中捕获信息。

for i, x in enumerate(inputs):
    x = x.reshape(1, -1)  # Ensure x is a row vector
    h = np.tanh(np.dot(x, self.weights_ih) + np.dot(h, self.weights_hh) + self.bias_h)
    self.last_hs[i + 1] = h

对于序列中的每个输入,代码会重塑输入以确保它是行向量,然后使用当前输入、先前的隐藏状态、权重和偏差来更新隐藏状态。这np.tanh函数引入了复杂模式识别所需的非线性。

y = np.dot(h, self.weights_ho) + self.bias_o

处理整个序列后,我们使用最后的隐藏状态、连接隐藏层和输出层的权重以及输出偏差来计算输出。

随时间反向传播

def backprop(self, d_y, learning_rate, clip_value=1):
    n = len(self.last_inputs)
  
    d_y_pred = (self.last_outputs - d_y) / d_y.size
    d_Whh = np.zeros_like(self.weights_hh)
    d_Wxh = np.zeros_like(self.weights_ih)
    d_Why = np.zeros_like(self.weights_ho)
    d_bh = np.zeros_like(self.bias_h)
    d_by = np.zeros_like(self.bias_o)
    d_h = np.dot(d_y_pred, self.weights_ho.T)
  
    for t in reversed(range(1, n + 1)):
        d_h_raw = (1 - self.last_hs[t] ** 2) * d_h
        d_bh += d_h_raw
        d_Whh += np.dot(self.last_hs[t - 1].T, d_h_raw)
        d_Wxh += np.dot(self.last_inputs[t - 1].reshape(1, -1).T, d_h_raw)
        d_h = np.dot(d_h_raw, self.weights_hh.T)
  
    for d in [d_Wxh, d_Whh, d_Why, d_bh, d_by]:
        np.clip(d, -clip_value, clip_value, out=d)
        
    self.weights_ih -= learning_rate * d_Wxh
    self.weights_hh -= learning_rate * d_Whh
    self.weights_ho -= learning_rate * d_Why
    self.bias_h -= learning_rate * d_bh
    self.bias_o -= learning_rate * d_by

backprop 方法实现了BPTT算法。它计算每个时间步的梯度并相应地更新权重和偏差。此外,它还通过使用合并了梯度裁剪np.clip以防止梯度爆炸问题。

6.2 提前停止机制

class EarlyStopping:
   def __init__(self, patience=7, verbose=False, delta=0):
        self.patience = patience
        self.verbose = verbose
        self.counter = 0
        self.best_score = None
        self.early_stop = False
        self.delta = delta

    def __call__(self, val_loss):
        score = -val_loss

        if self.best_score is None:
            self.best_score = score

        elif score < self.best_score + self.delta:
            self.counter += 1
            
            if self.counter >= self.patience:
                self.early_stop = True
        else:
            self.best_score = score
            self.counter = 0

该类在训练期间提供了提前停止机制。如果验证损失在一定数量的 epoch 后没有改善(patience),停止训练以防止过度拟合。

我不会深入探讨此类的解释,正如我在上一篇文章中详细解释的那样:

6.3 RNN训练器类

class RNNTrainer:
    def __init__(self, model, loss_func='mse'):
        self.model = model
        self.loss_func = loss_func
        self.train_loss = []
        self.val_loss = []

    def calculate_loss(self, y_true, y_pred):
        if self.loss_func == 'mse':
            return np.mean((y_pred - y_true)**2)
        
        elif self.loss_func == 'log_loss':
            return -np.mean(y_true*np.log(y_pred) + (1-y_true)*np.log(1-y_pred))
        
        elif self.loss_func == 'categorical_crossentropy':
            return -np.mean(y_true*np.log(y_pred))
        
        else:
            raise ValueError('Invalid loss function')

    def train(self, train_data, train_labels, val_data, val_labels, epochs, learning_rate, early_stopping=True, patience=10, clip_value=1):
        if early_stopping:
            early_stopping = EarlyStopping(patience=patience, verbose=True)
        for epoch in range(epochs):
            for X_train, y_train in zip(train_data, train_labels):
                outputs = self.model.forward(X_train)
                self.model.backprop(y_train, learning_rate, clip_value)
                train_loss = self.calculate_loss(y_train, outputs)
                self.train_loss.append(train_loss)

            val_loss_epoch = []
            for X_val, y_val in zip(val_data, val_labels):
                val_outputs = self.model.forward(X_val)
                val_loss = self.calculate_loss(y_val, val_outputs)
                val_loss_epoch.append(val_loss)

            val_loss = np.mean(val_loss_epoch)
            self.val_loss.append(val_loss)

            if early_stopping:
                early_stopping(val_loss)

                if early_stopping.early_stop:
                    print(f"Early stopping at epoch {epoch} | Best validation loss = {-early_stopping.best_score:.3f}")
                    break

            if epoch % 10 == 0:
                print(f'Epoch {epoch}: Train loss = {train_loss:.4f}, Validation loss = {val_loss:.4f}')

    def plot_gradients(self):
        for i, gradients in enumerate(zip(*self.gradients)):
            plt.plot(gradients, label=f'Neuron {i}')

        plt.xlabel('Time step')
        plt.ylabel('Gradient')
        plt.title('Gradients for each neuron over time')
        plt.legend()
        plt.show()

本课程结束了培训过程。它负责运行前向传播和反向传播,计算每个时期后的损失,并维护训练和验证损失的历史记录。

训练方法

上面我们定义了训练 RNN 模型的方法。它循环指定数量的 epoch,通过模型处理训练数据,应用反向传播,并跟踪训练和验证损失。

6.4 数据加载和预处理

class TimeSeriesDataset:
    def __init__(self, url, look_back=1, train_size=0.67):
        self.url = url
        self.look_back = look_back
        self.train_size = train_size

    def load_data(self):
        df = pd.read_csv(self.url, usecols=[1])
        df = self.MinMaxScaler(df.values)  # Convert DataFrame to numpy array
        train_size = int(len(df) * self.train_size)
        train, test = df[0:train_size,:], df[train_size:len(df),:]
        return train, test
    
    def MinMaxScaler(self, data):
        numerator = data - np.min(data, 0)
        denominator = np.max(data, 0) - np.min(data, 0)
        return numerator / (denominator + 1e-7)

    def create_dataset(self, dataset):
        dataX, dataY = [], []
        for i in range(len(dataset)-self.look_back-1):
            a = dataset[i:(i+self.look_back), 0]
            dataX.append(a)
            dataY.append(dataset[i + self.look_back, 0])
        return np.array(dataX), np.array(dataY)

    def get_train_test(self):
        train, test = self.load_data()
        trainX, trainY = self.create_dataset(train)
        testX, testY = self.create_dataset(test)
        return trainX, trainY, testX, testY

此类处理时间序列数据的加载、预处理和批处理。它旨在促进处理将输入 RNN 的数据。

def load_data(自身): 从 URL 指定的 CSV 文件加载数据。它使用 Pandas 处理 CSV 并提取必要的列。

def MinMaxScaler(自身,数据): 这是一个标准化函数,可将数据缩放到 0 到 1 之间。这是时间序列和其他类型数据处理中的常见做法,可帮助神经网络更有效地学习。

def create_dataset(自身,数据集):它将加载的数据重新格式化为合适的格式,其中 dataX 包含模型的输入序列和dataY包含每个序列的相应标签或目标。

def get_train_test(自身): 这将根据指定的比例将加载的数据拆分为训练数据集和测试数据集。

加载和准备数据

url = 'https://raw.githubusercontent.com/jbrownlee/Datasets/master/daily-min-temperatures.csv'
dataset = TimeSeriesDataset(url, look_back=1)
trainX, trainY, testX, testY = dataset.get_train_test()

在这里,我们指定数据集的 URL,实例化 TimeSeriesDataset 与一个 look_back 为 1,这意味着每个输入序列(用于训练 RNN)将包含 1 个时间步长。然后将数据分为训练集和测试集。

trainX = np.reshape(trainX, (trainX.shape[0], 1, trainX.shape[1]))
testX = np.reshape(testX, (testX.shape[0], 1, testX.shape[1]))

输入数据需要重新整形以适应 RNN 输入要求,通常期望数据格式为[样本、时间步长、特征]。

6.5 训练RNN

rnn = RNN(look_back, 256, 1, init_method='xavier')
trainer = RNNTrainer(rnn, 'mse')
trainer.train(trainX, trainY, testX, testY, epochs=100, learning_rate=0.01, early_stopping=True, patience=10, clip_value=1)

RNN 模型通过 Xavier 初始化进行实例化,然后使用RNNTrainer。训练器使用均方误差(“mse”)作为损失函数,适用于时间序列预测等回归任务。

此实现涵盖了设置、训练和使用 RNN 来执行简单时间序列预测任务所需的所有基本组件。代码结构有助于理解和修改更复杂或不同类型的序列建模任务。

7. 长短期记忆网络(LSTM)

在上面关于循环神经网络(RNN)的讨论中,我们研究了它们的设计如何让它们有效地处理序列。这使得它们非常适合数据顺序和上下文很重要的任务,例如分析时间序列数据或处理语言。

现在,我们正在研究一种 RNN,它可以解决传统 RNN 面临的重大挑战之一:管理长期数据依赖性。这些就是长短期记忆网络 (LSTM),其复杂性进一步提高。他们使用门系统来控制信息如何在网络中流动——决定在扩展序列中保留什么和忘记什么。

大模型技术:LLM 架构解释,RNN、LSTM 和 GRU

RNN v/s LSTM. a: RNNs use their internal state (memory) to process sequences of inputs, b: Long Short-Term Memory (LSTM) network is a varient of RNN, with addtional long term memory to remember past data. | Credits: ResearchGate

LSTM(长短期记忆)是循环神经网络(RNN)家族中的一员或一种特殊类型。 LSTM 可以作为默认行为 学习长期依赖通过长时间记住重要的相关信息。

让我们用一个简单的故事来分解 LSTM 背后的核心思想:

有一次,维克拉姆国王击败了XYZ国王,但去世了。他的儿子维克拉姆·朱尼尔接任,英勇作战,但也战死沙场。他的孙子维克拉姆·超级少年(Vikram Super Junior)虽然没有那么强大,但利用他的智慧最终击败了国王 XYZ,为家人报仇。

当阅读这个故事或任何一系列事件时,我们的注意力首先集中在眼前的细节上。例如,我们处理维克拉姆国王的胜利和死亡。但随着更多角色的引入,我们调整了对故事的长期理解,继续追踪 Vikram Junior 和 Super Junior。这种上下文的不断更新反映了 LSTM 的工作原理:随着新信息的流入,它们会维护和更新短期和长期记忆。

RNN 很难平衡短期和长期环境。就像我们清楚地记得节目的最新一集但忘记了之前的细节一样,随着新数据的到来,RNN 经常会丢失长期信息。 LSTM 通过创建两条路径(一条用于短期记忆,一条用于长期记忆)来解决这个问题,允许模型保留重要信息并丢弃不太重要的信息。

在 LSTM 中,信息流经 细胞状态,它就像一条传送带,向前传送有用的信息,同时选择性地忘记不相关的细节。与新数据覆盖旧数据的 RNN 不同,LSTM 应用仔细的数学运算(加法和乘法)来保留关键信息。这使他们能够有效地确定优先级并管理新数据和过去的数据。

每个细胞状态取决于 三个不同的 依赖关系。有:

  1. 之前的细胞状态 (上一时间步结束时存储的信息)
  2. 之前的隐藏状态 (与前一个单元格的输出相同)
  3. 当前时间步的输入(当前时间步的新信息/输入)。

话虽如此,让我们更详细地讨论 LSTM 的架构和功能。

8. LSTM架构

大模型技术:LLM 架构解释,RNN、LSTM 和 GRU

LSTM | Credits: Muhammed Fouzan

循环神经网络 (RNN) 架构具有一系列重复神经网络。这个重复模块有一个简单且单一的功能: tanh激活 功能。

大模型技术:LLM 架构解释,RNN、LSTM 和 GRU

LSTM 架构也与 RNN 相同,是一系列重复模块/神经网络。但 LSTM 重复模型并非只有一层 tanh 层,而是具有 四种不同的功能.

这四个功能操作是特别相关的。有

  • Sigmoid Activation Function
  • Tanh Activation Function
  • Pointwise Multiplication
  • Pointwise Addition
大模型技术:LLM 架构解释,RNN、LSTM 和 GRU

在整个网络中,信息以矢量形式传递。让我们讨论一下上图中提到的不同标志:

  • Square Box: 单个神经网络
  • Circle: 逐点运算意味着逐个元素执行运算
  • Arrow Mark: 矢量信息从一层转换到另一层
  • Joining two lines into one line: 连接两个向量
  • Splitting one line into two lines: 将相同的信息传输到两个不同的操作或层。

首先,我们来讨论一下 LSTM 架构中的主要功能和操作。

8.1 激活函数和线性运算

大模型技术:LLM 架构解释,RNN、LSTM 和 GRU

sigmoid函数

大模型技术:LLM 架构解释,RNN、LSTM 和 GRU

sigmoid 函数也称为 物流激活功能。该函数具有平滑的“S”形曲线。

sigmoid 的输出结果始终在 0 和 1 的范围内。

这 sigmoid激活函数 主要用于我们必须预测概率作为输出的模型。由于任何输入的概率仅存在于 0 和 1sigmoid逻辑激活函数 是正确的、最好的选择。

Tanh 激活函数

大模型技术:LLM 架构解释,RNN、LSTM 和 GRU

Tanh 激活 函数看起来也类似于 sigmoid/逻辑 功能。实际上,它是一个缩放的 sigmoid 函数。我们可以将 tanh 函数公式写成 sigmoid 函数。

tanh函数结果值的范围是 -1 到 +1。使用这个 tanh 函数,我们可以找到强正、中性或负输入。

逐点乘法

两个向量的逐点乘法是对各个元素的两个向量应用乘法运算。例如

  • A = [1,2,3,4]
  • B = [2,3,4,5]
  • 逐点相乘结果: [2,6,12,20]

逐点加法

两个向量的逐点相加是将两个向量元素分别相加的过程。例如

  • A = [1,2,3,4]
  • B = [2,3,4,5]
  • 逐点相加结果: [3,5,7,9]

8.2 LSTM算法背后的关键概念

大模型技术:LLM 架构解释,RNN、LSTM 和 GRU

Credits: Sachin Soni

LSTM 的主要独特行为是细胞状态;它充当 c传送带 和一些小事 线性相互作用.

这意味着该单元状态通过基本操作来移动信息,例如 加法和乘法;这就是为什么信息会随着细胞状态顺利流动,与原始状态相比没有太多变化。

细胞状态或 LSTM 的传送带是下图中突出显示的水平线。

大模型技术:LLM 架构解释,RNN、LSTM 和 GRU

LSTM 具有独特的结构来识别哪些信息是重要的或不重要的。 LSTM 可以根据重要性删除或添加信息到细胞状态。这些特殊类型的结构称为 盖茨.

门是一种独特的信息转换方式,LSTM 使用这些门来决定哪些信息要记住、删除以及传递到另一层等。

LSTM会根据这些信息向传送带(单元状态)删除或添加信息。每个门包括一个 sigmoid神经元 网络层和一个 逐点乘法 手术。

LSTM 具有三种门。有

  • Forget Gate
  • Input Gate
  • Output Gate

8.2.1 遗忘门

大模型技术:LLM 架构解释,RNN、LSTM 和 GRU

Forget Gate Operations | Credits: Michael Phi

在 LSTM 架构的重复模块中,我们拥有的第一个门是遗忘门。该门的主要任务是决定哪些信息应该保留或丢弃。

这意味着决定将哪些信息发送到单元状态以进一步处理。遗忘门将输入作为信息 之前的隐藏状态 和当前输入并结合两个状态的信息,并通过 sigmoid 函数发送。

之间的 sigmoid 函数结果 0 和 1。如果结果接近 0 则意味着忘记,如果结果接近 1 则意味着保留/记住。

8.2.2 输入门

大模型技术:LLM 架构解释,RNN、LSTM 和 GRU

Input gate operations | Credits: Michael Phi

LSTM 架构有一个输入门,用于更新遗忘门之后的单元状态信息。输入门有两种神经网络层,一种是 sigmoid,另一种是 tanh。两个网络层都将输入视为先前隐藏的状态信息和当前输入的信息。

Sigmoid网络层结果范围 0 到 1 之间,tanh 结果范围为 -1到1。 sigmoid 层决定保留哪些信息很重要,tanh 层负责调节网络。

在对隐藏信息和当前信息应用 sigmoid 和 tanh 函数后,我们将两个输出相乘。最后,sigmoid 输出将决定从 tanh 输出中保留哪些重要信息。

8.2.3 输出门

大模型技术:LLM 架构解释,RNN、LSTM 和 GRU

Sigmoid squishes values to be between 0 and 1 | Credits: Michael Phi

大模型技术:LLM 架构解释,RNN、LSTM 和 GRU

output gate operations | Credits: Michael Phi

LSTM 中的最后一个门是输出门。输出门的主要任务是决定哪些信息应该处于下一个隐藏状态。这意味着输出层的输出是下一个隐藏状态的输入。

与输入门相同,输出门也有两个神经网络层。但操作方式不同。从输入门,我们获得了更新的细胞状态信息。

我们必须通过 sigmoid 层发送隐藏状态和当前输入信息,并通过该输出门中的 tanh 层发送更新的单元状态信息。然后将 sigmoid 层和 tanh 层的结果相乘。

最终结果被发送到下一个隐藏层作为输入。

9. LSTM的工作流程

LSTM 架构中的第一步也是最重要的一步是决定哪些信息是必要的,哪些信息是从先前的细胞状态中丢弃的。 LSTM 中执行此过程的第一个门是“忘记门”。

忘记门接受输入,因为前一个时间步长是 隐藏层信息(ht-1)和当前时间步长输入 (xt) 并将其发送到 sigmoid 神经网络层。

结果是向量形式,其中包含 0 和 1 价值观。然后,对先前的单元状态应用逐点乘法运算 (CT-1) 信息(向量形式)和 sigmoid 函数的输出(ft)。

遗忘门的最终结果输出1代表“完全保留这个信息”,0代表“不保留这个信息”。

大模型技术:LLM 架构解释,RNN、LSTM 和 GRU

下一步是决定将哪些信息存储在 当前单元状态 (Ct)。另一个门将完成该任务,LSTM 架构中的第二个门是“输入门”。

用新的重要信息更新单元状态的整个过程将通过使用两种激活函数/神经网络层来完成;他们的 sigmoid 神经网络和 tanh 神经网络层。

第一个 sigmoid 网络像忘记门一样接受输入:之前的时间步长是 隐藏层信息(ht-1) 和当前时间步长 (xt)。

此过程决定我们将更新哪些值。然后,tanh 神经网络也采用与 sigmoid 神经网络层相同的输入。它以以下形式创建新的候选值 矢量(ct(上方破折号)) 以规范网络。

大模型技术:LLM 架构解释,RNN、LSTM 和 GRU

现在我们对 sigmoid 层和 tanh 层的输出应用逐点乘法。之后,我们必须对输出执行逐点加法运算 忘记门 以及输入门中逐点乘法的结果来更新当前 单元状态信息 (ct).

大模型技术:LLM 架构解释,RNN、LSTM 和 GRU

LSTM 架构的最后一步是决定我们将使用哪些信息作为输出;在 LSTM 中执行此过程的最后一个门是“输出门”。该输出将基于我们的单元状态,但将是过滤后的版本。

在这个门中,我们首先应用 sigmoid 神经网络,它接受像之前门的 sigmoid 层一样的输入:上一个时间步长隐藏层信息(ht-1) 当前时间输入 (xt) 决定细胞状态信息的哪些部分要输出。

然后通过tanh神经网络层发送更新的单元状态信息来调节网络 (将值推到 -1 和 1 之间)然后对 sigmoid 和 tanh 神经网络层的结果应用逐点乘法。

大模型技术:LLM 架构解释,RNN、LSTM 和 GRU

整个过程在 LSTM 架构的每个模块中都会重复。

10. LSTM 架构的类型

LSTM 是解决或处理序列预测问题的最有趣的起点。根据 LSTM 网络作为层的方式,我们可以将 LSTM 架构分为各种类型的 LSTM。

大模型技术:LLM 架构解释,RNN、LSTM 和 GRU

本节将讨论最常用的 五种不同类型 LSTM 架构。这些都是:

Vanilla LSTM

Vanilla LSTM 架构是基本的 LSTM 架构;它只有 一个隐藏层 和一个输出层来预测结果。

Stacked LSTM

Stacked LSTM 架构是压缩一个列表或多个 LSTM 层的 LSTM 网络模型。 Stacked LSTM 也称为 Deep LSTM 网络模型。

在此架构中,每个 LSTM 层都会预测发送到下一个 LSTM 层的输出序列,而不是预测单个输出值。然后最后的 LSTM 层预测单个输出。

CNN LSTM

CNN LSTM 架构是 CNN 和 LSTM 架构的组合。该架构使用 CNN 网络层从输入中提取基本特征,然后将其发送到 LSTM 层以支持序列预测。

该架构的一个示例应用是为输入图像或视频等图像序列生成文本描述。

Encoder-Decoder LSTM

编码器-解码器 LSTM 架构是一种特殊的 LSTM 架构。它主要是为了解决机器翻译、语音识别等序列到序列的问题而设计的。编码器-解码器LSTM的另一个名字是 seq2seq(序列到序列).

序列到序列问题是自然语言处理领域中具有挑战性的问题,因为在这些问题中,输入和输出项的数量可能会有所不同。

编码器-解码器 LSTM 架构有一个编码器,用于将输入转换为中间编码器向量。然后一个解码器将中间编码器向量转换为最终结果。编码器和解码器都是堆叠式 LSTM。

Bidirectional LSTM

双向 LSTM 架构是传统 LSTM 架构的扩展。该架构更适合情感分类、意图分类等序列分类问题。

双向 LSTM 架构使用两个 LSTM,而不是一个 LSTM,一个用于前向(从左到右),另一个 LSTM 用于后向(从右到左)。

与传统的 LSTM 相比,这种架构可以为网络提供更多的上下文信息,因为它会收集 双方的话,左侧和右侧。它将加速序列分类问题的性能。

11. 用 Python 从头开始​​构建 LSTM

在本节中,我们将逐步分解 Python 中 LSTM 的实现,并回顾本文前面介绍的数学基础和概念。我们将在 Google 股票数据上训练我们从头开始制作的模型。该数据集取自 Kaggle,可免费用于 商业用途.

11.1 导入和初始设置

numpy (np) 和 pandas(pd):用于所有数组和数据帧操作,这对于任何类型的数值计算,特别是神经网络的实现来说都是基础。

WeightInitializer、PlotManager 和 EarlyStopping 类是自定义类。

权重初始化器

import numpy as np
import pandas as pd

from src.model import WeightInitializer
from src.trainer import PlotManager, EarlyStopping

class WeightInitializer:
    def __init__(self, method='random'):
        self.method = method

    def initialize(self, shape):
        if self.method == 'random':
            return np.random.randn(*shape)
        elif self.method == 'xavier':
            return np.random.randn(*shape) / np.sqrt(shape[0])
        elif self.method == 'he':
            return np.random.randn(*shape) * np.sqrt(2 / shape[0])
        elif self.method == 'uniform':
            return np.random.uniform(-1, 1, shape)
        else:
            raise ValueError(f'Unknown initialization method: {self.method}')

权重初始化器是一个处理权重初始化的自定义类。这一点至关重要,因为不同的初始化方法会显着影响 LSTM 的收敛行为。

PlotManager

class PlotManager:
    def __init__(self):
        self.fig, self.ax = plt.subplots(3, 1, figsize=(6, 4))

    def plot_losses(self, train_losses, val_losses):
        self.ax.plot(train_losses, label='Training Loss')
        self.ax.plot(val_losses, label='Validation Loss')
        self.ax.set_title('Training and Validation Losses')
        self.ax.set_xlabel('Epoch')
        self.ax.set_ylabel('Loss')
        self.ax.legend()

    def show_plots(self):
        plt.tight_layout()

实用程序类来自 src.trainer 用于管理图,这将使我们能够绘制训练和验证损失。

EarlyStopping

class EarlyStopping:
    """
    Early stopping to stop the training when the loss does not improve after

    Args:
    -----
        patience (int): Number of epochs to wait before stopping the training.
        verbose (bool): If True, prints a message for each epoch where the loss
                        does not improve.
        delta (float): Minimum change in the monitored quantity to qualify as an improvement.
    """
    def __init__(self, patience=7, verbose=False, delta=0):
        self.patience = patience
        self.verbose = verbose
        self.counter = 0
        self.best_score = None
        self.early_stop = False
        self.delta = delta

    def __call__(self, val_loss):
        """
        Determines if the model should stop training.
        
        Args:
            val_loss (float): The loss of the model on the validation set.
        """
        score = -val_loss

        if self.best_score is None:
            self.best_score = score

        elif score < self.best_score + self.delta:
            self.counter += 1
            
            if self.counter >= self.patience:
                self.early_stop = True
        else:
            self.best_score = score
            self.counter = 0    Args:
    -----
        patience (int): Number of epochs to wait before stopping the training.
        verbose (bool): If True, prints a message for each epoch where the loss
                        does not improve.
        delta (float): Minimum change in the monitored quantity to qualify as an improvement.
    """
    def __init__(self, patience=7, verbose=False, delta=0):
        self.patience = patience
        self.verbose = verbose
        self.counter = 0
        self.best_score = None
        self.early_stop = False
        self.delta = delta

实用程序类来自 src.trainer 用于在训练过程中提前停止以防止过度拟合。您可以在本文中了解有关 EarlyStopping 的更多信息,以及它的功能如何对深度神经网络极其有用:

11.2 LSTM类

让我们首先看一下整个类的样子,然后将其分解为更易于管理的步骤:

class LSTM:
    """
    Long Short-Term Memory (LSTM) network.
    
    Parameters:
    - input_size: int, dimensionality of input space
    - hidden_size: int, number of LSTM units
    - output_size: int, dimensionality of output space
    - init_method: str, weight initialization method (default: 'xavier')
    """
    def __init__(self, input_size, hidden_size, output_size, init_method='xavier'):
        self.input_size = input_size
        self.hidden_size = hidden_size
        self.output_size = output_size
        self.weight_initializer = WeightInitializer(method=init_method)

        # Initialize weights
        self.wf = self.weight_initializer.initialize((hidden_size, hidden_size + input_size))
        self.wi = self.weight_initializer.initialize((hidden_size, hidden_size + input_size))
        self.wo = self.weight_initializer.initialize((hidden_size, hidden_size + input_size))
        self.wc = self.weight_initializer.initialize((hidden_size, hidden_size + input_size))

        # Initialize biases
        self.bf = np.zeros((hidden_size, 1))
        self.bi = np.zeros((hidden_size, 1))
        self.bo = np.zeros((hidden_size, 1))
        self.bc = np.zeros((hidden_size, 1))

        # Initialize output layer weights and biases
        self.why = self.weight_initializer.initialize((output_size, hidden_size))
        self.by = np.zeros((output_size, 1))

    @staticmethod
    def sigmoid(z):
        """
        Sigmoid activation function.
        
        Parameters:
        - z: np.ndarray, input to the activation function
        
        Returns:
        - np.ndarray, output of the activation function
        """
        return 1 / (1 + np.exp(-z))

    @staticmethod
    def dsigmoid(y):
        """
        Derivative of the sigmoid activation function.

        Parameters:
        - y: np.ndarray, output of the sigmoid activation function

        Returns:
        - np.ndarray, derivative of the sigmoid function
        """
        return y * (1 - y)

    @staticmethod
    def dtanh(y):
        """
        Derivative of the hyperbolic tangent activation function.

        Parameters:
        - y: np.ndarray, output of the hyperbolic tangent activation function

        Returns:
        - np.ndarray, derivative of the hyperbolic tangent function
        """
        return 1 - y * y

    def forward(self, x):
        """
        Forward pass through the LSTM network.

        Parameters:
        - x: np.ndarray, input to the network

        Returns:
        - np.ndarray, output of the network
        - list, caches containing intermediate values for backpropagation
        """
        caches = []
        h_prev = np.zeros((self.hidden_size, 1))
        c_prev = np.zeros((self.hidden_size, 1))
        h = h_prev
        c = c_prev

        for t in range(x.shape[0]):
            x_t = x[t].reshape(-1, 1)
            combined = np.vstack((h_prev, x_t))
            
            f = self.sigmoid(np.dot(self.wf, combined) + self.bf)
            i = self.sigmoid(np.dot(self.wi, combined) + self.bi)
            o = self.sigmoid(np.dot(self.wo, combined) + self.bo)
            c_ = np.tanh(np.dot(self.wc, combined) + self.bc)
            
            c = f * c_prev + i * c_
            h = o * np.tanh(c)

            cache = (h_prev, c_prev, f, i, o, c_, x_t, combined, c, h)
            caches.append(cache)

            h_prev, c_prev = h, c

        y = np.dot(self.why, h) + self.by
        return y, caches

    def backward(self, dy, caches, clip_value=1.0):
        """
        Backward pass through the LSTM network.

        Parameters:
        - dy: np.ndarray, gradient of the loss with respect to the output
        - caches: list, caches from the forward pass
        - clip_value: float, value to clip gradients to (default: 1.0)

        Returns:
        - tuple, gradients of the loss with respect to the parameters
        """
        dWf, dWi, dWo, dWc = [np.zeros_like(w) for w in (self.wf, self.wi, self.wo, self.wc)]
        dbf, dbi, dbo, dbc = [np.zeros_like(b) for b in (self.bf, self.bi, self.bo, self.bc)]
        dWhy = np.zeros_like(self.why)
        dby = np.zeros_like(self.by)

        # Ensure dy is reshaped to match output size
        dy = dy.reshape(self.output_size, -1)
        dh_next = np.zeros((self.hidden_size, 1))  # shape must match hidden_size
        dc_next = np.zeros_like(dh_next)

        for cache in reversed(caches):
            h_prev, c_prev, f, i, o, c_, x_t, combined, c, h = cache

            # Add gradient from next step to current output gradient
            dh = np.dot(self.why.T, dy) + dh_next
            dc = dc_next + (dh * o * self.dtanh(np.tanh(c)))

            df = dc * c_prev * self.dsigmoid(f)
            di = dc * c_ * self.dsigmoid(i)
            do = dh * self.dtanh(np.tanh(c))
            dc_ = dc * i * self.dtanh(c_)

            dcombined_f = np.dot(self.wf.T, df)
            dcombined_i = np.dot(self.wi.T, di)
            dcombined_o = np.dot(self.wo.T, do)
            dcombined_c = np.dot(self.wc.T, dc_)

            dcombined = dcombined_f + dcombined_i + dcombined_o + dcombined_c
            dh_next = dcombined[:self.hidden_size]
            dc_next = f * dc

            dWf += np.dot(df, combined.T)
            dWi += np.dot(di, combined.T)
            dWo += np.dot(do, combined.T)
            dWc += np.dot(dc_, combined.T)

            dbf += df.sum(axis=1, keepdims=True)
            dbi += di.sum(axis=1, keepdims=True)
            dbo += do.sum(axis=1, keepdims=True)
            dbc += dc_.sum(axis=1, keepdims=True)

        dWhy += np.dot(dy, h.T)
        dby += dy

        gradients = (dWf, dWi, dWo, dWc, dbf, dbi, dbo, dbc, dWhy, dby)

        # Gradient clipping
        for i in range(len(gradients)):
            np.clip(gradients[i], -clip_value, clip_value, out=gradients[i])

        return gradients

    def update_params(self, grads, learning_rate):
        """
        Update the parameters of the network using the gradients.

        Parameters:
        - grads: tuple, gradients of the loss with respect to the parameters
        - learning_rate: float, learning rate
        """
        dWf, dWi, dWo, dWc, dbf, dbi, dbo, dbc, dWhy, dby = grads

        self.wf -= learning_rate * dWf
        self.wi -= learning_rate * dWi
        self.wo -= learning_rate * dWo
        self.wc -= learning_rate * dWc

        self.bf -= learning_rate * dbf
        self.bi -= learning_rate * dbi
        self.bo -= learning_rate * dbo
        self.bc -= learning_rate * dbc

        self.why -= learning_rate * dWhy
        self.by -= learning_rate * dby

初始化
__init__ 方法初始化具有指定大小的输入层、隐藏层和输出层的 LSTM 实例,并选择权重初始化的方法。

门的权重已初始化(忘记 wf, 输入 wi, 输出 wo,和细胞 wc)并将最后一个隐藏状态连接到输出(why)。通常选择 Xavier 初始化,因为它是维持跨层激活方差的良好默认值。

所有门和输出层的偏差都初始化为零。这是一种常见的做法,尽管有时会添加一些小的常数来避免一开始就死亡的神经元。

前向传递法

我们首先设置之前的隐藏状态 h_prev 和细胞状态 c_prev 为零,这是第一个时间步长的典型情况。

def 前向(自身,x): 输入 x 逐个时间步处理,其中每个时间步更新门的激活、单元状态和隐藏状态。

for t in range(x.shape[0]):
    x_t = x[t].reshape(-1, 1)
    combined = np.vstack((h_prev, x_t))

在每个时间步,输入和先前的隐藏状态垂直堆叠以形成用于矩阵运算的单个组合输入。这对于一次性有效地执行线性变换至关重要。

f = self.sigmoid(np.dot(self.wf, combined) + self.bf)
    i = self.sigmoid(np.dot(self.wi, combined) + self.bi)
    o = self.sigmoid(np.dot(self.wo, combined) + self.bo)
    c_ = np.tanh(np.dot(self.wc, combined) + self.bc)
    
    c = f * c_prev + i * c_
    h = o * np.tanh(c)

每个门(忘记、输入、输出)使用 sigmoid 函数计算其激活,影响单元状态和隐藏状态的更新方式。

在这里,忘记门(f) 确定要保留的先前单元状态的数量。
输入门(
i)决定新候选细胞状态的多少(c_)来添加。
最后,输出门(
o)计算单元状态的哪一部分作为隐藏状态输出。

单元状态被更新为先前状态和新候选状态的加权和。隐藏状态是通过将更新的单元状态传递给tanh函数,然后用输出门对其进行门控。

cache = (h_prev, c_prev, f, i, o, c_, x_t, combined, c, h)
caches.append(cache)

我们将反向传播所需的相关值存储在 cache。这包括状态、门激活和输入。

y = np.dot(self.why, h) + self.by

最后,输出 y 计算为最后一个隐藏状态的线性变换。该方法返回输出和缓存值以供反向传播期间使用。

后向传递法

该方法用于计算损失函数相对于 LSTM 的权重和偏差的梯度。这些梯度对于在训练期间更新模型参数是必要的。

def backward(self, dy, caches, clip_value=1.0):
    dWf, dWi, dWo, dWc = [np.zeros_like(w) for w in (self.wf, self.wi, self.wo, self.wc)]
    dbf, dbi, dbo, dbc = [np.zeros_like(b) for b in (self.bf, self.bi, self.bo, self.bc)]
    dWhy = np.zeros_like(self.why)
    dby = np.zeros_like(self.by)

权重的所有梯度 (dWf, dWi, dWo, dWc, dWhy)和偏见(dbf, dbi, dbo, dbc, dby) 被初始化为零。这是必要的,因为梯度是在序列中的每个时间步上累积的。

dy = dy.reshape(self.output_size, -1)
dh_next = np.zeros((self.hidden_size, 1))
dc_next = np.zeros_like(dh_next)

在这里,我们确保 dy 是矩阵运算的正确形状。 dh_nextdc_next 存储梯度从后面的时间步回流。

for cache in reversed(caches):
        h_prev, c_prev, f, i, o, c_, x_t, combined, c, h = cache

每个时间步长的 LSTM 状态和门激活是从cache。处理从最后一个时间步开始并向后移动(reversed(caches)),这对于在递归神经网络(时间反向传播 – BPTT)中正确应用链式法则至关重要。

        dh = np.dot(self.why.T, dy) + dh_next
        dc = dc_next + (dh * o * self.dtanh(np.tanh(c)))
        df = dc * c_prev * self.dsigmoid(f)
        di = dc * c_ * self.dsigmoid(i)
        do = dh * self.dtanh(np.tanh(c))
        dc_ = dc * i * self.dtanh(c_)

dhdc 是损失相对于隐藏状态和单元状态的梯度。每个门的梯度(df, di, do)和候选细胞状态(dc_) 使用链式法则计算,涉及 sigmoid 的导数 (dsigmoid) 和 tanh (dtanh) 函数,这在门控机制中进行了讨论。

        dWf += np.dot(df, combined.T)
        dWi += np.dot(di, combined.T)
        dWo += np.dot(do, combined.T)
        dWc += np.dot(dc_, combined.T)
        dbf += df.sum(axis=1, keepdims=True)
        dbi += di.sum(axis=1, keepdims=True)
        dbo += do.sum(axis=1, keepdims=True)
        dbc += dc_.sum(axis=1, keepdims=True)

这些线累积每个权重和偏差的所有时间步长的梯度。

for i in range(len(gradients)):
    np.clip(gradients[i], -clip_value, clip_value, out=gradients[i])

为了防止梯度爆炸,我们将梯度裁剪到指定范围(clip_value),这是训练 RNN 的常见做法。

参数更新方法

def update_params(self, grads, learning_rate):
    dWf, dWi, dWo, dWc, dbf, dbi, dbo, dbc, dWhy, dby = grads
    ...
    self.wf -= learning_rate * dWf
    ...

每个权重和偏差通过减去一个分数(learning_rate)对应的梯度。此步骤调整模型参数以最小化损失函数。

11.3 训练和验证

class LSTMTrainer:
    """
    Trainer for the LSTM network.

    Parameters:
    - model: LSTM, the LSTM network to train
    - learning_rate: float, learning rate for the optimizer
    - patience: int, number of epochs to wait before early stopping
    - verbose: bool, whether to print training information
    - delta: float, minimum change in validation loss to qualify as an improvement
    """
    def __init__(self, model, learning_rate=0.01, patience=7, verbose=True, delta=0):
        self.model = model
        self.learning_rate = learning_rate
        self.train_losses = []
        self.val_losses = []
        self.early_stopping = EarlyStopping(patience, verbose, delta)

    def train(self, X_train, y_train, X_val=None, y_val=None, epochs=10, batch_size=1, clip_value=1.0):
        """
        Train the LSTM network.

        Parameters:
        - X_train: np.ndarray, training data
        - y_train: np.ndarray, training labels
        - X_val: np.ndarray, validation data
        - y_val: np.ndarray, validation labels
        - epochs: int, number of training epochs
        - batch_size: int, size of mini-batches
        - clip_value: float, value to clip gradients to
        """
        for epoch in range(epochs):
            epoch_losses = []
            for i in range(0, len(X_train), batch_size):
                batch_X = X_train[i:i + batch_size]
                batch_y = y_train[i:i + batch_size]
                losses = []
                
                for x, y_true in zip(batch_X, batch_y):
                    y_pred, caches = self.model.forward(x)
                    loss = self.compute_loss(y_pred, y_true.reshape(-1, 1))
                    losses.append(loss)
                    
                    # Backpropagation to get gradients
                    dy = y_pred - y_true.reshape(-1, 1)
                    grads = self.model.backward(dy, caches, clip_value=clip_value)
                    self.model.update_params(grads, self.learning_rate)

                batch_loss = np.mean(losses)
                epoch_losses.append(batch_loss)

            avg_epoch_loss = np.mean(epoch_losses)
            self.train_losses.append(avg_epoch_loss)

            if X_val is not None and y_val is not None:
                val_loss = self.validate(X_val, y_val)
                self.val_losses.append(val_loss)
                print(f'Epoch {epoch + 1}/{epochs} - Loss: {avg_epoch_loss:.5f}, Val Loss: {val_loss:.5f}')
                
                # Check early stopping condition
                self.early_stopping(val_loss)
                if self.early_stopping.early_stop:
                    print("Early stopping")
                    break
            else:
                print(f'Epoch {epoch + 1}/{epochs} - Loss: {avg_epoch_loss:.5f}')


    def compute_loss(self, y_pred, y_true):
        """
        Compute mean squared error loss.
        """
        return np.mean((y_pred - y_true) ** 2)

    def validate(self, X_val, y_val):
        """
        Validate the model on a separate set of data.
        """
        val_losses = []
        for x, y_true in zip(X_val, y_val):
            y_pred, _ = self.model.forward(x)
            loss = self.compute_loss(y_pred, y_true.reshape(-1, 1))
            val_losses.append(loss)
        return np.mean(val_losses)

培训师协调多个时期的培训过程,处理批量数据,并可选择验证模型。

for epoch in range(epochs):
      ...
      for i in range(0, len(X_train), batch_size):
          ...
          for x, y_true in zip(batch_X, batch_y):
              y_pred, caches = self.model.forward(x)
              ...

每一批数据都通过模型输入。前向传递生成预测并缓存反向传播的中间值。

dy = y_pred - y_true.reshape(-1, 1)
grads = self.model.backward(dy, caches, clip_value=clip_value)
self.model.update_params(grads, self.learning_rate)

计算损失后,相对于预测误差的梯度(dy) 用于执行反向传播。所得的梯度用于更新模型参数。

print(f'Epoch {epoch + 1}/{epochs} - Loss: {avg_epoch_loss:.5f}')

记录训练进度,以帮助监控模型随时间的表现。

11.4 数据预处理

class TimeSeriesDataset:
    """
    Dataset class for time series data.

    Parameters:
    - ticker: str, stock ticker symbol
    - start_date: str, start date for data retrieval
    - end_date: str, end date for data retrieval
    - look_back: int, number of previous time steps to include in each sample
    - train_size: float, proportion of data to use for training
    """
    def __init__(self, start_date, end_date, look_back=1, train_size=0.67):
        self.start_date = start_date
        self.end_date = end_date
        self.look_back = look_back
        self.train_size = train_size

    def load_data(self):
        """
        Load stock data.
        
        Returns:
        - np.ndarray, training data
        - np.ndarray, testing data
        """
        df = pd.read_csv('data/google.csv')
        df = df[(df['Date'] >= self.start_date) & (df['Date'] <= self.end_date)]
        df = df.sort_index()
        df = df.loc[self.start_date:self.end_date]
        df = df[['Close']].astype(float)  # Use closing price
        df = self.MinMaxScaler(df.values)  # Convert DataFrame to numpy array
        train_size = int(len(df) * self.train_size)
        train, test = df[0:train_size,:], df[train_size:len(df),:]
        return train, test
    
    def MinMaxScaler(self, data):
        """
        Min-max scaling of the data.
        
        Parameters:
        - data: np.ndarray, input data
        """
        numerator = data - np.min(data, 0)
        denominator = np.max(data, 0) - np.min(data, 0)
        return numerator / (denominator + 1e-7)

    def create_dataset(self, dataset):
        """
        Create the dataset for time series prediction.

        Parameters:
        - dataset: np.ndarray, input data

        Returns:
        - np.ndarray, input data
        - np.ndarray, output data
        """
        dataX, dataY = [], []
        for i in range(len(dataset)-self.look_back):
            a = dataset[i:(i + self.look_back), 0]
            dataX.append(a)
            dataY.append(dataset[i + self.look_back, 0])
        return np.array(dataX), np.array(dataY)

    def get_train_test(self):
        """
        Get the training and testing data.

        Returns:
        - np.ndarray, training input
        - np.ndarray, training output
        - np.ndarray, testing input
        - np.ndarray, testing output
        """
        train, test = self.load_data()
        trainX, trainY = self.create_dataset(train)
        testX, testY = self.create_dataset(test)
        return trainX, trainY, testX, testY

此类负责获取数据并将其预处理为适合训练 LSTM 的格式,包括缩放和分割为训练集和测试集。

11.5 模型训练

现在,让我们利用上面定义的所有代码来加载数据集、对其进行预处理并训练我们的 LSTM 模型。

首先,让我们加载数据集:

# Instantiate the dataset
dataset = TimeSeriesDataset( '2010-1-1', '2020-12-31', look_back=1)
trainX, trainY, testX, testY = dataset.get_train_test()

在本例中,它配置为从以下位置获取 Google (GOOGL) 的历史数据:卡格尔,时间为2010年1月1日至2020年12月31日。

look_back=1:此参数设置要包含在每个输入样本中的过去时间步数。在这里,每个输入样本将包含前一个时间步的数据,这意味着模型将使用某一天的数据来预测下一天的数据。

get_train_test():此方法处理获取的数据,对其进行标准化,并将其拆分为训练和测试数据集。这对于在一段数据上训练模型并在另一段数据上验证其性能以检查是否过度拟合至关重要。

# Reshape input to be [samples, time steps, features]
trainX = np.reshape(trainX, (trainX.shape[0], trainX.shape[1], 1))
testX = np.reshape(testX, (testX.shape[0], testX.shape[1], 1))

此重塑步骤将数据格式调整为 LSTM 期望的格式。 LSTM 要求输入的形状为[samples, time steps, features].
这里:

  • 样品:数据点的数量。
  • 时间步长:每个样本的时间步数(look_back).
  • 特征:每个时间步的特征数量(在本例中为 1,因为我们可能正在查看一维数据,例如收盘价)。
look_back = 1  # Number of previous time steps to include in each sample
hidden_size = 256  # Number of LSTM units
output_size = 1  # Dimensionality of the output space

lstm = LSTM(input_size=1, hidden_size=hidden_size, output_size=output_size)

在此代码中:

  • hidden_size:隐藏层中 LSTM 单元的数量,设置为 256。这定义了模型的容量,更多的单元可能捕获更复杂的模式,但也需要更多的计算能力和数据来有效训练。
  • output_size:输出维度(在本例中为 1)表明模型预测每个输入样本的单个值,例如第二天的股票价格。
trainer = LSTMTrainer(lstm, learning_rate=1e-3, patience=50, verbose=True, delta=0.001)
trainer.train(trainX, trainY, testX, testY, epochs=1000, batch_size=32)

这里我们将比率设置为 1e-3 (0.001)。学习率太高可能会导致模型过快收敛到次优解决方案,而学习率太低可能会使训练过程变慢并可能陷入困境。我们还将耐心指定为 50,如果验证损失在 50 个时期内没有改善,这将停止模型训练。

train() 方法在指定的时期数和批量大小上执行训练过程。在训练期间,模型将每 10 个 epoch 打印一次模型性能,从而产生类似于以下的输出:

Epoch 1/1000 - Loss: 0.25707, Val Loss: 0.43853
Epoch 11/1000 - Loss: 0.06463, Val Loss: 0.06056
Epoch 21/1000 - Loss: 0.05313, Val Loss: 0.02100
Epoch 31/1000 - Loss: 0.04862, Val Loss: 0.01134
Epoch 41/1000 - Loss: 0.04512, Val Loss: 0.00678
Epoch 51/1000 - Loss: 0.04234, Val Loss: 0.00395
Epoch 61/1000 - Loss: 0.04014, Val Loss: 0.00210
Epoch 71/1000 - Loss: 0.03840, Val Loss: 0.00095
Epoch 81/1000 - Loss: 0.03703, Val Loss: 0.00031
Epoch 91/1000 - Loss: 0.03595, Val Loss: 0.00004
Epoch 101/1000 - Loss: 0.03509, Val Loss: 0.00003
Epoch 111/1000 - Loss: 0.03442, Val Loss: 0.00021
Epoch 121/1000 - Loss: 0.03388, Val Loss: 0.00051
Epoch 131/1000 - Loss: 0.03346, Val Loss: 0.00090
Epoch 141/1000 - Loss: 0.03312, Val Loss: 0.00133
Early stopping

最后,让我们绘制训练和验证损失,以更好地了解可能的收敛/发散。我们可以使用以下代码行来实现:

plot_manager = PlotManager()

# Inside your training loop
plot_manager.plot_losses(trainer.train_losses, trainer.val_losses)

# After your training loop
plot_manager.show_plots()

这将绘制与以下类似的图表:

大模型技术:LLM 架构解释,RNN、LSTM 和 GRU

从图中,我们可以看到训练和验证在早期时期都快速下降,这表明我们的初始化技术(Xavier)可能不适合此目的。尽管提前停止是在大约 90 个 epoch 取得一些令人印象深刻的表现后触发的,但我们可以尝试降低学习率并运行更多的 epoch。此外,我们可以尝试使用其他技术,例如学习率调度程序或 Adam 优化。

12. 门控循环单元(GRU)

门控循环单元(GRU)是由 曹等人。 2014年解决梯度消失问题标准循环神经网络 (RNN)。 GRU 具有长短期记忆 (LSTM) 的许多特性。两种算法都使用门控机制来控制记忆过程。

想象一下,您正在尝试通过反复听来学习一首歌。基本的 RNN 可能会在歌曲结束时忘记歌曲的开头。 GRU 通过使用控制记住哪些信息和遗忘哪些信息的门来解决这个问题。

GRU 通过将输入门和遗忘门合并为单个更新门并添加重置门来简化长短期记忆 (LSTM) 网络的结构。这使得他们能够更快地训练、更容易合作,同时仍然保持长期记住重要信息的能力。

更新门:这个门决定了有多少过去的信息应该被带到未来。

重置门:这个门决定了要忘记多少过去的信息。

这些门帮助 GRU 在记住重要细节和忘记不重要细节之间保持平衡,类似于您专注于记住歌曲旋律而忽略背景噪音的方式。

GRU 非常适合数据按顺序出现的任务,例如预测股票市场、理解语言,甚至生成音乐。他们可以通过跟踪过去的信息并利用它来做出更好的预测来学习数据模式。这使得它们对于从以前的数据点了解上下文至关重要的任何应用程序都非常有用。

大模型技术:LLM 架构解释,RNN、LSTM 和 GRU

Credits: Michael Phi

12.1 与 LSTM 和普通 RNN 的比较

为了了解 GRU 的适用范围,我们将它们与 LSTM 和 Vanilla RNN 进行比较。

普通 RNN
将 Vanilla RNN 视为循环神经网络的基本版本。它们的工作原理是将信息从一个时间步骤传递到下一个时间步骤,就像一场接力赛,每个跑步者将接力棒传递给下一个。然而,他们有一个很大的缺陷:
他们往往会忘记长时间的事情。这是由于梯度消失问题造成的,这使得他们很难学习数据中的长期依赖性。

LSTM
长短期记忆网络就是为了解决这个问题而设计的。他们使用更复杂的结构,具有三种类型的门:
输入门、遗忘门和输出门。这些门就像一个复杂的文件系统,决定保留哪些信息、更新哪些信息以及丢弃哪些信息。这使得 LSTM 能够长时间记住重要信息,这使得它们非常适合许多时间步骤的上下文至关重要的任务,例如理解文本段落或识别长时间序列中的模式。

GRU
门控循环单元是 LSTM 的简化版本。他们通过将输入门和遗忘门组合成一个更新门来简化事情,并且它们还有一个重置门。这使得 GRU 比 LSTM 的计算强度更低,训练速度更快,同时仍然能够有效地处理长期依赖关系。

12.2 GRU 有何特殊之处并且比传统 RNN 更有效?

GRU 支持门控和隐藏状态来控制信息流。为了解决 RNN 中出现的问题,GRU 使用两个门:更新门重置门。

大模型技术:LLM 架构解释,RNN、LSTM 和 GRU

Credits: pluralsight

您可以将它们视为可以执行凸组合的两个向量条目 (0,1)。这些组合决定应更新(传递)哪些隐藏状态信息或在需要时重置隐藏状态。同样,网络学会跳过不相关的临时观察。

LSTM 由三个门组成:输入门、遗忘门和输出门。与 LSTM 不同,GRU 没有输出门,而是将输入和遗忘门组合成单个更新门。

让我们了解有关更新和重置门的更多信息。

12.2.1 更新门

更新门(z_t)负责确定需要沿着下一个状态传递的先前信息量(先前的时间步)。这是一个重要的单位。下面的架构显示了更新门的安排。

大模型技术:LLM 架构解释,RNN、LSTM 和 GRU

Credits: pluralsight

这里, x_t 是网络单元中提供的输入向量。它乘以它的参数权重(W_z) 矩阵。这t_1小时(t_1) 表示它保存了前一个单元的信息并乘以它的权重。接下来,将这些参数的值相加并传递给 sigmoid 激活函数。此处,sigmoid 函数将生成 0 到 1 限制之间的值。

12.2.2 复位门

复位门(r_t) 被模型用来决定需要忽略多少过去的信息。公式与更新门相同。它们的权重和门的使用有所不同,这将在下一节中讨论。下面的架构代表重置门。

大模型技术:LLM 架构解释,RNN、LSTM 和 GRU

Credits: pluralsight

有两个输入,x_th_t-1。乘以它们的权重,逐点相加,然后通过 sigmoid 函数传递。

13. Gates in Action

首先,重置门将过去时间步长的相关信息存储到新的内存内容中。然后它将输入向量和隐藏状态与其权重相乘。其次,它计算重置门和先前隐藏状态倍数之间的逐元素乘法(Hadamard)。综上所述,将上述步骤的非线性激活函数应用到结果上,得到h’_t.

考虑一个客户评论度假村的场景:“我到达这里时已经是深夜了。”几行之后,评论的结尾是:“我很享受这次住宿,因为房间很舒适。工作人员很友好。”要确定客户的满意度,您将需要评论的最后两行。该模型将扫描整个评论直至最后,并分配一个接近“0”的重置门向量值。

这意味着它将忽略过去的行并只关注最后的句子。

请参阅下图。

大模型技术:LLM 架构解释,RNN、LSTM 和 GRU

Credits: pluralsight

这是最后一步。在当前时间步的最终内存中,网络需要计算h_t。在这里,更新门将发挥至关重要的作用。该向量值将保存当前单元的信息并将其传递到网络。它将确定从当前内存内容中收集哪些信息(h’t) 和之前的时间步 小时(t-1)。逐元素乘法(Hadamard)应用于更新门,并且小时(t-1),并将其与 Hadamard 乘积运算相加(1-z_t)h’(t).

重温度假村点评的例子:这次在正文开头提到了预测的相关信息。该模型会将更新门向量值设置为接近 1。在当前时间步长,1-z_t 将接近于 0,并且它将忽略评论最后部分的块。请参阅下图。

大模型技术:LLM 架构解释,RNN、LSTM 和 GRU

Credits: pluralsight

继续往下看,可以看到 z_t 用于计算 1-z_t 其中,结合h’t 产生结果。 Hadamard产品操作在之间进行小时(t-1)z_t。乘积的输出作为逐点加法的输入给出h’t 在隐藏状态下产生最终结果。

14. 简单 GRU 的实现

为了强化我们所涵盖的概念,让我们采取实践方法并实现一个基本的门控循环单元(GRU)在Python中从头开始。

下面的代码片段展示了一个简化的 格鲁乌 类,强调了向前和向后传递的基本功能格鲁乌 建筑学。

import numpy as np

class SimpleGRU:
    def __init__(self, input_size, hidden_size, output_size):
        self.input_size = input_size
        self.hidden_size = hidden_size
        self.output_size = output_size

        # Initialize weights and biases
        self.W_z = np.random.randn(hidden_size, input_size)
        self.U_z = np.random.randn(hidden_size, hidden_size)
        self.b_z = np.zeros((hidden_size, 1))
        
        self.W_r = np.random.randn(hidden_size, input_size)
        self.U_r = np.random.randn(hidden_size, hidden_size)
        self.b_r = np.zeros((hidden_size, 1))
        
        self.W_h = np.random.randn(hidden_size, input_size)
        self.U_h = np.random.randn(hidden_size, hidden_size)
        self.b_h = np.zeros((hidden_size, 1))
        
        self.W_y = np.random.randn(output_size, hidden_size)
        self.b_y = np.zeros((output_size, 1))

    def sigmoid(self, x):
        return 1 / (1 + np.exp(-x))
    
    def tanh(self, x):
        return np.tanh(x)
    
    def softmax(self, x):
        exp_x = np.exp(x - np.max(x))
        return exp_x / exp_x.sum(axis=0, keepdims=True)

    def forward(self, x):
        T = len(x)
        h = np.zeros((self.hidden_size, 1))
        y_list = []

        for t in range(T):
            x_t = x[t].reshape(-1, 1)  # Reshape input to column vector

            # Update gate
            z = self.sigmoid(np.dot(self.W_z, x_t) + np.dot(self.U_z, h) + self.b_z)

            # Reset gate
            r = self.sigmoid(np.dot(self.W_r, x_t) + np.dot(self.U_r, h) + self.b_r)

            # Candidate hidden state
            h_tilde = self.tanh(np.dot(self.W_h, x_t) + np.dot(self.U_h, r * h) + self.b_h)

            # Hidden state update
            h = (1 - z) * h + z * h_tilde

            # Output
            y = np.dot(self.W_y, h) + self.b_y
            y_list.append(y)

        return y_list

    def backward(self, x, y_list, target):
        T = len(x)
        dW_z = np.zeros_like(self.W_z)
        dU_z = np.zeros_like(self.U_z)
        db_z = np.zeros_like(self.b_z)
        
        dW_r = np.zeros_like(self.W_r)
        dU_r = np.zeros_like(self.U_r)
        db_r = np.zeros_like(self.b_r)
        
        dW_h = np.zeros_like(self.W_h)
        dU_h = np.zeros_like(self.U_h)
        db_h = np.zeros_like(self.b_h)
        
        dW_y = np.zeros_like(self.W_y)
        db_y = np.zeros_like(self.b_y)
        
        dh_next = np.zeros_like(y_list[0])

        for t in reversed(range(T)):
            dy = y_list[t] - target[t]
            dW_y += np.dot(dy, np.transpose(h))
            db_y += dy
            
            dh = np.dot(np.transpose(self.W_y), dy) + dh_next
            
            dh_tilde = dh * (1 - self.sigmoid(np.dot(self.W_z, x[t].reshape(-1, 1)) + np.dot(self.U_z, h) + self.b_z))
            dW_h += np.dot(dh_tilde, np.transpose(x[t].reshape(1, -1)))
            db_h += dh_tilde
            
            dr = np.dot(np.transpose(self.W_h), dh_tilde)
            dU_h += np.dot(dr * h * (1 - self.tanh(np.dot(self.W_h, x[t].reshape(-1, 1)) + np.dot(self.U_h, r * h) + self.b_h)), np.transpose(h))
            dW_h += np.dot(dr * h * (1 - self.tanh(np.dot(self.W_h, x[t].reshape(-1, 1)) + np.dot(self.U_h, r * h) + self.b_h)), np.transpose(x[t].reshape(1, -1)))
            db_h += dr * h * (1 - self.tanh(np.dot(self.W_h, x[t].reshape(-1, 1)) + np.dot(self.U_h, r * h) + self.b_h))
            
            dz = np.dot(np.transpose(self.U_r), dr * h * (self.tanh(np.dot(self.W_h, x[t].reshape(-1, 1)) + np.dot(self.U_h, r * h) + self.b_h) - h_tilde))
            dU_z += np.dot(dz * h * z * (1 - z), np.transpose(h))
            dW_z += np.dot(dz * h * z * (1 - z), np.transpose(x[t].reshape(1, -1)))
            db_z += dz * h * z * (1 - z)
            
            dh_next = np.dot(np.transpose(self.U_z), dz * h * z * (1 - z))
        
        return dW_z, dU_z, db_z, dW_r, dU_r, db_r, dW_h, dU_h, db_h, dW_y, db_y

    def update_parameters(self, dW_z, dU_z, db_z, dW_r, dU_r, db_r, dW_h, dU_h, db_h, dW_y, db_y, learning_rate):
        self.W_z -= learning_rate * dW_z
        self.U_z -= learning_rate * dU_z
        self.b_z -= learning_rate * db_z
        
        self.W_r -= learning_rate * dW_r
        self.U_r -= learning_rate * dU_r
        self.b_r -= learning_rate * db_r
        
        self.W_h -= learning_rate * dW_h
        self.U_h -= learning_rate * dU_h
        self.b_h -= learning_rate * db_h
        
        self.W_y -= learning_rate * dW_y
        self.b_y -= learning_rate * db_y

# Example usage
input_size = 4
hidden_size = 3
output_size = 2

gru = SimpleGRU(input_size, hidden_size, output_size)

# Generate random data
sequence_length = 5
data = [np.random.randn(input_size) for _ in range(sequence_length)]
target = [np.random.randn(output_size) for _ in range(sequence_length)]

# Forward pass
y_list = gru.forward(data)

# Backward pass
dW_z, dU_z, db_z, dW_r, dU_r, db_r, dW_h, dU_h, db_h, dW_y, db_y = gru.backward(data, y_list, target)

# Update weights and biases
learning_rate = 0.1
gru.update_parameters(dW_z, dU_z, db_z, dW_r, dU_r, db_r, dW_h, dU_h, db_h, dW_y, db_y, learning_rate)

在上面的实现中,我们引入了一个简化的 SimpleGRU 课程提供对核心机制的见解格鲁乌。示例用法演示了如何初始化 格鲁乌,为输入序列和目标输出创建随机数据,执行前向和后向传递,然后使用计算的梯度更新权重和偏差。

14.1 GRU 的优点和缺点

GRU 的优点

  1. 顺序数据建模: GRU 擅长处理序列,使其非常适合语言处理、语音识别和时间序列分析等任务。
  2. 可变长度输入: GRU 可以处理不同长度的序列,适应输入具有不同大小的应用。
  3. 计算效率: 与 LSTM 等更复杂的循环架构相比,GRU 由于其设计更简单,因此计算效率更高。
  4. 减轻消失梯度: GRU 比传统 RNN 更有效地解决梯度消失问题,使它们能够捕获数据中的长期依赖性。

GRU 的局限性

  1. 长期记忆有限: 尽管 GRU 与标准 RNN 相比,它们更擅长捕获长期依赖性,但对于具有复杂依赖性的超长序列,它们可能不如 LSTM 有效。
  2. 表达能力较差: GRU 在某些情况下,特别是在对高度复杂的序列进行建模时,可能无法像 LSTM 那样有效地捕获复杂的模式。
  3. 具体应用: 对于需要显式内存控制或复杂上下文建模的任务,LSTM 或更高级的架构可能更合适。

14.2 GRU 和 LSTM 之间的选择

使用门控循环单元之间的决定(GRU)或长短期记忆(LSTM)网络取决于您的具体问题和数据集。以下是一些注意事项:

在以下情况下使用 GRU:

  • 计算资源有限: GRU 与 LSTM 相比,计算强度较小,这使得它们成为存在资源限制时的首选。
  • 简单性很重要: 如果您想要一个更简单的模型,但仍然可以很好地捕获顺序依赖关系,GRU 是一个不错的选择。
  • 较短的序列: 对于涉及具有较短依赖性的序列的任务, GRU 可以提供足够的性能,而不需要 LSTM 复杂的内存管理。

在以下情况下使用 LSTM:

  • 捕获长期依赖关系: LSTM 更适合捕获长程依赖性至关重要的任务,例如语言建模、语音识别和某些时间序列预测。
  • 细粒度内存控制: LSTM 提供了对内存更明确的控制,这使得它们在需要精确的内存处理时成为更好的选择。
  • 复杂序列: 如果您的数据表现出复杂的顺序模式和依赖性,那么 LSTM 在对这些复杂性进行建模时通常会更有效。

在实践中,最好在您的特定任务中尝试使用 GRU 和 LSTM 来确定哪种架构性能更好。有时,两者之间的选择取决于对数据集的实证测试和验证。

15. 结论

在本文中,我们探讨了循环神经网络 (RNN),深入研究其核心机制、训练挑战以及提高其性能的先进设计。这是一个快速概述:

我们分解了 RNN 的结构,强调它们通过内部记忆状态处理序列的能力。讨论了前向传播和随时间反向传播 (BPTT) 等关键过程,解释了 RNN 如何处理顺序数据。

我们还强调了主要的训练挑战,包括梯度消失和爆炸,这可能会扰乱学习。为了解决这些问题,我们探索了梯度裁剪和初始化策略等解决方案,这有助于稳定训练并提高网络从较长序列中学习的能力。

门控循环单元 (GRU) 是 RNN 的强大变体,专为高效顺序数据处理而设计。它们有效地缓解了梯度消失等问题,并擅长捕获序列中的依赖关系,使其成为自然语言处理、语音识别和时间序列分析等任务的理想选择。

GRU 使用门控机制来控制信息流,使它们能够捕获长期依赖性,同时保持计算效率。了解 GRU 背后的架构和数学是在机器学习任务中有效利用它们的关键。

在 GRU 和 LSTM 之间进行选择时,有几个因素会发挥作用,包括数据复杂性、计算资源和要建模的依赖关系的长度。两种架构都有其优点和缺点,因此最佳选择取决于任务的具体要求。

参考:

https://medium.com/@vipra_singh/llm-architectures-explained-rnn-lstm-grus-part-3-c5e1cbfeda1d

免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://yundeesoft.com/84911.html

(0)

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注微信