圖神經網絡整理

關於圖的概念可以參考圖論整理 ,這裏不再贅述。

圖神經網絡Graph neural networks(GNNs)是深度學習在圖領域的基本方法,它既不屬於CNN,也不屬於RNN。CNN和RNN能做的事情,GNN都能做。CNN、RNN不能做的事情,GNN也能做。

在上圖的左邊,我們用不同的節點和有關係的邊會構成一個圖。在右側的圖上,我們會看到一系列的數字,對於一個節點,我們用N來表示,對於邊用E來表示。而在右邊出現的這一串數字,我們用X(屬性、特徵)來表示。屬性或者是特徵在不同的領域有不同的定義或者是方式,比如在CNN領域,我們會用各種各樣的卷積網絡,經過若干層得到一個feature map,這些feature map就是特徵。在RNN領域,我們將文本轉化爲詞向量(embedding),那麼這些詞向量就是特徵。而在GNN這裏,它都可以代替。所以可以有萬物皆GNN。

信息傳遞(Message Passing)

如果節點和邊的關係完整,那麼圖就可以在節點之間進行信息傳遞。這個過程也稱爲鄰居間的聚合(Neighborhood Aggregation),在上圖中,某一個參考節點(Reference Node)周邊的鄰居,都通過邊的指向,向該節點進行信息傳遞。

傳遞過去之後,都會在參考節點中發生一次信息的聚合。信息的聚合有很多種,在GCNN中使用加權平均的方式來聚合。通過鄰居節點的聚合,我們把信息全部收集了起來,再完成一次更新,那麼這個參考節點的特徵就發生了一次變化。這個是圖神經網絡核心的概念。

圖任務(Graph Tasks)

上圖是一個節點的分類任務,在左邊的部分,有些節點有顏色,有些節點沒顏色。在訓練的過程中,我們希望用有顏色的節點(歸類過的節點)和邊的連接關係來推導出沒有顏色的節點屬於什麼顏色。比如在推薦系統中,面對不同種類興趣愛好的用戶,隨着他們關係網絡的建立,我們對其需要進行精準推送,就需要去進行該種圖任務的建立。

關係預測任務,在上圖的左邊的節點間有些是實線邊,有些是虛線邊。我們希望知道這些虛線邊能否成爲實線邊。比如在同樣作爲音樂迷的一羣人,有些是周杰倫迷,有些是張學友迷,但是他們彼此之間是否有關係,這個不得而知。此時我們想知道他們是否有其他彼此的共同偶像來確定他們是否存在關係。

GNN爲什麼難

對於CNN來說,無論是圖像分類,目標檢測,語義分割還是姿態檢測,輸入的都是一副圖像

那麼圖像都是由像素組成的,每兩個相鄰像素之間的距離是相等的,它們的排列是有序的,網格化的。

對於RNN來說,每一段話都是由一個一個字組成的,它依然是有序的

但是對於GNN來說

用GNN的眼光來看CNN和RNN簡直就是弟弟,GNN所處理的不再是這種規則的歐拉數據(Euclidean data),而是上圖中左邊這種完全無序的數據。

每一個GNN的圖沒有一個固定的格式,雖然上面的兩個圖的鄰接矩陣是一樣的,但是考慮到邊的位置,那麼它們就是不一樣的圖。

圖神經網絡很難可視化,它不像圖像或者聲音那樣有可以確定的樣子和波形,過於抽象,錯綜複雜。

GNN可以很方便處理關係(人際關係等)、同時可以把很多複雜的問題簡化成一個比較簡單的表示,或者是從其他視角上來進行轉換。比如說推薦系統、知識圖譜。

CNN和GNN的對比

從本質來說,CNN和GNN都是聚合鄰域信息的運算,但是它們作用的對象是完全不同的。

上圖的左邊是GCN的某一層的數學表示,CNN相較於GCN中的卷積計算,它們最大的區別是CNN沒有顯式的描述鄰接矩陣,但是實際計算的時候依然需要數據之間的關係。

