BiLSTM之二:工程应用须知
现在有很多成熟的深度学习框架集成了BiLSTM模型,但想使用它们并非没有门槛,至少要对说明文档中的参数的释义有充分的理解。我之前写过一篇介绍BiLSTM的文章(以下用「上一篇」来指代该文章),其侧重于模型的内部结构而非工程实现,作为对该文章的补充,本文以计算BiLSTM的参数数量为切入点,再深入理解一下模型的工程实现。
建议不熟悉BiLSTM的读者在阅读本文之前先阅读上一篇文章,本文的公式及符号与该文章保持一致。
1、paddlepaddle
中的LSTM模型
为叙述方便,我们将上一篇文章中的一个LSTM cell的内部结构图粘贴到此:
cell可以被翻译为“神经元”,但在LSTM的场景中容易让人误解,因为一层的LSTM模型只有一个“神经元”——这看上去似乎不够「深度学习」,所以我将不再翻译,并直接使用cell来指代这个结构。请读者仔细思考全连接神经网络中的“神经元”的概念以及此处这个cell的概念之间的区别。
观察这个cell结构,我们不难发现,整个计算中存在维度变化的地方只在于at−1a_{t-1}at−1与XtX_tXt的合并向量与权重相乘时。
在飞浆中,通过调用paddle.nn.LSTM
类就可以实现LSTM的搭建。提醒读者注意,类的实例化是搭建神经网络结构的过程;而真正的前向计算,是通过调用该实例的forward()
方法实现的。
首先,导入基础模块:
from paddle import nn
然后,实例化一个LSTM
对象:
lstm = nn.LSTM(input_size=3, hidden_size=9)
上一行代码初始化了一个LSTM
实例。对于LSTM
类而言,只有前两个参数是必须的,即input_size
和hidden_size
,这里赋值分别为3
和9
。input_size
指的是cell新输入的数据的维度,即XtX_tXt的维度。hidden_size
指的是XtX_tXt(其实还有at−1a_{t-1}at−1,但它并不影响维度)经过与权重做矩阵乘法后的输出维度,这个维度完全由权重矩阵的形状决定,因此被称为“隐藏层大小”。
介绍完关键参数之后,接下来看一下参数数量是如何计算的。
2、参数的数量
首先,不考虑at−1a_{t-1}at−1和Ct−1C_{t-1}Ct−1的输入时,给定一个(3,1)(3,1)(3,1)的输入向量,那么会得到一个(9,1)(9,1)(9,1)的输出向量,其计算过程如下:
这里以「输出门」为例,直观地展现了计算过程中的维度变化情况。其他的「输入门」、「遗忘门」以及与WWW的计算过程都是类似的。也就是说,在不考虑at−1a_{t-1}at−1和Ct−1C_{t-1}Ct−1的情况下,所需要的参数总量一共是:
(3∗9+9)∗4=144(3*9+9)*4=144(3∗9+9)∗4=144
在LSTM中,at−1a_{t-1}at−1和Ct−1C_{t-1}Ct−1与oto_tot具有相同的维度,在这里为9。因此,at−1a_{t-1}at−1与XtX_tXt同时参与运算的过程如下:
此时的参数数量一共为:
(9∗9+9∗1+9∗3+9∗1)∗4=504(9*9+9*1+9*3+9*1)*4=504(9∗9+9∗1+9∗3+9∗1)∗4=504
至此,我们已经得到了单层的LSTM的参数数量的计算公式:
如果输入向量的维度为nnn,隐藏层维度为mmm,则参数总量为4(m2+2m+mn)4(m^2+2m+mn)4(m2+2m+mn)。
3、BiLSTM
如果想要构造BiLSTM,则可以在实例化LSTM
类时指定direction
参数:
lstm = nn.LSTM(input_size=3, hidden_size=9, direction='bidirect')
其参数数量是相同设置的LSTM的参数数量的2倍。
不同的深度学习框架基于LSTM构造BiLSTM的方法略有不同。
4、一个回归的例子
4.1 基础版本
我们不涉及任何具体的业务,也不涉及数据预处理过程,只讨论如何基于飞浆建立一个BiLSTM回归模型。
4.1.1 原始数据
首先让我们生成数据:
import numpy as np
import pandas as pd
import paddle
from paddle import nnnp.random.seed(1234)data = np.random.random(size=(10000, 8))
df = pd.DataFrame(data, columns=[f'x{i}' for i in range(1, 8)] + ['y'])
print(df.head())
由于指定了随机数种子,输出一定是下面的内容:
x1 x2 x3 ... x6 x7 y
0 0.191519 0.622109 0.437728 ... 0.272593 0.276464 0.801872
1 0.958139 0.875933 0.357817 ... 0.712702 0.370251 0.561196
2 0.503083 0.013768 0.772827 ... 0.615396 0.075381 0.368824
3 0.933140 0.651378 0.397203 ... 0.568099 0.869127 0.436173
4 0.802148 0.143767 0.704261 ... 0.924868 0.442141 0.909316
[5 rows x 8 columns]
现在,假设我们面临的是一个价格预测问题:y
是我们的目标列,表示价格;x1
到x7
为特征列;样本是按照时间顺序排列的。于是,我们的目标是建立一个基于BiLSTM的回归模型来对其进行预测。
4.1.2 模型修改
假设样本数据直接输入BiLSTM模型,那么它的输入大小为7,我们再定义其隐藏层的大小为32,于是,定义网络的代码为:
bilstm = LSTM(7, 32, direction="bidirect")
通过上面的分析可知,对于一个特定的样本(例如,第一行数据),利用bilstm
对其进行前向计算后输出分为三部分:输出o0o_0o0,长期记忆C0C_0C0以及短期记忆a0a_0a0;其中的C0C_0C0和a0a_0a0又将和下一个样本(第二行数据)一起再进行前向计算。
这里所有的下标与python保持一致,从0开始。
如果我们指定的时间步长为5,于是,模型将重复上述过程直到它遍历到第5个样本。这时,我们会得到一个输出o4o_4o4,并将它作为这一组样本所预测的输出。
但这个输出的维度是64,再与下一个时刻的真实价格y5y_5y5计算误差前,首先需要将其变为1维。这很简单,再接一个维度为(64,1)(64,1)(64,1)的全连接层即可。于是,我们计算y^\hat yy^与y5y_5y5的误差后,就可以将该误差反向传播并更新网络参数了。
为了将BiLSTM网络和全连接网络连接到一起,我们可以使用「组网」的方式。它的API是paddle.nn.Sequential
,基本用法是:
model = paddle.nn.Sequential(net1,net2,...
)
它的作用是很直观的:将不同的网络结构堆叠起来,前面网络的输出作为后续网络的输入,从而实现快速建模。
但使用这个API的时候要注意:前一个网络的输出的形状必须与后一个网络的输入的形状一致。我们知道,BiLSTM的输出有三部分:第一部分是y^\hat yy^,后面两部分存储在一个元组中,分别表示aaa和CCC。所以,为了能够正确地只将第一部分传入全连接网络,需要对基础的paddle提供的LSTM
类进行改写:
class MyLSTM(nn.LSTM):def __init(self, *args, **kwargs):# 实例化时与父类保持一致super().__init__(*args, **kwargs)def forward(self, inputs):# 调用父类的前向函数来计算,但只取返回结果的第一部分output, _ = super().forward(inputs)return output[:, -1, :] # 在第二个维度上只取最后一组值,其实就是获取最后一个时间步输出的y_hat
有的读者可能会产生一个疑问:这样修改前向运算的输出之后,第一个时间步的输出就少了aaa和CCC,那下一个时间步在运算时岂不是就无法捕获长短时的记忆了?
这里就需要解释一下LSTM的实现机制了。在paddle中,forward
其实是在计算完所有的时间步后才一次性输出的。也就是说,假设我们的时间窗口选的是5,那么forward
的第一个输出其实是包含了对应于这5个样本点的5个输出值,第二个输出的aaa和CCC只保留最终的状态,即各有一个值。
注意:
- 这里的「值」代表的是向量。
- 在真实的运算中还需要指定batch_size,这里默认为1,在讨论中省略,实际上即使为1也需要对在输入的第一个维度进行指定。
读者可以通过以下的代码来验证一下:
import paddle# 用上文的data来创建一个tensor,注意这里没有留y,所以input size是8
tiny_tensor = paddle.to_tensor(data[:5], dtype=np.float32)# 通过两次设置随机数种子,可以是lstm和mylstm的权重完全相同
paddle.seed(1234)
lstm = nn.LSTM(8, 32)
paddle.seed(1234)
mylstm = MyLSTM(8, 32)tiny_tensor = tiny_tensor.reshape(shape=(-1, 5, 8)) # 必须指定batch_size,这里自动计算
my_out = mylstm(tiny_tensor)
out, _ = lstm(tiny_tensor)
print(my_out == out[:, -1, :])
应该打印以下内容:
Tensor(shape=[1, 32], dtype=bool, place=CPUPlace, stop_gradient=False,[[True, True, True, True, True, True, True, True, True, True, True, True,True, True, True, True, True, True, True, True, True, True, True, True,True, True, True, True, True, True, True, True]])
4.1.3 模型组网
接下来就可以进行模型组网了,非常简单:
model = nn.Sequential(MyLSTM(7, 32, direction='bidirect', dropout=0.5),nn.Linear(64, 1)
)# 将模型封装
model = paddle.Model(model)# 定义优化器、损失函数
model.prepare(paddle.optimizer.RMSProp(0.0001, parameters=model.parameters()),paddle.nn.MSELoss())
在调用.fit()
方法进行训练之前,我们还需要对训练数据进行一些封装。
4.1.4 数据的处理
在训练开始之前,还需要对数据做如下处理:
- 按照指定的时间步长转化成「序列」的形式,划分训练集测试集;
- 封装成paddle可接收的数据格式。
对于第一部分,通过以下函数可以实现:
def create_sequence(df, window: int = 5):"""为了能够输入LSTM模型,将数据处理成序列的形式。假设输入的df一共有N行,那么处理后的数据的维度为:[N-window, window, 7]"""N = df.shape[0]ret = np.empty(shape=(N - window, window, 7))y = np.empty(N - window)for i in range(N - window):end = i + windowarr = df[['x1', 'x2', 'x3', 'x4', 'x5', 'x6', 'x7']].iloc[i: end].valuesret[i] = arry[i] = df['y'].iloc[i + window]return ret, yX, y = create_sequence(df)
接着,划分训练集与测试集:
# 按80/20的比例划分
train_size = int(train_X.shape[0] * 0.8)
test_size = train_X.shape[0] - train_size
train_X = X[:train_size]
train_y = y[:train_size]
test_X = X[-test_size:]
test_y = y[-test_size:]
对于第二部分,我们首先需要将数据转化为Tensor:
train_X_tensor = paddle.to_tensor(train_X, dtype=np.float32)
train_y_tensor = paddle.to_tensor(train_y, dtype=np.float32)
test_X_tensor = paddle.to_tensor(test_X, dtype=np.float32)
test_y_tensor = paddle.to_tensor(test_y, dtype=np.float32)
接下来,传给.fit()
的训练数据需要满足一定的格式,这里是通过继承Dataset
类来实现的。使用这种方法只需要重写Dataset
的__getitem__
和__len__
方法即可:
class MyDataset(paddle.io.Dataset):def __init__(self, dataset_type):self.dataset_type = dataset_typedef __getitem__(self, idx):if self.dataset_type == 'train': # 训练集的话,返回特征和标签return train_X_tensor[idx], train_y_tensor[idx]if self.dataset_type == 'test': # 测试集的话,返回特征return test_X_tensor[idx]def __len__(self):if self.dataset_type == 'train': return train_X_tensor.shape[0]if self.dataset_type == 'test':return test_X_tensor.shape[0]
最终,可以对模型进行训练了:
model.fit(MyDataset('train'), batch_size=32)
然后可以预测:
model.predict(MyDataset('test'))
当然,由于用的是随机数,结果不具备评价意义。
4.2 进阶版本
有时,作为输入的特征不止x1,...,x7
,还有历史价格。
换言之,模型由:
变成了:
这对于训练过程的影响倒是不大,只需要将create_sequence
函数对应的输入特征和输入维度增加,在模型组网时修改输入的维度即可。
但在预测时,问题变得有些麻烦。
我们在预测未来的多个时刻的价格时,需要逐时刻预测,并且将上一时刻的预测价格填充到下一时刻的输入特征中,因为我们没有上一时刻的真实价格数据。
目前,我没有找到paddle中关于处理这种情形的方案,因此,我对MyDataset
类和预测的代码做了一些修改:
class MyDataset(Dataset):def __init__(self, dataset_type, sub_tensor=None):self.dataset_type = dataset_typeself.sub_tensor = sub_tensordef __getitem__(self, idx):# 训练时不变,但在预测时,必须逐tensor进行预测,这样才能在传入# 模型前对输入的tensor进行价格填充if self.dataset_type == 'train':return train_X_tensor[idx], train_y_tensor[idx]if self.dataset_type == 'test':# 直接返回输入的tensorreturn self.sub_tensordef __len__(self):if self.dataset_type == 'train':return len(train_y_tensor)if self.dataset_type == 'test':# 对应的长度永远是1return 1
同时,预测的过程也做了修改,用一个定长的队列来存储预测过的价格:
from collections import dequelast_pred = deque(maxlen=5) # 最大长度为时间窗口长度,再之前的数据对于预测下一个时刻无用
pred_value = [] # 存储预测结果
for i in range(test_X_tensor.shape[0]):sub_tensor = test_X_tensor[i] # 获取待输入的tensorif last_pred: # 已经存在预测结果,则将该结果填充到tensor中num = len(last_pred)# paddle的tensor修改貌似比较复杂,我没深入研究,采取了个笨办法sub_array = sub_tensor.numpy()sub_array[-num:][:,-1] = list(last_pred)sub_tensor = paddle.to_tensor(sub_array)res = model.predict(MyDataset('test', sub_tensor))pred_value.append(res)last_pred.append(res) # 更新队列
如果有其他的解决方案,欢迎评论让我知道。
参考:
- LSTM and Bidirectional LSTM for Regression
- paddlepaddle的LSTM如何写到Sequential中
BiLSTM之二:工程应用须知相关推荐
- BiLSTM+CRF(二)命名实体识别
前言 前一篇博客[https://blog.csdn.net/jmh1996/article/details/83476061 BiLSTM+CRF (一)双向RNN 浅谈 ]里面,我们已经提到了如何 ...
- BILSTM原理介绍
BILSTM介绍 一.介绍 1.1 什么是LSTM和BILSTM? 1.2 为什么使用LSTM与BILSTM? 二.BILSTM原理简介 2.1 LSTM介绍 2.1.1 总体框架 2.1.2 详细介 ...
- bilstm-crf
LSTM+CRF 解析(原理篇) 超级详细手把手讲解BiLSTM+CRF完成命名实体识别(一) 超级详细手把手讲解BiLSTM+CRF完成命名实体识别(二) 超级详细手把手讲解BiLSTM+CRF完成 ...
- 命名实体识别新SOTA:改进Transformer模型
2019-11-27 05:02:16 作者 | 刘旺旺 编辑 | 唐里 TENER: Adapting Transformer Encoder for Name Entity Recognition ...
- 细粒度情感分析任务(ABSA)的最新进展
作者丨邴立东.李昕.李正.彭海韵.许璐 单位丨阿里巴巴达摩院等 任务简介 我们略过关于 sentiment analysis 重要性的铺陈,直接进入本文涉及的任务.先上例子,对于一句餐馆评论:&quo ...
- 多多视频如何快速涨粉(赚钱变现)
多多视频是一个很好的短视频平台,我也是2023年春节前后才开始在多多视频创作的,19号那天我的一个视频爆了16万多的播放量,一天涨了2000个粉丝,顿时有一种小有成就的感觉,另外多多视频现在的V计划是 ...
- matlab用jc法计算可靠度,基于MATLAB的截尾分布下JC法计算可靠度
摘 要: 在水工结构可靠度分析中,随机变量的分布形式常因几何尺寸.物理环境等条件限制,传统JC法已经不适用,因此需要对部分变量进行截尾分布处理.在此借助MATLAB丰富的函数资源,编制出截尾分布处理后 ...
- 【论文笔记】From the Detection of Toxic Spans in Online Discuss to the Analysis of Toxic-to-Civil Transfer
From the Detection of Toxic Spans in Online Discussions to the Analysis of Toxic-to-Civil Transfer 文 ...
- 细粒度情感三元组抽取任务及其最新进展
©作者 | 邴立东.彭海韵.许璐.谢耀赓 单位 | 阿里巴巴达摩院自然语言智能实验室 研究方向 | 自然语言处理 ABSA 和 ASTE 任务简介 情感分析作为自然语言理解里最重要也是最有挑战的主要任 ...
最新文章
- Oracle Sql Developer
- WinCE开机Logo的实现(USB下载图片到nandflash)
- Gartner 2015新兴技术发展周期简评:大数据实用化、机器学习崛起
- nginx的upstream模块安装
- shell python比较_shell中的条件判断以及与python中的对比
- 用Python绘制一套“会跳舞”的动态图形给你看看
- Laravel源码解析之ENV配置
- 如何给小朋友解释单摆运动_法国教育学者:如何培养儿童的逻辑思维和时间观念...
- C是一个结构化语言它的重点在于算法和数据结构
- python人工智能爬虫系列:怎么查看python版本_电脑计算机编程入门教程自学
- 共空间模式算法(CSP)
- aceAdmin框架依赖
- 避障车(L293D电机驱动)
- 渗透之代理小知识--
- windowsapps文件夹无法删除_Windows实战之快速安全删除Windows.old文件夹
- centos中startup.sh启动服务脚本
- SQL SERVER DAY函数
- java利用redis的setIfAbsent和incr,实现自增,限制总数
- druid连接池监控
- 极好的搜索引擎: Goolgle 本网站和www搜索插件
热门文章
- 浏览器请求服务器静态文件的实现
- Nginx安装rtmp模块及配置
- 面 试 题 葵 花 宝 典
- DESK CHECK,你做对了吗?
- HTML简单练习案例
- HTML实战案例素材1:制作树形菜单页面
- vb6 combo根据index显示_教育部青少年普法网站竞赛入口http://shttp://static.qspfw.com/xfweb/index.html...
- python读取微信消息_Python实现微信消息同步!
- 餐饮管理系统开源java_java课程设计餐饮管理系统
- Java项目:SSM餐厅点餐收银管理系统