TCN(Temporal Convolutional Network)时间卷积网络pytorch实战

前言

实验表明,RNN 在几乎所有的序列问题上都有良好表现,包括语音/文本识别、机器翻译、手写体识别、序列数据分析(预测)等。  在实际应用中,RNN 在内部设计上存在一个严重的问题:由于网络一次只能处理一个时间步长,后一步必须等前一步处理完才能进行运算。这意味着 RNN 不能像 CNN 那样进行大规模并行处理,特别是在 RNN/LSTM 对文本进行双向处理时。这也意味着 RNN 极度地计算密集,因为在整个任务运行完成之前,必须保存所有的中间结果

CNN 在处理图像时,将图像看作一个二维的“块”(m*n 的矩阵)。迁移到时间序列上,就可以将序列看作一个一维对象(1*n 的向量)。通过多层网络结构,可以获得足够大的感受野。这种做法会让 CNN 非常深,但是得益于大规模并行处理的优势,无论网络多深,都可以进行并行处理,节省大量时间。这就是 TCN 的基本思想。  2018年 Google、Facebook 相继发表了研究成果,其中一篇叙述比较全面的论文是 "An Empirical Evaluation of Generic Convolutional and Recurrent Networks"。业界将这一新架构命名为时间卷积网络(Temporal Convolutional Network,TCN)。TCN 模型以 CNN 模型为基础,并做了如下改进: 

  • 适用序列模型:因果卷积(Causal Convolution)
  • 记忆历史:空洞卷积/膨胀/扩张卷积(Dilated Convolution),
  • 残差模块(Residual block)

 下面我们会一一进行介绍。

 TCN

一维卷积

假设有一个时间序列,总共有五个时间点,比方说股市,有一个股票的价格波动:[10,13,12,14,15]:

TCN中,或者说因果卷积中,使用的卷积核大小都是2(我也不知道为啥不用更大的卷积核,看论文中好像没有说明这个),那么可想而知,对上面5个数据做一个卷积核大小为2的卷积是什么样子的:

五个数据经过一次卷积,可以变成四个数据,但是每一个卷积后的数据都是基于两个原始数据得到的,所以说,目前卷积的视野域是2。可以看到是输入是5个数据,但是经过卷积,变成4个数据了,在图像中有一个概念是通过padding来保证卷积前后特征图尺寸不变,所以在时间序列中,依然使用padding来保证尺寸不变:

padding是左右两头都增加0,如果padding是1的话,就是上图的效果,其实会产生6个新数据,但是秉着:“输入输出尺寸相同”和“我们不能知道未来的数据”,所以最后边那个未来的padding,就省略掉了,之后会在代码中会体现出来。  总之,现在我们大概能理解,对时间序列卷积的大致流程了,也就是对一维数据卷积的过程(图像卷积算是二维)。

下面看如何使用Pytorch来实现一维卷积

net = nn.Conv1d(in_channels=1,out_channels=1,kernel_size=2,stride=1,padding=1,dilation=1)

其中的参数跟二维卷积非常类似,也是有通道的概念的。这个好好品一下,一维数据的通道跟图像的通道一样,是根据不同的卷积核从相同的输入中抽取出来不同的特征。kernel_size=2之前也说过了,padding=1也没问题,不过这个公式中假如输入5个数据+padding=1,会得到6个数据,最后一个数据被舍弃掉。dilation是膨胀系数,下面的下面会讲。  

因果卷积(Causal Convolutions)

因果卷积(Causal Convolutions)是在wavenet这个网络中提出的,之后被用在了TCN中。之前已经讲了一维卷积的过程了,那么因果卷积,其实就是一维卷积在时间序列中的一种应用吧。 因为要处理序列问题(时序性),就必须使用新的 CNN 模型,这就是因果卷积。

因果卷积有两个特点:

  • 不考虑未来的信息。给定输入序列 $x_1,⋯,x_T$,预测 $y_1,⋯,y_T$。但是在预测 $y_t$时,只能使用已经观测到的序列 $x_1,⋯,x_t$,而不能使用 $x_{t+1},x_{t+2},...$ 。
  • 追溯历史信息越久远,隐藏层越多。上图中,假设我们以第二层隐藏层作为输出,它的最后一个节点关联了输入的三个节点,即 $x_{t−2},x_{t−1},x_t$ ;假设以输出层作为输出,它的最后一个节点关联了输入的四个节点,即 $x_{t−3},x_{t−2},x_{t−1},x_t$。