感受野,從模型的層面來說,感受野會隨着模型層數的增加而變大,每多一層卷積的運算,中間一層就能更多的融合進更外面一層的信息。在a圖的CNN中的中間層是一個3*3的卷積核,往下擴大就是一個5*5的卷積核,再往下擴大的話可以擴大到7*7、9*9。類似的b圖的GCN中,在上面的一層,我們可以從中心節點開始融合,先融合一階鄰居,隨着層數的增加,可以擴大到二階鄰居。比方說老師回答了同學a的問題,那麼老師和同學a就構成了一階鄰居,那麼同學a弄懂之後又告訴了同學b,老師和同學b並沒有發生直接關係,那麼老師和同學b就構成了二階鄰居。所以在GCN中,感受野也會隨着卷積層的增加而增大。

上圖的a圖和b圖展示了CNN的兩大任務,一個是圖像分類,一個是圖像分割。c圖是不同的人的大腦腦波信號,雖然它們大致一樣,但是不同的人的腦波圖結構中的節點特徵不一樣,通過這些不一樣的特徵,也可以對正常人和患病的人進行一個分類。在d圖中,可以對圖結構中的每一個節點也可以進行分類,我們可以把每一個節點類比於圖像分割中每一個像素的分類。

GNN的應用

一切皆是圖

圖神經網絡可以應用於物理上,也可以應用於化學上或者生物上。

上圖是一個目標檢測的場景,當我們對圖像中的目標進行目標框檢測出來之後,我們想分析目標之間的關係。比如說圖像中有人騎在馬上,狗仰望着人,人俯視着狗等等這些關係。我們將圖像中的目標當作節點,目標之間的關係當作邊,通過這樣的方式來構成一張圖(Graph)。場景圖可以將關係非常複雜的圖像簡化成一個關係非常明確的圖。場景圖有很多很多的應用,如圖像合成、圖像檢索、視覺推理等。

這個是在文字上的應用,這個小貓很可愛。對於每一個文字我們可以構成一個節點,通過每一個節點研究每一個文字之間的關係來構成一個圖。通過此來研究這句話每個單詞的內容。

社交網絡,代表着人或組織之間的社會關係,比如上圖展示了一個在線的社交用戶的關注網絡,用戶爲節點,用戶之間的關係是否關注作爲邊。我們可以研究用戶的重要性排名,或者是否給某個用戶推薦另外一個用戶等。當然我們可以不光把人作爲節點還可以把很多媒體對象補充到社交網絡當中,比如段視頻、小故事等等,都可以作爲一個節點,然後構成一個新的圖,稱之爲異構圖。

這是一個屬性圖,張三作爲一個人的節點,他屬於某一個公司(組織節點),張三愛好科技(話題節點),張三和李四(用戶節點)是朋友,這個就叫異構圖。

交通網絡,每一個站點作爲節點,站與站是否聯通作爲邊來構成一個圖。

這是一個電子商務的結構圖,業務數據可以用用戶、商品等來作爲節點來描述。它們的關係比較複雜,比方說用戶可以瀏覽一個商品,可以收藏一個商品,可以購買一個商品,那麼這邊就可以進行分類,每個邊代表不同的屬性。在推薦系統中,用戶與商品之間的關係反映了用戶的消費偏好。

圖卷積神經網絡(Graph Convolution Networks)

GCN是一種可以直接作用於圖上的卷積神經網絡,它能夠提取圖的特徵。GCN可以解決圖中節點的分類問題(node classification)。在上圖中有5個節點,誰和誰相連是知道的。每一個節點都有一串數字,對應圖的特徵。我們還知道紅色和藍色的節點各屬於一類。還有2個節點,我們不知道它們屬於哪一類。

