当前位置:   article > 正文

LSTM入门例子:根据前9年的数据预测后3年的客流(PyTorch实现)_lstm 数据预测 网站

lstm 数据预测 网站

原文地址:https://zhuanlan.zhihu.com/p/94757947。
(为什么网上有文章我要转载?因为公司的电脑有时候限制某些网站,我怕某些时候看不到!)


在这里插入图片描述

LSTM入门例子:根据前9年的数据预测后3年的客流(PyTorch实现)
LSTM入门例子:根据前9年的数据预测后3年的客流(PyTorch实现)
曾伊言
曾伊言
强化学习 ElegantRL
373 人赞同了该文章
网络上流传广泛的LSTM结构图:
在这里插入图片描述

LSTM结构图,我将Concatenate操作涂上橙色,标上图注,标上了隐藏状态ht/ct,标上了三个gate
在这里插入图片描述

GRU结构图,在第二行,我将h_{t-1} 与 x_{t} 的concatenate 结果写在了一起,消除重叠,简化图示
以上两张图片的出处 ↓,入门可看

Understanding LSTM Networks. 2015-08. colah’s blog. 介绍了LSTM与其变种GRU
Understanding LSTM Networks 的中文翻译:Evan:深入理解lstm及其变种gru
与Recurrent Neural Networks (RNN) 相关的论文有:

LSTM 原论文:Long Short Term Memory networks. 1997.
GRU 原论文:Gated Recurrent Unit. 2014.
建议阅读介绍LSTM的文章或论文后,再运行下面的入门Demo。

目录:
LSTM入门例子:根据前9年的数据预测后3年的客流(PyTorch实现)

前言:关于序列预测
数据:使用的数据较少,方便快速学习
模型:以LSTM为例
训练:运行时间仅需几分钟
评估
回复评论
更新日志:
2019-12-04 初版,更新目录
2019-12-05 根据评论、私信,对个别语句进行修改
2020-02-12 回复评论关于 验证集、客流预测的问题
2020-02-18 回复评论区关于 hidden state (output_y_hc)的问题
2020-07-03 根据评论区修改文中错误,重写部分语句,使其简洁

  1. 前言:关于序列预测
    有些“RNN入门例子”需要下载超过100MB的训练数据,不利于快速学习,因此我在这里提供一个轻量LSTM入门例子:使用LSTM利用前9年的客流预测后3年。这个例子将在几KB的数据上,在一分钟内完成对LSTM的训练,以此增强对LSTM的理解。

我们使用的数据是来自 Github 《深度学习入门之PyTorch》第五章RNN, 时间序列,里面也提供了基于LSTM的短期序列预测Demo(只能预测未来一个时间间隔),而本文的例子是长期序列预测(能预测未来一整段时间)。感谢
@快乐的是我呀
指出我的错误。

下图是我的结果,它训练LSTM根据前9年的数据预测后3年的客流。完整的代码见Github Yonv1943,只需要运行 Demo_RNN_time_seq_predict.py 这一个文件,数据和代码都在里面,若觉得有用就给星星吧。

在这里插入图片描述

纵坐标是客流数量(做了归一化处理),横坐标是第几个月(1949-01~1960-12,一共12年,144个月)蓝色是真实结果,红色是预测结果。
2. 数据
我直接把数据放在代码里。这组数据是首都国际机场1949~1960 这的客流量,共144个月的数据。前9年的数据(75%)作为训练集,后3年的数据(25%)作为测试集。显然,客流量的变化周期是12个月,因此我为其加上年、月这两个维度的标记。

@快乐的是我呀
指出:需要设置验证集 validation
按规范,我们需要像对待其他机器学习任务一样,为RNN设置训练集:验证集:测试集(6:2:2)。因为此数据集数据过少,且为了教程简洁,因此我没有设置验证集。本教程的训练次数 training step 是我随便定的。
def load_data():
# passengers number of international airline , 1949-01 ~ 1960-12 per month
seq_number = np.array(
[112., 118., 132., 129., 121., 135., 148., 148., 136., 119., 104.,
118., 115., 126., 141., 135., 125., 149., 170., 170., 158., 133.,
114., 140., 145., 150., 178., 163., 172., 178., 199., 199., 184.,
162., 146., 166., 171., 180., 193., 181., 183., 218., 230., 242.,
209., 191., 172., 194., 196., 196., 236., 235., 229., 243., 264.,
272., 237., 211., 180., 201., 204., 188., 235., 227., 234., 264.,
302., 293., 259., 229., 203., 229., 242., 233., 267., 269., 270.,
315., 364., 347., 312., 274., 237., 278., 284., 277., 317., 313.,
318., 374., 413., 405., 355., 306., 271., 306., 315., 301., 356.,
348., 355., 422., 465., 467., 404., 347., 305., 336., 340., 318.,
362., 348., 363., 435., 491., 505., 404., 359., 310., 337., 360.,
342., 406., 396., 420., 472., 548., 559., 463., 407., 362., 405.,
417., 391., 419., 461., 472., 535., 622., 606., 508., 461., 390.,
432.], dtype=np.float32)
# assert seq_number.shape == (144, )
# plt.plot(seq_number)
# plt.ion()
# plt.pause(1)
seq_number = seq_number[:, np.newaxis]