假设想用上面讲到的概念,做一个股票的预测决策模型,然后希望决策模型可以考虑到这个时间点之前的4个时间点的股票价格进行决策,总共有3种决策:

  • 0:不操作,
  • 1:买入,
  • 2:卖出

所以其实就是一个分类问题。因为要求视野域是4,所以按照上面的设想,要堆积3个卷积核为2的1维卷积层:

三次卷积,可以让最后的输出,拥有4个视野域。就像是上图中红色的部分,就是做出一个决策的过程。 

股票数据,往往是按照分钟记录的,那少说也是十万、百万的数据量,我们决策,想要考虑之前1000个时间点呢?视野域要是1000,那意味着要999层卷积?(每经过一层,节点相对于前层减少一个,我们最后的输出只有一个节点,如果输入视野为1000,需要经过999层才能变为最后输出的一个节点),啥计算机吃得消这样的计算。所以引入了膨胀因果卷积。

膨胀/空洞/扩张因果卷积(Dilated Causal Convolution)

  • 单纯的因果卷积还是存在传统卷积神经网络的问题,即对时间的建模长度是受限于卷积核大小的,如果要想抓去更长的依赖关系,就需要线性的堆叠很多的层
  • 标准的 CNN 可以通过增加 pooling 层来获得更大的感受野,而经过 pooling 层后肯定存在信息损失的问题。       