我們再來看一下這兩張圖,對於這個公式,如果去掉A',F是特徵,可以用x來代替就是Z=σ(Wx+b),那麼它就是一個CNN中的神經元的計算公式,而這個A‘是圖的結構。其實對於CNN來說,也可以加上這個A',但由於每一個圖像像素彼此之間的聯繫都是一樣的,所以A'可以省略;但是對於GCN來說就不一樣了,因爲每個圖結構節點彼此之間的連接都不一樣。

GCN最後的輸出是這個樣子的,l是層的意思,代表下一層的輸出,其中代表的是A',是當前層。是可訓練參數,至於b就不要了。

在上圖中,我們已知了幾個節點和它們的特徵以及所有藍色節點的label,而綠色節點的標籤是未知的,我們將要判別類別的節點(圖中的綠色節點)周圍的鄰居節點的特徵全部匯聚到該節點以及包含它自己的特徵計算一個平均值,得到了一組新的特徵值,再將這組新的特徵值送入到一個類似CNN的神經網絡中,最後輸出一個二分類的概率值,通過這個概率值來判定該節點屬於哪個分類。

對於GCN的這個神經網絡,我們只能說它是類似於CNN的卷積神經網絡,但它們並不一樣。第一步,對於原始圖結構的每一個節點,我們都會進行一次鄰居節點以及其本身節點的特徵的平均值,也就是說這個平均值的數量是有多少個節點就有多少個平均值。此時我們得到的新的特徵圖的結構跟原始特徵圖是一樣的,只不過每一個節點的特徵值不一樣。然後再來一遍。

GCN中圖的表示

在圖論中我們知道,我們可以用鄰接矩陣和鄰接列表來表示圖,鄰接列表是一種類似於鏈表的數據結構。

這是一個有向無權圖,它用鄰接列表可以表示爲

但是這種鄰接列表是一種計算機數據結構的表達方式,不是一種數學表達,所以我們在GCN中真正要使用的只有鄰接矩陣。鄰接矩陣其實就是一種one-hot編碼

如果在鄰接矩陣中,1的數量比較少,我們稱爲稀疏圖,反之則稱爲稠密圖。如果全是1,則稱爲完全稠密圖。

度矩陣(Degree Matrix)

度矩陣也是一個n*n,但是隻有主對角線上有值的圖的矩陣。度指的是無向圖相鄰頂點的邊數,當然在有向圖中,我們有出度和入度。

上面的這個無向圖的度矩陣爲

這裏需要說明的是,在無向圖中如果存在自環邊,則度數需要加1,因爲它包含了自己的出度和入度,所以1這個節點的度數爲4。

消息傳遞(Message Passing)

在上圖中,我們如何判定A和B節點的關係呢?它們都在一個圖中,並且經過了千絲萬縷的聯繫。

在上面的圖結構中,我們知道了它的鄰接矩陣、度矩陣以及每一個節點的特徵(3*1)。第一步,我們需要更新鄰接矩陣

這裏的A是鄰接矩陣,λI是單位矩陣,是更新後的鄰接矩陣。

因爲原始的鄰接矩陣只有鄰居節點的信息,沒有自己的信息,這樣會造成信息丟失(特徵的丟失),所以這裏需要恢復自己的信息,也就是加上一個單位矩陣。第二步,更新度矩陣

這裏也是原度矩陣加上一個單位矩陣,得到新的度矩陣。然後對新的度矩陣取逆

神經網絡對輸入的數據規模特別敏感,一般來說我們希望對所有的向量進行歸一化處理,就是在本身的矩陣中乘以一個對角線矩陣。這樣就可以減小規模。從數學角度上來說,要根據節點的度數對諸多向量來減少規模,度矩陣本身就是一個對角線矩陣,本身也反映了圖的結構。此時我們直接對度矩陣取逆再做歸一化操作。

在上圖中,我們將新度矩陣的逆與新的鄰接矩陣相乘,再乘以特徵X,就有

