如何評價ST-GCN動作識別演算法?
加入極市 專業CV交流群,與 6000+來自騰訊,華為,百度,北大,清華,中科院 等名企名校視覺開發者互動交流!更有機會與 李開復老師 等大牛群內互動!
同時提供每月大咖直播分享、真實專案需求對接、乾貨資訊彙總,行業技術交流 。 點選文末“ 閱讀原文 ”立刻申請入群~
香港中大-商湯科技聯合實驗室的 AAAI 會議論文「Spatial Temporal Graph Convolution Networks for Skeleton Based Action Recognition」,提出了一種新的 ST-GCN,即時空圖卷積網路模型,用於解決基於人體骨架關鍵點的人類動作識別問題,本文是對這一工作的解讀分析。
作者 | 縱橫
論文 | https://arxiv.org/pdf/1801.07455.pdf
來源 |
https://www.zhihu.com/question/276101856/answer/638672980
質勝文則野,文勝質則史,文質彬彬,然後君子。
GCN 升溫的這兩年裡,動作識別領域出了不少好文章。這也不奇怪,畢竟動作識別以前就有 Graph 的相關應用,套用一下 GCN 總是會有提升的。不過,一年過去了,超過 Spatial Temporal Graph Convolution Networks for Skeleton Based Action Recognition 的工作仍然寥寥可數。我等屁民還是挺佩服的~
還在這個領域耕耘的同學們也不用灰心喪氣,ST-GCN 作為一篇開山作(或者說佔坑文),很多地方都從簡了。要想提升不太困難~ 用大粗話來說,作者的主要工作就兩點:
-
使用 OpenPose 處理了視訊,提出了一個數據集
-
結合 GCN 和 TCN 提出了模型,在資料集上效果還不錯
但是,這篇文章在工程和學術上都做到了文質彬彬:
-
從質上講,文中針對性的改進著實有效,結果比較令人滿意
-
從文上講,故事講的很棒,從新的視角整合了卷積、圖卷積和時間卷積
-
從程式碼講,結構清晰、實現優雅,可以當做模板
很多同學比較關心 st-gcn 到底做了什麼,這裡用個簡單的思路說說我的理解。
OpenPose 預處理
OpenPose 是一個標註人體的關節(頸部,肩膀,肘部等),連線成骨骼,進而估計人體姿態的演算法。作為視訊的預處理工具,我們只需要關注 OpenPose 的輸出就可以了。

總的來說,視訊的骨骼標註結果維數比較高。在一個視訊中,可能有很多幀(Frame)。每個幀中,可能存在很多人(Man)。每個人又有很多關節(Joint)。每一個關節又有不同特徵(位置、置信度)。

對於一個 batch 的視訊,我們可以用一個 5 維矩陣 表示。
-
代表視訊的數量,通常一個 batch 有 256 個視訊(其實隨便設定,最好是 2 的指數)。
-
代表關節的特徵,通常一個關節包含
等 3 個特徵(如果是三維骨骼就是 4 個)。
-
代表關鍵幀的數量,一般一個視訊有 150 幀。
-
代表關節的數量,通常一個人標註 18 個關節。
-
代表一幀中的人數,一般選擇平均置信度最高的 2 個人。
-
所以,OpenPose 的輸出,也就是 ST-GCN 的輸入,形狀為
。
想要搞 End2End 的同學還是要稍微關注一下 OpenPose 的實現的。最近還有基於 heatmap 的工作,效果也不錯~
ST-GCN 網路結構
論文中給出的模型描述很豐滿,要是隻看骨架,網路結構如下:

主要分為三部分:

首先,對輸入矩陣進行歸一化,具體實現如下:
N, C, T, V, M = x.size()
# 進行維度交換後記得呼叫 contiguous 再呼叫 view 保持視訊記憶體連續
x = x.permute(0, 4, 3, 1, 2).contiguous()
x = x.view(N * M, V * C, T)
x = self.data_bn(x)
x = x.view(N, M, V, C, T)
x = x.permute(0, 1, 3, 4, 2).contiguous()
x = x.view(N * M, C, T, V)
歸一化是在時間和空間維度下進行的( )。也就是將一個關節在不同幀下的位置特徵(x 和 y 和 acc)進行歸一化。
這個操作是利遠大於弊的:
-
關節在不同幀下的關節位置變化很大,如果不進行歸一化不利於演算法收斂
-
在不同 batch 不同幀下的關節位置基本上服從隨機分佈,不會造成不同 batch 歸一化結果相差太大,而導致準確率波動。