膨胀卷积是在标准的卷积里注入空洞,以此来增加感受野。和传统卷积不同的是,膨胀卷积允许卷积时的输入存在间隔采样,采样率受超参数 dilation rate控制,指的是做卷积操作时kernel里面的元素之间的下标间隔(标准的 CNN 中 dilatation rate = 1,dilatation rate=2,表示输入时每2个点采样一个作为输入)。空洞的好处是不做 pooling 损失信息的情况下,增加了感受野,让每个卷积输出都包含较大范围的信息。下图展示了标准 CNN (左)和 Dilated Convolution (右),右图中的 dilatation rate 等于 2 。

 如下图,这个就是dilation=2的时候的膨胀因果卷积的情况,

 

 与之前的区别有两个:

  • 看红色区域:可以看到卷积核大小依然是2,但是卷积核之间变得空洞了,每2个点采样一个作为输入;如果dilation=3的话,那么可以想而知,这个卷积核中间会空的更大,每3个点采样一个作为输入。
  • 看淡绿色数据:因为dilation变大了,所以相应的padding的数量从1变成了2,所以为了保证输入输出的特征维度相同,padding的数值(在卷积核是2的情况下)等于dalition的数值(一般情况下,padding=(kernel_size-1)*dilation,空洞因果卷积的感受野范围大小为(每个卷积核元素之间有dilation-1个空洞节点):(kernel_size-1)*(dilation-1) + kernel_size = (kernel_size-1)*(dilation-1) + (kernel_size-1) + 1 = (kernel_size-1)*dilation + 1,或者(从第一个节点开始,每dilation个节点进行采样):(kernel_size-1)*dilation + 1。以输入中的第一个元素作为空洞因果卷积的最后一个元素,则它的左边需要padding的个数为:(kernel_size-1)*dilation + 1 - 1 = (kernel_size-1)*dilation

然后我们依然实现上面那个例子,每次决策想要视野域为4:

 

可以看到,第一次卷积使用dilation=1的卷积,然后第二次使用dilation=2的卷积,这样通过两次卷积就可以实现视野域是4.

那么假设事业域要是8呢?那就再加一个dilation=4的卷积。在实践中,通常随网络层数增加, dilation以 2 的指数增长,dilation的值是2的次方,然后视野域也是2的次方的增长。

因为研究对象是时间序列,TCN 采用一维的卷积网络。下图是 TCN 架构中的因果卷积与空洞卷积,

可以看到,

  • 每一层 $t$ 时刻的值只依赖于上一层 $t,t−1,..$ 时刻的值,体现了因果卷积的特性;
  • 而每一层对上一层信息的提取,都是跳跃式的,且逐层 dilated rate 以 2 的指数增长,体现了空洞卷积的特性
  • 由于采用了空洞卷积,因此每一层都要做 padding(通常情况下补 0),padding 的大小为:padding=(kernel_size-1)*dilation 。 

残差模块(Residual block)

CNN 能够提取 low/mid/high-level 的特征,网络的层数越多,意味着能够提取到不同 level的特征越丰富。并且,越深的网络提取的特征越抽象,越具有语义信息。 

如果简单地增加深度,会导致梯度消失或梯度爆炸。对于该问题的解决方法是权重参数初始化和采用正则化层(Batch Normalization),这样可以训练几十层的网络。

解决了梯度问题,还会出现另一个问题:网络退化问题。随着网络层数的增加,在训练集上的准确率趋于饱和甚至下降了。注意这不是过拟合问题,因为过拟合会在训练集上表现的更好。下图是一个网络退化的例子,20 层的网络比 56 层的网络表现更好。

理论上 56 层网络的解空间包括了 20 层网络的解空间,因此 56 层网络的表现应该大于等于20 层网络。但是从训练结果来看,56 层网络无论是训练误差还是测试误差都大于 20 层网络(这也说明了为什么不是过拟合现象,因为 56 层网络本身的训练误差都没有降下去)。这是因为虽然 56 层网络的解空间包含了 20 层网络的解空间,但是我们在训练中用的是随机梯度下降策略,往往得到的不是全局最优解,而是局部最优解。显然 56 层网络的解空间更加的复杂,所以导致使用随机梯度下降无法得到最优解。 

假设已经有了一个最优的网络结构,是 18 层。当我们设计网络结构时,我们并不知道具体多少层的网络拥有最优的网络结构,假设设计了 34 层的网络结构。那么多出来的 16 层其实是冗余的,我们希望训练网络的过程中,模型能够自己训练这 16 层为恒等映射,也就是经过这16 层时的输入与输出完全一样。但是往往模型很难将这 16 层恒等映射的参数学习正确,这样的网络一定比最优的 18 层网络表现差,这就是随着网络加深,模型退化的原因因此解决网络退化的问题,就是解决如何让网络的冗余层产生恒等映射(深层网络等价于一个浅层网络)

通常情况下,让网络的某一层学习恒等映射函数 $H(x)=x$ 比较困难,但是如果我们把网络设计为 $H(x)=F(x)+x$ ,我们就可以将学习恒等映射函数转换为学习一个残差函数 $F(x)=H(x)−x$ ,只要 $F(x)=0$ ,就构成了一个恒等映射 $H(x)=x$ 。在参数初始化的时候,一般权重参数都比较小,非常适合学习 $F(x)=0$ ,因此拟合残差会更加容易,这就是残差网络的思想

下图为残差模块的结构

该模块提供了两种选择方式,也就是

  • identity mapping即 x ,右侧“弯弯的线",称为 shortcut 连接
  • residual mapping即 F(x) ),

如果网络已经到达最优,继续加深网络,residual mapping 将被 push 为 0,只剩下 identity mapping,这样理论上网络一直处于最优状态了,网络的性能也就不会随着深度增加而降低了。

这种残差模块结构可以通过前向神经网络 + shortcut 连接实现。而且 shortcut 连接相当于简单执行了同等映射,不会产生额外的参数,也不会增加计算复杂度,整个网络依旧可以通过端到端的反向传播训练。 

上图中残差模块包含两层网络。实验证明,残差模块往往需要两层以上,单单一层的残差模块 并不能起到提升作用。shortcut 有两种连接方式:

  • identity mapping 同等维度的映射( $F(x)$ 与 $x$ 维度相同): 

$F(x)=W_2σ(W_1x+b_1)+b_2,H(x)=F(x)+x$  

  • identity mapping 不同维度的映射( $F(x)$ 与 $x$ 维度不同): 

$F(x)=W_2σ(W_1x+b_1)+b_2,H(x)=F(x)+W_sx$  