在這裏,我們將作爲的一個常數因子(Scale Factor),雖然新鄰接矩陣是一個比較複雜,頗具規模的矩陣,但是新度矩陣的逆是一個對角矩陣,除了對角線有數以外,其他都是0,這樣它們相乘後,實際上就是降低了數據的規模。根據矩陣乘法的性質,它相當於對新鄰接矩陣的每一行做了一次歸一化操作。但是對新鄰接矩陣的每一列卻沒有處理,這時可以再乘以一個

這樣就有了

對新鄰接矩陣的每一列做了一次歸一化處理。這樣就同時對新鄰接矩陣的行和列都做了歸一化處理。由於前後都乘了一次,齊次化了兩次,爲了進一步減小規模,我們對進行開方,上式就變成了

這就是的由來,而A'就是

由於是做分類,最終我們需要再套一個softmax。

同樣,它的損失函數跟CNN是一樣的,爲交叉熵(cross-entropy error)損失函數

GCN的代碼基礎

先從最簡單建圖開始

import dgl
import torch
import networkx as nx
import matplotlib.pyplot as plt

if __name__ == '__main__':

    g = dgl.DGLGraph()
    # 建立一個10個節點的圖
    g.add_nodes(10)
    # 建立一個從1到3指向0的有向邊
    for i in range(1, 4):
        g.add_edge(i, 0)
    # 用列表的方法建立從4到6指向0的有向邊
    src = list(range(4, 7))
    dst = [0] * 3
    g.add_edges(src, dst)
    # 用張量的形式建立從7到9指向0的有向邊
    src = torch.tensor([7, 8, 9])
    g.add_edges(src, 0)
    nx.draw(g.to_networkx(), with_labels=True)
    plt.show()

運行結果

給節點賦特徵值

x = torch.rand(10, 3)
print(x)
# 直接將x設爲圖節點的特徵
g.ndata['x'] = x
print(g.nodes[:].data['x'])

運行結果

tensor([[0.6127, 0.9122, 0.8311],
        [0.5199, 0.1088, 0.8050],
        [0.5773, 0.6138, 0.5709],
        [0.1529, 0.4860, 0.2772],
        [0.7792, 0.6176, 0.2838],
        [0.8339, 0.7008, 0.7178],
        [0.6055, 0.8056, 0.2257],
        [0.1695, 0.1825, 0.0911],
        [0.5644, 0.9415, 0.6474],
        [0.9301, 0.2012, 0.0065]])
tensor([[0.6127, 0.9122, 0.8311],
        [0.5199, 0.1088, 0.8050],
        [0.5773, 0.6138, 0.5709],
        [0.1529, 0.4860, 0.2772],
        [0.7792, 0.6176, 0.2838],
        [0.8339, 0.7008, 0.7178],
        [0.6055, 0.8056, 0.2257],
        [0.1695, 0.1825, 0.0911],
        [0.5644, 0.9415, 0.6474],
        [0.9301, 0.2012, 0.0065]])

修改特徵值

# 修改特徵值
g.nodes[0].data['x'] = torch.zeros(1, 3)
g.nodes[[1, 2, 3]].data['x'] = torch.ones(3, 3)
g.nodes[torch.tensor([4, 5, 6])].data['x'] = torch.zeros((3, 3))
print(g.nodes[:].data['x'])

運行結果

tensor([[0.0000, 0.0000, 0.0000],
        [1.0000, 1.0000, 1.0000],
        [1.0000, 1.0000, 1.0000],
        [1.0000, 1.0000, 1.0000],
        [0.0000, 0.0000, 0.0000],
        [0.0000, 0.0000, 0.0000],
        [0.0000, 0.0000, 0.0000],
        [0.1695, 0.1825, 0.0911],
        [0.5644, 0.9415, 0.6474],
        [0.9301, 0.2012, 0.0065]])
  • 同構圖
import dgl
import torch
import networkx as nx
import matplotlib.pyplot as plt

if __name__ == '__main__':

    u = torch.tensor([0, 0, 0, 1])
    v = torch.tensor([1, 2, 3, 3])
    g = dgl.graph((u, v))
    nx.draw(g.to_networkx(), with_labels=True)
    plt.show()

    g.ndata['x'] = torch.ones(g.num_nodes(), 3)
    print(g.nodes[:].data['x'])

