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来实现一维卷积

1
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核心代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# 导入库
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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:  

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
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:  

1
2
3
4
5
6
7
8
9
10
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:  

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
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   朴素贝叶斯  阅读(16200)  评论(5编辑  收藏  举报

相关博文:
阅读排行:
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 如何使用 Uni-app 实现视频聊天(源码,支持安卓、iOS)
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
历史上的今天:
2021-10-21 python 直接赋值、浅拷贝和深度拷贝解析

导航

< 2025年2月 >
26 27 28 29 30 31 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 1
2 3 4 5 6 7 8

统计

点击右上角即可分享
微信分享提示