1. 程式人生 > >LSTM的備胎,用卷積處理時間序列——TCN與因果卷積(理論+Python實踐)

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

## 什麼是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]: ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200414224957412.png) TCN中,或者說因果卷積中,使用的卷積核大小都是2,我也不知道為啥不用更大的卷積核,看論文中好像沒有說明這個,如果有小夥伴知道原因或者有猜想,可以下方評論處一起討論討論。 卷積核是2,那麼可想而知,對上面5個數據做一個卷積核為2的卷積是什麼樣子的: ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200414225401799.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM0MTA3NDI1,size_16,color_FFFFFF,t_70) 五個資料經過一次卷積,可以變成四個資料,但是每一個卷積後的資料都是基於兩個原始資料得到的,所以說,目前卷積的視野域是2。 可以看到是輸入是5個數據,但是經過卷積,變成4個數據了,在影象中有一個概念是通過padding來保證卷積前後特徵圖尺寸不變,所以在時間序列中,依然使用padding來保證尺寸不變: ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200414230107910.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM0MTA3NDI1,size_16,color_FFFFFF,t_70) padding是左右兩頭都增加0,如果padding是1的話,就是上圖的效果,其實會產生6個新資料,但是秉著:“輸入輸出尺寸相同”和“我們不能知道未來的資料”,所以最後邊那個未來的padding,就省略掉了,之後再程式碼中會體現出來。 總之,現在我們大概能理解,對時間序列卷積的大致流程了,也就是對一維資料卷積的過程(影象卷積算是二維)。 **下面看如何使用Pytorch來實現一維卷積:** ```python 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的[論文連結](https://arxiv.org/pdf/1803.01271.pdf): - 因果卷積應為就是:Causal Convolutions。 之前已經講了一維卷積的過程了,那麼因果卷積,其實就是一維卷積的一種應用吧算是。 假設想用上面講到的概念,做一個股票的預測決策模型,然後希望決策模型可以考慮到這個時間點之前的4個時間點的股票價格進行決策,總共有3種決策: - 0:不操作,1:買入,2:賣出 所以其實就是一個分類問題。因為要求視野域是4,所以按照上面的設想,要堆積3個卷積核為2的1維卷積層: ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/202004142316119.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM0MTA3NDI1,size_16,color_FFFFFF,t_70) 三次卷積,可以讓最後的輸出,擁有4個視野域。就像是上圖中紅色的部分,就是做出一個決策的過程。 股票資料,往往是按照分鐘記錄的,那少說也是十萬、百萬的資料量,我們決策,想要考慮之前1000個時間點呢?視野域要是1000,那意味著要999層卷積?啥計算機吃得消這樣的計算。所以引入了膨脹因果卷積。 ## 膨脹因果卷積 - 英文是Dilated Causal Convolution。這個其實就是空洞卷積啦,不確定在之前的博文中有沒有講過這個概念(最近別人要求在寫一個非常長的教程,和部落格中的博文可能會有記憶混亂的情況2333) - 反正就是,這個空洞卷積、或者叫擴張卷積、或者叫膨脹卷積就是操作dilation這個引數。 ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200414232940794.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM0MTA3NDI1,size_16,color_FFFFFF,t_70) 如圖,這個就是dilation=2的時候的情況,與之前的區別有兩個: - 看紅色區域:可以看到卷積核大小依然是2,但是卷積核之間變得空洞了,隔過去了一個數據;如果dilation=3的話,那麼可以想而知,這個卷積核中間會空的更大,會隔過去兩個資料。 - 看淡綠色資料:因為dilation變大了,所以相應的padding的數量從1變成了2,所以為了保證輸入輸出的特徵維度相同,padding的數值等於dalition的數值(在卷積核是2的情況下,嚴格說說:padding=(kernel_size-1)*dilation) 然後我們依然實現上面那個例子,每次決策想要視野域為4: ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200414233611255.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM0MTA3NDI1,size_16,color_FFFFFF,t_70) 可以看到,第一次卷積使用dilation=1的卷積,然後第二次使用dilation=2的卷積,這樣通過兩次卷積就可以實現視野域是4. 那麼假設事業域要是8呢?那就再加一個dilation=4的卷積。dilation的值是2的次方,然後視野域也是2的次方的增長,那麼就算是要1000視野域,那十層大概就行了。 這裡有一個動圖,挺好看的: ![](https://img-blog.csdnimg.cn/20190829091941330.gif) ## 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是個啥](https://blog.csdn.net/qq_34107425/article/details/104128626)其實不看也行,不妨礙理解TCN ## 模型的PyTorch實現(最好了解一點PyTorch) 如果不瞭解的話,emm,我要安利我的博文了2333: [從零學習pytorch 第5課 PyTorch模型搭建三要素](https://blog.csdn.net/qq_34107425/article/details/104128626) ```python # 匯入庫 import torch import torch.nn as nn from torch.nn.utils import weight_norm ``` ```python # 這個函式是用來修剪卷積之後的資料的尺寸,讓其與輸入資料尺寸相同。 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個數字。 ```python # 這個就是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的主網路了: ```python 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就這麼多,謝謝