以上是基于全连接层的表示,实际上残差模块可以用于卷积层。加法变为对应 channel 间的两个 feature map 逐元素相加。 对于残差网络,维度匹配的 shortcut 连接为实线,反之为虚线。在残差网络中,有很多残差模块,下图是一个残差网络。每个残差模块包含两层,相同维度残差模块之间采用实线连接,不同维度残差模块之间采用虚线连接。网络的 2、3 层执行 3x3x64 的卷积,他们的 channel 个数相同,所以采用计算: $H(x)=F(x)+x$ ;网络的 4、5 层执行 3x3x128 的卷积,与第 3 层的 channel 个数不同 (64 和 128),所以采用计算方式: $H(x)=F(x)+W_sx$ 。其中 $W_s$ 是卷积操作(用 128 个 3x3x64 的 filter),用来调整 x 的 channel 个数

下图是 TCN 架构中的残差模块如下图所示:

TCN的基本模块TemporalBlock()

  •  卷积并进行weight_norm结束后会因为padding导致卷积之后的新数据的尺寸B>输入数据的尺寸A,所以只保留输出数据中前面A个数据
  • 卷积之后加上个ReLU和Dropout层,不过分吧这要求。
  • 然后TCN中并不是每一次卷积都会扩大一倍的dilation,而是每两次扩大一倍的dilation
  • 总之,TCN中的基本组件:TemporalBlock()是两个dilation相同的卷积层,卷积+修改数据尺寸+relu+dropout+卷积+修改数据尺寸+relu+dropout
  • 之后弄一个Resnet残差连接来避免梯度消失,结束!

TCN原作者pytorch核心代码实现

# 导入库
import torch
import torch.nn as nn
#Applies weight normalization to a parameter in the given module.
from torch.nn.utils import weight_norm

# 这个函数是用来修剪卷积之后的数据的尺寸,让其与输入数据尺寸相同。
class Chomp1d(nn.Module):
    def __init__(self, chomp_size):
        super(Chomp1d, self).__init__()
        self.chomp_size = chomp_size#这个chomp_size就是padding的值

    def forward(self, x):
        return x[:, :, :-self.chomp_size].contiguous()


# 这个就是TCN的基本模块,包含8个部分,两个(卷积+修剪+relu+dropout)
# 里面提到的downsample就是下采样,其实就是实现残差链接的部分。不理解的可以无视这个
class TemporalBlock(nn.Module):
    def __init__(self, n_inputs, n_outputs, kernel_size, stride, dilation, padding, dropout=0.2):
        super(TemporalBlock, self).__init__()
        
        self.conv1 = weight_norm(nn.Conv1d(n_inputs, n_outputs, kernel_size,
                                           stride=stride, padding=padding, dilation=dilation))
        self.chomp1 = Chomp1d(padding)
        self.relu1 = nn.ReLU()
        self.dropout1 = nn.Dropout(dropout)


        self.conv2 = weight_norm(nn.Conv1d(n_outputs, n_outputs, kernel_size,
                                           stride=stride, padding=padding, dilation=dilation))
        self.chomp2 = Chomp1d(padding)
        self.relu2 = nn.ReLU()
        self.dropout2 = nn.Dropout(dropout)


        self.net = nn.Sequential(self.conv1, self.chomp1, self.relu1, self.dropout1,
                                 self.conv2, self.chomp2, self.relu2, self.dropout2)
        self.downsample = nn.Conv1d(n_inputs, n_outputs, 1) if n_inputs != n_outputs else None
        self.relu = nn.ReLU()
        self.init_weights()

    def init_weights(self):
        self.conv1.weight.data.normal_(0, 0.01)
        self.conv2.weight.data.normal_(0, 0.01)
        if self.downsample is not None:
            self.downsample.weight.data.normal_(0, 0.01)

    def forward(self, x):
        out = self.net(x)
        res = x if self.downsample is None else self.downsample(x)
        return self.relu(out + res)


#最后就是TCN的主网络了
class TemporalConvNet(nn.Module):
    def __init__(self, num_inputs, num_channels, kernel_size=2, dropout=0.2):
        super(TemporalConvNet, self).__init__()
        layers = []
        num_levels = len(num_channels)
        for i in range(num_levels):
            dilation_size = 2 ** i
            in_channels = num_inputs if i == 0 else num_channels[i-1]
            out_channels = num_channels[i]
            layers += [TemporalBlock(in_channels, out_channels, kernel_size, stride=1, dilation=dilation_size,
                                     padding=(kernel_size-1) * dilation_size, dropout=dropout)]

        self.network = nn.Sequential(*layers)

    def forward(self, x):
        return self.network(x)

