LLM架构从基础到精通之循环神经网络(RNN)
以下是已更新文章:
1. LLM大模型架构专栏|| 从NLP基础谈起
2.LLM大模型架构专栏|| 自然语言处理(NLP)之建模
3. LLM大模型架构之词嵌入(Part1)
4. LLM大模型架构之词嵌入(Part2)
5. LLM大模型架构之词嵌入(Part3)
6. LLM大模型架构之词嵌入(Part4)
欢迎关注公众号【柏企科技圈】【柏企阅文】
在人工智能的领域中,神经网络是推动技术发展的核心力量。今天,让我们深入探讨循环神经网络(RNN)及其重要变体——长短期记忆网络(LSTM)和门控循环单元(GRU),了解它们的工作原理、优势以及应用场景。
一、神经网络基础
(一)什么是神经网络?
神经网络,又称人工神经网络,其设计灵感源于人类大脑的运作模式。它由众多被称为“节点”的处理单元构成,这些节点之间相互传递数据,恰似大脑中的神经元传递电脉冲。
在机器学习领域,神经网络扮演着关键角色。尤其是在深度学习中,它能够从无标签数据中提取有价值的信息,实现诸如识别照片中未知物品等复杂任务。像 ChatGPT 这样的大型语言模型、DALL-E 等 AI 图像生成器以及预测性 AI 模型等,都在很大程度上依赖于神经网络技术。
(二)神经网络的学习过程
神经网络的学习过程是一个迭代的过程,主要包括前向传播、损失函数计算和反向传播三个关键步骤。
在前向传播阶段,数据从输入层开始,依次经过隐藏层的处理,最终到达输出层。在这个过程中,每个神经元都会根据输入数据和自身的权重、偏差进行计算。权重决定了输入数据的相对重要性,而偏差则影响着神经元的激活程度。最初,我们会为权重和偏差赋予非零的随机值,这就是网络的参数初始化过程。
计算完成后,输出层的结果即为前向传播的最终输出,我们将其与真实值(ground-truth value)进行对比,通过损失函数(loss function)来衡量模型的性能。损失函数计算预测值与真实值之间的误差,常见的损失函数包括均方误差(Mean Squared Error,MSE)、平均绝对误差(Mean Absolute Error,MAE)、二元交叉熵(Binary Cross-entropy)、多类交叉熵(Multi-class Cross-entropy)等,它们分别适用于不同类型的任务,如回归问题或分类问题。
如果前向传播得到的预测值与真实值相差较大,说明我们需要调整网络的参数以降低损失函数的值。这就引出了反向传播过程,在这个阶段,我们会计算损失函数关于模型参数的梯度,并利用优化算法(如梯度下降、Adam 等)来更新参数。优化算法的目标是找到损失函数的全局最小值,但在实际操作中,由于复杂的损失函数可能存在多个局部最小值,因此这是一个极具挑战性的任务。
(三)训练相关的重要概念:Epochs、Batch Size 和 Iterations
- Epochs:一个 Epoch 表示将整个数据集完整地通过神经网络进行一次前向传播和反向传播。由于数据集通常较大,一次性处理可能会超出计算机的内存限制,因此我们会将其划分为多个较小的批次。
- Batch Size:指的是单个批次中包含的训练样本数量。例如,如果我们有 1000 个训练样本,将其划分为大小为 500 的批次,那么 Batch Size 就是 500。
- Iterations:完成一个 Epoch 所需的批次数量即为 Iterations。在上述例子中,需要 2 个 Iterations 才能完成一个 Epoch。
(四)神经网络的类型
神经网络的架构丰富多样,大致可分为以下几类:
- 浅层神经网络:通常只有一个隐藏层,其优点是计算速度快,对计算资源的需求相对较低,但在处理复杂任务时能力有限。
- 深层神经网络:包含多个隐藏层,能够处理更为复杂的任务,但需要更多的计算资源和时间进行训练。
常见的神经网络架构还包括感知机神经网络、多层感知机神经网络、前馈神经网络、循环神经网络、模块化神经网络、径向基函数神经网络、液态状态机神经网络、残差神经网络等。每种架构都有其独特的特点和适用场景,本文将重点关注循环神经网络及其变体。
二、循环神经网络(RNNs)
(一)RNN 的定义与应用场景
循环神经网络(RNN)是一种专门设计用于处理顺序数据的神经网络架构。与传统的前馈神经网络不同,RNN 能够通过内部状态(记忆)来保留之前输入的信息,从而更好地处理序列数据。在处理时间序列数据、语言建模或视频序列等任务时,RNN 表现出了独特的优势,因为这些任务中输入数据的顺序至关重要。
(二)顺序数据的概念
顺序数据是指具有特定顺序且顺序会影响数据含义的信息。例如,在句子 “The quick brown fox jumps over the lazy dog.” 中,每个单词都是数据的一部分,单词的顺序决定了句子的语义。如果将单词顺序打乱,如 “Fox brown quick the jumps over lazy dog”,句子就会变得毫无意义。其他常见的顺序数据还包括时间序列数据(如股票价格、温度读数、网站流量等)和语音信号等。
(三)RNN 与前馈神经网络的对比
- 前馈神经网络:数据在网络中仅沿一个方向流动,从输入层到输出层,不存在反馈回路。这种架构适用于模式识别等任务,但在处理顺序数据时存在局限性,因为它无法利用之前的输入信息。
- 循环神经网络:通过网络中的反馈回路,信号可以在前后时间步之间传递,使得网络能够“记住”之前的输入,从而更好地处理顺序数据。这种动态的信息处理方式使其在处理时间序列相关的任务时表现出色。
(四)为什么使用 RNN?
传统的人工神经网络(ANN)在处理顺序数据(如文本)时面临挑战,因为它们要求输入数据具有固定的大小。在 ANN 中,每个输入被独立处理,无法捕捉元素之间的顺序和关系。例如,当使用零填充方法处理不同长度的序列时,虽然可以使所有序列达到相同的长度,但填充的零并不包含任何有意义的信息,反而增加了计算负担。此外,如果输入的长度超出预期,ANN 无法有效地处理这种情况。
三、RNN 的架构
(一)RNN 的时间展开
RNN 的关键在于其内部状态或记忆,它能够跟踪已处理的数据。从结构上看,RNN 主要由输入层、一个或多个隐藏层和输出层组成。可以将 RNN 视为多个前馈神经网络在时间上的链式执行,每个时间步都有一个相同的网络结构在处理输入数据。
- 输入层:在每个时间步,输入层接收一个输入数据,并将其传递给隐藏层。与前馈网络不同,RNN 的输入是逐个时间步进行处理的,这使得网络能够适应动态变化的数据序列。在 Python 中,我们可以使用如下方式初始化输入层的权重矩阵:
self.weights_ih = np.random.randn(input_size, hidden_size) * 0.01
其中,input_size
是输入层的神经元数量,hidden_size
是隐藏层的神经元数量,self.weights_ih
是连接输入层和隐藏层的权重矩阵,通过正态分布随机初始化,并乘以 0.01 进行缩放,以保持权重值较小。
- 隐藏状态:隐藏层在 RNN 中起着核心作用,它不仅处理当前输入,还会保留之前输入的信息。隐藏状态
h_t
在时间步t
是根据当前输入X_t
和前一个隐藏状态h_{t - 1}
计算得出的,计算公式如下:
$$h_t = \tanh(W \cdot [h_{t - 1}, X_t] + b_h)$$
其中,W
是隐藏层的权重矩阵,b_h
是隐藏层的偏差向量,tanh
是一种常用的非线性激活函数。在实际计算中,我们通常先将隐藏状态初始化为零向量,然后在每个时间步更新隐藏状态。例如:
h = np.zeros((1, self.hidden_size))
for i, x in enumerate(inputs):
x = x.reshape(1, -1)
h = np.tanh(np.dot(x, self.weights_ih) + np.dot(h, self.weights_hh) + self.bias_h)
self.last_hs[i + 1] = h
在上述代码中,inputs
是输入数据序列,self.weights_hh
是连接隐藏层在不同时间步之间的权重矩阵,self.bias_h
是隐藏层的偏差向量。通过这种方式,RNN 能够在处理序列数据时保持对之前信息的记忆。
- 输出序列:RNN 的输出方式非常灵活,可以在每个时间步都产生输出(多对多),也可以在序列结束时产生一个单一输出(多对一),甚至可以从单个输入生成一个序列输出(一对多)。例如,对于多对多的 RNN,时间步
t
的输出O_t
可以通过以下公式计算:
$$O_t = V \cdot h_t + b_o$$
其中,V
是输出层的权重矩阵,b_o
是输出层的偏差向量。如果 RNN 用于分类任务,通常会在输出层之后使用 softmax 函数将输出转换为各个类别的概率分布。
(二)RNN 的关键操作
- 前向传播:在 RNN 的前向传播过程中,对于每个时间步
t
,网络会结合当前输入X_t
和前一个隐藏状态h_{t - 1}
来计算新的隐藏状态h_t
和输出O_t
。这个过程中会使用一些非线性激活函数(如sigmoid
或tanh
)来引入非线性变换,帮助网络更好地处理复杂的信息。以下是前向传播的数学公式表示:
$$h_t = \tanh(U \cdot X_t + W \cdot h_{t - 1} + b_h)$$
$$O_t = V \cdot h_t + b_o$$
在 Python 代码中,前向传播的实现如下:
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)
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
- 反向传播时间(BPTT):RNN 的训练涉及一种特殊的反向传播方法,即 BPTT。与传统的反向传播不同,BPTT 会在时间上展开整个数据序列,并在每个时间步计算梯度,然后利用这些梯度来调整权重,以降低总体损失。然而,BPTT 计算复杂度较高,并且容易出现梯度消失和梯度爆炸的问题。
假设我们有一个长度为 T
的时间序列数据,在每个时间步 t
都有一个简单的损失函数 L_t
(如回归任务中的均方误差或分类任务中的交叉熵),那么总损失 L_{total}
是每个时间步损失的总和:
$$L_{total} = \sum_{t = 1}^{T} L_t$$
为了更新权重,我们需要计算 L_{total}
关于权重的梯度。对于权重矩阵 U
(输入到隐藏层)、W
(隐藏层到隐藏层)和 V
(隐藏层到输出层),梯度的计算公式如下:
$$\frac{\partial L_{total}}{\partial U} = \sum_{t = 1}^{T} \frac{\partial L_t}{\partial h_t} \frac{\partial h_t}{\partial U}$$
$$\frac{\partial L_{total}}{\partial W} = \sum_{t = 1}^{T} \frac{\partial L_t}{\partial h_t} \frac{\partial h_t}{\partial W}$$
$$\frac{\partial L_{total}}{\partial V} = \sum_{t = 1}^{T} \frac{\partial L_t}{\partial O_t} \frac{\partial O_t}{\partial V}$$
这些梯度是通过链式法则计算得出的,从最终时间步开始,逐步向后计算。在实际代码中,BPTT 的实现如下:
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
- 权重更新:在计算出梯度后,我们使用优化算法(如随机梯度下降,SGD)来更新权重。权重更新的公式如下:
$$W = W - \eta \cdot \frac{\partial L_{total}}{\partial W}$$
其中,\eta
是学习率,它控制着权重更新的步长。在实际训练中,选择合适的学习率非常重要,如果学习率过大,可能会导致模型无法收敛;如果学习率过小,训练过程会非常缓慢。
四、RNN 训练中的挑战
(一)梯度消失问题
在反向传播过程中,当梯度从输出层向输入层传播时,梯度的值可能会逐渐减小,甚至趋近于零。这会导致初始层或较低层的权重几乎无法更新,使得梯度下降无法收敛到最优解,这种现象被称为梯度消失问题。
(二)梯度爆炸问题
与梯度消失相反,在某些情况下,梯度可能会在反向传播过程中不断增大,导致权重更新过大,使得梯度下降过程发散,这就是梯度爆炸问题。
(三)梯度消失/爆炸的原因
某些激活函数(如 sigmoid
函数)的输入和输出方差差异较大,当输入值较大时,sigmoid
函数会饱和,其导数趋近于零。在反向传播过程中,这会导致梯度无法有效地向后传播,使得较低层的权重无法得到更新。此外,如果网络的初始权重设置不当,可能会导致在更新过程中梯度累积过大,从而引发梯度爆炸问题。
(四)如何判断模型是否存在梯度消失/爆炸问题?
可以通过观察训练过程中的梯度大小来判断模型是否存在梯度问题。如果梯度值持续趋近于零或变得非常大,那么很可能是出现了梯度消失或梯度爆炸问题。
五、解决梯度消失/爆炸问题的方法
(一)适当的权重初始化
Xavier Glorot、Antoine Bordes 和 Yoshua Bengio 提出了一种有效的方法来缓解梯度问题。他们认为,为了保证信号在网络中的正常流动,每层的输出方差应该与输入方差相等,并且在反向传播过程中,梯度的方差在经过每层时也应该保持不变。虽然在实际网络中很难完全满足这两个条件,但他们提出了一种折中的初始化方法,即 Xavier 初始化(也称为 Glorot 初始化)。
对于具有 fanin
个输入和 fanout
个神经元的层,使用正态分布进行权重初始化时,均值为 0,方差为 $\sigma^2 = \frac{1}{fanavg}$,其中 $fanavg = \frac{fanin + fanout}{2}$;或者使用均匀分布,范围在 $[-r, +r]$ 之间,其中 $r = \sqrt{\frac{3}{fanavg}}$。
在 Keras 中,默认使用 Xavier 初始化策略(均匀分布)。如果需要使用其他初始化方法,可以在创建层时通过 kernel_initializer
参数进行设置。例如:
keras.layer.Dense(25, activation="relu", kernel_initializer="he_normal")
或
keras.layer.Dense(25, activation="relu", kernel_initializer="he_uniform")
如果想要基于 fanavg
进行初始化,可以使用 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(Rectified Linear Unit,修正线性单元)的定义为:$ReLU(z)=\max(0,z)$,对于任何负输入,它的输出为 0,其输出范围是$[0,+\infty)$。
但不幸的是,在某些情况下,ReLU 函数也不是网络中间层的完美选择。它存在一个被称为“死亡 ReLU”的问题,即一些神经元在训练过程中可能会一直输出 0,从而失去作用。
一些可以缓解网络中间层梯度消失问题的 ReLU 替代函数包括 LReLU、PReLU、ELU 和 SELU 等:
- LReLU(Leaky ReLU,带泄漏的 ReLU):$LeakyReLU_{\alpha}(z)=\max(\alpha z,z)$,其中“泄漏”量由超参数$\alpha$控制,它是$z<0$时函数的斜率。较小的泄漏斜率确保了由 Leaky ReLU 驱动的神经元不会死亡;尽管在长时间的训练阶段它们可能会进入一种类似昏迷的状态,但始终有机会最终被激活。
- PReLU(Parametric Leaky ReLU,参数化带泄漏的 ReLU):在这种变体中,$\alpha$被视为一个参数而不是超参数,模型可以在训练过程中学习$\alpha$的值。
- ELU(Exponential Linear Unit,指数线性单元):当$z<0$时,它取负值,这使得单元的平均输出更接近 0,从而缓解了梯度消失问题。并且在$z<0$时,其梯度不为 0,避免了死亡神经元问题。当$\alpha = 1$时,该函数在各处都是平滑的,这有助于加速梯度下降,因为在$z = 0$附近不会出现左右跳动的情况。在深度学习中,该函数的缩放版本(SELU:Scaled ELU)也经常被使用。
6. 从头构建 RNN
为了进行演示,我们将使用 Air passenger 数据集,这是一个托管在 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)
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 类,其中__init__
方法根据每层(输入层、隐藏层、输出层)的神经元数量和权重初始化方法来初始化 RNN。这里通过调用initialize_weights
方法,根据指定的初始化方法('random'、'xavier' 或 'he')来设置权重。每组权重连接网络的不同层:weights_ih
连接输入层和隐藏层,weights_hh
连接隐藏层在不同时间步之间(体现了 RNN 的“循环”部分),weights_ho
连接隐藏层和输出层。偏置项bias_h
和bias_o
初始化为零向量,它们将在训练过程中进行调整。
forward
函数用于处理输入序列,通过循环计算隐藏状态和最终输出。首先将隐藏状态初始化为零向量,然后对于输入序列中的每个元素,将其重塑为行向量,并结合当前输入、前一隐藏状态、权重和偏置来更新隐藏状态。这里使用np.tanh
函数引入非线性,这对于复杂模式识别是必要的。最后,根据最后一个隐藏状态、连接隐藏层和输出层的权重以及输出偏置来计算输出。
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
EarlyStopping
类提供了一种在训练过程中的早停机制。如果验证损失在一定数量的轮次(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()
RNNTrainer
类封装了训练过程。它负责运行前向传播和反向传播,在每个轮次后计算损失,并维护训练和验证损失的历史记录。
train
方法通过循环指定的轮次,将训练数据输入模型进行处理,应用反向传播,并跟踪训练和验证损失。如果启用了早停机制,它会根据验证损失来判断是否停止训练。
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)
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
TimeSeriesDataset
类负责处理时间序列数据的加载、预处理和批处理。它通过load_data
方法从指定的 URL 加载数据,并使用MinMaxScaler
方法将数据归一化到 0 到 1 之间,这是一种常见的时间序列和其他类型数据处理的做法,有助于神经网络更有效地学习。create_dataset
方法将加载的数据重新格式化为适合模型输入的格式,其中dataX
包含输入序列,dataY
包含每个序列的相应标签或目标。get_train_test
方法根据指定的比例将加载的数据分割为训练集和测试集。
6.5 训练 RNN
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()
trainX = np.reshape(trainX, (trainX.shape[0], 1, trainX.shape[1]))
testX = np.reshape(testX, (testX.shape[0], 1, testX.shape[1]))
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)
首先,我们指定数据集的 URL,并使用TimeSeriesDataset
类实例化一个数据集对象,其中look_back=1
表示每个输入序列(用于训练 RNN)将由 1 个时间步组成。然后,我们获取训练集和测试集,并将输入数据重塑为符合 RNN 输入要求的格式,通常为[samples, time steps, features]
。
接下来,我们实例化一个 RNN 模型,使用 Xavier 初始化方法,并使用RNNTrainer
类来训练模型。训练器使用均方误差('mse')作为损失函数,这适用于时间序列预测等回归任务。
这个实现涵盖了设置、训练和使用 RNN 进行简单时间序列预测任务所需的所有基本组件。代码结构便于理解和修改,以适应更复杂或不同类型的序列建模任务。
7. 长短期记忆网络(LSTMs)
在前面关于循环神经网络(RNNs)的讨论中,我们了解了它们的设计如何使其能够有效地处理序列数据,这使得它们非常适合处理数据的序列和上下文很重要的任务,如分析时间序列数据或处理语言。
关于长短期记忆网络(LSTMs)将于下次分享,欢迎关注知乎【柏企】公众号【柏企阅文】
评论