赞
踩
时间序列预测-LSTNet模型
LSTNet详解
LSTNet时间序列预测
多元时间序列预测之(二)LSTNet模型
LSTNet学习
源码
数据集
LSTnet文章提出该模型针对多元时间序列预测问题,在文章中,主要应用在太阳能发电厂能量输出,电力消耗和交通堵塞等情况的预测。
提供的源码项目里就是对这些内容的实战例子。
直接引用参考文章里的解释:LSTNet有啥特点呢?它利用卷积层的优势发现多维输入变量之间的局部依赖模式,并循环层捕获复杂的长期依赖模式(It leverages the strengths of both the convolutional layer to discover the local dependency patterns among multi-dimensional input variables and the recurrent layer to captures complex long-term dependencies)。还要啥其他的创新吗?有,循环跳跃(Recurrent-skip):其被设计用来捕获特别长期依赖的模式,并利用了输入时间序列信号的周期性使优化变得更容易(designed for capturing very long-term dependence patterns and making the optimization easier as it utilizes the periodic property of the input time series signals)。
创新点就是将时序数据通过卷积层进行特征提取,用于捕捉短期局部信息,使用LSTM捕捉长期宏观信息。
整个模型的思想其实很简单,正如上图所示,先将多维时序数据通过CNN进行提取,然后通过RNN网络进行常规的LSTM模型训练,最后将最初的CNN提取内容跟经过LSTM的结果进行合并输出预测结果。
在参考文章LSTNet详解中,介绍了基于不同框架的LSTnet的项目,这里的源码用的是pytorch
框架的。
下载完源码后,在安装配置完pytorch
环境后,打开项目,如下图:
其中,画圈的是作者提示的项目运行指令,如solar.sh
中的内容如下:
python main.py --gpu 0 --data data/solar_AL.txt --save save/solar_AL.pt --hidSkip 10 --output_fun Linear
直接在终端输入这条指令即可运行整个项目。
这里要做Debug
,所以换一个方式运行,打开main.py
文件,在空白处右键,然后打开修改运行配置
将作者提示的形参传入下图红框部分--gpu 0 --data data/solar_AL.txt --save save/solar_AL.pt --hidSkip 10 --output_fun Linear
源代码里--gpu 4
,这里的电脑只用1给gpu,所以设置为0。不用多卡训练的花,这个部分可以删了,不传入形参。系统调用cpu训练。
然后开始运行,在运行前需要创建save
文件夹,用来保存权重的,将数据集放到项目里,创建data
文件夹,然后将数据集对应的丢进来:
如果出行以下错误:
原因是torch的版本跟项目的版本不一样导致的,将data[0]改为data.item()
即可。
每个形参后都有英文介绍
parser = argparse.ArgumentParser(description='PyTorch Time series forecasting') parser.add_argument('--data', type=str, required=True, help='location of the data file') parser.add_argument('--model', type=str, default='LSTNet', help='') parser.add_argument('--hidCNN', type=int, default=100, help='number of CNN hidden units') parser.add_argument('--hidRNN', type=int, default=100, help='number of RNN hidden units') parser.add_argument('--window', type=int, default=24 * 7, help='window size') parser.add_argument('--CNN_kernel', type=int, default=6, help='the kernel size of the CNN layers') parser.add_argument('--highway_window', type=int, default=24, help='The window size of the highway component') parser.add_argument('--clip', type=float, default=10., help='gradient clipping') parser.add_argument('--epochs', type=int, default=100, help='upper epoch limit') parser.add_argument('--batch_size', type=int, default=128, metavar='N', help='batch size') parser.add_argument('--dropout', type=float, default=0.2, help='dropout applied to layers (0 = no dropout)') parser.add_argument('--seed', type=int, default=54321, help='random seed') parser.add_argument('--gpu', type=int, default=None) parser.add_argument('--log_interval', type=int, default=2000, metavar='N', help='report interval') parser.add_argument('--save', type=str, default='model/model.pt', help='path to save the final model') parser.add_argument('--cuda', type=str, default=True) parser.add_argument('--optim', type=str, default='adam') parser.add_argument('--lr', type=float, default=0.001) parser.add_argument('--horizon', type=int, default=12) parser.add_argument('--skip', type=float, default=24) parser.add_argument('--hidSkip', type=int, default=5) parser.add_argument('--L1Loss', type=bool, default=True) parser.add_argument('--normalize', type=int, default=2) parser.add_argument('--output_fun', type=str, default='sigmoid') args = parser.parse_args() args.cuda = args.gpu is not None if args.cuda: torch.cuda.set_device(args.gpu) # Set the random seed manually for reproducibility. #希望结果复现,通过设置随机数种子的方法来实现。 torch.manual_seed(args.seed) if torch.cuda.is_available(): if not args.cuda: print("WARNING: You have a CUDA device, so you should probably run with --cuda") else: torch.cuda.manual_seed(args.seed)
Data = Data_utility(args.data, 0.6, 0.2, args.cuda, args.horizon, args.window, args.normalize);
Data_utility
类的行参:
def init(self, file_name, train, valid, cuda, horizon, window, normalize = 2):
引用LSTNet时间序列预测的解释:
在这篇文章中,我们关注多变量时间预测的任务。正式的说,给定一系列观察到的时间序列信号Y = {y(1),y(2),…,y(T)},其中,yt∈Rn, n为变量维数,我们的目标是以滚动预测的方式预测一系列未来信号。为了预测y(T+h),我们需要提供{y(1),y(2),…,y(T)}的数据,其中h是当前时间戳的理想界限(horizon我不知道该如何翻译,暂且认为是一个极限值)。同样的,为了预测下一个时间戳的值y(T+h+1),需要提供{y(1),y(2),…,y(T),y(T+1)}。因此我们把时间戳T的输入矩阵表示为X(T)={y1,y2,…,yT},这个矩阵的维度是R(n*T)。
class Data_utility(object): # train and valid is the ratio of training set and validation set. test = 1 - train - valid def __init__(self, file_name, train, valid, cuda, horizon, window, normalize = 2): self.cuda = cuda; self.P = window; self.h = horizon #打开数据 fin = open(file_name); #读取数据 self.rawdat = np.loadtxt(fin,delimiter=','); self.dat = np.zeros(self.rawdat.shape); self.n, self.m = self.dat.shape; self.normalize = 2 self.scale = np.ones(self.m); #数据标准化 self._normalized(normalize); #训练集跟验证集分割 self._split(int(train * self.n), int((train+valid) * self.n), self.n); #浮点型便于运算 #时序数据每个维度上的所有数,例如100组时序数据,1个时序数据有10维,self.scale获得的是这10个维度上所有的数。 self.scale = torch.from_numpy(self.scale).float(); tmp = self.test[1] * self.scale.expand(self.test[1].size(0), self.m); if self.cuda: self.scale = self.scale.cuda(); self.scale = Variable(self.scale); #标准化 self.rse = normal_std(tmp); #相对绝对值误差 self.rae = torch.mean(torch.abs(tmp - torch.mean(tmp)));
def _normalized(self, normalize): #normalized by the maximum value of entire matrix. #不做标准化处理 if (normalize == 0): self.dat = self.rawdat #找到数据集中的最大值,以它做标标准化 if (normalize == 1): self.dat = self.rawdat / np.max(self.rawdat); #normlized by the maximum value of each row(sensor). #行方向最大值归一化 if (normalize == 2): for i in range(self.m): #取出这一列的数值 self.scale[i] = np.max(np.abs(self.rawdat[:,i])); #找到这一列的最大值,将这一列上的每个值都做相对这一列最大值的标准化 self.dat[:,i] = self.rawdat[:,i] / np.max(np.abs(self.rawdat[:,i]));
给定一系列观察到的时间序列信号Y = {y(1),y(2),…,y(T)},其中,yt∈Rn, n为变量维数,我们的目标是以滚动预测的方式预测一系列未来信号。为了预测y(T+h),我们需要提供{y(1),y(2),…,y(T)}的数据,其中h是当前时间戳的理想界限(horizon我不知道该如何翻译,暂且认为是一个极限值)。同样的,为了预测下一个时间戳的值y(T+h+1),需要提供{y(1),y(2),…,y(T),y(T+1)}。因此我们把时间戳T的输入矩阵表示为X(T)={y1,y2,…,yT},这个矩阵的维度是R(n*T)。
图画的有点潦草,大致就是,多维序列(图中是2维)取一个window
的数据作为输入数据,再取window长度+horizon
处的数据,作为训练时求解损失函数的真实值,这样子训练出来的模型,也是那window中的数据,预测经过horizon数据戳
后的数据值的情况。数据集按这种方式,向下滑动,window统一向下移动一格
,对应的真实值部分也向下移动一格,如途中蓝色框所示。
self._split(int(train * self.n), int((train+valid) * self.n), self.n);
def _split(self, train, valid, test): train_set = range(self.P+self.h-1, train); #时间戳的极限+滑动窗口开始 valid_set = range(train, valid); test_set = range(valid, self.n); self.train = self._batchify(train_set, self.h); self.valid = self._batchify(valid_set, self.h); self.test = self._batchify(test_set, self.h); def _batchify(self, idx_set, horizon): n = len(idx_set); X = torch.zeros((n,self.P,self.m));#先做个容器,大小为【window,时序数据的维度】 Y = torch.zeros((n,self.m));#y是求损失函数时用到的ground truth 大小为【总时序数据集数量,时序数据的维度】 #将数据按滑动窗口【这里滑动窗口取24*7=168个】的形式一组组的装起来, for i in range(n): end = idx_set[i] - self.h + 1; start = end - self.P; X[i,:,:] = torch.from_numpy(self.dat[start:end, :]); #Y这里取的是时间戳horizon里的数据 Y[i,:] = torch.from_numpy(self.dat[idx_set[i], :]); return [X, Y];
分类好的训练集如下图所示,输入数据为168个时序数据,数据维度为137,对应的真实值为这组序列第168个数据往后挪动horizon
的数据,数据长度为1,数据维度为137.:
本模型具有卷积组件、递归组件、递归-跳过层、时间注意层、自回归组件这五大模块。具体解读可以看多元时间序列预测之(二)LSTNet模型
作者在LSTnet.py
的__init__
将要用到的函数全部搭建后,模型训练时候,向前传播,调用这些函数进行模型计算,这些函数就组成上述的几大模块。
输入尺寸为[128,168,137]
,128批时序数据,每批时序数据中有168组时序数据,每组时序数据有137维。下列是模型每一层输出情况:
=============================================================== out torch.Size([128, 137]) model Model( (conv1): Conv2d(1, 100, kernel_size=(6, 137), stride=(1, 1)) (GRU1): GRU(100, 100) (dropout): Dropout(p=0.2, inplace=False) (GRUskip): GRU(100, 5) (linear1): Linear(in_features=220, out_features=137, bias=True) (highway): Linear(in_features=24, out_features=1, bias=True) ) ========================================================================================== Layer (type:depth-idx) Output Shape Param # ========================================================================================== Model [128, 137] -- ├─Conv2d: 1-1 [128, 100, 163, 1] 82,300 ├─Dropout: 1-2 [128, 100, 163, 1] -- ├─GRU: 1-3 [163, 128, 100] 60,600 ├─Dropout: 1-4 [128, 100] -- ├─GRU: 1-5 [6, 3072, 5] 1,605 ├─Dropout: 1-6 [128, 120] -- ├─Linear: 1-7 [128, 137] 30,277 ├─Linear: 1-8 [17536, 1] 25 ==========================================================================================
每一层的输出分别对应着论文的示意图:
下列代码是训练前的常规准备:
#eval()去引号,其实就是LSTNet.Model(args, Data) model = eval(args.model).Model(args, Data); if args.cuda: model.cuda() #模型总参数计算 nParams = sum([p.nelement() for p in model.parameters()]) print('* number of parameters: %d' % nParams) #损失函数求解方法 if args.L1Loss: criterion = nn.L1Loss(size_average=False); else: criterion = nn.MSELoss(size_average=False); evaluateL2 = nn.MSELoss(size_average=False); evaluateL1 = nn.L1Loss(size_average=False) if args.cuda: criterion = criterion.cuda() evaluateL1 = evaluateL1.cuda(); evaluateL2 = evaluateL2.cuda(); best_val = 10000000; #优化器策略 optim = Optim.Optim( model.parameters(), args.optim, args.lr, args.clip, )
try: print('begin training'); #开始训练 for epoch in range(1, args.epochs+1): epoch_start_time = time.time() #统计时间 #正式开始训练,并返回训练损失值。 train_loss = train(Data, Data.train[0], Data.train[1], model, criterion, optim, args.batch_size) #验证集的损失值,相对误差,错误率求解 val_loss, val_rae, val_corr = evaluate(Data, Data.valid[0], Data.valid[1], model, evaluateL2, evaluateL1, args.batch_size); print('| end of epoch {:3d} | time: {:5.2f}s | train_loss {:5.4f} | valid rse {:5.4f} | valid rae {:5.4f} | valid corr {:5.4f}'.format(epoch, (time.time() - epoch_start_time), train_loss, val_loss, val_rae, val_corr)) # Save the model if the validation loss is the best we've seen so far. #保存最佳的模型 if val_loss < best_val: with open(args.save, 'wb') as f: torch.save(model, f) best_val = val_loss #每五轮进行一次测试,计算测试集的准确率等 if epoch % 5 == 0: test_acc, test_rae, test_corr = evaluate(Data, Data.test[0], Data.test[1], model, evaluateL2, evaluateL1, args.batch_size); print ("test rse {:5.4f} | test rae {:5.4f} | test corr {:5.4f}".format(test_acc, test_rae, test_corr)) #通过键盘退出训练 except KeyboardInterrupt: print('-' * 89) print('Exiting from training early') #训练到最后一轮时候计算模型的训练性能 # Load the best saved model. with open(args.save, 'rb') as f: model = torch.load(f) test_acc, test_rae, test_corr = evaluate(Data, Data.test[0], Data.test[1], model, evaluateL2, evaluateL1, args.batch_size); print ("test rse {:5.4f} | test rae {:5.4f} | test corr {:5.4f}".format(test_acc, test_rae, test_corr))
模型通过上述代码中的
train_loss = train(Data, Data.train[0], Data.train[1], model, criterion, optim, args.batch_size)
进行训练,解读train函数:
def train(data, X, Y, model, criterion, optim, batch_size):
model.train();#训练模式,会进行梯度下降
total_loss = 0;#本轮训练的总损失
n_samples = 0;#样本总数
for X, Y in data.get_batches(X, Y, batch_size, True):#通过迭代器data.get_batches()往外给处每批训练数据
model.zero_grad();
output = model(X);#模型输出对应批次的预测结果
scale = data.scale.expand(output.size(0), data.m)
loss = criterion(output * scale, Y * scale);#损失求解 默认用的L1loss 可修改
loss.backward();#反向传播
grad_norm = optim.step();#根据参数的梯度和超参数(如学习率)来更新模型的参数,从而使得损失函数最小化
total_loss += loss.data.item();#将损失存起来
n_samples += (output.size(0) * data.m);
return total_loss / n_samples
下面是模型向前传播代码以及传播操作示意图。
def forward(self, x): batch_size = x.size(0); #CNN [batchsize,C,H,W] c = x.view(-1, 1, self.P, self.m); c = F.relu(self.conv1(c)); c = self.dropout(c); c = torch.squeeze(c, 3); # RNN r = c.permute(2, 0, 1).contiguous(); _, r = self.GRU1(r); r = self.dropout(torch.squeeze(r,0)); #skip-rnn if (self.skip > 0): s = c[:,:, int(-self.pt * self.skip):].contiguous(); s = s.view(batch_size, self.hidC, self.pt, self.skip); s = s.permute(2,0,3,1).contiguous(); s = s.view(self.pt, batch_size * self.skip, self.hidC); _, s = self.GRUskip(s); s = s.view(batch_size, self.skip * self.hidS); s = self.dropout(s); r = torch.cat((r,s),1); res = self.linear1(r); #highway if (self.hw > 0): z = x[:, -self.hw:, :]; z = z.permute(0,2,1).contiguous().view(-1, self.hw); z = self.highway(z); z = z.view(-1,self.m); res = res + z; if (self.output): res = self.output(res); return res;
把模型结构这一部分放一起:
=============================================================== out torch.Size([128, 137]) model Model( (conv1): Conv2d(1, 100, kernel_size=(6, 137), stride=(1, 1)) (GRU1): GRU(100, 100) (dropout): Dropout(p=0.2, inplace=False) (GRUskip): GRU(100, 5) (linear1): Linear(in_features=220, out_features=137, bias=True) (highway): Linear(in_features=24, out_features=1, bias=True) ) ========================================================================================== Layer (type:depth-idx) Output Shape Param # ========================================================================================== Model [128, 137] -- ├─Conv2d: 1-1 [128, 100, 163, 1] 82,300 ├─Dropout: 1-2 [128, 100, 163, 1] -- ├─GRU: 1-3 [163, 128, 100] 60,600 ├─Dropout: 1-4 [128, 100] -- ├─GRU: 1-5 [6, 3072, 5] 1,605 ├─Dropout: 1-6 [128, 120] -- ├─Linear: 1-7 [128, 137] 30,277 ├─Linear: 1-8 [17536, 1] 25 ==========================================================================================
每一层的输出分别对应着论文的示意图:
预测阶段其实就是向数据输入模型,然后得到值,将值保存起来,这些得到的值就是预测值,相对训练阶段,少了一个梯度下降,反向传播过程。就略过。
val_loss, val_rae, val_corr = evaluate(Data, Data.valid[0], Data.valid[1], model, evaluateL2, evaluateL1, args.batch_size);
def evaluate(data, X, Y, model, evaluateL2, evaluateL1, batch_size): model.eval(); total_loss = 0; total_loss_l1 = 0; n_samples = 0; predict = None; test = None; for X, Y in data.get_batches(X, Y, batch_size, False): output = model(X); if predict is None: predict = output; test = Y; else: predict = torch.cat((predict,output)); test = torch.cat((test, Y)); scale = data.scale.expand(output.size(0), data.m) total_loss += evaluateL2(output * scale, Y * scale).data.item() total_loss_l1 += evaluateL1(output * scale, Y * scale).data.item() n_samples += (output.size(0) * data.m); rse = math.sqrt(total_loss / n_samples)/data.rse rae = (total_loss_l1/n_samples)/data.rae predict = predict.data.cpu().numpy(); Ytest = test.data.cpu().numpy(); sigma_p = (predict).std(axis = 0); sigma_g = (Ytest).std(axis = 0); mean_p = predict.mean(axis = 0) mean_g = Ytest.mean(axis = 0) index = (sigma_g!=0); correlation = ((predict - mean_p) * (Ytest - mean_g)).mean(axis = 0)/(sigma_p * sigma_g); correlation = (correlation[index]).mean(); return rse, rae, correlation;
上面生成的图片是直接在LSTnet.py
文件中调用torchviz
跟torchinfo
库来实现,直接运行这个.py
文件即可生成可视化图
import torch import torch.nn as nn import torch.nn.functional as F from torchviz import make_dot import argparse from torchinfo import summary class Model(nn.Module): def __init__(self, args, data): super(Model, self).__init__() self.use_cuda = args.cuda self.P = args.window; self.m = data.m # self.m = 137 #配合该文件if __name__ == '__main__':中可视化生成图的时候开启 self.hidR = args.hidRNN;#隐藏称RNN的数量 self.hidC = args.hidCNN;#隐藏称CNN的数量 self.hidS = args.hidSkip; self.Ck = args.CNN_kernel; self.skip = args.skip; self.pt = int((self.P - self.Ck)/self.skip) self.hw = args.highway_window self.conv1 = nn.Conv2d(1, self.hidC, kernel_size = (self.Ck, self.m)); self.GRU1 = nn.GRU(self.hidC, self.hidR); self.dropout = nn.Dropout(p = args.dropout); if (self.skip > 0): self.GRUskip = nn.GRU(self.hidC, self.hidS); self.linear1 = nn.Linear(self.hidR + self.skip * self.hidS, self.m); else: self.linear1 = nn.Linear(self.hidR, self.m); if (self.hw > 0): self.highway = nn.Linear(self.hw, 1); self.output = None; if (args.output_fun == 'sigmoid'): self.output = F.sigmoid; if (args.output_fun == 'tanh'): self.output = F.tanh; def forward(self, x): batch_size = x.size(0); #CNN [batchsize,C,H,W] c = x.view(-1, 1, self.P, self.m); c = F.relu(self.conv1(c)); c = self.dropout(c); c = torch.squeeze(c, 3); # RNN r = c.permute(2, 0, 1).contiguous(); _, r = self.GRU1(r); r = self.dropout(torch.squeeze(r,0)); #skip-rnn if (self.skip > 0): s = c[:,:, int(-self.pt * self.skip):].contiguous(); s = s.view(batch_size, self.hidC, self.pt, self.skip); s = s.permute(2,0,3,1).contiguous(); s = s.view(self.pt, batch_size * self.skip, self.hidC); _, s = self.GRUskip(s); s = s.view(batch_size, self.skip * self.hidS); s = self.dropout(s); r = torch.cat((r,s),1); res = self.linear1(r); #highway if (self.hw > 0): z = x[:, -self.hw:, :]; z = z.permute(0,2,1).contiguous().view(-1, self.hw); z = self.highway(z); z = z.view(-1,self.m); res = res + z; if (self.output): res = self.output(res); return res; if __name__ == '__main__': parser = argparse.ArgumentParser(description='PyTorch Time series forecasting') parser.add_argument('--hidCNN', type=int, default=100, help='number of CNN hidden units') parser.add_argument('--hidRNN', type=int, default=100, help='number of RNN hidden units') parser.add_argument('--window', type=int, default=24 * 7, help='window size') parser.add_argument('--CNN_kernel', type=int, default=6, help='the kernel size of the CNN layers') parser.add_argument('--highway_window', type=int, default=24, help='The window size of the highway component') parser.add_argument('--clip', type=float, default=10., help='gradient clipping') parser.add_argument('--epochs', type=int, default=100, help='upper epoch limit') parser.add_argument('--batch_size', type=int, default=128, metavar='N', help='batch size') parser.add_argument('--dropout', type=float, default=0.2, help='dropout applied to layers (0 = no dropout)') parser.add_argument('--seed', type=int, default=54321, help='random seed') parser.add_argument('--log_interval', type=int, default=2000, metavar='N', help='report interval') parser.add_argument('--optim', type=str, default='adam') parser.add_argument('--lr', type=float, default=0.001) parser.add_argument('--horizon', type=int, default=12) parser.add_argument('--skip', type=float, default=24) parser.add_argument('--hidSkip', type=int, default=5) parser.add_argument('--L1Loss', type=bool, default=True) parser.add_argument('--normalize', type=int, default=2) parser.add_argument('--output_fun', type=str, default='sigmoid') args = parser.parse_args() x = torch.randn(128,168,137) x = x.cuda() model = Model(args, x) model = model.cuda() y = model(x) #生成模型结构图 vise = make_dot(y, params=dict(model.named_parameters())) vise.view() #查看模型输出尺寸信息 # input = torch.from_numpy(input).to('cuda:1').to(torch.float32).requires_grad_() print('===============================================================') print('out', y.shape) print('model', model) summary(model=model, input_size=(128,168,137), device="cpu")
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。