时间序列模型(二)—— LSTM
Chen Kai Architect

长短期记忆网络(Long Short-Term Memory, LSTM)是一种特殊的递归神经网络(Recurrent Neural Network, RNN),由Sepp Hochreiter和Jürgen Schmidhuber在1997年提出。LSTM被设计用来解决传统RNN在处理长序列时的梯度消失和梯度爆炸问题。它通过引入记忆单元(Memory Cell)和门控机制(Gating Mechanisms),实现了对长期依赖关系的有效捕捉。LSTM在自然语言处理、时间序列预测、音频处理等领域有广泛应用。

LSTM的基本结构

记忆单元与门控机制

LSTM的核心是其独特的记忆单元和三个门(输入门、遗忘门、输出门),这些门通过不同的方式控制信息在记忆单元中的流动和存储。我们可以把LSTM比作一个智能记事本。这个记事本不仅能记录信息,还能智能地决定哪些信息应该记住,哪些信息应该忘记,以及哪些信息应该输出。

  1. 记忆单元(Memory Cell):存储长期信息的单元。
  2. 输入门(Input Gate):控制新信息如何流入记忆单元。
  3. 遗忘门(Forget Gate):决定记忆单元中哪些信息需要被遗忘。
  4. 输出门(Output Gate):控制记忆单元的输出。

数学公式

为当前时间步,为输入向量,为隐藏状态,为记忆单元状态,为权重矩阵,为偏置向量。具体的计算步骤如下:

  1. 遗忘门:决定哪些信息需要遗忘。遗忘门通过一个sigmoid函数来控制遗忘的比例,输出一个0到1之间的数值。这个数值越接近1,表示越不需要遗忘;越接近0,表示越需要遗忘。

  2. 输入门:决定哪些新信息需要加入记忆单元。输入门同样通过一个sigmoid函数来控制新信息的加入比例,输入门的输出是一个0到1之间的数值,表示新信息加入的程度。然后,通过一个tanh函数生成新的候选记忆,这个候选记忆可以加入到记忆单元中。

  3. 更新记忆单元:结合遗忘门和输入门的作用更新记忆单元状态。记忆单元的状态由遗忘门的输出和之前的记忆状态以及输入门的输出和新的候选记忆共同决定。表示逐元素乘法。

  4. 输出门:决定记忆单元的输出。输出门通过一个sigmoid函数控制记忆单元的输出比例,最终的隐藏状态由输出门的输出和当前记忆单元的状态经过tanh函数处理后得到。

LSTM的Python实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import torch
import torch.nn as nn

class LSTM(nn.Module):
def __init__(self, input_size, hidden_size, num_layers):
super(LSTM, self).__init__()
self.hidden_size = hidden_size
self.num_layers = num_layers
self.lstm = nn.LSTM(input_size, hidden_size, num_layers, batch_first=True)

def forward(self, x):
h0 = torch.zeros(self.num_layers, x.size(0), self.hidden_size).to(x.device)
c0 = torch.zeros(self.num_layers, x.size(0), self.hidden_size).to(x.device)
out, _ = self.lstm(x, (h0, c0))
return out

input_size = 10
hidden_size = 20
num_layers = 2
lstm = LSTM(input_size, hidden_size, num_layers)

__init__ 方法是类的构造函数。它接受三个参数:

  • input_size:输入特征的维度。
  • hidden_size:隐藏层的特征维度。
  • num_layers:LSTM的层数。

在这个方法中,首先调用了父类nn.Module的构造函数,然后初始化了LSTM的属性:

  • self.hidden_size:设置隐藏层的特征维度。
  • self.num_layers:设置LSTM的层数。
  • self.lstm:定义了一个LSTM层。nn.LSTM 构造函数接受以下参数:
    • input_size:输入特征的维度。
    • hidden_size:隐藏层的特征维度。
    • num_layers:LSTM的层数。
    • batch_first=True:指定输入和输出的形状为(batch_size, sequence_length, feature_dimension)。

forward方法定义了模型的前向传播过程。

  • x:输入张量,其形状为(batch_size, sequence_length, input_size)。

在这个方法中,首先初始化隐藏状态和细胞状态:

  • h0:初始化隐藏状态,形状为(num_layers, batch_size, hidden_size)。
  • c0:初始化细胞状态,形状为(num_layers, batch_size, hidden_size)。

然后,将输入张量x与初始化的隐藏状态和细胞状态一起传递给LSTM层:

  • self.lstm(x, (h0, c0)):执行LSTM前向传播,返回输出张量out和隐藏状态_(这里只使用输出张量out)。

最后,返回输出张量out

LSTM的高级应用

注意力机制与LSTM的结合

注意力机制最早在机器翻译任务中引入,其思想是让模型在进行预测时,不是简单地依赖于最后一个隐藏状态,而是通过一种加权的方式,利用整个输入序列的所有隐藏状态。这个加权的过程通过注意力得分来实现,这些得分表示了每个时间步的重要性。注意力机制(Attention Mechanism)通过赋予输入序列中不同部分不同的重要性权重,进一步提升LSTM的性能。常见的注意力机制有Bahdanau Attention和Luong Attention。

Bahdanau Attention