運行結果

tensor([[1., 1., 1.],
        [1., 1., 1.],
        [1., 1., 1.],
        [1., 1., 1.]])
  • 異構圖

import dgl
import torch

if __name__ == '__main__':

    graph_data = {
        ('user0', 'follows', 'user1'): (torch.tensor([0, 1]), torch.tensor([1, 2])),
        ('user0', 'plays', 'game0'): (torch.tensor([0, 2]), torch.tensor([2, 3])),
        ('user0', 'plays', 'game1'): (torch.tensor([0, 3]), torch.tensor([2, 3])),
        ('user1', 'plays', 'game1'): (torch.tensor([1]), torch.tensor([2])),
        ('user1', 'plays', 'game2'): (torch.tensor([3]), torch.tensor([2]))
    }
    # 構建異構圖
    g = dgl.heterograph(graph_data)
    # 打印節點索引
    print(g.nodes('user0'))
    print(g.nodes('game2'))
    # 打印節點類型
    print('ntypes', g.ntypes)
    # 打印邊類型
    print('etypes', g.etypes)
    # 設置節點特徵
    g.nodes['user0'].data['hv'] = torch.ones(4, 2)
    # 打印節點特徵
    print(g.nodes['user0'].data['hv'])

運行結果

tensor([0, 1, 2, 3])
tensor([0, 1, 2])
ntypes ['game0', 'game1', 'game2', 'user0', 'user1']
etypes ['follows', 'plays', 'plays', 'plays', 'plays']
tensor([[1., 1.],
        [1., 1.],
        [1., 1.],
        [1., 1.]])
  • GCN圖神經網絡
import dgl
import dgl.function as fn
import torch
import torch.nn as nn
import torch.nn.functional as F
from dgl.data import citation_graph as citegrh
import networkx as nx
import numpy as np
import time
import matplotlib.pyplot as plt

class NodeApplyModule(nn.Module):
    # 使用全連接層來更新節點特徵
    def __init__(self, in_feats, out_feats, activation):
        super(NodeApplyModule, self).__init__()
        self.linear = nn.Linear(in_feats, out_feats)
        self.activation = activation

    def forward(self, nodes):
        h = self.linear(nodes.data['h'])
        if self.activation is not None:
            h = self.activation(h)
        return {'h': h}

class GCN(nn.Module):

    def __init__(self, in_feats, out_feats, activation):
        super(GCN, self).__init__()
        self.apply_mod = NodeApplyModule(in_feats, out_feats, activation)

    def forward(self, g, feature):
        # 給節點賦特徵值
        g.ndata['h'] = feature
        # 在全部節點上執行消息傳遞並計算平均特徵值
        g.update_all(msg, reduce)
        # 將所有的平均特徵值(2708*1433)送入全連接網絡
        g.apply_nodes(func=self.apply_mod)
        return g.ndata.pop('h')

class Net(nn.Module):

    def __init__(self):
        super(Net, self).__init__()
        self.gcn1 = GCN(1433, 16, F.relu)
        self.gcn2 = GCN(16, 7, None)

    def forward(self, g, features):
        # 2708*16
        x = self.gcn1(g, features)
        # 2708*7
        x = self.gcn2(g, x)
        return x

def load_cora_data():
    data = citegrh.load_cora()
    features = torch.FloatTensor(data.features)
    labels = torch.LongTensor(data.labels)
    train_mask = torch.BoolTensor(data.train_mask)
    test_mask = torch.BoolTensor(data.test_mask)
    g = data.graph
    g.remove_edges_from(nx.selfloop_edges(g))
    g = dgl.DGLGraph(g)
    g.add_edges(g.nodes(), g.nodes())
    print(features.size())
    nx.draw(g.to_networkx(), with_labels=True)
    plt.show()
    return g, features, labels, train_mask, test_mask