TCN时间序列实战

本部分我们将考虑解决下面的多变量输入时间序列问题

$Y_t= Y_{t-1} - (R_{1,t-1}+ R_{1,t-2})+ 4R_{2,t-3}(R_{3,t-4}+ R_{3,t-6})$  

这里,

  • $R_{1,t}$是一个随机变量,
  • $R_{2,t}$是一个随机变量
  • $R_{3,t}$是一个随机变量,它以0.25的概率输出1,其他情况下输出0,,

        可视化依赖时间序列$Y_t$
如果你不是提前知道这里的依赖关系的话,这些依赖是非常困难捕捉的。
下面让我们看一看TCN模型在这个数据集上的性能。
整个项目在vscode中的结构如下所示:
 

 我们来逐个文件来看里面的代码。

model.py

import torch.nn as nn
from torch.nn.utils import weight_norm


class Crop(nn.Module):

    def __init__(self, crop_size):
        super(Crop, self).__init__()
        self.crop_size = crop_size

    def forward(self, x):
        return x[:, :, :-self.crop_size].contiguous()


class TemporalCasualLayer(nn.Module):

    def __init__(self, n_inputs, n_outputs, kernel_size, stride, dilation, dropout = 0.2):
        super(TemporalCasualLayer, self).__init__()
        padding = (kernel_size - 1) * dilation
        conv_params = {
            'kernel_size': kernel_size,
            'stride':      stride,
            'padding':     padding,
            'dilation':    dilation
        }

        self.conv1 = weight_norm(nn.Conv1d(n_inputs, n_outputs, **conv_params))
        self.crop1 = Crop(padding)
        self.relu1 = nn.ReLU()
        self.dropout1 = nn.Dropout(dropout)

        self.conv2 = weight_norm(nn.Conv1d(n_outputs, n_outputs, **conv_params))
        self.crop2 = Crop(padding)
        self.relu2 = nn.ReLU()
        self.dropout2 = nn.Dropout(dropout)

        self.net = nn.Sequential(self.conv1, self.crop1, self.relu1, self.dropout1,
                                 self.conv2, self.crop2, self.relu2, self.dropout2)
        #shortcut connect
        self.bias = nn.Conv1d(n_inputs, n_outputs, 1) if n_inputs != n_outputs else None
        self.relu = nn.ReLU()

    def forward(self, x):
        y = self.net(x)
        b = x if self.bias is None else self.bias(x)
        return self.relu(y + b)


class TemporalConvolutionNetwork(nn.Module):

    def __init__(self, num_inputs, num_channels, kernel_size = 2, dropout = 0.2):
        super(TemporalConvolutionNetwork, self).__init__()
        layers = []
        num_levels = len(num_channels)
        tcl_param = {
            'kernel_size': kernel_size,
            'stride':      1,
            'dropout':     dropout
        }
        for i in range(num_levels):
            dilation = 2**i
            in_ch = num_inputs if i == 0 else num_channels[i - 1]
            out_ch = num_channels[i]
            tcl_param['dilation'] = dilation
            tcl = TemporalCasualLayer(in_ch, out_ch, **tcl_param)
            layers.append(tcl)

        self.network = nn.Sequential(*layers)

    def forward(self, x):
        return self.network(x)


class TCN(nn.Module):

    def __init__(self, input_size, output_size, num_channels, kernel_size, dropout):
        super(TCN, self).__init__()
        self.tcn = TemporalConvolutionNetwork(input_size, num_channels, kernel_size = kernel_size, dropout = dropout)
        self.linear = nn.Linear(num_channels[-1], output_size)

    def forward(self, x):
        y = self.tcn(x)#[N,C_out,L_out=L_in]
        return self.linear(y[:, :, -1])

ts.py

import random
import numpy as np


