溫州有沒有專門的企業(yè)網站最近幾天發(fā)生的新聞大事
一、引言
? ? ? ? 引言部分不是論文的重點,主要講述了交通預測的重要性以及一些傳統(tǒng)方法的不足之處。進而推出了自己的模型——STGCN。
二、交通預測與圖卷積
????????
? ? ? ? 第二部分講述了交通預測中路圖和圖卷積的概念。
? ? ? ? 首先理解道路圖,交通預測被定義為典型的時間序列預測問題,即根據歷史數據預測未來的交通情況。在該工作中,交通網絡被建模為圖結構,節(jié)點表示監(jiān)控站,邊表示監(jiān)控站之間的連接。
? ? ? ? 其次是圖卷積,由于傳統(tǒng)的卷積操作無法應用于圖數據,作者介紹了兩種擴展卷積到圖數據的方法,即擴展卷積的空間定義、譜圖卷積。這里需要著重注意譜圖卷積,因為STGCN就是采用了傅里葉變換的方法,這里的公式后面也要提到。
三、STGCN結構與原理(重點)
????????先看整體網絡結構:
1. 空域卷積塊(圖卷積塊)
1.1 論文
? ? ? ? 這里提到了兩種方法,即切比雪夫多項式和一階近似。對應于代碼中的 ChebGraphConv 和 GraphConv。
? ? ? ? 為什么要用切比雪夫多項式計算?因為傳統(tǒng)的拉普拉斯計算起來非常復雜,使用了切比雪夫多項式以后,通過使用多項式來表示卷積核,圖卷積操作可以在局部化的節(jié)點鄰域內進行,從而避免了全圖的計算。
????????一階近似進一步簡化了圖卷積的計算,將圖拉普拉斯算子(公式中的L)的高階近似簡化為一階。這使得圖卷積操作只依賴于當前節(jié)點及其直接相鄰節(jié)點,計算復雜度大幅降低。
1.2 代碼(GraphConvLayer)
? ? ? ? 這里有我加了注釋的代碼:OracleRay/STGCN_pytorch: The PyTorch implementation of STGCN. (github.com)
????????時空卷積塊和輸出層的定義都在 layers.py 中。
? ? ? ? ChebGraphConv 類的前向傳播方法:
def forward(self, x):# bs, c_in, ts, n_vertex = x.shapex = torch.permute(x, (0, 2, 3, 1)) # 將時序維度和頂點維度排列到一起,方便后續(xù)的圖卷積計算if self.Ks - 1 < 0:raise ValueError(f'ERROR: the graph convolution kernel size Ks has to be a positive integer, but received {self.Ks}.')elif self.Ks - 1 == 0:x_0 = xx_list = [x_0] # 只使用第 0 階,即不考慮鄰居節(jié)點的影響,直接使用輸入特征elif self.Ks - 1 == 1:x_0 = x# hi 是鄰接矩陣的索引,btij 是輸入張量 x 的索引,bthj為更新后的特征表示x_1 = torch.einsum('hi,btij->bthj', self.gso, x) # 鄰接矩陣gso和輸入特征x進行相乘x_list = [x_0, x_1] # 使用第 0 階和第 1 階的節(jié)點信息elif self.Ks - 1 >= 2:x_0 = xx_1 = torch.einsum('hi,btij->bthj', self.gso, x)x_list = [x_0, x_1]for k in range(2, self.Ks): # 根據切比雪夫多項式的定義來計算,利用前兩階多項式來構建第 k 階多項式x_list.append(torch.einsum('hi,btij->bthj', 2 * self.gso, x_list[k - 1]) - x_list[k - 2])x = torch.stack(x_list, dim=2) # 將所有階的節(jié)點特征堆疊在一起,形成一個新的張量cheb_graph_conv = torch.einsum('btkhi,kij->bthj', x, self.weight)if self.bias is not None:cheb_graph_conv = torch.add(cheb_graph_conv, self.bias) # 添加偏置項else:cheb_graph_conv = cheb_graph_conv # 強迫癥return cheb_graph_conv
????????Ks表示空間卷積核大小,gso代表交通預測圖的鄰接矩陣。在前向傳播的過程中,判斷 Ks - 1 的值的大小。等于0和等于1分別代表著切比雪夫多項式的第 0 階和第 1 階,當 Ks - 1 大于等于 2 時,不僅需要考慮第 0 階和第 1 階的鄰居節(jié)點,還會通過遞歸關系計算更高階的鄰居節(jié)點信息。
? ? ? ? 構建 k 階多項式時,需要鄰接矩陣 gso 和 (k - 1) 階相乘,然后與?(k - 2) 階相減。這樣就滿足了切比雪夫多項式的要求,即:
? ? ? ? ?最后把得到的值乘以權重(weight),如果有偏置項,再加偏置值(bias)即可。
????????GraphConv 類與此類似。
? ? ? ? 然后在 GraphConvLayer 中就可選擇到底是使用切比雪夫多項式(ChebGraphConv)還是一階近似(GraphConv)。
def forward(self, x):x_gc_in = self.align(x)if self.graph_conv_type == 'cheb_graph_conv':x_gc = self.cheb_graph_conv(x_gc_in)elif self.graph_conv_type == 'graph_conv':x_gc = self.graph_conv(x_gc_in)x_gc = x_gc.permute(0, 3, 1, 2)x_gc_out = torch.add(x_gc, x_gc_in) # 殘差連接return x_gc_out
2. 時域卷積塊
2.1 論文
???????時域卷積塊最關鍵的兩個內容就是因果卷積和門控機制GLU,不過這兩個并不是這篇論文里提出來的。
? ? ? ? 因果卷積(代碼中 CausalConv 類)是時域卷積網絡模型(TCN)中的重要內容。在本篇論文中,使用了1-D因果卷積來確保當前時刻只依賴于過去的輸入數據,這樣未來的信息在當前時刻就不會被使用。
? ? ? ? GLU是2016年由Yann N. Dauphin在論文《Language Modeling with Gated Convolutional Networks》中提出的。時域卷積塊采用門控線性單元(GLU)作為非線性激活函數可以控制哪些輸入是重要的,而不是所有信息都平等對待,這樣有助于在時間序列中提取關鍵特征。
? ? ? ? 殘差連接、瓶頸策略、并行訓練等作者只是提了一嘴,不是重點。
2.2 代碼(TemporalConvLayer)
? ? ? ? 首先看因果卷積的代碼:
class CausalConv2d(nn.Conv2d):def __init__(self, in_channels, out_channels, kernel_size, stride=1, enable_padding=False, dilation=1, groups=1,bias=True):kernel_size = nn.modules.utils._pair(kernel_size) # 卷積核大小,表示對多少個像素(或特征)進行卷積。stride = nn.modules.utils._pair(stride) # 步長,控制卷積核滑動的步幅dilation = nn.modules.utils._pair(dilation) # dilation:膨脹系數,控制采樣間隔,用來擴大卷積核的感受野if enable_padding == True: # 啟用零填充self.__padding = [int((kernel_size[i] - 1) * dilation[i]) for i in range(len(kernel_size))]else:self.__padding = 0self.left_padding = nn.modules.utils._pair(self.__padding)super(CausalConv2d, self).__init__(in_channels, out_channels, kernel_size, stride=stride, padding=0,dilation=dilation, groups=groups, bias=bias)def forward(self, input):if self.__padding != 0:# F.pad() 函數用于在高度和寬度方向上添加填充input = F.pad(input, (self.left_padding[1], 0, self.left_padding[0], 0))result = super(CausalConv2d, self).forward(input)return result
? ? ? ? 在初始化部分,如果需要零填充,則要先計算填充量。填充量的計算公式為:(kernel_size[i] - 1) * dilation[i] 。
? ? ? ? 其中 kernel_size[i] - 1 表示在每個維度(高度和寬度)上,卷積核“超出”當前步長的部分。例如一個 3 × 3 大小的卷積核,就會有2個位置影響當前位置(左一個右一個)。而膨脹系數 dilation[i] 則意味著卷積核中元素之間有多少間隔。最后相乘即可求出需要填充幾個位置。
? ? ? ? 最后在前向傳播過程中用 F.pad() 函數就可以實現左填充。
? ? ? ? 然后再看時域卷積塊(TemporalConvLayer)的前向傳播代碼:
def forward(self, x):x_in = self.align(x)[:, :, self.Kt - 1:, :] # 對其輸入通道數x_causal_conv = self.causal_conv(x) # 進行因果卷積if self.act_func == 'glu' or self.act_func == 'gtu':x_p = x_causal_conv[:, : self.c_out, :, :] # 分割出前半部分x_q = x_causal_conv[:, -self.c_out:, :, :] # 分割出后半部分if self.act_func == 'glu':# 通過門控機制選擇性保留某些時間步的特征,這對時間序列建模非常有效x = torch.mul((x_p + x_in), torch.sigmoid(x_q)) # 對 x_p 和輸入的對齊結果 x_in 進行線性加和,并與 x_q 的 sigmoid 值進行點乘else:# tanh(x_p + x_in) ⊙ sigmoid(x_q)x = torch.mul(torch.tanh(x_p + x_in), torch.sigmoid(x_q)) # 使用 tanh 代替線性加和,具有非線性變換的特性elif self.act_func == 'relu':x = self.relu(x_causal_conv + x_in)elif self.act_func == 'silu':x = self.silu(x_causal_conv + x_in)else:raise NotImplementedError(f'ERROR: The activation function {self.act_func} is not implemented.')return x
? ? ? ? 這里的代碼只干了兩件事:因果卷積和選擇激活函數,與論文中的時域卷積塊的思想大致相同。這里著重理解這行代碼:
x = torch.mul((x_p + x_in), torch.sigmoid(x_q))
? ? ? ? (x_p + x_in)?將前半部分 x_p 與輸入 x_in 的對齊結果進行線性加和,表示對主要特征的組合。sigmoid(x_q) 將后半部分 x_q?通過 sigmoid 函數轉化為 0 到 1 之間的值,作為控制門。最后用⊙符號逐元素相乘,GLU就能決定 x_p 能通過多少信息。
? ? ? ? 默認使用GLU激活函數,其他 if-else 語句中的激活函數不使用。
3. 時空卷積塊
3.1 論文
? ? ? ? 前面知道,兩個時域卷積塊 + 一個空域卷積塊 = 一個時空卷積塊。而且是兩個時域卷積塊夾著一個空域卷積塊的三明治結構。這種設計可以同時處理交通網絡中的 時間依賴 和 空間依賴,即模型可以同時從時序信息和圖結構中提取重要特征。
????????中間的圖卷積層負責從圖結構(如道路網絡)中提取空間特征。通過使用前面提到的圖卷積方法(如切比雪夫多項式近似或一階近似),可以高效地捕捉交通站點之間的連接關系?。
????????上下兩個時間卷積層負責提取時間依賴特征。通過因果卷積的方式,可以確保預測時只使用當前時刻及之前的交通數據,避免未來信息泄露?。
3.2 代碼(STConvBlock)
class STConvBlock(nn.Module):def __init__(self, Kt, Ks, n_vertex, last_block_channel, channels, act_func, graph_conv_type, gso, bias, droprate):super(STConvBlock, self).__init__()# “三明治”結構:兩個時域卷積塊,一個空域卷積塊self.tmp_conv1 = TemporalConvLayer(Kt, last_block_channel, channels[0], n_vertex, act_func)self.graph_conv = GraphConvLayer(graph_conv_type, channels[0], channels[1], Ks, gso, bias)self.tmp_conv2 = TemporalConvLayer(Kt, channels[1], channels[2], n_vertex, act_func)self.tc2_ln = nn.LayerNorm([n_vertex, channels[2]], eps=1e-12) # 歸一化:緩解梯度消失或梯度爆炸問題self.relu = nn.ReLU()self.dropout = nn.Dropout(p=droprate) # 正則化:dropout率為0.5def forward(self, x):x = self.tmp_conv1(x)x = self.graph_conv(x)x = self.relu(x)x = self.tmp_conv2(x)x = self.tc2_ln(x.permute(0, 2, 3, 1)).permute(0, 3, 1, 2)x = self.dropout(x)return x
? ? ? ? 在初始化函數中,先定義“三明治”結構,使用之前寫好的時域卷積塊和圖卷積塊。然后確定歸一化方法,激活函數,正則化方法。
4. 輸出層
4.1 論文
? ? ? ? 論文中的輸出層是由一個時域卷積塊和一個全連接層組成的。
? ? ? ? 當最后一個時空卷積塊處理完數據之后,輸出的是一個三維張量(M × n × C),其中M是時間步數(例如,過去60分鐘的交通數據),n是交通網絡中的節(jié)點數(即監(jiān)測站或路段數),C是特征通道數。而論文使用了一個時域卷積層將這些特征進一步壓縮成一個單步的時間預測輸出。這意味著,時間卷積會提取多時間步數據中的關鍵信息,并最終輸出一個代表未來某一時刻(如未來15分鐘)的預測結果。
????????接下來,在時間卷積之后,論文使用了一個全連接層將卷積層輸出的特征張量映射到一個單一的輸出值,通常是每個節(jié)點的交通狀態(tài)(如車速或流量),最后生成預測結果 v。
4.2 代碼(OutputBlock)
class OutputBlock(nn.Module):def __init__(self, Ko, last_block_channel, channels, end_channel, n_vertex, act_func, bias, droprate):super(OutputBlock, self).__init__()self.tmp_conv1 = TemporalConvLayer(Ko, last_block_channel, channels[0], n_vertex, act_func)self.fc1 = nn.Linear(in_features=channels[0], out_features=channels[1], bias=bias)self.fc2 = nn.Linear(in_features=channels[1], out_features=end_channel, bias=bias)self.tc1_ln = nn.LayerNorm([n_vertex, channels[0]], eps=1e-12) # 歸一化self.relu = nn.ReLU()self.dropout = nn.Dropout(p=droprate) # 正則化def forward(self, x):x = self.tmp_conv1(x)x = self.tc1_ln(x.permute(0, 2, 3, 1))x = self.fc1(x)x = self.relu(x)x = self.dropout(x)x = self.fc2(x).permute(0, 3, 1, 2) # 負責將時空特征映射為最終的預測值return x
?????????代碼部分使用了一個時域卷積塊和兩個全連接層。這是為什么?這樣的設計雖然與論文描述的輸出層結構有所不同,但增加了額外的全連接層是為了增強模型的表達能力和預測精度。
? ? ? ? 第一個全連接層用于對時域卷積輸出的特征進行降維或變換。通過這個全連接層,模型可以將高維度的時空特征壓縮或轉化為新的特征表示,使得模型能夠更好地抽象復雜的關系。第二個全連接層才用于最終的輸出,即生成最后的預測結果。
5. 其他代碼
5.1 models.py
? ? ? ? 這個python文件里主要是對整個STGCN模型進行整合,一共有兩個類,分別是 STGCNChebGraphConv 和 STGCNGraphConv 。這分別代表著使用切比雪夫多項式還是一階近似。
????????其中的大多數代碼都是對 layers.py 中的函數方法調用,傳參。有一行代碼需要理解:
Ko = args.n_his - (len(blocks) - 3) * 2 * (args.Kt - 1)
????????這句代碼的作用是計算經過多個時空卷積塊處理后,保留下來的時間維度的大小。
- args.n_his:是輸入數據的時間維度大小,通常指輸入的歷史時間步數。
- len(blocks) - 3:blocks表示 STGCN 模型中不同層的配置的列表。它的長度再減3是為了去掉輸出層相關的三層結構(
TNFF
,即兩個全連接層和最后的時序處理層),僅關注時空卷積部分。- 2 * (args.Kt - 1):Kt 是每個時空卷積塊中時間卷積核的大小。它的大小再減1是因為每次卷積操作后時間維度會 - 1(時間卷積是滑動窗口的形式)。而每個時空卷積塊中有兩個時間卷積層,所以總的時間維度減少量會再乘2。
? ? ? ? 因此,對輸入的時間維度 n_his,每經過一個時空卷積塊,時間維度會減少 2 * (args.Kt - 1)。有 len(blocks) - 3 個這樣的時空卷積塊,因此總的時間維度減少的量是 (len(blocks) - 3) * 2 * (args.Kt - 1)。
5.2 main.py
? ? ? ? main 函數是對整個代碼運行環(huán)境的配置,包括配置環(huán)境變量,設置命令行參數,數據類型轉換,確定模型、優(yōu)化器等等。這些代碼在其他網絡模型同樣受用,大差不差。