IT评测·应用市场-qidao123.com技术社区

标题: LLM架构解析:长短期记忆网络(LSTM)(第三部分)—— 从基础原理到实践应用的深度探索 [打印本页]

作者: 十念    时间: 3 天前
标题: LLM架构解析:长短期记忆网络(LSTM)(第三部分)—— 从基础原理到实践应用的深度探索
本专栏深入探究从循环神经网络(RNN)到Transformer等自然语言处置惩罚(NLP)模型的架构,以及基于这些模型构建的应用程序。
本系列文章内容:

1. 长短期记忆网络(LSTMs,Long Short-Term Memory Networks)

在我们之前关于循环神经网络(RNNs)的讨论中,我们了解了它们的设计怎样使其能够有效地处置惩罚序列数据。这使得它们非常适合处置惩罚数据的序列和上下文至关重要的使命,好比分析时间序列数据或处置惩罚自然语言。
如今,我们要探讨一种能够办理传统循环神经网络面临的巨大挑衅之一的RNN类型:处置惩罚长期数据依赖关系。这就是长短期记忆网络(LSTMs),其复杂水平更上一层楼。它们使用门控系统来控制信息在网络中的流动,从而决定在长序列中保存哪些信息以及忘记哪些信息。

   循环神经网络(RNN)与长短期记忆网络(LSTM)对比。
a:RNN利用其内部状态(记忆)来处置惩罚输入序列;
b:长短期记忆(LSTM)网络是RNN的一种变体,具有额外的长期记忆功能,可记住过去的数据
  长短期记忆(LSTM)是循环神经网络(RNN)家属中的一员,大概说是一种特殊的循环神经网络。LSTM能够通过长时间记住重要且相关的信息,来学习长期依赖关系,这是其默认的本领。
让我们通过一个简朴的故事来剖析LSTM背后的核心思想:
曾经,维克拉姆国王打败了XYZ国王,但随后去世了。他的儿子小维克拉姆继位,英勇作战,但也在战斗中牺牲了。他的孙子小小维克拉姆,没有那么强壮,但凭借聪明最终打败了XYZ国王,为家属报了仇。
当我们阅读这个故事或任何一系列事件时,我们的大脑首先会关注眼前的细节。例如,我们会先处置惩罚维克拉姆国王的胜利和殒命信息。但随着更多角色的出现,我们会调整对故事的长期明白,同时记住小维克拉姆和小小维克拉姆的环境。这种对上下文的不断更新,反映了LSTM的工作方式:当新信息流入时,它们会维护并更新短期记忆和长期记忆。
循环神经网络难以平衡短期和长期上下文信息。就像我们会清楚地记得电视剧的最新一集,但却会忘记早期的细节一样,当新数据到来时,RNN常常会丢失长期信息。LSTM通过创建两条路径来办理这个问题,一条用于短期记忆,一条用于长期记忆,这使得模型能够保存重要信息,并抛弃不太重要的信息。
在LSTM中,信息通过细胞状态流动,细胞状态就像一条传送带,在向前传递有用信息的同时,有选择地忘记不相关的细节。与RNN中用新数据覆盖旧数据不同,LSTM会应用精心设计的数学运算(加法和乘法)来保存关键信息。这使得它们能够有效地对新数据和过去的数据进行优先级排序和管理。
每个细胞状态取决于三种不同的依赖关系,分别是:

说了这么多,让我们更详细地讨论LSTM的架构和功能。
2. LSTM架构


循环神经网络(RNN)的架构是由连续串重复的神经网络构成。这个重复的模块具有一个简朴且单一的功能:使用tanh激活函数。

LSTM的架构也和RNN类似,同样是由连续串重复的模块/神经网络构成。但LSTM的重复模块并非只有一个tanh层,而是包罗四个不同的功能。
这四个功能操作之间有着特殊的毗连,它们分别是:


在整个网络中,信息以向量的情势进行传递。让我们来讨论一下上图中提到的不同符号的寄义:

首先,让我们讨论一下LSTM架构中的重要功能和操作。
2.1 激活函数和线性操作


Sigmoid函数


Sigmoid函数也被称为逻辑激活函数。这个函数有一条平滑的“S”形曲线。
Sigmoid函数的输出结果始终在0到1的范围内。
Sigmoid激活函数重要用于那些我们必须将概率作为输出进行猜测的模型中。由于任何输入的概率只存在于0到1的范围内,以是Sigmoid或逻辑激活函数是正确且最佳的选择。
Tanh激活函数


Tanh激活函数看起来也与Sigmoid/逻辑函数相似。实际上,它是一个经过缩放的Sigmoid函数。我们可以将tanh函数的公式写成Sigmoid函数的情势。
Tanh函数的结果值范围是-1到+1。使用这个tanh函数,我们可以判断输入是强正、中性还是负。
逐元素乘法

两个向量的逐元素乘法是对两个向量的各个元素分别进行乘法运算。例如:
  1. A = [1,2,3,4]
  2. B = [2,3,4,5]
  3. 逐元素乘法结果 : [2,6,12,20]
复制代码
逐元素加法