def generate_time_series(len):
    backshift = 10
    # np.random.random(size=None) 
    # Return random floats in the half-open interval [0.0, 1.0).
    r1 = np.random.random(len + backshift)
    r2 = np.random.random(len + backshift)

    # random.choices(population,weights=None,*,cum_weights=None,k=1)
    # 从population中随机选取k次数据,返回一个列表,可以设置权重。  
    # 注意:每次选取都不会影响原序列,每一次选取都是基于原序列。
    # 参数weights设置相对权重,它的值是一个列表,设置之后,每一个成员被抽取到的概率就被确定了。
    # 比如weights=[1,2,3,4,5],那么第一个成员的概率就是P=1/(1+2+3+4+5)=1/15。
    # cum_weights设置累加权重,Python会自动把相对权重转换为累加权重,即如果你直接给出累加权重,
    # 那么就不需要给出相对权重,且Python省略了一步执行。
    # 比如weights=[1,2,3,4],那么cum_weights=[1,3,6,10]
    # 这也就不难理解为什么cum_weights=[1,1,1,1,1]输出全是第一个成员1了。
    rm = [random.choices([0, 0, 0, 1])[0]
          for _ in range(len + backshift)]

    ts = np.zeros([len + backshift, 4])
    for i in range(backshift, len + backshift):
        ts[i, 1] = r1[i]
        ts[i, 2] = r2[i]
        ts[i, 3] = rm[i]

        ts[i, 0] = ts[i - 1, 0] -\
                   (r1[i - 1] + r1[i - 2]) +\
                   4 * r2[i - 3] * (rm[i - 4] + rm[i - 6])

    return ts[backshift:]

training_datasets.py:  

import os

import pandas as pd
import torch


def sliding_window(ts, features, target_len = 1):
    X = []
    Y = []
    # 产生的样本x为:[(i-target_len) - features,i - target_len)
    # y为:[i-target_len,i)
    # 可以看出,这里产生的样本是用过去历史上features个时刻的信息去
    # 去预测未来target_len(这里target_len=1)个时刻
    for i in range(features + target_len, len(ts) + 1):
        X.append(ts[(i-target_len) - features:i - target_len])
        Y.append(ts[i - target_len:i])

    return X, Y


# 对时间序列做差分处理
def ts_diff(ts):
    diff_ts = [0] * len(ts)
    for i in range(1, len(ts)):
        diff_ts[i] = ts[i] - ts[i - 1]
    return diff_ts

# 从预测出的各个时刻的差分值还原出实际的各个时刻的值
def ts_int(ts_diff, ts_base, start = 0):
    ts = []
    for i in range(len(ts_diff)):
        if i == 0:
            ts.append(start + ts_diff[0])
        else:
            ts.append(ts_diff[i] + ts_base[i - 1])
    return ts


def get_aep_timeseries():
    dir_path = os.path.dirname(os.path.realpath(__file__))
    df = pd.read_csv(f'{dir_path}/data/AEP_hourly.csv')
    ts = df['AEP_MW'].astype(int).values.reshape(-1, 1)[-3000:]
    return ts


def get_pjme_timeseries():
    dir_path = os.path.dirname(os.path.realpath(__file__))
    df = pd.read_csv(f'{dir_path}/data/PJME_hourly.csv')
    ts = df['PJME_MW'].astype(int).values.reshape(-1, 1)[-3000:]
    return ts


def get_ni_timeseries():
    dir_path = os.path.dirname(os.path.realpath(__file__))
    df = pd.read_csv(f'{dir_path}/data/NI_hourly.csv')
    ts = df['NI_MW'].astype(int).values.reshape(-1, 1)[-3000:]
    return ts


def get_training_datasets(ts, features, test_len, train_ratio = .7, target_len = 1):
    X, Y = sliding_window(ts, features, target_len)

    X_train, Y_train, X_test, Y_test = X[0:-test_len],\
                                       Y[0:-test_len],\
                                       X[-test_len:],\
                                       Y[-test_len:]

    train_len = round(len(ts) * train_ratio)

    X_train, X_val, Y_train, Y_val = X_train[0:train_len],\
                                     X_train[train_len:],\
                                     Y_train[0:train_len],\
                                     Y_train[train_len:]

    x_train = torch.tensor(data = X_train).float()
    y_train = torch.tensor(data = Y_train).float()

    x_val = torch.tensor(data = X_val).float()
    y_val = torch.tensor(data = Y_val).float()

    x_test = torch.tensor(data = X_test).float()
    y_test = torch.tensor(data = Y_test).float()

    return x_train, x_val, x_test, y_train, y_val, y_test

dummy.py:  