接著,通過 ST-GCN 單元,交替的使用 GCN 和 TCN,對時間和空間維度進行變換:
# N*M(256*2)/C(3)/T(150)/V(18)
Input:[512, 3, 150, 18]
ST-GCN-1:[512, 64, 150, 18]
ST-GCN-2:[512, 64, 150, 18]
ST-GCN-3:[512, 64, 150, 18]
ST-GCN-4:[512, 64, 150, 18]
ST-GCN-5:[512, 128, 75, 18]
ST-GCN-6:[512, 128, 75, 18]
ST-GCN-7:[512, 128, 75, 18]
ST-GCN-8:[512, 256, 38, 18]
ST-GCN-9:[512, 256, 38, 18]
空間維度是關節的特徵(開始為 3),時間的維度是關鍵幀數(開始為 150)。在經過所有 ST-GCN 單元的時空卷積後,關節的特徵維度增加到 256,關鍵幀維度降低到 38。
個人感覺這樣設計是因為,人的動作階段並不多,但是每個階段內的動作比較複雜。比如,一個揮高爾夫球杆的動作可能只需要分解為 5 步,但是每一步的手部、腰部和腳部動作要求卻比較多。

最後,使用平均池化、全連線層(或者叫 FCN)對特徵進行分類,具體實現如下:
# self.fcn = nn.Conv2d(256, num_class, kernel_size=1)
# global pooling
x = F.avg_pool2d(x, x.size()[2:])
x = x.view(N, M, -1, 1, 1).mean(dim=1)
# prediction
x = self.fcn(x)
x = x.view(x.size(0), -1)
Graph 上的平均池化可以理解為對 Graph 進行 read out,即彙總節點特徵表示整個 graph 特徵的過程。這裡的 read out 就是彙總關節特徵表示動作特徵的過程了。通常我們會使用基於統計的方法,例如對節點求 等等。mean 魯棒性比較好,所以這裡使用了 mean。
插句題外話,這裡的 卷積和全連線層等效,最近在用 matconvnet 的時候,發現它甚至不提供全連線層,只使用 的卷積。
GCN
從結果上看,最簡單的圖卷積似乎已經能取得很好的效果了,具體實現如下:
def normalize_digraph(A):
Dl = np.sum(A, 0)
num_node = A.shape[0]
Dn = np.zeros((num_node, num_node))
for i in range(num_node):
if Dl[i] > 0:
Dn[i, i] = Dl[i]**(-1)
AD = np.dot(A, Dn)
return AD
作者在實際專案中使用的圖卷積公式就是:
公式可以進行如下化簡:
其實就是以邊為權值對節點特徵求加權平均。其中, 可以理解為卷積核。

Multi-Kernal
考慮到動作識別的特點,作者並未使用單一的卷積核,而是使用『圖劃分』,將 分解成了 。(作者其實提出了幾種不同的圖劃分策略,但是隻有這個比較好用)

表示的所有邊如上圖右側所示:
-
兩個節點之間有一條雙向邊
-
節點自身有一個自環
作者結合運動分析研究,將其劃分為三個子圖,分別表達向心運動、離心運動和靜止的動作特徵。

對於一個根節點,與它相連的邊可以分為 3 部分。
-
第 1 部分連線了空間位置上比本節點更遠離整個骨架重心的鄰居節點(黃色節點),包含了離心運動的特徵。
-
第 2 部分連線了更為靠近重心的鄰居節點(藍色節點),包含了向心運動的特徵。
-
第 3 部分連線了根節點本身(綠色節點),包含了靜止的特徵。