两个向量的逐元素加法是将两个向量的元素分别相加的过程。例如:
  1. A = [1,2,3,4]
  2. B = [2,3,4,5]
  3. 逐元素加法结果 : [3,5,7,9]
复制代码
2.2 LSTM算法背后的关键概念


LSTM的重要独特之处在于细胞状态;它就像一条传送带,进行一些轻微的线性交互。
这意味着细胞状态通过加法和乘法等基本运算来传递信息;这就是为什么信息能够沿着细胞状态安稳流动,与原始信息相比不会有太多变革。
LSTM的细胞状态或传送带是下图中突出表现的水平线。

LSTM具有独特的结构,能够识别哪些信息是重要的,哪些是不重要的。LSTM可以根据信息的重要性,对细胞状态进行信息的删除或添加。这些特殊的结构被称为
门是一种独特的信息转换方式,LSTM利用这些门来决定哪些信息必要记住、删除,以及传递到另一层等等。
LSTM会根据这些信息对传送带(细胞状态)进行信息的删除或添加。每个门都由一个Sigmoid神经网络层和一个逐元素乘法操作构成。
LSTM有三种门,分别是:

2.2.1 忘记门


在LSTM架构的重复模块中,第一个门是忘记门。这个门的重要使命是决定哪些信息应该被保存,哪些信息应该被抛弃。
这意味着要决定将哪些信息发送到细胞状态以便进一步处置惩罚。忘记门的输入是来自上一个隐蔽状态的信息当前输入,它将这两个状态的信息结合起来,然后通过Sigmoid函数进行处置惩罚。
Sigmoid函数的结果介于0和1之间。如果结果接近0,则表示忘记;如果结果接近1,则表示保存/记住。
2.2.2 输入门


LSTM架构中有一个输入门,用于在忘记门之后更新细胞状态信息。输入门有两种神经网络层,一种是Sigmoid层,另一种是Tanh层。这两个网络层都以上一个隐蔽状态的信息和当前输入的信息作为输入。
Sigmoid网络层的结果范围在0到1之间,而Tanh层的结果范围是-1到1。Sigmoid层决定哪些信息是重要的并必要保存,Tanh层则对网络进行调节。
在对上一个隐蔽状态信息和当前输入信息应用Sigmoid函数和Tanh函数之后,我们将两个输出相乘。最后,Sigmoid层的输出将决定从Tanh层的输出中保存哪些重要信息。
2.2.3 输出门



LSTM中的最后一个门是输出门。输出门的重要使命是决定下一个隐蔽状态中应该包罗哪些信息。这意味着输出层的输出将作为下一个隐蔽状态的输入。
输出门也有两个神经网络层,与输入门相同。但操作有所不同。从输入门我们得到了更新后的细胞状态信息。
在这个输出门中,我们必要将隐蔽状态和当前输入信息通过Sigmoid层,将更新后的细胞状态信息通过Tanh层。然后将Sigmoid层和Tanh层的两个结果相乘。
最终结果将作为输入发送到下一个隐蔽层。
3. LSTM的工作过程

LSTM架构中的首要步调是决定哪些信息是重要的,哪些信息必要从上一个细胞状态中抛弃。在LSTM中实行这一过程的第一个门是“忘记门”。
忘记门的输入是上一个时间步的隐蔽层信息(                                             h                                       t                               −                               1                                                 h_{t-1}                  ht−1​)和当前时间步的输入(                                             x                            t                                       x_t                  xt​),然后将其通过Sigmoid神经网络层。
结果是以向量情势出现,包罗0和1的值。然后,对上一个细胞状态(                                             C                                       t                               −                               1                                                 C_{t-1}                  Ct−1​)的信息(向量情势)和Sigmoid函数的输出(                                             f                            t                                       f_t                  ft​)进行逐元素乘法操作。
忘记门的最终输出中,1表示“完全保存这条信息”,0表示“不保存这条信息”。

接下来的步调是决定将哪些信息存储在当前细胞状态(                                             C                            t                                       C_t                  Ct​)中。另一个门会实行这个使命,LSTM架构中的第二个门是“输入门”。
用新的重要信息更新细胞状态的整个过程将通过两种激活函数/神经网络层来完成,即Sigmoid神经网络层和Tanh神经网络层。
首先,Sigmoid网络层的输入和忘记门一样:上一个时间步的隐蔽层信息(                                             h                                       t                               −                               1                                                 h_{t-1}                  ht−1​)和当前时间步(                                             x                            t                                       x_t                  xt​)。
这个过程决定了我们将更新哪些值。然后,Tanh神经网络层也接收与Sigmoid神经网络层相同的输入。它以向量(                                                        C                               ~                                      t                                       \tilde{C}_t                  C~t​)的情势创建新的候选值,以调节网络。

如今,我们对Sigmoid层和Tanh层的输出进行逐元素乘法操作。之后,我们必要对忘记门的输出和输入门中逐元素乘法的结果进行逐元素加法操作,以更新当前细胞状态信息(                                             C                            t                                       C_t                  Ct​)。