def evaluate(model, g, features, labels, mask):
    model.eval()
    with torch.no_grad():
        logits = model(g, features)
        logits = logits[mask]
        labels = labels[mask]
        _, indices = torch.max(logits, dim=1)
        correct = torch.sum(indices == labels)
        return correct.item() * 1.0 / len(labels)

if __name__ == '__main__':

    # 發送節點特徵,用m來標識消息是源節點
    msg = fn.copy_src(src='h', out='m')
    # 鄰居節點和當前節點特徵平均值
    reduce = fn.mean(msg='m', out='h')
    net = Net()
    g, features, labels, train_mask, test_mask = load_cora_data()
    optimizer = torch.optim.Adam(net.parameters(), lr=1e-3)
    dur = []
    for epoch in range(1300):
        if epoch >= 3:
            t0 = time.time()
        logits = net(g, features)
        # 在softmax的基礎上再進行一次log運算
        logp = F.log_softmax(logits, 1)
        # F.nll_loss不包含softmax的交叉熵函數
        # nn.CrossEntropyLoss爲包含softmax的交叉熵函數
        loss = F.nll_loss(logp[train_mask], labels[train_mask])
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        if epoch >= 3:
            dur.append(time.time() - t0)
        acc = evaluate(net, g, features, labels, test_mask)
        print("Epoch {:05d} | loss {:.4f} | Test Acc {:.4f} | Time(s) {:.4f}".format(
            epoch, loss.item(), acc, np.mean(dur)
        ))

運行結果

部分日誌

NumNodes: 2708
  NumEdges: 10556
  NumFeats: 1433
  NumClasses: 7
  NumTrainingSamples: 140
  NumValidationSamples: 500
  NumTestSamples: 1000
torch.Size([2708, 1433])
...
Epoch 01282 | loss 0.0626 | Test Acc 0.7860 | Time(s) 0.0315
Epoch 01283 | loss 0.0625 | Test Acc 0.7860 | Time(s) 0.0315
Epoch 01284 | loss 0.0623 | Test Acc 0.7860 | Time(s) 0.0315
Epoch 01285 | loss 0.0622 | Test Acc 0.7860 | Time(s) 0.0315
Epoch 01286 | loss 0.0621 | Test Acc 0.7860 | Time(s) 0.0315
Epoch 01287 | loss 0.0619 | Test Acc 0.7860 | Time(s) 0.0315
Epoch 01288 | loss 0.0618 | Test Acc 0.7860 | Time(s) 0.0315
Epoch 01289 | loss 0.0617 | Test Acc 0.7860 | Time(s) 0.0315
Epoch 01290 | loss 0.0616 | Test Acc 0.7860 | Time(s) 0.0315
Epoch 01291 | loss 0.0614 | Test Acc 0.7860 | Time(s) 0.0315
Epoch 01292 | loss 0.0613 | Test Acc 0.7860 | Time(s) 0.0315
Epoch 01293 | loss 0.0612 | Test Acc 0.7860 | Time(s) 0.0315
Epoch 01294 | loss 0.0610 | Test Acc 0.7860 | Time(s) 0.0315
Epoch 01295 | loss 0.0609 | Test Acc 0.7860 | Time(s) 0.0315
Epoch 01296 | loss 0.0608 | Test Acc 0.7860 | Time(s) 0.0315
Epoch 01297 | loss 0.0607 | Test Acc 0.7860 | Time(s) 0.0315
Epoch 01298 | loss 0.0605 | Test Acc 0.7860 | Time(s) 0.0315
Epoch 01299 | loss 0.0604 | Test Acc 0.7860 | Time(s) 0.0315

回憶一般的CNN網絡,我們只需要送一張圖片到網絡中去提取特徵圖(feature map),我們可以把圖片理解成原始特徵,而在圖GCN網絡中,我們不僅僅要將原始特徵送進網絡,還要帶上圖的結構才能進行前向運算。

logits = net(g, features)