import torch.nn as nn

# dummy prediction model (Y_t= Y_{t-1})
class Dummy(nn.Module):

    def __init__(self):
        super(Dummy, self).__init__()

    def forward(self, x):# x(x_test):[n_test,4,features]
        return x[:, 0, -1].unsqueeze(1)# 返回值形状:[n_test,1]

example.py:  

import copy
import random
import sys
import os
os.environ["KMP_DUPLICATE_LIB_OK"]="TRUE"

import numpy as np
import matplotlib.pyplot as plt

import torch

from dummy import Dummy
from model import TCN
from ts import generate_time_series
from training_datasets import get_training_datasets, ts_diff, ts_int

seed = 12
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)

# time series input
features = 20
# training epochs
epochs = 1_000
# synthetic time series dataset
ts_len = 5_000
# test dataset size
test_len = 300
# temporal casual layer channels
channel_sizes = [10] * 4
# convolution kernel size
kernel_size = 5
dropout = .0

ts = generate_time_series(ts_len)

# 对时间序列进行差分处理
ts_diff_y = ts_diff(ts[:, 0])
ts_diff = copy.deepcopy(ts)
ts_diff[:, 0] = ts_diff_y

# x:[N,features,4],y:[N,1,1]
x_train, x_val, x_test, y_train, y_val, y_test =\
    get_training_datasets(ts_diff, features, test_len)
# [n_train,4,features]
x_train = x_train.transpose(1, 2)
# [n_val,4,features]
x_val = x_val.transpose(1, 2)
# [n_test,4,features]
x_test = x_test.transpose(1, 2)

# [N,1]
y_train = y_train[:, :, 0]
y_val = y_val[:, :, 0]
y_test = y_test[:, :, 0]

# device = torch.device("cuda")
# for x in [x_train,x_val,x_test,y_train,y_val,y_test]:
#     x = x.to(device)

train_len = x_train.size()[0]

model_params = {
    # 'input_size',C_in
    'input_size':   4,
    # 单步,预测未来一个时刻
    'output_size':  1,
    'num_channels': channel_sizes,
    'kernel_size':  kernel_size,
    'dropout':      dropout
}
model = TCN(**model_params)
# model = model.to(device)

optimizer = torch.optim.Adam(params = model.parameters(), lr = .005)
mse_loss = torch.nn.MSELoss()

best_params = None
min_val_loss = sys.maxsize

training_loss = []
validation_loss = []

for t in range(epochs):

    prediction = model(x_train)
    loss = mse_loss(prediction, y_train)

    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

    val_prediction = model(x_val)
    val_loss = mse_loss(val_prediction, y_val)

    training_loss.append(loss.item())
    validation_loss.append(val_loss.item())

    if val_loss.item() < min_val_loss:
        best_params = copy.deepcopy(model.state_dict())
        min_val_loss = val_loss.item()

    if t % 100 == 0:
        diff = (y_train - prediction).view(-1).abs_().tolist()
        print(f'epoch {t}. train: {round(loss.item(), 4)}, '
              f'val: {round(val_loss.item(), 4)}')

plt.title('Training Progress')
plt.yscale("log")
plt.plot(training_loss, label = 'train')
plt.plot(validation_loss, label = 'validation')
plt.ylabel("Loss")
plt.xlabel("Epoch")
plt.legend()# 图例
plt.show()

best_model = TCN(**model_params)
best_model.eval()
best_model.load_state_dict(best_params)

tcn_prediction = best_model(x_test)
dummy_prediction = Dummy()(x_test)

tcn_mse_loss = round(mse_loss(tcn_prediction, y_test).item(), 4)
dummy_mse_loss = round(mse_loss(dummy_prediction, y_test).item(), 4)

plt.title(f'Test| TCN: {tcn_mse_loss}; Dummy: {dummy_mse_loss}')
plt.plot(
    ts_int(
        tcn_prediction.view(-1).tolist(),
        ts[-test_len:, 0],
        start = ts[-test_len - 1, 0]
    ),
    label = 'tcn')
plt.plot(ts[-test_len-1:, 0], label = 'real')
plt.legend()
plt.show()

 

  

posted on 2022-10-21 15:29  朴素贝叶斯  阅读(9696)  评论(5编辑  收藏  举报

导航