# print(repr(seq))
# 1949~1960, 12 years, 12*12==144 month
seq_year = np.arange(12)
seq_month = np.arange(12)
seq_year_month = np.transpose(
    [np.repeat(seq_year, len(seq_month)),
     np.tile(seq_month, len(seq_year))],
)  # Cartesian Product

seq = np.concatenate((seq_number, seq_year_month), axis=1)

# normalization
seq = (seq - seq.mean(axis=0)) / seq.std(axis=0)
return seq
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

因此总的数据的维度是 (144, 3),即144个月的【客流量,年份,月份】这个3维度的数据。我对数据进行了归一化处理(减去平均值,除以标准差)。

@王后浪
他指出直接对全部数据进行归一化处理是不正确的,应该改为对用于训练的前9个月数据进行归一化处理。
我非常认同他的观点,归一化的时候不应该把测试用的数据也包括进去。我为了教程简洁而没有做最规范的做法。
3. 模型
我们希望输入前9年的客流数据,让LSTM预测后3年的客流,那么我们可以先在前9年的数据上,训练LSTM根据前几个月的数据预测下一个月的客流。等训练完成后,我们让LSTM根据前9年的数据预测出下一个月的客流,把刚刚输出的预测客流作为输入,迭代求得后3年的客流。

请注意,虽然通常情况下张量的第一个维度是批次大小batch size,但是PyTorch建议我们输入循环网络的时候张量的第一个维度是序列长度,而第二个维度才是批次数量。

那么输入此LSTM的 input.size() == (seq_len, batch_size, inp_dim)

在我们的LSTM时间序列预测任务中:
seq_len 时间序列的长度,在这里前9年共 9*12 == 108 个月,则 seq_len == 108
batch_size 同个批次中输入的序列条数
inp_dim 输入数据的维度,在这里输入数据由【客流量,年份,月份】三组数据构成,则 inp_dim == 3

如果是自然语言处理 (NLP) ,那么:
seq_len 将对应句子的长度
batch_size 同个批次中输入的句子数量
inp_dim 句子中用来表示每个单词(中文分词)的矢量维度
接下来根据输入,我们可以确定LSTM的参数:

rnn = nn.LSTM(inp_dim, mid_dim, num_layers)

inp_dim 是LSTM输入张量的维度,我们已经根据我们的数据确定了这个值是3

mid_dim 是LSTM三个门 (gate) 的网络宽度,也是LSTM输出张量的维度

num_layers 是使用两个LSTM对数据进行预测,然后将他们的输出堆叠起来。

input = torch.randn(seq_len, batch_size, inp_dim)
output = rnn(input)
assert output.size() == (seq_len, batch_size, mid_dim)
num_layers 一般设为1或2, PyTorch自己的解释:E.g. setting num_layers=2would mean stacking two LSTMs together to form a stacked LSTM, with the second LSTM taking in outputs of the first LSTM and computing the final results. 感谢
@胡慕雲
指出我的错误
为了进行时间序列预测,我们在LSTM后面接上两层全连接层(1层亦可),同时改变最终输出张量的维度,我们只需要预测客流量这一个值,因此out_dim 为1。在LSTM后方的全连接层也可以看做是一个回归操作 regression。

在LSTM后面接上两层全连接层,为何是两层: 理论上足够宽,并且至少存在一层具有任何一种“挤压”性质的激活函数的两层全连接层就能拟合任何连续函数。最先提出这个理论证明的是 Barron et al., 1993,使用了UAT (Universal Approximation Theorem),指出了可以在compact domain拟合任意多项式函数。”
实际上对于过于复杂的连续函数,这个「足够宽」不容易满足。并且拟合训练数据并让神经网络具备足够的泛化性的前提是:良好的训练方法(比如批次训练数据满足 独立同分布 (i.i.d.),良好的损失函数,满足Lipschitz连续 etc.),具体内容可以看:(引号内容为摘抄 数学爱好者在知乎上的回答 )“
reg = nn.Sequential(
nn.Linear(mid_dim, mid_dim),
nn.Tanh(),
nn.Linear(mid_dim, out_dim),
) # regression

