LSTM的備胎,用卷積處理時間序列——TCN與因果卷積(理論+Python實踐)

忽逢桃林發表於2020-05-20

什麼是TCN

TCN全稱Temporal Convolutional Network,時序卷積網路,是在2018年提出的一個卷積模型,但是可以用來處理時間序列。

卷積如何處理時間序列

時間序列預測,最容易想到的就是那個馬爾可夫模型:

\[P(y_k|x_k,x_{k-1},...,x_1) \]

就是計算某一個時刻的輸出值,已知條件就是這個時刻之前的所有特徵值。上面公式中,P表示概率,可以不用管這個,\(y_k\)表示k時刻的輸出值(標籤),\(x_k\)表示k時刻的特徵值。

如果使用LSTM或者是GRU這樣的RNN模型,自然是可以處理這樣的時間序列模型的,畢竟RNN生來就是為了這個的。

但是這個時間序列模型,巨集觀上思考的話,其實就是對這個這個時刻之前的資料做某個操作,然後生成一個標籤,回想一下在卷積在影像中的操作,其實有異曲同工。(這裡不理解也無妨,因為我之前搞了一段時間影像處理,所以對卷積相對熟悉一點)。

一維卷積

假設有一個時間序列,總共有五個時間點,比方說股市,有一個股票的價格波動:[10,13,12,14,15]:
在這裡插入圖片描述
TCN中,或者說因果卷積中,使用的卷積核大小都是2,我也不知道為啥不用更大的卷積核,看論文中好像沒有說明這個,如果有小夥伴知道原因或者有猜想,可以下方評論處一起討論討論。

卷積核是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是膨脹係數,下面的下面會講。

因果卷積

  • 因果卷積是在wavenet這個網路中提出的,之後被用在了TCN中。
    TCN的論文連結
  • 因果卷積應為就是:Causal Convolutions。

之前已經講了一維卷積的過程了,那麼因果卷積,其實就是一維卷積的一種應用吧算是。

假設想用上面講到的概念,做一個股票的預測決策模型,然後希望決策模型可以考慮到這個時間點之前的4個時間點的股票價格進行決策,總共有3種決策:

  • 0:不操作,1:買入,2:賣出

所以其實就是一個分類問題。因為要求視野域是4,所以按照上面的設想,要堆積3個卷積核為2的1維卷積層:
在這裡插入圖片描述
三次卷積,可以讓最後的輸出,擁有4個視野域。就像是上圖中紅色的部分,就是做出一個決策的過程。

股票資料,往往是按照分鐘記錄的,那少說也是十萬、百萬的資料量,我們決策,想要考慮之前1000個時間點呢?視野域要是1000,那意味著要999層卷積?啥計算機吃得消這樣的計算。所以引入了膨脹因果卷積。

膨脹因果卷積

  • 英文是Dilated Causal Convolution。這個其實就是空洞卷積啦,不確定在之前的博文中有沒有講過這個概念(最近別人要求在寫一個非常長的教程,和部落格中的博文可能會有記憶混亂的情況2333)
  • 反正就是,這個空洞卷積、或者叫擴張卷積、或者叫膨脹卷積就是操作dilation這個引數。
    在這裡插入圖片描述
    如圖,這個就是dilation=2的時候的情況,與之前的區別有兩個:
  • 看紅色區域:可以看到卷積核大小依然是2,但是卷積核之間變得空洞了,隔過去了一個資料;如果dilation=3的話,那麼可以想而知,這個卷積核中間會空的更大,會隔過去兩個資料。
  • 看淡綠色資料:因為dilation變大了,所以相應的padding的數量從1變成了2,所以為了保證輸入輸出的特徵維度相同,padding的數值等於dalition的數值(在卷積核是2的情況下,嚴格說說:padding=(kernel_size-1)*dilation)

然後我們依然實現上面那個例子,每次決策想要視野域為4:
在這裡插入圖片描述
可以看到,第一次卷積使用dilation=1的卷積,然後第二次使用dilation=2的卷積,這樣通過兩次卷積就可以實現視野域是4.

那麼假設事業域要是8呢?那就再加一個dilation=4的卷積。dilation的值是2的次方,然後視野域也是2的次方的增長,那麼就算是要1000視野域,那十層大概就行了。

這裡有一個動圖,挺好看的:

TCN結構

TCN基本就是一個膨脹因果卷積的過程,只是上面我們實現因果卷積就只有一個卷積層。而TCN的稍微複雜一點(但是不難!)

  • 卷積結束後會因為padding導致卷積之後的新資料的尺寸B>輸入資料的尺寸A,所以只保留輸出資料中前面A個資料;
  • 卷積之後加上個ReLU和Dropout層,不過分吧這要求。
  • 然後TCN中並不是每一次卷積都會擴大一倍的dilation,而是每兩次擴大一倍的dilation
  • 總之TCN中的基本元件:TemporalBlock()是兩個dilation相同的卷積層,卷積+修改資料尺寸+relu+dropout+卷積+修改資料尺寸+relu+dropout
  • 之後弄一個Resnet殘差連線來避免梯度消失,結束!
    關於Resnet的內容:【從零學習PyTorch】 如何殘差網路resnet作為pre-model +程式碼講解+殘差網路resnet是個啥其實不看也行,不妨礙理解TCN

模型的PyTorch實現(最好了解一點PyTorch)

如果不瞭解的話,emm,我要安利我的博文了2333:
從零學習pytorch 第5課 PyTorch模型搭建三要素

# 匯入庫
import torch
import torch.nn as nn
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

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

可以看出來,這個函式就是第一個資料到倒數第chomp_size的資料,這個chomp_size就是padding的值。比方說輸入資料是5,padding是1,那麼會產生6個資料沒錯吧,那麼就是保留前5個數字。

# 這個就是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)

咋用的呢?就是num_inputs就是輸入資料的通道數,一般就是1; num_channels應該是個列表,其他的np.array也行,比方說是[2,1]。那麼整個TCN模型包含兩個TemporalBlock,整個模型共有4個卷積層,第一個TemporalBlock的兩個卷積層的膨脹係數\(dilation=2^0=1\),第二個TemporalBlock的兩個卷積層的膨脹係數是\(dilation=2^1=2\).

沒了,整個TCN挺簡單的,如果之前學過PyTorch和影像處理的一些內容,然後用TCN來上手時間序列,效果會和LGM差不多。(根據最近做的一個比賽),沒有跟Wavenet比較過,Wavenet的pytorch資源看起來怪複雜的,因為wavenet是用來處理音訊生成的,會更加複雜一點。

總之TCN就這麼多,謝謝大家。

相關文章