LSTM架构中的最后一步是决定将哪些信息作为输出;在LSTM中实行这一过程的最后一个门是“输出门”。这个输出将基于我们的细胞状态,但会是经过筛选的版本。
在这个门中,我们首先应用Sigmoid神经网络,它的输入和之前门的Sigmoid层一样:上一个时间步的隐蔽层信息(                                             h                                       t                               −                               1                                                 h_{t-1}                  ht−1​)和当前时间输入(                                             x                            t                                       x_t                  xt​),以决定细胞状态信息的哪些部分将作为输出。
然后将更新后的细胞状态信息通过Tanh神经网络层进行调节(将值压缩到-1和1之间),然后对Sigmoid神经网络层和Tanh神经网络层的两个结果进行逐元素乘法操作。

整个过程在LSTM架构的每个模块中重复进行。
4. LSTM架构的类型

LSTM是办理或处置惩罚序列猜测问题的一个非常有趣的切入点。根据LSTM网络作为层的使用方式,我们可以将LSTM架构分为多种类型。

本节将讨论最常用的五种不同类型的LSTM架构,它们分别是:

5. 用Python从零构建LSTM

在本节中,我们将参考本文前面介绍的数学基础和概念,逐步剖析在Python中实现LSTM的过程。我们将使用谷歌股票数据来训练我们从零构建的模型。该数据集是从Kaggle上获取的,可免费用于商业用途。
5.1 导入库和初始设置


WeightInitializer类
  1. import numpy as np
  2. import pandas as pd
  3. from src.model import WeightInitializer
  4. from src.trainer import PlotManager, EarlyStopping
  5. class WeightInitializer:
  6.     def __init__(self, method='random'):
  7.         self.method = method
  8.     def initialize(self, shape):
  9.         if self.method == 'random':
  10.             return np.random.randn(*shape)
  11.         elif self.method == 'xavier':
  12.             return np.random.randn(*shape) / np.sqrt(shape[0])
  13.         elif self.method == 'he':
  14.             return np.random.randn(*shape) * np.sqrt(2 / shape[0])
  15.         elif self.method == 'uniform':
  16.             return np.random.uniform(-1, 1, shape)
  17.         else:
  18.             raise ValueError(f'Unknown initialization method: {self.method}')
复制代码
WeightInitializer是一个自定义类,用于处置惩罚权重的初始化。这一点至关重要,由于不同的初始化方法会显著影响LSTM的收敛行为。
PlotManager类
  1. class PlotManager:
  2.     def __init__(self):
  3.         self.fig, self.ax = plt.subplots(3, 1, figsize=(6, 4))
  4.     def plot_losses(self, train_losses, val_losses):
  5.         self.ax.plot(train_losses, label='Training Loss')
  6.         self.ax.plot(val_losses, label='Validation Loss')
  7.         self.ax.set_title('Training and Validation Losses')
  8.         self.ax.set_xlabel('Epoch')
  9.         self.ax.set_ylabel('Loss')
  10.         self.ax.legend()
  11.     def show_plots(self):
  12.         plt.tight_layout()
复制代码
这是src.trainer中的实用类,用于管理绘图,它使我们能够绘制训练损失和验证损失的图像。
EarlyStopping类
  1. class EarlyStopping:
  2.     """
  3.     Early stopping to stop the training when the loss does not improve after
  4.     Args:
  5.     -----
  6.         patience (int): Number of epochs to wait before stopping the training.
  7.         verbose (bool): If True, prints a message for each epoch where the loss
  8.                         does not improve.
  9.         delta (float): Minimum change in the monitored quantity to qualify as an improvement.
  10.     """
  11.     def __init__(self, patience=7, verbose=False, delta=0):
  12.         self.patience = patience
  13.         self.verbose = verbose
  14.         self.counter = 0
  15.         self.best_score = None
  16.         self.early_stop = False
  17.         self.delta = delta
  18.     def __call__(self, val_loss):
  19.         """
  20.         Determines if the model should stop training.
  21.         
  22.         Args:
  23.             val_loss (float): The loss of the model on the validation set.
  24.         """
  25.         score = -val_loss
  26.         if self.best_score is None:
  27.             self.best_score = score
  28.         elif score < self.best_score + self.delta:
  29.             self.counter += 1
  30.             
  31.             if self.counter >= self.patience:
  32.                 self.early_stop = True
  33.         else:
  34.             self.best_score = score
  35.             self.counter = 0   
复制代码
这是src.trainer中的实用类,用于在训练过程中处置惩罚提前停止操作,以防止过拟合。你可以在这篇文章中了解更多关于EarlyStopping的信息,以及它的功能对深度神经网络为何极其有用。
5.2 LSTM类