input = output_of_LSTM
seq_len, batch_size, mid_dim = input.shape
input = input.view(seq_len * batch_size, mid_dim)
output = reg(input)
output = output.view(seq_len, batch_size, out_dim)
4. 训练
同一批次中序列长度不同,需要使用 from torch.nn.utils.rnn import pad_sequence

我们只有一组训练数据,即前9年的客流量。我们可以在同一批次中,训练LSTM预测不同月份的客流量。1~t月的输入对应了t+1月的客流量。由于同一批次里面训练序列长度不统一,直接在末尾补0的操作不优雅,所以我们需要借助torch 自带的工具 pad_sequence的协助,具体如下:

var_x = torch.tensor(train_x, dtype=torch.float32, device=device)
var_y = torch.tensor(train_y, dtype=torch.float32, device=device)

batch_var_x = list()
batch_var_y = list()

for i in range(batch_size):
j = train_size - i
batch_var_x.append(var_x[j:]) # 在训练中作为输入的客流量数据(年份、月份、本月的客流量)
batch_var_y.append(var_y[j:]) # 在训练中作为标签的预测数据(下个月的客流量)

from torch.nn.utils.rnn import pad_sequence
batch_var_x = pad_sequence(batch_var_x)
batch_var_y = pad_sequence(batch_var_y)
放入pad_sequence 的序列必须从长到短放置,随着反向传播的进行,PyTorch 会逐步忽略完成梯度计算的短序列。具体解释请看PyTorch官网。