經過這一步,系統會自動的去計算圖的鄰接矩陣、度矩陣,然後經過一系列的計算去獲取圖卷積神經網絡的最終特徵值再去做softmax進行分類

logp = F.log_softmax(logits, 1)

而在

# 發送節點特徵,用m來標識消息是源節點
msg = fn.copy_src(src='h', out='m')
# 鄰居節點和當前節點特徵平均值
reduce = fn.mean(msg='m', out='h')

以及

def forward(self, g, feature):
    # 給節點賦特徵值
    g.ndata['h'] = feature
    # 在全部節點上執行消息傳遞並計算平均特徵值
    g.update_all(msg, reduce)
    # 將所有的平均特徵值(2708*1433)送入全連接網絡
    g.apply_nodes(func=self.apply_mod)
    return g.ndata.pop('h')

說明,此處是對每一個節點都獲取周圍鄰居的特徵以及自己的特徵來計算一個平均特徵值,再將這些平均特徵值送入到全連接網絡中。2708*1433指的是2708個節點,每個節點都有1433個特徵。它經過了兩次全連接層

self.gcn1 = GCN(1433, 16, F.relu)
self.gcn2 = GCN(16, 7, None)

第一次是將1433個神經元與16個神經元相連,並且帶有激活函數。第二次是將16個神經元與7個神經元相連,因爲做分類,就沒有使用激活函數。但是這裏需要注意的是,它依然是將2708*16個特徵重新放入圖節點中作爲新的特徵值,再進行一次消息傳遞計算平均值才送入全連接層的,這跟第一次是一樣的

當然我們也可以不寫的這麼複雜,使用dgl的內置圖卷積GraphConv一步到位,它的作用跟上面的代碼是一樣的。

import dgl
import torch
import torch.nn as nn
import torch.nn.functional as F
from dgl.data import citation_graph as citegrh
import networkx as nx
import numpy as np
import time
import matplotlib.pyplot as plt
from dgl.nn.pytorch import GraphConv

class Net(nn.Module):

    def __init__(self):
        super(Net, self).__init__()
        self.gcn1 = GraphConv(1433, 16)
        self.gcn2 = GraphConv(16, 7)

    def forward(self, g, features):
        # 2708*16
        x = self.gcn1(g, features)
        x = torch.relu(x)
        # 2708*7
        x = self.gcn2(g, x)
        return x

def load_cora_data():
    data = citegrh.load_cora()
    features = torch.FloatTensor(data.features)
    labels = torch.LongTensor(data.labels)
    train_mask = torch.BoolTensor(data.train_mask)
    test_mask = torch.BoolTensor(data.test_mask)
    g = data.graph
    g.remove_edges_from(nx.selfloop_edges(g))
    g = dgl.DGLGraph(g)
    g.add_edges(g.nodes(), g.nodes())
    print(features.size())
    nx.draw(g.to_networkx(), with_labels=True)
    plt.show()
    return g, features, labels, train_mask, test_mask

def evaluate(model, g, features, labels, mask):
    model.eval()
    with torch.no_grad():
        logits = model(g, features)
        logits = logits[mask]
        labels = labels[mask]
        _, indices = torch.max(logits, dim=1)
        correct = torch.sum(indices == labels)
        return correct.item() * 1.0 / len(labels)

if __name__ == '__main__':

    net = Net()
    g, features, labels, train_mask, test_mask = load_cora_data()
    optimizer = torch.optim.Adam(net.parameters(), lr=1e-3)
    dur = []
    for epoch in range(1300):
        if epoch >= 3:
            t0 = time.time()
        logits = net(g, features)
        # 在softmax的基礎上再進行一次log運算
        logp = F.log_softmax(logits, 1)
        # F.nll_loss不包含softmax的交叉熵函數
        # nn.CrossEntropyLoss爲包含softmax的交叉熵函數
        loss = F.nll_loss(logp[train_mask], labels[train_mask])
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        if epoch >= 3:
            dur.append(time.time() - t0)
        acc = evaluate(net, g, features, labels, test_mask)
        print("Epoch {:05d} | loss {:.4f} | Test Acc {:.4f} | Time(s) {:.4f}".format(
            epoch, loss.item(), acc, np.mean(dur)
        ))