首先,让我们看看整个类的样子,然后将其分解为更易于管理的步调:
  1. class LSTM:
  2.     """
  3.     Long Short-Term Memory (LSTM) network.
  4.    
  5.     Parameters:
  6.     - input_size: int, dimensionality of input space
  7.     - hidden_size: int, number of LSTM units
  8.     - output_size: int, dimensionality of output space
  9.     - init_method: str, weight initialization method (default: 'xavier')
  10.     """
  11.     def __init__(self, input_size, hidden_size, output_size, init_method='xavier'):
  12.         self.input_size = input_size
  13.         self.hidden_size = hidden_size
  14.         self.output_size = output_size
  15.         self.weight_initializer = WeightInitializer(method=init_method)
  16.         # Initialize weights
  17.         self.wf = self.weight_initializer.initialize((hidden_size, hidden_size + input_size))
  18.         self.wi = self.weight_initializer.initialize((hidden_size, hidden_size + input_size))
  19.         self.wo = self.weight_initializer.initialize((hidden_size, hidden_size + input_size))
  20.         self.wc = self.weight_initializer.initialize((hidden_size, hidden_size + input_size))
  21.         # Initialize biases
  22.         self.bf = np.zeros((hidden_size, 1))
  23.         self.bi = np.zeros((hidden_size, 1))
  24.         self.bo = np.zeros((hidden_size, 1))
  25.         self.bc = np.zeros((hidden_size, 1))
  26.         # Initialize output layer weights and biases
  27.         self.why = self.weight_initializer.initialize((output_size, hidden_size))
  28.         self.by = np.zeros((output_size, 1))
  29.     @staticmethod
  30.     def sigmoid(z):
  31.         """
  32.         Sigmoid activation function.
  33.         
  34.         Parameters:
  35.         - z: np.ndarray, input to the activation function
  36.         
  37.         Returns:
  38.         - np.ndarray, output of the activation function
  39.         """
  40.         return 1 / (1 + np.exp(-z))
  41.     @staticmethod
  42.     def dsigmoid(y):
  43.         """
  44.         Derivative of the sigmoid activation function.
  45.         Parameters:
  46.         - y: np.ndarray, output of the sigmoid activation function
  47.         Returns:
  48.         - np.ndarray, derivative of the sigmoid function
  49.         """
  50.         return y * (1 - y)
  51.     @staticmethod
  52.     def dtanh(y):
  53.         """
  54.         Derivative of the hyperbolic tangent activation function.
  55.         Parameters:
  56.         - y: np.ndarray, output of the hyperbolic tangent activation function
  57.         Returns:
  58.         - np.ndarray, derivative of the hyperbolic tangent function
  59.         """
  60.         return 1 - y * y
  61.     def forward(self, x):
  62.         """
  63.         Forward pass through the LSTM network.
  64.         Parameters:
  65.         - x: np.ndarray, input to the network
  66.         Returns:
  67.         - np.ndarray, output of the network
  68.         - list, caches containing intermediate values for backpropagation
  69.         """
  70.         caches = []
  71.         h_prev = np.zeros((self.hidden_size, 1))
  72.         c_prev = np.zeros((self.hidden_size, 1))
  73.         h = h_prev
  74.         c = c_prev
  75.         for t in range(x.shape[0]):
  76.             x_t = x[t].reshape(-1, 1)
  77.             combined = np.vstack((h_prev, x_t))
  78.             
  79.             f = self.sigmoid(np.dot(self.wf, combined) + self.bf)
  80.             i = self.sigmoid(np.dot(self.wi, combined) + self.bi)
  81.             o = self.sigmoid(np.dot(self.wo, combined) + self.bo)
  82.             c_ = np.tanh(np.dot(self.wc, combined) + self.bc)
  83.             
  84.             c = f * c_prev + i * c_
  85.             h = o * np.tanh(c)
  86.             cache = (h_prev, c_prev, f, i, o, c_, x_t, combined, c, h)
  87.             caches.append(cache)
  88.             h_prev, c_prev = h, c
  89.         y = np.dot(self.why, h) + self.by
  90.         return y, caches
  91.     def backward(self, dy, caches, clip_value=1.0):
  92.         """
  93.         Backward pass through the LSTM network.
  94.         Parameters:
  95.         - dy: np.ndarray, gradient of the loss with respect to the output
  96.         - caches: list, caches from the forward pass
  97.         - clip_value: float, value to clip gradients to (default: 1.0)
  98.         Returns:
  99.         - tuple, gradients of the loss with respect to the parameters
  100.         """
  101.         dWf, dWi, dWo, dWc = [np.zeros_like(w) for w in (self.wf, self.wi, self.wo, self.wc)]
  102.         dbf, dbi, dbo, dbc = [np.zeros_like(b) for b in (self.bf, self.bi, self.bo, self.bc)]
  103.         dWhy = np.zeros_like(self.why)
  104.         dby = np.zeros_like(self.by)
  105.         # Ensure dy is reshaped to match output size
  106.         dy = dy.reshape(self.output_size, -1)
  107.         dh_next = np.zeros((self.hidden_size, 1))  # shape must match hidden_size
  108.         dc_next = np.zeros_like(dh_next)
  109.         for cache in reversed(caches):
  110.             h_prev, c_prev, f, i, o, c_, x_t, combined, c, h = cache
  111.             # Add gradient from next step to current output gradient
  112.             dh = np.dot(self.why.T, dy) + dh_next
  113.             dc = dc_next + (dh * o * self.dtanh(np.tanh(c)))
  114.             df = dc * c_prev * self.dsigmoid(f)
  115.             di = dc * c_ * self.dsigmoid(i)
  116.             do = dh * self.dtanh(np.tanh(c))
  117.             dc_ = dc * i * self.dtanh(c_)
  118.             dcombined_f = np.dot(self.wf.T, df)
  119.             dcombined_i = np.dot(self.wi.T, di)
  120.             dcombined_o = np.dot(self.wo.T, do)
  121.             dcombined_c = np.dot(self.wc.T, dc_)
  122.             dcombined = dcombined_f + dcombined_i + dcombined_o + dcombined_c
  123.             dh_next = dcombined[:self.hidden_size]
  124.             dc_next = f * dc
  125.             dWf += np.dot(df, combined.T)
  126.             dWi += np.dot(di, combined.T)
  127.             dWo += np.dot(do, combined.T)
  128.             dWc += np.dot(dc_, combined.T)
  129.             dbf += df.sum(axis=1, keepdims=True)
  130.             dbi += di.sum(axis=1, keepdims=True)
  131.             dbo += do.sum(axis=1, keepdims=True)
  132.             dbc += dc_.sum(axis=1, keepdims=True)
  133.         dWhy += np.dot(dy, h.T)
  134.         dby += dy
  135.         gradients = (dWf, dWi, dWo, dWc, dbf, dbi, dbo, dbc, dWhy, dby)
  136.         # Gradient clipping
  137.         for i in range(len(gradients)):
  138.             np.clip(gradients[i], -clip_value, clip_value, out=gradients[i])
  139.         return gradients
  140.     def update_params(self, grads, learning_rate):
  141.         """
  142.         Update the parameters of the network using the gradients.
  143.         Parameters:
  144.         - grads: tuple, gradients of the loss with respect to the parameters
  145.         - learning_rate: float, learning rate
  146.         """
  147.         dWf, dWi, dWo, dWc, dbf, dbi, dbo, dbc, dWhy, dby = grads
  148.         self.wf -= learning_rate * dWf
  149.         self.wi -= learning_rate * dWi
  150.         self.wo -= learning_rate * dWo
  151.         self.wc -= learning_rate * dWc
  152.         self.bf -= learning_rate * dbf
  153.         self.bi -= learning_rate * dbi
  154.         self.bo -= learning_rate * dbo
  155.         self.bc -= learning_rate * dbc
  156.         self.why -= learning_rate * dWhy
  157.         self.by -= learning_rate * dby
