1:論文信息
來自IJCAI 2018的一篇論文:《Spatio-Temporal Graph Convolutional Networks: A Deep Learning Framework for Traffic Forecasting 》node
1.1:論文思路
使用Kipf & Welling 2017的近似譜圖卷積獲得的圖卷積做爲空間上的卷積操做,時間上使用一維卷積TCN對全部頂點進行卷積,二者交替進行,組成了時空卷積塊,在加州PeMS和北京市的兩個數據集上作了驗證。論文中圖的構建方法並非基於實際路網,而是經過數學方法構建了一個基於距離關係的網絡。python
1.2:摘要和引言總結
-
在交通研究中,交通流的基本變量,也就是速度、流量和密度(實際中,還有排隊長度,時間佔有率,空間佔有率,車頭時距等多個變量),這些變量一般做爲監控當前交通狀態以及將來預測的指示指標。根據預測的長度,主要是指預測時間窗口的大小,交通預測大致分爲兩個尺度:短時間(5~30min),中和長期預測(超過30min)。大多數流行的統計方法(好比,線性迴歸)能夠在短時間預測上表現的很好。然而,因爲交通流的不肯定性和複雜性,這些方法在相對長期的預測上不是頗有效。git
-
中長期交通預測上的研究能夠分爲兩類:動態建模和數據驅動的方法。github
- 動態建模方法:使用了數學工具(好比微分方程)和物理知識經過計算模擬來形式化交通問題。爲了達到一個穩定的狀態,模擬進程不只須要複雜的系統編程,還須要消耗大量的計算資源。模型中不切實際的假設和化簡也會下降預測的精度。所以,隨着交通數據收集和存儲技術的快速發展,一大羣研究者正在將他們的目光投向數據驅動的方法。
- 數據驅動方法:主要是統計學和機器學習模型。在時間序列分析上,自迴歸移動平均模型(ARIMA)和它的變形是衆多統一的方法中基於傳通通計學的方法。可是,這種類型的模型受限於時間序列的平穩分佈,不能捕捉時空間依賴關係。所以,這些方法限制了高度非線性的交通流的表示能力。相對傳通通計學方法,機器學習能夠得到更高精度的預測結果,對更復雜的數據建模,好比k近鄰(KNN),支持向量機(SVM)和神經網絡(NN)。
-
深度學習方法:深度學習已經普遍且成功地應用於各式各樣的交通任務中,並取得了很顯著的成果,好比層疊自編碼器(SAE)。然而,這些全鏈接神經網絡很難從輸入中提取空間和時間特徵。並且,空間屬性的嚴格限制甚至徹底缺失,這些網絡的表示能力被限制的很嚴重。爲了充分利用空間特徵,一些研究者使用了卷積神經網絡來捕獲交通網絡中的臨近信息,同時也在時間軸上部署了循環神經網絡。經過組合LSTM和1維卷積,好比特徵層面融合的架構CLTFP來預測短時間交通情況。CLTFP是第一個嘗試對時間和空間規律性對齊的方法。後來,有學者提出帶有嵌入卷積層的全鏈接網絡:ConvLSTM。可是常規卷積操做只適合處理規則化的網絡結構,好比圖像或者視頻,而大多數領域是非結構化的,好比社交網絡,交通路網等。此外,RNN對於序列的學習須要迭代訓練,這會致使偏差的積累。而且還有難以訓練和耗時的缺點。編程
-
針對以上問題和缺陷:該文引入了一些策略來有效的對交通流的時間動態和空間依賴進行建模。爲了徹底利用空間信息,利用廣義圖對交通網絡建模,而不是將交通流當作各個離散的部分(好比網格或碎塊)。爲了處理循環神經網絡的缺陷,咱們在時間軸上部署了一個全卷積結構來加速模型的訓練過程。綜上所述,該文提出了一個新的神經網絡架構-時空圖卷積網絡,來預測交通情況。這個架構由多個時空圖卷積塊組成。網絡
-
主要貢獻:架構
- 該文研在交通研究中第一次應用純卷積層(TCN)來同時從圖結構的時間序列中提取時空信息。
- 該文提出了一個新的由時空塊組成的神經網絡結構。因爲這個架構中是純卷積操做,它比基於RNN的模型的訓練速度快10倍以上,並且須要的參數更少。這個架構可讓咱們更有效地處理更大的路網。
- 實驗在兩個真實交通數據集上驗證了提出來的網絡。這個實驗顯示出咱們的框架比已經存在的在多長度預測和網絡尺度上的模型表現的更好
1.3:上述總結
2:正文
2.1:數據處理
文章首先將網格數據改成圖數據做爲輸入,圖能夠用鄰接矩陣來表示,圖中的W就是圖的鄰接矩陣,實驗中使用的數據集PeMSD7(M)共有228個數據點,至關於一個具備228個頂點的圖,由於這個模型主要是對速度進行預測,因此每一個頂點只有一個特徵就是:速度。
app
2.2:網絡架構
網絡架構是本文的重點部分。以下圖所示,STGCN有多個時空卷積塊組成,每個都是像一個「三明治」結構的組成,有兩個門序列卷積層和一個空間圖卷積層在中間。
組成結構:STGCN 有兩個ST-Conv Block(淡藍色部分)快和一個全鏈接輸出layer(綠色部分),其中每一個ST-Conv Block塊有包括兩個時間卷積塊(橙色部分)和一個空間卷積塊(淺黃色部分)
框架
2.3:提取空間特徵的圖卷積神經網絡
提取時間特徵的圖卷積神經網絡,即對應網絡結構中的Spatial Graph-Conv 模塊。
交通路網是非結構化的圖像,爲了捕獲空間上的相關性,本篇論文采用的是切比雪夫近似與一階近似後的圖卷積公式。只須要看最終的那個卷積公式,其中D爲圖的度矩陣,A_hat爲圖的鄰接矩陣+單位矩陣,爲的是在卷積過程當中不只考慮鄰居節點的狀態,也考慮自身的狀態。
機器學習
2.4:提取時間特徵的門控捲積神經網絡
提取空間特徵的圖卷積神經網絡,即對應網絡結構中的Temporal Gated-Conv 模塊。
在時間維度上,本文采用門控捲積來捕獲時間依賴性,並且與傳統的卷積方法不一樣,因爲還要考慮時間序列的問題,因此這裏採用的是因果卷積(TCN)。使用卷積操做,就不在像採用RNN的方法依賴於以前的輸出,而且還能夠對數據進行並行處理,這樣使得模型訓練速度更快。
此外,採用還採用了GLU操做,GLU是在這篇論文中提出的:Language Modeling with Gated Convolutional Networks。在STGCN這篇論文,文章只是簡單提到採用這種操做能夠緩解梯度消失等現象還能夠保留模型的非線性能力。
2.5:時空卷積塊ST-Conv Block
將以上的圖卷積和門控CNN組合成如圖所示的結構,其中使用了瓶頸策略來實現尺度壓縮和特徵壓縮。並在每層以後接歸一化層來防止過擬合。
ST-Conv Block的公式就是圖的另外一個解釋,輸入數據先作時間維度卷積,輸出結果再作圖卷積,圖卷積的輸出結果通過一個RELU,在進行一個時間維度卷積,就是整個ST-Conv Block的輸出。
2.6:模型輸出
最後的模型是堆疊兩個St-Conv Block以後接一個輸出層,其中輸出層首先用時間維度的卷積將以前的輸出數據的時間維度進行合併,合併以後在通過一個卷積輸出最終的預測數據,預測數據就是下一個時間維度的一張圖[1,228,1]。模型採用的是L2損失。
2.7:模型總結
- STGCN 是處理結構化時間序列的通用框架。它不只可以解決交通網絡建模和預測問題,並且能夠應用於更通常的時空序列學習任務。
- 時空卷積塊結合了圖卷積和門控時間卷積,可以提取出最有用的空間特徵,並連貫地捕捉到最基本的時間特徵。
- 該模型徹底由卷積結構組成,在輸入端實現並行化,參數更少,訓練速度更 快。更重要的是,這種經濟架構容許模型以更高的效率處理大規模網絡。
3:源碼解讀
源碼以Pytorch爲例
3.1:讀取數據
原始數據時間窗口長度是5min一個數據片,即:每5分鐘記錄該時間窗口內,各個路網節點的數據信息,每條數據信息包含2個維度信息。
utlis.load_metr_la_data函數
讀取數據,而且利用 Z-score method進行歸一化:
X = X - means.reshape(1, -1, 1)和 X = X / stds.reshape(1, -1, 1),最終返回數據格式爲:X:[207, 2, 34272],表示有圖中有207個節點,34272個時間片,每一個時間片對應的節點數據維度是2
def load_metr_la_data(): if (not os.path.isfile("data/adj_mat.npy") or not os.path.isfile("data/node_values.npy")): with zipfile.ZipFile("data/METR-LA.zip", 'r') as zip_ref: zip_ref.extractall("data/") A = np.load("data/adj_mat.npy") X = np.load("data/node_values.npy").transpose((1, 2, 0)) X = X.astype(np.float32) # Normalization using Z-score method means = np.mean(X, axis=(0, 2)) X = X - means.reshape(1, -1, 1) stds = np.std(X, axis=(0, 2)) X = X / stds.reshape(1, -1, 1) return A, X, means, stds
utlis.get_normalized_adj函數
讀取鄰接矩陣,並返回度信息
def get_normalized_adj(A): """ Returns the degree normalized adjacency matrix. """ A = A + np.diag(np.ones(A.shape[0], dtype=np.float32)) D = np.array(np.sum(A, axis=1)).reshape((-1,)) D[D <= 10e-5] = 10e-5 # Prevent infs diag = np.reciprocal(np.sqrt(D)) A_wave = np.multiply(np.multiply(diag.reshape((-1, 1)), A), diag.reshape((1, -1))) return A_wave
utlis.generate_dataset函數
- 歷史時間窗口長度:num_timesteps_input = 12,預測將來時間窗口長度:num_timesteps_output = 3,即用過去12個時間片信息,來預測接下來3個時間片的數值。
- 在main函數中,訓練數據集爲60%,即訓練數量共34272*0.6=20563。main函數調用generate_dataseth函數,其原始輸入爲 (num_vertices, num_features,num_timesteps),即 (207, 2, 20563),207是節點數量,2表示每一個時間片對應的特徵數量,訓練時間片數量爲20563。
- 最後generate_dataseth返回X的結果爲:(num_samples, num_vertices, num_features, num_timesteps_input),即[20549, 207, 12, 2],20549表示處理後的時間片數量,207表示節點數量,12表示特徵數量也就是過去12個時間片,2表示特徵的通道數量,generate_dataseth返回Y的結果爲:(num_samples, num_vertices, num_features, num_timesteps_output),即[20549, 207, 3],其中的第一個通道的數值表示預測數值對應的通道。
def generate_dataset(X, num_timesteps_input, num_timesteps_output): """ Takes node features for the graph and divides them into multiple samples along the time-axis by sliding a window of size (num_timesteps_input+ num_timesteps_output) across it in steps of 1. :param X: Node features of shape (num_vertices, num_features, num_timesteps) :return: - Node features divided into multiple samples. Shape is (num_samples, num_vertices, num_features, num_timesteps_input). - Node targets for the samples. Shape is (num_samples, num_vertices, num_features, num_timesteps_output). """ # Generate the beginning index and the ending index of a sample, which # contains (num_points_for_training + num_points_for_predicting) points indices = [(i, i + (num_timesteps_input + num_timesteps_output)) for i in range(X.shape[2] - ( num_timesteps_input + num_timesteps_output) + 1)] # Save samples features, target = [], [] for i, j in indices: features.append( X[:, :, i: i + num_timesteps_input].transpose( (0, 2, 1))) target.append(X[:, 0, i + num_timesteps_input: j]) return torch.from_numpy(np.array(features)), \ torch.from_numpy(np.array(target))
3.2:模型輸入
這裏batch_size設置爲50,同時爲了比較快速清晰的查看各個模塊的輸出,咱們就以一個batch爲例,即:輸入的總時間片長度爲50
STGCN包含三個模型,其中兩個時空STGCNBlock模塊和一個TimeBlock模塊和最後一個全鏈接層,而一個STGCNBlock又包含兩個temporal(temporal)模塊,(也就是TimeBlock模塊模塊)和 一個Theta(spatial)模塊,一個TimeBlock模塊包含因果卷積操做目的就是爲了提取時間信息(主要是W維度上的)TimeBlock模塊主要是爲了提取時間信息,利用的是一維卷積,kernel_size=3,卷積核大小爲(1,kernel_size=3,分別對應H,W)。
- GLU操做:Theta(spatial)模塊中的切比雪夫近似與一階近似後的圖卷積與上一個TimeBlock進行矩陣相乘計算,GLU單元來緩解梯度消失等現象還能夠保留模型的非線性能力。
- TimeBlock模塊的原始輸入爲:[50, 207, 12, 2]即[B,H,W,C],通過尺度變換轉換爲pytorch最終的輸入爲[50, 2, 207, 12],即[B,C,H,W],尺度變換的緣由在於要與卷積覈對應,由於
卷積核大小爲(1,kernel_size=3,分別對應H,W),調用一次TimeBlock模塊H不變也就是207數值不變,W減少2,也就是12-2,即提取一個時間切片下的時間信息。 - STGCN包含三個模型,其中兩個時空STGCNBlock模塊(分別爲block1和block2)和一個TimeBlock模塊(last_temporal),block1和block2均包含2個TimeBlock,每一個TimeBlock計算後,W要減小2,所以,兩個block1和block2後,W共減小8,所以block2的輸出結果爲[50, 207, 4, 64]。
- 再利用切比雪夫近似與一階近似後的圖卷積公式進行計算,從而提取空間信息,STGCN中最後包含的TimeBlock模塊主要是爲了維度信息的縮放,輸出通道統一爲64,這樣保證後面全鏈接的時候,
- 參數一致,如原始batch輸入爲[50, 207, 12, 2],在通過整個STGCN的前兩個時空STGCNBlock模塊後,其輸出大小爲out3:[50, 207, 2, 64],在通過最後一層的全鏈接以前,要進行維度調整,(out3.shape[0], out3.shape[1], -1)返回的是[50, 207, 3]。
- 最後再通過一個全鏈接層:self.fully = nn.Linear((num_timesteps_input - 2 * 5) *64,num_timesteps_output)返回預測的3個時間片結果
3.3:模型總結
- 數據導入和劃分:
數據導入後的格式爲X:[207, 2, 34272],60%用做訓練集,數量共34272*0.6=20563,num_timesteps_input = 12,num_timesteps_output = 3,數據集生成函數返回的X結果爲[20549, 207, 12, 2]。Y結果爲[20549, 207, 3],batch設置的數值爲50,也就是每50個時間片數據進行訓練。所以TimeBlock模塊的輸入爲[50, 207, 12, 2]即[B,H,W,C]。 - 維度變換:
STGCN = block1+block2+last_temporal:
第一個Block中的第一個TimeBlock模塊的定義爲self.temporal1TimeBlock(in_channels=in_channels,out_channels=out_channels),TimeBlock輸入爲[50, 207, 12, 2]即[B,H,W,C],但須要進一步轉換,轉換後的結果爲[50, 2, 207, 12],即[B,C,H,W],由於通常狀況下,圖網絡數據轉換爲網格數據時,H通常表示網格節點數量,W表示特徵數量(時空數據下的同一時間片的歷史數據量),另外一個尺度變換的緣由在於要與卷積覈對應,由於卷積核大小爲(1,kernel_size=3,分別對應H,W),C表示通道,B表示batch的大小,若是保持原始的[B,H,W,C],則一維卷積則對W,C進行卷積,顯然是不合理的。 - TimeBlock計算的結果是[50, 64, 207, 10],即[B,C,H,W](以調用一次TimeBlock爲例,W減小2),再進行一次維度變換,返回的結果是[50, 207, 10, 64],即[B,H,W,C],由於後面還會調用TimeBlock,因此與上一次輸入格式保持一致。
- GLU的定義爲:self.Theta1 = nn.Parameter(torch.FloatTensor(out_channels,spatial_channels) — spatial_channels=16
通過矩陣相乘,GLU的操做轉換輸出結果爲[50, 207, 10, 16],做爲下一個TimeBlock的輸入。 - 第一個block1中的第二個TimeBlock模塊的定義爲self.temporal2=self.temporal2 = TimeBlock(in_channels=spatial_channels,out_channels=out_channels)與第一個TimeBlock模塊輸出結果相似,self.tempora2的輸出結果爲[50, 207, 8, 64],即self.temporal => self.Theta1 => self.tempora2 ,通道數量從64 => 16 => 64,也就是說self.Theta1模塊通過一次非線性變換,只須要將輸入,輸出通道數量對齊便可。最後通過一層BN:self.tempora2的輸出結果爲[50, 207, 8, 64],通過BN其輸出維度保持不變:[50, 207, 8, 64]同理,再通過第二個block2後,BN層的輸出結果爲[50, 207, 4, 64],最後一個last_temporal(TimeBlock):其輸入爲[50, 207, 4, 64],輸出爲[50, 207, 2, 64]
- 最後全鏈接層:self.fully = nn.Linear((num_timesteps_input - 2 * 5) *64,num_timesteps_output),out4 = self.fully(out3.reshape((out3.shape[0], out3.shape[1], -1))),輸出結果爲[50, 207, 3]
TimeBlock模塊:
class TimeBlock(nn.Module): """ Neural network block that applies a temporal convolution to each node of a graph in isolation. """ def __init__(self, in_channels, out_channels, kernel_size=3): """ :param in_channels: Number of input features at each node in each time step. :param out_channels: Desired number of output channels at each node in each time step. :param kernel_size: Size of the 1D temporal kernel. """ super(TimeBlock, self).__init__() self.conv1 = nn.Conv2d(in_channels, out_channels, (1, kernel_size)) self.conv2 = nn.Conv2d(in_channels, out_channels, (1, kernel_size)) self.conv3 = nn.Conv2d(in_channels, out_channels, (1, kernel_size)) def forward(self, X): """ :param X: Input data of shape (batch_size, num_nodes, num_timesteps, num_features=in_channels) :return: Output data of shape (batch_size, num_nodes, num_timesteps_out, num_features_out=out_channels) """ # Convert into NCHW format for pytorch to perform convolutions. X = X.permute(0, 3, 1, 2) temp = self.conv1(X) + torch.sigmoid(self.conv2(X)) out = F.relu(temp + self.conv3(X)) # Convert back from NCHW to NHWC out = out.permute(0, 2, 3, 1) return out
STGCNBlock模塊
class STGCNBlock(nn.Module): """ Neural network block that applies a temporal convolution on each node in isolation, followed by a graph convolution, followed by another temporal convolution on each node. """ def __init__(self, in_channels, spatial_channels, out_channels, num_nodes): """ :param in_channels: Number of input features at each node in each time step. :param spatial_channels: Number of output channels of the graph convolutional, spatial sub-block. :param out_channels: Desired number of output features at each node in each time step. :param num_nodes: Number of nodes in the graph. """ super(STGCNBlock, self).__init__() self.temporal1 = TimeBlock(in_channels=in_channels, out_channels=out_channels) self.Theta1 = nn.Parameter(torch.FloatTensor(out_channels, spatial_channels)) self.temporal2 = TimeBlock(in_channels=spatial_channels, out_channels=out_channels) self.batch_norm = nn.BatchNorm2d(num_nodes) self.reset_parameters() def reset_parameters(self): stdv = 1. / math.sqrt(self.Theta1.shape[1]) self.Theta1.data.uniform_(-stdv, stdv) def forward(self, X, A_hat): """ :param X: Input data of shape (batch_size, num_nodes, num_timesteps, num_features=in_channels). :param A_hat: Normalized adjacency matrix. :return: Output data of shape (batch_size, num_nodes, num_timesteps_out, num_features=out_channels). """ t = self.temporal1(X) lfs = torch.einsum("ij,jklm->kilm", [A_hat, t.permute(1, 0, 2, 3)]) # t2 = F.relu(torch.einsum("ijkl,lp->ijkp", [lfs, self.Theta1])) t2 = F.relu(torch.matmul(lfs, self.Theta1)) t3 = self.temporal2(t2) return self.batch_norm(t3) # return t3
STGCN模塊:
class STGCN(nn.Module): """ Spatio-temporal graph convolutional network as described in https://arxiv.org/abs/1709.04875v3 by Yu et al. Input should have shape (batch_size, num_nodes, num_input_time_steps, num_features). """ def __init__(self, num_nodes, num_features, num_timesteps_input, num_timesteps_output): """ :param num_nodes: Number of nodes in the graph. :param num_features: Number of features at each node in each time step. :param num_timesteps_input: Number of past time steps fed into the network. :param num_timesteps_output: Desired number of future time steps output by the network. """ super(STGCN, self).__init__() self.block1 = STGCNBlock(in_channels=num_features, out_channels=64, spatial_channels=16, num_nodes=num_nodes) self.block2 = STGCNBlock(in_channels=64, out_channels=64, spatial_channels=16, num_nodes=num_nodes) self.last_temporal = TimeBlock(in_channels=64, out_channels=64) self.fully = nn.Linear((num_timesteps_input - 2 * 5) * 64, num_timesteps_output) def forward(self, A_hat, X): """ :param X: Input data of shape (batch_size, num_nodes, num_timesteps, num_features=in_channels). :param A_hat: Normalized adjacency matrix. """ out1 = self.block1(X, A_hat) out2 = self.block2(out1, A_hat) out3 = self.last_temporal(out2) out4 = self.fully(out3.reshape((out3.shape[0], out3.shape[1], -1))) return out4