圖時空網絡

行爲識別與分析(Human Acitivity Recognition)

行爲識別與分析的目的是爲了從未知的視頻幀中識別出人體動作的類型,它是一種分類任務。

時空圖卷積網絡(Spatial Temporal Graph Convolution Networks)

時空圖卷積網絡簡稱ST-GCN,訓練幀非常多,導致非常耗能,考慮計算力的話,並不是一種特別好的方法。但它的思想值得借鑑。它是基於一系列的骨架圖來做分析。它的輸入是一系列連續幀的圖,然後進行姿態估計。姿態估計的介紹可以參考人體姿態檢測概述 。通過姿態估計得到關鍵點,之後通過這一系列的關鍵點,最後對行爲進行分類(Action Classification),在上圖的最右邊,我們看到跑的概率是最大的,我們就把這一系列的圖像幀定義爲"跑"。

  • ST-GCN

G = (V, E)

節點:人體關節點

邊:有兩種連接方法,一起構成了整個圖的邊

  1. 根據空間結構構建的人體骨骼連接,這是自然的邊
  2. 在時間序列上,將相鄰兩幀的相同關節點連接起來,構成時序邊

一般來說,圖有三大任務:1、節點分類;2、圖分類;3、邊預測。

  • 分區策略(Partitioning Strategies)

分區策略是爲了構造卷積運算,比如

對於一張骨骼幀,身體關鍵點是用藍色點表示的,焦點的感受野是用範圍爲1的區域紅色虛線表示。紅色的點稱爲焦點。

進一步,我們將所有焦點的鄰居節點以及焦點本身標記爲相同顏色的節點,上圖爲綠色。這裏稱爲Uni-labeling partitioning strategy。

分區距離,我們將焦點本身的距離定義爲0,而鄰居節點的距離定義爲1。

空間配置分區,所有的鄰居節點會根據焦點(綠色的點)到重心點(黒叉)的距離比較來劃分,向心距離短的會被劃分爲藍色,向心距離長的會被劃分爲黃色。

我們把所有關節點的平均座標作爲骨架重心。但是一般如果所有關節點中在胸部有點的話,我們一般將胸部作爲重心點。但是如果沒有胸部關節點的話,我們一般採用脖子作爲重心點。在同一片焦點區域中,焦點到重心點的向心距離,我們定義爲0;而焦點的鄰居節點到重心點的距離如果小於焦點到重心點的距離,則該向心距離,我們定義爲1;焦點的鄰居節點到重心點的距離如果大於焦點到重心點的距離,則該向心距離,我們定義爲2;這裏需要注意向心距離不是實際距離,它們是兩個概念。

動結圖卷積網絡(Actional-Structural Graph Convolutional Networks)

動結圖卷積網絡簡稱AS-GCN。

上圖是一個基於時間幀的動作輸入,它分成了動作的連接(Actional Links)和結構的連接(Structural Links)。在右邊的部分成了兩部分,一部分是AS-GCN,一部分是ST-GCN。但不論是哪部分,每個關節點都有一個紅色的圓的區域,這個圓的區域代表着當前關節點運動的強烈程度,如果該圓的紅色區域越大,顏色越深就代表該關節點的運動越強烈。對於圖中的矩形框框住的手和腳進行比較,我們會發現對於AS-GCN來說,它的運動比較強烈,而對於ST-GCN來說,它的運動很小。

對於動作的連接和結構的連接來說,結構的連接很好理解,它就是關節點的連線。而對於動作的連接來說,藍色的線代表的是骨骼節點的連線,而黃色的線代表人體物理上不同節點之間的依賴關係,如果黃線越粗代表兩個節點之間的關係越強,如果黃線越細代表在當前動作下兩個節點之間的聯繫越淺。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章