复制代码
初始化

__init__方法使用指定的输入层、隐蔽层和输出层的巨细来初始化一个LSTM实例,并选择一种权重初始化方法。
为门(忘记门wf、输入门wi、输出门wo和细胞门wc)以及将最后一个隐蔽状态毗连到输出的权重(why)初始化权重。通常选择Xavier初始化方法,由于它是保持各层激活值方差的一个很好的默认选择。
所有门和输出层的偏置都初始化为零。这是一种常见的做法,尽管有时会添加小的常数以避免在开始时出现神经元殒命的环境。
前向传播方法

我们首先将上一个隐蔽状态h_prev和细胞状态c_prev设置为零,这对于第一个时间步来说是典型的做法。
  1. def forward(self, x):
复制代码
输入x按时间步进行处置惩罚,每个时间步都会更新门的激活值、细胞状态和隐蔽状态。
  1. for t in range(x.shape[0]):
  2.     x_t = x[t].reshape(-1, 1)
  3.     combined = np.vstack((h_prev, x_t))
复制代码
在每个时间步,输入和上一个隐蔽状态垂直堆叠,形成一个用于矩阵运算的单个组合输入。这对于一次性高效地实行线性变换至关重要。
  1. f = self.sigmoid(np.dot(self.wf, combined) + self.bf)
  2. i = self.sigmoid(np.dot(self.wi, combined) + self.bi)
  3. o = self.sigmoid(np.dot(self.wo, combined) + self.bo)
  4. c_ = np.tanh(np.dot(self.wc, combined) + self.bc)
  5.    
  6. c = f * c_prev + i * c_
  7. h = o * np.tanh(c)
复制代码
每个门(忘记门、输入门、输出门)使用Sigmoid函数计算其激活值,这会影响细胞状态和隐蔽状态的更新方式。
在这里,忘记门(f)决定保存上一个细胞状态的多少。
输入门(i)决定添加多少新的候选细胞状态(c_)。
最后,输出门(o)计算将细胞状态的哪一部分作为隐蔽状态输出。
细胞状态作为上一个状态和新候选状态的加权和进行更新。隐蔽状态是通过将更新后的细胞状态通过一个Tanh函数,然后用输出门进行门控得到的。
  1. cache = (h_prev, c_prev, f, i, o, c_, x_t, combined, c, h)
  2. caches.append(cache)
复制代码
我们将反向传播所需的相关值存储在cache中,这包括状态、门激活值和输入。
  1. y = np.dot(self.why, h) + self.by
复制代码
最后,输出y计算为最后一个隐蔽状态的线性变换。该方法返回输出和缓存的值,以便在反向传播期间使用。
反向传播方法

此方法用于计算损失函数关于LSTM权重和偏置的梯度。在训练过程中,这些梯度对于更新模型参数是必不可少的。
  1. def backward(self, dy, caches, clip_value=1.0):
  2.     dWf, dWi, dWo, dWc = [np.zeros_like(w) for w in (self.wf, self.wi, self.wo, self.wc)]
  3.     dbf, dbi, dbo, dbc = [np.zeros_like(b) for b in (self.bf, self.bi, self.bo, self.bc)]
  4.     dWhy = np.zeros_like(self.why)
  5.     dby = np.zeros_like(self.by)