Bahdanau Attention的实现包括以下几个步骤:

  1. 计算注意力权重:对于每一个输入序列中的时间步,通过当前隐藏状态和编码器输出计算注意力得分。
  2. 生成上下文向量:对所有时间步的编码器输出进行加权求和,得到上下文向量。
  3. 结合上下文向量和当前隐藏状态:将上下文向量与当前时间步的隐藏状态结合,用于最终的预测。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class BahdanauAttention(nn.Module):
def __init__(self, hidden_size):
super(BahdanauAttention, self).__init__()
self.attn = nn.Linear(hidden_size * 2, hidden_size)
self.v = nn.Parameter(torch.rand(hidden_size))

def forward(self, hidden, encoder_outputs):
seq_len = encoder_outputs.size(1)
hidden = hidden.repeat(seq_len, 1, 1).transpose(0, 1)
attn_energies = self.score(hidden, encoder_outputs)
return F.softmax(attn_energies, dim=1).unsqueeze(1)

def score(self, hidden, encoder_outputs):
energy = torch.tanh(self.attn(torch.cat((hidden, encoder_outputs), 2)))
energy = energy.transpose(2, 1)
v = self.v.repeat(encoder_outputs.size(0), 1).unsqueeze(1)
energy = torch.bmm(v, energy)
return energy.squeeze(1)

在上述代码中:

  1. self.attn是一个线性层,将隐藏状态和编码器输出连接起来。
  2. self.v是一个可训练的参数,用于计算注意力得分。
  3. forward方法中,hidden是解码器的当前隐藏状态,encoder_outputs是编码器的所有输出。注意力得分通过score方法计算,并通过softmax进行归一化。
  4. score方法中,通过将隐藏状态和编码器输出连接后传入tanh激活函数,得到能量值energy,再与参数v进行矩阵乘法,得到最终的注意力得分。

LSTM在自然语言处理中的应用

LSTM在自然语言处理(NLP)中的应用非常广泛,例如机器翻译(Machine Translation)、文本生成(Text Generation)、情感分析(Sentiment Analysis)等。在机器翻译中,LSTM常与编码器-解码器(Encoder-Decoder)结构结合使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class EncoderLSTM(nn.Module):
def __init__(self, input_size, hidden_size, num_layers):
super(EncoderLSTM, self).__init__()
self.hidden_size = hidden_size
self.num_layers = num_layers
self.lstm = nn.LSTM(input_size, hidden_size, num_layers, batch_first=True)

def forward(self, x):
h0 = torch.zeros(self.num_layers, x.size(0), self.hidden_size).to(x.device)
c0 = torch.zeros(self.num_layers, x.size(0), self.hidden_size).to(x.device)
out, (hn, cn) = self.lstm(x, (h0, c0))
return out, (hn, cn)

class DecoderLSTM(nn.Module):
def __init__(self, hidden_size, output_size, num_layers):
super(DecoderLSTM, self).__init__()
self.hidden_size = hidden_size
self.num_layers = num_layers
self.lstm = nn.LSTM(hidden_size, hidden_size, num_layers, batch_first=True)
self.fc = nn.Linear(hidden_size, output_size)

def forward(self, x, hidden):
out, (hn, cn) = self.lstm(x, hidden)
out = self.fc(out[:, -1, :])
return out, (hn, cn)

在上述代码中:

  1. EncoderLSTM类定义了编码器部分,其输入是一个序列,输出是LSTM的隐藏状态和记忆单元状态。
  2. DecoderLSTM类定义了解码器部分,其输入是编码器输出的隐藏状态和记忆单元状态,输出是最终的预测结果。
  3. 在实际应用中,编码器和解码器可以通过注意力机制进行连接,以进一步提升模型的性能。

一些小问题

问题1:LSTM在处理长序列时仍然会面临哪些挑战?

LSTM在处理非常长的序列时,尽管缓解了梯度消失问题,但仍然可能出现计算复杂度高、训练时间长等问题。此外,LSTM依赖于顺序计算,难以并行化,这在处理大规模数据时是一个瓶颈。

问题2:如何提升LSTM在处理不平衡数据集时的性能?

在处理不平衡数据集时,可以使用采样技术(如上采样和下采样)或代价敏感学习(如调整损失函数权重)来提升LSTM的性能。此外,集成方法(如Bagging和Boosting)也可以有效地应对不平衡问题。

问题3:LSTM与GRU(门控循环单元)的主要区别是什么?

GRU(Gated Recurrent Unit)是LSTM的简化版本,只有两个门(重置门和更新门),没有单独的记忆单元。相比LSTM,GRU的计算效率更高,但在某些任务上,LSTM的表现可能更好。因此,选择使用哪种模型需要根据具体任务进行实验验证。

问题4:在模型训练过程中,如何避免LSTM的过拟合问题?

避免过拟合的方法包括使用正则化技术(如L2正则化和Dropout)、数据增强(如时间序列数据的滑动窗口技术)和早停法(Early Stopping)。此外,通过交叉验证来选择合适的超参数也可以有效避免过拟合。

  • Post title:时间序列模型(二)—— LSTM
  • Post author:Chen Kai
  • Create time:2022-06-13 12:00:00
  • Post link:https://www.chenk.top/时间序列模型(二)—— LSTM/
  • Copyright Notice:All articles in this blog are licensed under BY-NC-SA unless stating additionally.
 Comments