对NPL有所了解的友友们肯定对BI-LSTM非常熟悉,而这篇博客将从我这一个纯小白的视角试着为大家经行解析,若有纰漏错误的地方敬请指正!!!
首先我们知道,LSTM是RNN的变形,是循环神经网络的形式之一,主要解决传统RNN记忆问题,距离较远的文本信息无法得到利用、距离较近但语义上关联不大的文本信息造成了太大的干扰。(像是一个健忘的大学生,记不住太久远之前的信息,所以就出现了LSTM,让他只记住有用的信息)
而传统RNN的问题分析:比如输入文本是我今天要上早八,首先要定闹钟,然后…,最后一道按时到达教室.这个时候我们要让RNN来判断我们到底有没有准时到达,RNN可能给出没有这个答案,然后就反向的去推导误差,在循环神经网络中,由于模型每个时刻的状态都是由之前的状态的乘积得来的,那么根据链式法则,如果每一层神经元对上一层的输出的偏导乘上权重结果都小于 1,则在经过足够多的传播之后,误差对输入层的梯度会趋近于 0,即梯度消失;反之,则梯度会随着反向传播层数的增加而呈指数增长,导致梯度爆炸(梯度爆炸可以用剪裁来解决)
这里我们再来了解一下梯度爆炸和梯度消失产生的原因:从深层网络角度来讲,不同的层学习的速度差异很大,表现为网络中靠近输出的层学习的情况很好,靠近输入的层学习的很慢,有时甚至训练了很久,前几层的权值和刚开始随机初始化的值差不多。因此,梯度消失、爆炸,其根本原因在于反向传播训练法则,只要你使用这个方法去求解问题,就会遇到梯度爆炸和消失。
为了解决上面出现的梯度消失的问题,就出现了LSTM(长短期记忆)
那么具体的LSTM模型是怎么实现的呢?
相对于传统的RNN,LSTM的输入由x(t)[输入数据],a(t-1)[隐藏层信息]两个数据变成了x(t)[输入数据],a(t-1)[隐藏信息],c(t-1)[记忆单元],不仅仅是参数数量上的变化,还有就是a(t-1)的求法也不一样
Bi-LSTM的产生
在单向的循环神经网络中,模型实际上只使用到了“上文”的信息,而没有考虑到“下文”的信息。在实际场景中,预测可能需要使用到整个输入序列的信息。因此,目前业内主流使用的都是双向的循环神经网络。顾名思义,双向循环神经网络结合了序列起点移动的一个循环神经网络和令一个从序列末尾向序列起点移动的循环神经网络。而作为循环神经网络的一种拓展,LSTM 自然也可以结合一个逆向的序列,组成双向长短时记忆网络
(Bi-directional Long Short-Term Memory, Bi-LSTM)。Bi-LSTM 目前在自然语言处理领域的应用场景很多,并且都取得了不错的效果。
双向长短时记忆(Bidirectional Long ShortTerm Memory,BLSTM)在LSTM的基础上,结合了输入序列在前向和后向两个方向上的信息。对于t时刻的输出,前向LSTM层具有输入序列中t时刻以及之前时刻的信息,而后向LSTM层中具有输入序列中t时刻以及之后时刻的信息。前向LSTM层t时刻的输出记作 ,后向LSTM层t时刻的输出结果记作 ,两个LSTM层输出的向量可以使用相加、平均值或连接等方式进行处理
以下是一个实例代码,用来求解航班信息的双向长短时记忆:
# 导入所需的库 import torch import torch.nn as nn import seaborn as sns import numpy as np import pandas as pd import matplotlib.pyplot as plt # 加载航班数据集 flight_data = sns.load_dataset("flights") # 只使用乘客列 all_data = flight_data['passengers'].values.astype(float) # 划分训练集和测试集 test_data_size = 12 train_data = all_data[:-test_data_size] test_data = all_data[-test_data_size:] # 定义数据标准化的函数 def normalize_data(data): # 计算最大值和最小值 max_value = np.max(data) min_value = np.min(data) # 返回归一化后的数据和缩放因子 return (data - min_value) / (max_value - min_value), max_value - min_value, min_value # 标准化训练集和测试集 train_data_normalized, scale_factor, min_value = normalize_data(train_data) test_data_normalized, _, _ = normalize_data(test_data) # 定义数据转换为张量序列的函数 def create_sequences(data, seq_length): # 创建一个空列表 sequences = [] # 遍历数据,每次取seq_length个元素作为一个序列 for i in range(len(data) - seq_length): # 将序列转换为张量并添加到列表中 sequences.append(torch.FloatTensor(data[i:i+seq_length])) # 返回张量序列列表 return sequences # 设置序列长度为12 seq_length = 12 # 将训练集和测试集转换为张量序列 train_sequences = create_sequences(train_data_normalized, seq_length) test_sequences = create_sequences(test_data_normalized, seq_length) # 定义双向LSTM的模型类 class BiLSTM(nn.Module): def __init__(self, input_size, hidden_size, num_layers, output_size, batch_size): super().__init__() self.input_size = input_size # 输入维度 self.hidden_size = hidden_size # 隐藏层维度 self.num_layers = num_layers # LSTM层数 self.output_size = output_size # 输出维度 self.num_directions = 2 # 双向 self.batch_size = batch_size # 批量大小 # 定义LSTM层,设置batch_first=True,输入和输出的形状为(batch_size, seq_len, input_size/hidden_size) self.lstm = nn.LSTM(self.input_size, self.hidden_size, self.num_layers, batch_first=True, bidirectional=True) # 定义线性层,将双向的隐藏层输出映射到输出维度 self.linear = nn.Linear(self.num_directions * self.hidden_size, self.output_size) def forward(self, input_seq): # 初始化隐状态和细胞状态,形状为(num_directions * num_layers, batch_size, hidden_size) h_0 = torch.randn(self.num_directions * self.num_layers, self.batch_size, self.hidden_size) c_0 = torch.randn(self.num_directions * self.num_layers, self.batch_size, self.hidden_size) # input_seq的形状为(batch_size, seq_len, input_size) # output的形状为(batch_size, seq_len, num_directions * hidden_size) # h_n和c_n的形状为(num_directions * num_layers, batch_size, hidden_size) output, (h_n, c_n) = self.lstm(input_seq, (h_0, c_0)) # 将output输入到线性层,得到预测值 # pred的形状为(batch_size, seq_len, output_size) pred = self.linear(output) # 只返回最后一个时刻的预测值 # pred的形状为(batch_size, output_size) pred = pred[:, -1, :] return pred # 设置模型参数 input_size = 1 # 输入维度为1,即每个时刻只有一个特征值 hidden_size = 32 # 隐藏层维度为32 num_layers = 2 # LSTM层数为2 output_size = 1 # 输出维度为1,即每个时刻只预测一个值 batch_size = 1 # 批量大小为1,即每次只输入一个序列 # 创建模型实例 model = BiLSTM(input_size, hidden_size, num_layers, output_size, batch_size) # 打印模型结构 print(model) # 定义损失函数为均方误差 loss_function = nn.MSELoss() # 定义优化器为Adam optimizer = torch.optim.Adam(model.parameters(), lr=0.001) # 设置训练轮数 num_epochs = 100 # 训练模型 for epoch in range(num_epochs): # 遍历训练集中的每个序列 for seq in train_sequences: # 将序列调整为(batch_size, seq_len, input_size)的形状 seq = seq.reshape(1, seq_length, input_size) # 前向传播,得到预测值 pred = model(seq) # 计算真实值,即序列的最后一个元素 true = seq[:, -1, :] # 计算损失 loss = loss_function(pred, true) # 反向传播,更新梯度 optimizer.zero_grad() loss.backward() optimizer.step() # 每10轮打印一次损失值 if (epoch + 1) % 10 == 0: print(f"Epoch {epoch+1}, Loss: {loss.item():.4f}") # 预测测试集 predictions = [] # 遍历测试集中的每个序列 for seq in test_sequences: # 将序列调整为(batch_size, seq_len, input_size)的形状 seq = seq.reshape(1, seq_length, input_size) # 前向传播,得到预测值 pred = model(seq) # 将预测值添加到列表中 predictions.append(pred.item()) # 定义数据反标准化的函数 def inverse_normalize_data(data, scale_factor, min_value): # 返回反标准化后的数据 return data * scale_factor + min_value # 反标准化预测值和真实值 predictions = inverse_normalize_data(np.array(predictions), scale_factor, min_value) true_values = test_data # 绘制预测值和真实值的折线图 plt.plot(predictions, label='Predictions') plt.plot(true_values, label='True Values') plt.legend() plt.show()