复制代码
所有权重(dWf、dWi、dWo、dWc、dWhy)和偏置(dbf、dbi、dbo、dbc、dby)的梯度都初始化为零。这是必要的,由于梯度是在序列的每个时间步上累加的。
  1. dy = dy.reshape(self.output_size, -1)
  2. dh_next = np.zeros((self.hidden_size, 1))
  3. dc_next = np.zeros_like(dh_next)
复制代码
在这里,我们确保dy的形状适合进行矩阵运算。dh_next和dc_next存储从后续时间步反向传播回来的梯度。
  1. for cache in reversed(caches):
  2.     h_prev, c_prev, f, i, o, c_, x_t, combined, c, h = cache
复制代码
从缓存中提取每个时间步的LSTM状态和门的激活值。处置惩罚从最后一个时间步开始并向前推进(reversed(caches)),这对于在循环神经网络中正确应用链式法则(通逾期间的反向传播 - BPTT)至关重要。
  1. dh = np.dot(self.why.T, dy) + dh_next
  2. dc = dc_next + (dh * o * self.dtanh(np.tanh(c)))
  3. df = dc * c_prev * self.dsigmoid(f)
  4. di = dc * c_ * self.dsigmoid(i)
  5. do = dh * self.dtanh(np.tanh(c))
  6. dc_ = dc * i * self.dtanh(c_)
复制代码
dh和dc分别是损失关于隐蔽状态和细胞状态的梯度。每个门(df、di、do)和候选细胞状态(dc_)的梯度使用链式法则计算,涉及Sigmoid(dsigmoid)和双曲正切(dtanh)函数的导数,这些在门控机制部分已经讨论过。
  1. dWf += np.dot(df, combined.T)
  2. dWi += np.dot(di, combined.T)
  3. dWo += np.dot(do, combined.T)
  4. dWc += np.dot(dc_, combined.T)
  5. dbf += df.sum(axis=1, keepdims=True)
  6. dbi += di.sum(axis=1, keepdims=True)
  7. dbo += do.sum(axis=1, keepdims=True)
  8. dbc += dc_.sum(axis=1, keepdims=True)
复制代码
这些代码行将每个权重和偏置在所有时间步上的梯度进行累加。
  1. for i in range(len(gradients)):
  2.     np.clip(gradients[i], -clip_value, clip_value, out=gradients[i])
复制代码
为了防止梯度爆炸,我们将梯度裁剪到指定的范围(clip_value),这是训练循环神经网络时的常见做法。
参数更新方法

  1. def update_params(self, grads, learning_rate):
  2.     dWf, dWi, dWo, dWc, dbf, dbi, dbo, dbc, dWhy, dby = grads
  3.     ...
  4.     self.wf -= learning_rate * dWf
  5.     ...
复制代码
每个权重和偏置通过减去相应梯度的一部分(learning_rate)来更新。这一步调整模型参数以最小化损失函数。
5.3 训练与验证

  1. class LSTMTrainer:
  2.     """
  3.     LSTM网络的训练器。
  4.     参数:
  5.     - model: LSTM,要训练的LSTM网络
  6.     - learning_rate: float,优化器的学习率
  7.     - patience: int,在提前停止训练前等待的轮数
  8.     - verbose: bool,是否打印训练信息
  9.     - delta: float,验证损失有改善的最小变化量
  10.     """
  11.     def __init__(self, model, learning_rate=0.01, patience=7, verbose=True, delta=0):
  12.         self.model = model
  13.         self.learning_rate = learning_rate
  14.         self.train_losses = []
  15.         self.val_losses = []
  16.         self.early_stopping = EarlyStopping(patience, verbose, delta)
  17.     def train(self, X_train, y_train, X_val=None, y_val=None, epochs=10, batch_size=1, clip_value=1.0):
  18.         """
  19.         训练LSTM网络。
  20.         参数:
  21.         - X_train: np.ndarray,训练数据
  22.         - y_train: np.ndarray,训练标签
  23.         - X_val: np.ndarray,验证数据
  24.         - y_val: np.ndarray,验证标签
  25.         - epochs: int,训练轮数
  26.         - batch_size: int,小批量的大小
  27.         - clip_value: float,梯度裁剪的值
  28.         """
  29.         for epoch in range(epochs):
  30.             epoch_losses = []
  31.             for i in range(0, len(X_train), batch_size):
  32.                 batch_X = X_train[i:i + batch_size]
  33.                 batch_y = y_train[i:i + batch_size]
  34.                 losses = []
  35.                
  36.                 for x, y_true in zip(batch_X, batch_y):
  37.                     y_pred, caches = self.model.forward(x)
  38.                     loss = self.compute_loss(y_pred, y_true.reshape(-1, 1))
  39.                     losses.append(loss)
  40.                     
  41.                     # 反向传播以获取梯度
  42.                     dy = y_pred - y_true.reshape(-1, 1)
  43.                     grads = self.model.backward(dy, caches, clip_value=clip_value)
  44.                     self.model.update_params(grads, self.learning_rate)
  45.                 batch_loss = np.mean(losses)
  46.                 epoch_losses.append(batch_loss)
  47.             avg_epoch_loss = np.mean(epoch_losses)
  48.             self.train_losses.append(avg_epoch_loss)
  49.             if X_val is not None and y_val is not None:
  50.                 val_loss = self.validate(X_val, y_val)
  51.                 self.val_losses.append(val_loss)
  52.                 print(f'轮数 {epoch + 1}/{epochs} - 损失: {avg_epoch_loss:.5f}, 验证损失: {val_loss:.5f}')
  53.                
  54.                 # 检查提前停止条件
  55.                 self.early_stopping(val_loss)
  56.                 if self.early_stopping.early_stop:
  57.                     print("提前停止")
  58.                     break
  59.             else:
  60.                 print(f'轮数 {epoch + 1}/{epochs} - 损失: {avg_epoch_loss:.5f}')
  61.     def compute_loss(self, y_pred, y_true):
  62.         """
  63.         计算均方误差损失。
  64.         """
  65.         return np.mean((y_pred - y_true) ** 2)
  66.     def validate(self, X_val, y_val):
  67.         """
  68.         在单独的数据集上验证模型。
  69.         """
  70.         val_losses = []
  71.         for x, y_true in zip(X_val, y_val):
  72.             y_pred, _ = self.model.forward(x)
  73.             loss = self.compute_loss(y_pred, y_true.reshape(-1, 1))
  74.             val_losses.append(loss)
  75.         return np.mean(val_losses)