「构建用于训练的序列」:避免输入相同起始裁剪位点的序列用于训练(这部分内容写用于解答评论区的疑惑,差不多有三分之一的评论都需要看这部分内容,以下内容写于 2020-10-01,人太多就不一一
@飞天小女警

在本文提供的例子之中,我们的全部数据只是一条长度为144的短序列,因此我们要最大化地利用这条序列。我们作为人类,无论是否看到完整的序列,我们都可以对序列进行预测。因此,为了尽可能地利用手上的数据,我们可以对序列进行裁剪。(在图像的训练中也有类似的做法:我对图像进行轻微的旋转、裁剪去做数据增广用于增加训练数据)。
在这里插入图片描述

在RNN(LSTM)中,对循环网络的训练与对普通深度神经网络的训练是不同的,因此我们不能随意地裁剪序列用于RNN的训练。上图我们随便截取 1949到1950的数据输入RNN用于训练,这相当于让RNN在“以1949.01月为开头的所有序列”上都进行了训练。(疑惑不解的人可以回到文章开头看我推荐的RNN论文与博客)。所以我们要在不同的起始位点进行裁剪来为RNN提供「没有训练过的材料」。下面这一段代码就裁剪出了足够的多的序列用于训练。

batch_size = 48
train_size = int(len(data_x) * 0.75)

for i in range(batch_size):
j = train_size - i # 不同的起始裁剪位点
batch_var_x.append(var_x[j:]) # 在训练中作为输入的客流量数据(年份、月份、本月的客流量)
batch_var_y.append(var_y[j:]) # 在训练中作为标签的预测数据(下个月的客流量)

由于评论区中有人误以为此处只用了 48个月的数据所以我特地解释一下

这里构建了48条序列 batch_size = 48,都放在同一个batch里

这里是48条序列var_x[j:],而不是48个序列var_x[j](重要)

这48条序列的起始裁剪位点都不一样(非常重要),因此在RNN内不会重复训练(非常重要)

请注意,输入多个拥有相同起始裁剪位点的序列给RNN用于训练是完全错误的训练方式。当你输入一个序列给RNN时,合格的深度学习框架中的RNN就已经帮你训练了拥有相同起始位点的所有序列,这个时候还额外地输入其他拥有相同起始裁剪位点的序列给RNN会导致RNN更快地过拟合。由于数据是这么地少,因此我们只能使用同一个batch进行训练(非常容易过拟合),但因为有这个数据构建方法,所以我们不需要训练太久,几秒钟就可以了。

batch_var_x.append(var_x[j:]) # 不同的起始裁剪位点,正确的裁剪方法
batch_var_x.append(var_x[:j]) # 相同的起始裁剪位点,完全错误的裁剪方法
接下来是测试时的问题:测试的时候,即便我们没有输入前9年的完整数据也是可以的,因为我们人类也可以看前9年、前6年、前3年的数据去预测后3年的情况。(当然,输入用于预测的数据越少,神经网络得到的信息越少,越不容易做出准确的预测)

「计算损失函数时,需要忽略序列开头的预测误差」

作为一个人类,我可以只根据下图 0~100 的曲线画出(预测)100以后的曲线。但是我无法只根据01的曲线对后面的值进行预测,甚至我也没有把握值根据12个值预测出下一个月的客流量。受到人类的启发,我认为我也不应该强求LSTM在输入序列长度不足的情况下进行预测,因此我为loss 设置了权重,输入序列的长度越短,其误差的权重就越小。

在这里插入图片描述

绿色的线就损失的权重,在0~1之间。这是一条 tanh 曲线。在x=0+处其斜率为 自然底数e 除以train_size
criterion = nn.MSELoss() # L2_loss

with torch.no_grad():
weights = np.tanh(np.arange(len(train_y)) * (np.e / len(train_y)))
weights = torch.tensor(weights, dtype=torch.float32, device=device)

for e in range(384):
out = net(batch_var_x)

# loss = criterion(out, batch_var_y)
loss = (out - batch_var_y) ** 2 * weights
loss = loss.mean()

optimizer.zero_grad()
loss.backward()
optimizer.step()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  1. 评估
    使用前9年的数据作为输入,预测得到下一个月的客流,并将此预测结果加到输入序列中,从而逐步预测后3年的客流。如同修路:踏上在刚铺好的路面上并继续往前铺路。

请注意,预测的时候使用 net.output_y_hc(self, x, hc)。它保存hidden state 用于迭代预测,比较优雅。而每一次都重新输入整条序列进行预测的方法不够优雅,因为它随着预测年份的增加,计算复杂度呈O(n**2)增长。

5.1 完整代码里面REGLSTM这个类里面定义的成员函数output_y_hc,有什么作用?(答复评论区的@杨晓明)

我们需要保存LSTM的隐藏状态(hidden state),用于恢复序列中断后的计算。举例子,我有完整的序列 seq12345:

我输入seq12345 到LSTM后,我能得到6,即seq123456。
我也可以先输入 seq123 以及默认的隐藏状态hc,得到4和新的hc。然后我接着把 seq45 以及 hc一起输入到LSTM,我也能得到6,即seq123456。
(hc 指 h和c,是两个张量,本文开头的LSTM结构图注明了何为 h与c)

test_x = data_x.copy() # data_x 是9+3年的数据
test_x[train_size:, 0] = 0 # 我们真的只拿出了前9年的数据

‘’‘simple way but no elegant’‘’

for i in range(train_size, len(data) - 2):

test_y = net(test_x[:i])

test_x[i, 0, 0] = test_y[-1]

‘’‘elegant way but slightly complicated’‘’
eval_size = 1
zero_ten = torch.zeros((mid_layers, eval_size, mid_dim), dtype=torch.float32, device=device)
test_y, hc = net.output_y_hc(test_x[:train_size], (zero_ten, zero_ten))
test_x[train_size + 1, 0, 0] = test_y[-1]
for i in range(train_size + 1, len(data) - 2):
test_y, hc = net.output_y_hc(test_x[i:i + 1], hc)
test_x[i + 1, 0, 0] = test_y[-1]

The ‘text_x[1:, 0, 0]’ is our prediction.

pred_y = test_x[1:, 0, 0]
pred_y = pred_y.cpu().data.numpy()

Draw it.

plt.plot(pred_y, ‘r’, label=‘pred’)
完整的代码见Github Yonv1943,只需要运行 Demo_RNN_time_seq_predict.py 这一个文件,数据和代码都在里面。若觉得有用就给星星吧。

如果有任何疑问/质疑,欢迎评论,不要私信。好的讨论才能让所有人看到,并且我不需要重复回答。
6. 回复评论区与私信
《深度学习入门之PyTorch》第五章的Demo是短期预测,适合入门
在这里插入图片描述

蓝线是真实值,红线是预测值,图片来自《深度学习入门之PyTorch》第五章RNN:时间序列 https://github.com/L1aoXingyu/code-of-learn-deep-learning-with-pytorch/blob/master/chapter5_RNN/time-series/lstm-time-series.ipynb
这里例子的问题在于:它使用前两个月的客流序列的真实值作为输入,并得到下一个月客流的预测值。这种操作有两个问题:

这意味着我们只能使用前9年的数据预测后一个月的客流,如果缺少后3年的客流数据,那么这个方法就不能预测后三年的客流,照这个方法训练出来的模型有什么用?即便他们只是想要预测后一个月的客流,那么正确的做法也应该是每个月都根据当月得到的最新数据重新对RNN进行训练。
使用RNN的序列预测模型的输入是一个序列,构建“每个月的历史客流序列”就可以了,没有必要构建“每两个月的历史客流序列”,重复地给RNN前两个月的数据并不会对性能有任何提高。
本文是长期预测,是稍微进阶一点的
@川南雁
评论能帮助很多人,感谢,我搬到正文方便查看:

建议pytorch都没学过的萌新,用作者引用的原来的教程就好了。。作者这篇可以算是稍微进阶一点了。。给萌新的意见:
一定要注意作者只使用了一个batch,只使用了数据集的前48个数据,所以如果你照抄程序,数据集大一点就肯定不行了。比如我2000个数据,只用前48个训练(本文作者:我并没有只使用前48个数据去训练,参见本页面的「构建用于训练的序列」),最后测试集惨不忍睹。
基本上网上搜到的其他所有pytorch的lstm入门教程都是定长的timestep,而作者使用的是不定长的timestep,然后pad sequence成定长的timestep来训练(因为pytorch要求timestep必须定长)。这个真的比较进阶了。。对萌新(我)来说理解稍微困难一点。好处就是学会了pad sequence。
最后再解释一下为什么作者引用的原教程“使用12年的数据来预测12年,而并不是用前9年预测后3年”。其实是短时预测和长时预测的区别。作者引用的原教程是一种短时预测,目的只是为了预测下一个月的数据。而本文章的预测是一种长时预测,使用前9年来预测后3年。
@快乐的是我呀
指出:这个客流量的原计划是不是,用前9年的做训练数据,后3年的做测试数据,这个模型本身要训练的就是两个月对一个月的预测能力呢?
不是。它在训练的时候,输入了第1~n-1个月的数据,训练它输出第n个月的客流量,而不是只输入了前两个月的流量 (n-2~n-1)。《深度学习入门之PyTorch》第五章RNN:时间序列,Github的相关注释代码中,存在着误导性极强的一句话,如下:

我们想通过前面几个月的流量来预测当月的流量,比如我们希望通过前两个月的流量来预测当月的流量,我们可以将前两个月的流量当做输入,当月的流量当做输出。
月份: 1, 2, 3, … , n-2, n-1

原本正常的方法:
输入 (1, ), (2, ), (3, ), … , (n-2), (n-1), 输出 n

他们使用的方法:
输入 (1, 2), (2, 3), … , (n-2, n-1), 输出 n
可以看到,在他们“输入前两个月”的方法中,数据被重复地输入RNN。
并且这种方法不能描述成具有误导性的“将前两个月的流量当做输入”
而应该被描述成“选取相邻的两个月的流量按月份组合成一个序列当做输入”

他们使用的代码如下:

https://github.com/L1aoXingyu/code-of-learn-deep-learning-with-pytorch/blob/master/chapter5_RNN/time-series/lstm-time-series.ipynb

这段代码来自 《深度学习入门之PyTorch》第五章RNN, 时间序列 ↑,不是我写的

data_X = data_X.reshape(-1, 1, 2) # 这里直接输入了前9年与后3年的客流序列的真实值,这是不恰当的
data_X = torch.from_numpy(data_X)
var_data = Variable(data_X)
pred_test = net(var_data)
下面的折线图,绿色线条是使用短期预测的代码“强行”进行长期预测的结果。
在这里插入图片描述

蓝色的线是分割线,此线左边是训练数据,右边是预测数据。
回复评论区
@什么都不懂

在数据处理部分时候是否可以借鉴原例子(《深度学习入门之PyTorch》第五章RNN的Demo)中的处理方式 即将输入输出错位 构建出很多个时间序列 例如第一行是1~7个月的 第二行是2~8个月的 例如可以构造出100行数据 因为序列数目越多 网络的泛化能力越好 我如何尽可能的利用这些数据进行训练? 此时的输入为[100,7] 所以seqlen = 7 batch_size =100?
不能。原例子中“将输入输出错位 构建出很多个时间序列”的方法在RNN中是累赘的,这不会给模型带来提升(不会提升预测精度,更不会提升泛化性)。会增加模型运行时间(需要计算的东西变多了)。

编辑于 2021-04-23 22:46
LSTM
预测模型
PyTorch

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/笔触狂放9/article/detail/978302
推荐阅读
相关标签
  

闽ICP备14008679号