使用這樣的分解方法,1 個圖分解成了 3 個子圖。卷積核也從 1 個變為了 3 個,即 變為
。3 個卷積核的卷積結果分別表達了不同尺度的動作特徵。要得到卷積的結果,只需要使用每個卷積核分別進行卷積,在進行加權平均(和影象卷積相同)。
具體實現如下:
A = []
for hop in valid_hop:
a_root = np.zeros((self.num_node, self.num_node))
a_close = np.zeros((self.num_node, self.num_node))
a_further = np.zeros((self.num_node, self.num_node))
for i in range(self.num_node):
for j in range(self.num_node):
if self.hop_dis[j, i] == hop:
if self.hop_dis[j, self.center] == self.hop_dis[
i, self.center]:
a_root[j, i] = normalize_adjacency[j, i]
elif self.hop_dis[j, self.
center] > self.hop_dis[i, self.
center]:
a_close[j, i] = normalize_adjacency[j, i]
else:
a_further[j, i] = normalize_adjacency[j, i]
if hop == 0:
A.append(a_root)
else:
A.append(a_root + a_close)
A.append(a_further)
A = np.stack(A)
self.A = A
Multi-Kernal GCN
現在,我們可以寫出帶有 個卷積核的圖卷積表示式了:
表示式可以用愛因斯坦求和約定表示 。其中,
-
表示所有視訊中的人數(batch * man)
-
表示卷積核數(使用上面的分解方法 k=3)
-
表示關節特徵數(64 ... 128)
-
表示關鍵幀數(150 ... 38)
-
和 表示關節數(使用 OpenPose 的話有 18 個節點)
對 求和代表了節點的加權平均,對 求和代表了不同卷積核 feature map 的加權平均,具體實現如下:
# self.conv = nn.Conv2d(
# in_channels,
# out_channels * kernel_size,
# kernel_size=(t_kernel_size, 1),
# padding=(t_padding, 0),
# stride=(t_stride, 1),
# dilation=(t_dilation, 1),
# bias=bias)
x = self.conv(x)
n, kc, t, v = x.size()
x = x.view(n, self.kernel_size, kc//self.kernel_size, t, v)
x = torch.einsum('nkctv,kvw->nctw', (x, A))
return x.contiguous(), A
如果要類比的話,其實和 GoogleNet 的思路有些相似:
都在一個卷積單元中試圖利用不同感受野的卷積核,提取不同分量的特徵。

TCN
GCN 幫助我們學習了到空間中相鄰關節的區域性特徵。在此基礎上,我們需要學習時間中關節變化的區域性特徵。如何為 Graph 疊加時序特徵,是圖網路面臨的問題之一。這方面的研究主要有兩個思路:時間卷積(TCN)和序列模型(LSTM)。
ST-GCN 使用的是 TCN,由於形狀固定,我們可以使用傳統的卷積層完成時間卷積操作。為了便於理解,可以類比影象的卷積操作。st-gcn 的 feature map 最後三個維度的形狀為 ,與影象 feature map 的形狀
相對應。
-
影象的通道數 對應關節的特徵數 。
-
影象的寬 對應關鍵幀數 。
-
影象的高 對應關節數 。
在影象卷積中,卷積核的大小為『w』 『1』,則每次完成 w 行畫素,1 列畫素的卷積。『stride』為 s,則每次移動 s 畫素,完成 1 行後進行下 1 行畫素的卷積。

在時間卷積中,卷積核的大小為『temporal_kernel_size』 『1』,則每次完成 1 個節點,temporal_kernel_size 個關鍵幀的卷積。『stride』為 1,則每次移動 1 幀,完成 1 個節點後進行下 1 個節點的卷積。
具體實現如下:
padding = ((kernel_size[0] - 1) // 2, 0)
self.tcn = nn.Sequential(
nn.BatchNorm2d(out_channels),
nn.ReLU(inplace=True),
nn.Conv2d(
out_channels,
out_channels,
(temporal_kernel_size, 1),
(1, 1),
padding,
),
nn.BatchNorm2d(out_channels),
nn.Dropout(dropout, inplace=True),
)
再列舉幾個序列模型的相關工作,感興趣的同學可以嘗試一下:
-
AGC-Seq2Seq 使用的是 Seq2Seq + Attention。
-
ST-MGCN 使用的是 CGRNN。
-
DCRNN 使用的是 GRU。
Attention
作者在進行圖卷積之前,還設計了一個簡易的注意力模型(ATT)。
# 注意力引數
# 每個 st-gcn 單元都有自己的權重引數用於訓練
self.edge_importance = nn.ParameterList([
nn.Parameter(torch.ones(self.A.size()))
for i in self.st_gcn_networks
])
# st-gcn 卷積
for gcn, importance in zip(self.st_gcn_networks, self.edge_importance):
print(x.shape)
# 關注重要的邊資訊
x, _ = gcn(x, self.A * importance)
其實很好理解,在運動過程中,不同的軀幹重要性是不同的。例如腿的動作可能比脖子重要,通過腿部我們甚至能判斷出跑步、走路和跳躍,但是脖子的動作中可能並不包含多少有效資訊。
因此,ST-GCN 對不同軀幹進行了加權(每個 st-gcn 單元都有自己的權重引數用於訓練)。
結束
上面的內容主要是在講『文質彬彬』中的『質』,其實我感覺『文』才是比較難的部分。在寫論文的過程中,找到一個好的視角,流暢地表達出模型的可解釋性是非常可貴的。
研一這一年,導師都在教我如何講好一個故事,與君共勉吧~
*延伸閱讀
-
從CNN到GCN的聯絡與區別——GCN從入門到精(fang)通(qi)
-
何愷明等最新突破:視訊識別快慢結合,取得人體動作AVA資料集最佳水平
-
極市乾貨|孫書洋 CVPR 2018論文詳解:光流導向特徵在視訊動作識別中的應用
點選左下角 “ 閱讀原文 ”, 即可申請加入極市 目標跟蹤、目標檢測、工業檢測、人臉方向、視覺競賽等技術交流群, 更有每月大咖直播分享、真實專案需求對接、乾貨資訊彙總,行業技術交流, 一起來讓思想之光照的更遠吧~
覺得有用麻煩給個在看啦~