复制代码
训练器会在多个轮次中协调训练过程,处置惩罚数据批次,并可选择对模型进行验证。
  1. for epoch in range(epochs):
  2.     ...
  3.     for i in range(0, len(X_train), batch_size):
  4.         ...
  5.         for x, y_true in zip(batch_X, batch_y):
  6.             y_pred, caches = self.model.forward(x)
  7.             ...
复制代码
每一批数据都会输入到模型中。前向传播会天生猜测结果,并缓存用于反向传播的中心值。
  1. dy = y_pred - y_true.reshape(-1, 1)
  2. grads = self.model.backward(dy, caches, clip_value=clip_value)
  3. self.model.update_params(grads, self.learning_rate)
复制代码
计算损失后,使用关于猜测误差的梯度(dy)进行反向传播。得到的梯度用于更新模型参数。
  1. print(f'轮数 {epoch + 1}/{epochs} - 损失: {avg_epoch_loss:.5f}')
复制代码
记录训练进度,以帮助长期监控模型的性能。
5.4 数据预处置惩罚

  1. class TimeSeriesDataset:
  2.     """
  3.     时间序列数据的数据集类。
  4.     参数:
  5.     - ticker: str,股票代码
  6.     - start_date: str,数据检索的开始日期
  7.     - end_date: str,数据检索的结束日期
  8.     - look_back: int,每个样本中包含的前几个时间步的数量
  9.     - train_size: float,用于训练的数据比例
  10.     """
  11.     def __init__(self, start_date, end_date, look_back=1, train_size=0.67):
  12.         self.start_date = start_date
  13.         self.end_date = end_date
  14.         self.look_back = look_back
  15.         self.train_size = train_size
  16.     def load_data(self):
  17.         """
  18.         加载股票数据。
  19.         返回:
  20.         - np.ndarray,训练数据
  21.         - np.ndarray,测试数据
  22.         """
  23.         df = pd.read_csv('data/google.csv')
  24.         df = df[(df['Date'] >= self.start_date) & (df['Date'] <= self.end_date)]
  25.         df = df.sort_index()
  26.         df = df.loc[self.start_date:self.end_date]
  27.         df = df[['Close']].astype(float)  # 使用收盘价
  28.         df = self.MinMaxScaler(df.values)  # 将DataFrame转换为numpy数组
  29.         train_size = int(len(df) * self.train_size)
  30.         train, test = df[0:train_size,:], df[train_size:len(df),:]
  31.         return train, test
  32.    
  33.     def MinMaxScaler(self, data):
  34.         """
  35.         对数据进行最小 - 最大缩放。
  36.         参数:
  37.         - data: np.ndarray,输入数据
  38.         """
  39.         numerator = data - np.min(data, 0)
  40.         denominator = np.max(data, 0) - np.min(data, 0)
  41.         return numerator / (denominator + 1e-7)
  42.     def create_dataset(self, dataset):
  43.         """
  44.         创建用于时间序列预测的数据集。
  45.         参数:
  46.         - dataset: np.ndarray,输入数据
  47.         返回:
  48.         - np.ndarray,输入数据
  49.         - np.ndarray,输出数据
  50.         """
  51.         dataX, dataY = [], []
  52.         for i in range(len(dataset)-self.look_back):
  53.             a = dataset[i:(i + self.look_back), 0]
  54.             dataX.append(a)
  55.             dataY.append(dataset[i + self.look_back, 0])
  56.         return np.array(dataX), np.array(dataY)
  57.     def get_train_test(self):
  58.         """
  59.         获取训练和测试数据。
  60.         返回:
  61.         - np.ndarray,训练输入
  62.         - np.ndarray,训练输出
  63.         - np.ndarray,测试输入
  64.         - np.ndarray,测试输出
  65.         """
  66.         train, test = self.load_data()
  67.         trainX, trainY = self.create_dataset(train)
  68.         testX, testY = self.create_dataset(test)
  69.         return trainX, trainY, testX, testY
复制代码
这个类负责获取数据并将其预处置惩罚成适合训练LSTM的格式,包括缩放数据以及将其划分为训练集和测试集。
5.5 模型训练

如今,让我们利用上述定义的所有代码来加载数据集、对其进行预处置惩罚,并训练我们的LSTM模型。
首先,让我们加载数据集:
  1. # 实例化数据集
  2. dataset = TimeSeriesDataset('2010-1-1', '2020-12-31', look_back=1)
  3. trainX, trainY, testX, testY = dataset.get_train_test()
复制代码
在这个实例中,它被配置为从Kaggle获取谷歌(GOOGL)从2010年1月1日到2020年12月31日的历史数据。
look_back = 1:这个参数设置了每个输入样本中包罗的过去时间步的数目。在这里,每个输入样本将包罗前一个时间步的数据,这意味着模型将使用一天的数据来猜测下一天的数据。
get_train_test():这个方法会处置惩罚获取到的数据,对其进行归一化,并将其划分为训练集和测试集。这对于在数据的一部分上训练模型,并在另一部分上验证其性能以检查是否过拟合是至关重要的。
  1. # 重塑输入,使其形状为 [样本数, 时间步, 特征数]
  2. trainX = np.reshape(trainX, (trainX.shape[0], trainX.shape[1], 1))
  3. testX = np.reshape(testX, (testX.shape[0], testX.shape[1], 1))
复制代码
这个重塑步调将数据格式调整为LSTM所盼望的格式。LSTM要求输入的形状为[样本数, 时间步, 特征数]。
这里:

  1. look_back = 1  # 每个样本中包含的前几个时间步的数量
  2. hidden_size = 256  # LSTM单元的数量
  3. output_size = 1  # 输出空间的维度
  4. lstm = LSTM(input_size=1, hidden_size=hidden_size, output_size=output_size)
复制代码
在这段代码中:

  1. trainer = LSTMTrainer(lstm, learning_rate=1e-3, patience=50, verbose=True, delta=0.001)
  2. trainer.train(trainX, trainY, testX, testY, epochs=1000, batch_size=32)
复制代码
这里我们将学习率设置为1e - 3(0.001)。学习率过高会导致模型过快收敛到一个次优解,而过低则会使训练过程变慢,甚至可能陷入停滞。我们还将patience设置为50,如果验证损失在50个轮次内没有改善,模型训练将停止。
train()方法会在指定的轮数和批次巨细下实行训练过程。在训练期间,模型会每10个轮次打印一次模型性能,输出结果类似于:
  1. 轮数 1/1000 - 损失: 0.25707, 验证损失: 0.43853
  2. 轮数 11/1000 - 损失: 0.06463, 验证损失: 0.06056
  3. 轮数 21/1000 - 损失: 0.05313, 验证损失: 0.02100
  4. 轮数 31/1000 - 损失: 0.04862, 验证损失: 0.01134
  5. 轮数 41/1000 - 损失: 0.04512, 验证损失: 0.00678
  6. 轮数 51/1000 - 损失: 0.04234, 验证损失: 0.00395
  7. 轮数 61/1000 - 损失: 0.04014, 验证损失: 0.00210
  8. 轮数 71/1000 - 损失: 0.03840, 验证损失: 0.00095
  9. 轮数 81/1000 - 损失: 0.03703, 验证损失: 0.00031
  10. 轮数 91/1000 - 损失: 0.03595, 验证损失: 0.00004
  11. 轮数 101/1000 - 损失: 0.03509, 验证损失: 0.00003
  12. 轮数 111/1000 - 损失: 0.03442, 验证损失: 0.00021
  13. 轮数 121/1000 - 损失: 0.03388, 验证损失: 0.00051
  14. 轮数 131/1000 - 损失: 0.03346, 验证损失: 0.00090
  15. 轮数 141/1000 - 损失: 0.03312, 验证损失: 0.00133
  16. 提前停止
复制代码
最后,让我们绘制训练损失和验证损失,以便更好地了解模型可能的收敛或发散环境。我们可以使用以下代码实现:
  1. plot_manager = PlotManager()
  2. # 在训练循环中
  3. plot_manager.plot_losses(trainer.train_losses, trainer.val_losses)
  4. # 在训练循环结束后
  5. plot_manager.show_plots()
复制代码
这将绘制一个类似于下面的图表:

从图表中我们可以看到,在早期轮次中,训练损失和验证损失都迅速下降,这表明我们的初始化技能(Xavier)可能不太适合这个使命。尽管在约莫90个轮次后触发了提前停止,取得了一些令人印象深刻的性能,但我们可以尝试降低学习率并增加训练轮数。此外,我们还可以尝试使用其他技能,如学习率调理器或Adam优化器。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。




欢迎光临 IT评测·应用市场-qidao123.com技术社区 (https://dis.qidao123.com/) Powered by Discuz! X3.4