5.2 《Semi-Supervised Classification with Graph Convolutional Networks》
這篇論文受到譜圖卷積的局部一階近似可以用于對局部圖結(jié)構(gòu)與節(jié)點的特征進行編碼從而確定卷積網(wǎng)絡(luò)結(jié)構(gòu)的啟發(fā)压怠,提出了一種可擴展的圖卷積的實現(xiàn)方法搪花,可用于具有圖結(jié)構(gòu)數(shù)據(jù)的半監(jiān)督學(xué)習(xí)
5.2.1 GCN定義
按照圖傅里葉變換的性質(zhì)摘投,可以得到如下圖卷積的定義:
其中:
- 對于圖
的傅里葉變換為的傅里葉變換為的傅里葉變換為
- 對于卷積核的圖傅里葉變換:
其中
,按照矩陣形式就是
- 對兩者的傅里葉變換向量
和
求
element-wise乘積
,等價于將組織成對角矩陣此改,即
串塑,然后再求
和
矩陣乘法
- 求上述結(jié)果的傅里葉逆變換战转,即左乘
深度學(xué)習(xí)中的卷積就是要設(shè)計trainable的卷積核躬审,從上式可以看出枉证,就是要設(shè)計矮男,由此,可以直接將其變?yōu)榫矸e核
室谚,而不需要再將卷積核進行傅里葉變換毡鉴,由此,相當(dāng)于直接將變換后的參量進行學(xué)習(xí)
第一代GCN
其中秒赤,就是graph上對應(yīng)每個節(jié)點的feature構(gòu)成的向量,
入篮,這里暫時對每個節(jié)點都使用標(biāo)量陈瘦,然后經(jīng)過激活之后,得到輸出
潮售,之后傳入下一層
一些缺點:
- 需要對拉普拉斯矩陣進行譜分解來求
痊项,在graph很大的時候復(fù)雜度很高。另外酥诽,還需要計算矩陣乘積鞍泉,復(fù)雜度為
- 卷積核參數(shù)為
,當(dāng)graph很大的時候肮帐,
會很大
- 卷積核的spatial localization不好
第二代GCN
圖傅里葉變換是關(guān)于特征值(相當(dāng)于普通傅里葉變換的頻率)的函數(shù)咖驮,也就是,即
训枢,因此游沿,將卷積核
寫成
,然后肮砾,將
定義為如下k階多項式:
將卷積公式帶入,可以得到:
這一代的GCN不需要做特征分解袋坑,可以直接對Laplacian矩陣做變換仗处,通過事先將Laplacian矩陣求出來,以及求出來枣宫,前向傳播的時候婆誓,可以直接使用,復(fù)雜度為
對于每一次Laplacian矩陣和
相乘也颤,對于節(jié)點
洋幻,相當(dāng)于從鄰居節(jié)點
傳遞一次信息給節(jié)點
,由于連續(xù)乘以了
次Laplacian矩陣翅娶,那么相當(dāng)于n節(jié)點的k-hop之內(nèi)的節(jié)點能夠傳遞信息給
文留,因此好唯,實際上只利用了節(jié)點的K-Localized信息
另外,可以使用切比雪夫展開式來近似燥翅,任何k次多項式都可以使用切比雪夫展開式來近似骑篙,由此,引入切比雪夫多項式的
階截斷獲得
近似森书,從而獲得對
的近似
其中靶端,,
為切比雪夫向量凛膏,
為第
個分量杨名,切比雪夫多項式
使用遞歸的方式進行定義:
,其中猖毫,
台谍,此時,帶入到卷積公式:
其中鄙麦,典唇,因此,可以得到輸出為:
第三代GCN
直接取切比雪夫多項式中胯府,此時模型是1階近似介衔,將
,
帶入可以得到:
其中骂因,歸一化拉普拉斯矩陣炎咖。為了進一步簡化,令
寒波,此時只含有一個參數(shù)
由于的譜半徑
太大乘盼,使用歸一化的trick:
其中,俄烁,
由此绸栅,帶入卷積公式
如果推廣到多通道,相當(dāng)于每一個節(jié)點的信息是向量
其中页屠,是節(jié)點數(shù)量粹胯,
是通道數(shù),或者稱作表示節(jié)點的信息維度數(shù)辰企。
是節(jié)點的特征矩陣风纠。相應(yīng)的卷積核參數(shù)變化:
其中,為卷積核數(shù)量牢贸,那么卷積結(jié)果寫成矩陣形式為:
上述操作可以疊加多層竹观,對上述輸出激活一下,就可以作為下一層節(jié)點的特征矩陣
一些特點:
- 取
,相當(dāng)于直接取鄰域信息臭增,類似于
的卷積核
- 由于卷積核寬度減小懂酱,可以通過增加卷積層數(shù)來擴大感受野,從而增強網(wǎng)絡(luò)的表達(dá)能力
- 增加了參數(shù)約束速址,比如
玩焰,引入歸一化操作
5.2.2 論文模型
論文采用兩層的GCN,用來在graph上進行半監(jiān)督的節(jié)點分類任務(wù)芍锚,鄰接矩陣為昔园,首先計算出
,由此并炮,前向網(wǎng)絡(luò)模型形式如下:
其中默刚,為輸入層到隱藏層的權(quán)重矩陣,隱藏層的特征維度為
逃魄,
為隱藏層到輸出層的權(quán)重矩陣荤西,softmax激活函數(shù)定義為
,
伍俘,相當(dāng)于對每一列做softmax邪锌,由此得到交叉熵?fù)p失函數(shù)為:
其中,為帶有標(biāo)簽的節(jié)點集合
5.2.3 代碼實現(xiàn)
import torch_geometric.nn as gnn
class GCN(nn.Module):
def __init__(self, config, in_channels, out_channels):
'''
in_channels : num of node features
out_channels: num of class
'''
super().__init__()
self.config = config
self.hidden_dim = config.hidden_dim
self.dropout_rate = config.dropout_rate
self.conv1 = gnn.GCNConv(in_channels, self.hidden_dim, improved = False, cached=True, bias=True, normalize=True)
self.conv2 = gnn.GCNConv(self.hidden_dim, out_channels, improved = False, cached=True, bias=True, normalize=True)
def forward(self, data):
x, edge_index = data.x, data.edge_index
x = self.conv1(x, edge_index)
x = F.relu(x)
#x = F.dropout(x, p=self.dropout_rate) # If no drop out, accuracy 0.75 --> 0.80
x = self.conv2(x, edge_index)
#x = F.dropout(x, p=self.dropout_rate) # there are two dropout.. But performance bad.
return x
5.3 《Diffusion-Convolutional Neural Networks》
該模型對每一個節(jié)點(或邊癌瘾、或圖)采用H個hop的矩陣進行表示觅丰,每一個hop都表示該鄰近范圍的鄰近信息,由此妨退,對于局部信息的獲取效果比較好妇萄,得到的節(jié)點的representation的表示能力很強
5.3.1 任務(wù)定義
- 一個graph數(shù)據(jù)集
- graph定義為
,其中咬荷,
為節(jié)點集合冠句,
為邊集合
- 所有節(jié)點的特征矩陣定義為
,大小為
幸乒,其中懦底,
為圖
的節(jié)點個數(shù),
為節(jié)點特征維度
- 邊信息
定義為
的鄰接矩陣
罕扎,由此可以計算出節(jié)點度(degree)歸一化的轉(zhuǎn)移概率矩陣
基茵,表示從
節(jié)點轉(zhuǎn)移到
節(jié)點的概率
模型的目標(biāo)為預(yù)測,也就是預(yù)測每一個圖的節(jié)點標(biāo)簽壳影,或者邊的標(biāo)簽,或者每一個圖的標(biāo)簽弥臼,在每一種情況中宴咧,模型輸入部分帶有標(biāo)簽的數(shù)據(jù)集合,然后預(yù)測剩下的數(shù)據(jù)的標(biāo)簽径缅。DCNN模型輸入圖
掺栅,返回硬分類預(yù)測值
或者條件分布概率
烙肺。該模型將每一個預(yù)測的目標(biāo)對象(節(jié)點、邊或圖)轉(zhuǎn)化為一個
diffusion-convolutional representation
氧卧,大小為桃笙,
表示擴散的hops,表示為
- 對于節(jié)點分類任務(wù)沙绝,表示為
為大小為
的矩陣
- 對于圖分類任務(wù)搏明,張量
為大小為
的矩陣
- 對于邊分類任務(wù),張量
為大小為
的矩陣
5.3.2 論文模型
-
對于節(jié)點分類任務(wù)闪檬,假設(shè)
為
的power series星著,大小為
,那么對于圖
的節(jié)點
粗悯,第
個hop虚循,第
維特征值
計算公式為:
使用矩陣表示為:
其中表示
element-wise multiplication
,由于模型只考慮跳的參數(shù)样傍,即參數(shù)量為
横缔,使得
diffusion-convolutional representation
不受輸入大小的限制在計算出
之后,過一層全連接得到輸出
衫哥,使用
表示硬分類預(yù)測結(jié)果茎刚,使用
表示預(yù)測概率,計算方式如下:
對于圖分類任務(wù)炕檩,直接采用所有節(jié)點表示的均值作為graph的representation
其中斗蒋,是全為1的
的向量
對于邊分類任務(wù),通過將每一條邊轉(zhuǎn)化為一個節(jié)點來進行訓(xùn)練和預(yù)測笛质,這個節(jié)點與原來的邊對應(yīng)的首尾節(jié)點相連泉沾,轉(zhuǎn)化后的圖的鄰接矩陣
可以直接從原來的鄰接矩陣
增加一個
incidence matrix
得到:
之后,使用來計算
妇押,并用來替換
來進行分類跷究,對于模型訓(xùn)練,使用梯度下降法敲霍,并采用early-stop方式得到最終模型
5.3.3 代碼實現(xiàn)
import lasagne
import lasagne.layers
import theano
import theano.tensor as T
import numpy as np
class DCNNLayer(lasagne.layers.MergeLayer):
"""A node-level DCNN layer.
This class contains the (symbolic) Lasagne internals for a node-level DCNN layer. This class should
be used in conjunction with a user-facing model class.
"""
def __init__(self, incomings, parameters, layer_num,
W=lasagne.init.Normal(0.01),
num_features=None,
**kwargs):
super(DCNNLayer, self).__init__(incomings, **kwargs)
self.parameters = parameters
if num_features is None:
self.num_features = self.parameters.num_features
else:
self.num_features = num_features
self.W = T.addbroadcast(
self.add_param(W,
(1, parameters.num_hops + 1, self.num_features), name='DCNN_W_%d' % layer_num), 0)
self.nonlinearity = params.nonlinearity_map[self.parameters.dcnn_nonlinearity]
def get_output_for(self, inputs, **kwargs):
"""Compute diffusion convolutional activation of inputs."""
Apow = inputs[0]
X = inputs[1]
Apow_dot_X = T.dot(Apow, X)
Apow_dot_X_times_W = Apow_dot_X * self.W
out = self.nonlinearity(Apow_dot_X_times_W)
return out
def get_output_shape_for(self, input_shapes):
"""Return the layer output shape."""
shape = (None, self.parameters.num_hops + 1, self.num_features)
return shape
5.4 《Improved Semantic Representations From Tree-Structured Long Short-Term Memory Networks》
將序列型的LSTM模型擴展到樹型的LSTM模型俊马,簡稱Tree-LSTM,并根據(jù)孩子節(jié)點是否有序肩杈,論文提出了兩個模型變體柴我,
Child-Sum Tree-LSTM
模型和N-ary Tree-LSTM
模型。和序列型的LSTM模型的主要不同點在于扩然,序列型的LSTM從前一時刻獲取隱藏狀態(tài)艘儒,而樹型的LSTM從其所有的孩子節(jié)點獲取隱藏狀態(tài)
5.4.1 論文模型
Tree-LSTM模型對于每一個孩子節(jié)點都會產(chǎn)生一個遺忘門
¥f_{jk}¥,這個使得模型能夠從所有的孩子節(jié)點選擇性地獲取信息和結(jié)合信息
-
Child-Sum Tree-LSTMs
該模型的更新方程如下:
其中,表示
節(jié)點的鄰居節(jié)點的個數(shù)界睁,
表示節(jié)點
的隱藏狀態(tài)觉增,
表示節(jié)點
的
輸入門
,表示節(jié)點
的鄰居節(jié)點
的
遺忘門
翻斟,表示節(jié)點
的
輸出門
這里的關(guān)鍵點在于第三個公式的
薯嗤,這個模型對節(jié)點
的每個鄰居節(jié)點
都計算了對應(yīng)的
遺忘門
向量哼凯,然后在第六行中計算時對鄰居節(jié)點的信息進行
遺忘
和組合
由于該模型是對所有的孩子節(jié)點求和,所以這個模型對于節(jié)點順序不敏感的,適合于孩子節(jié)點無序的情況
N-ary Tree-LSTMs
假如一個樹的最大分支數(shù)為(即孩子節(jié)點最多為
個)同规,而且孩子節(jié)點是有序的盏浇,對于節(jié)點
嬉愧,對于該節(jié)點的第
個孩子節(jié)點的隱藏狀態(tài)和記憶單元分別用
和
表示曼氛,模型的方程如下:
該模型為每個孩子節(jié)點都單獨地設(shè)置了參數(shù)
5.4.2 訓(xùn)練策略
- 分類任務(wù):
分類任務(wù)定義為在類別集中預(yù)測出正確的標(biāo)簽
,對于每一個節(jié)點
阳柔,使用一個softmax分類器來預(yù)測節(jié)點標(biāo)簽
焰枢,分類器取每個節(jié)點的隱藏狀態(tài)
作為輸入:
損失函數(shù)使用negative log-likelihood
:
其中,是帶有標(biāo)簽的節(jié)點數(shù)量舌剂,
是
是正則化超參
- 語義相關(guān)性任務(wù):
該任務(wù)給定一個句子對(sentence pair)济锄,模型需要預(yù)測出一個范圍在之間的實數(shù)值,這個值越高霍转,表示相似度越高荐绝。首先對每一個句子產(chǎn)生一個representation,兩個句子的表示分別用
和
表示避消,得到這兩個representation之后低滩,從distance和angle兩個方面考慮,使用神經(jīng)網(wǎng)絡(luò)來得到相似度:
其中岩喷,恕沫,模型期望根據(jù)訓(xùn)練得到的參數(shù)
得到的結(jié)果:
。由此纱意,定義一個目標(biāo)分布
:
其中婶溯,,損失函數(shù)為
和
之間的KL散度:
5.4.3 代碼實現(xiàn)
class TreeLSTM(torch.nn.Module):
'''PyTorch TreeLSTM model that implements efficient batching.
'''
def __init__(self, in_features, out_features):
'''TreeLSTM class initializer
Takes in int sizes of in_features and out_features and sets up model Linear network layers.
'''
super().__init__()
self.in_features = in_features
self.out_features = out_features
# bias terms are only on the W layers for efficiency
self.W_iou = torch.nn.Linear(self.in_features, 3 * self.out_features)
self.U_iou = torch.nn.Linear(self.out_features, 3 * self.out_features, bias=False)
# f terms are maintained seperate from the iou terms because they involve sums over child nodes
# while the iou terms do not
self.W_f = torch.nn.Linear(self.in_features, self.out_features)
self.U_f = torch.nn.Linear(self.out_features, self.out_features, bias=False)
def forward(self, features, node_order, adjacency_list, edge_order):
'''Run TreeLSTM model on a tree data structure with node features
Takes Tensors encoding node features, a tree node adjacency_list, and the order in which
the tree processing should proceed in node_order and edge_order.
'''
# Total number of nodes in every tree in the batch
batch_size = node_order.shape[0]
# Retrive device the model is currently loaded on to generate h, c, and h_sum result buffers
device = next(self.parameters()).device
# h and c states for every node in the batch
h = torch.zeros(batch_size, self.out_features, device=device)
c = torch.zeros(batch_size, self.out_features, device=device)
# populate the h and c states respecting computation order
for n in range(node_order.max() + 1):
self._run_lstm(n, h, c, features, node_order, adjacency_list, edge_order)
return h, c
def _run_lstm(self, iteration, h, c, features, node_order, adjacency_list, edge_order):
'''Helper function to evaluate all tree nodes currently able to be evaluated.
'''
# N is the number of nodes in the tree
# n is the number of nodes to be evaluated on in the current iteration
# E is the number of edges in the tree
# e is the number of edges to be evaluated on in the current iteration
# F is the number of features in each node
# M is the number of hidden neurons in the network
# node_order is a tensor of size N x 1
# edge_order is a tensor of size E x 1
# features is a tensor of size N x F
# adjacency_list is a tensor of size E x 2
# node_mask is a tensor of size N x 1
node_mask = node_order == iteration
# edge_mask is a tensor of size E x 1
edge_mask = edge_order == iteration
# x is a tensor of size n x F
x = features[node_mask, :]
# At iteration 0 none of the nodes should have children
# Otherwise, select the child nodes needed for current iteration
# and sum over their hidden states
if iteration == 0:
iou = self.W_iou(x)
else:
# adjacency_list is a tensor of size e x 2
adjacency_list = adjacency_list[edge_mask, :]
# parent_indexes and child_indexes are tensors of size e x 1
# parent_indexes and child_indexes contain the integer indexes needed to index into
# the feature and hidden state arrays to retrieve the data for those parent/child nodes.
parent_indexes = adjacency_list[:, 0]
child_indexes = adjacency_list[:, 1]
# child_h and child_c are tensors of size e x 1
child_h = h[child_indexes, :]
child_c = c[child_indexes, :]
# Add child hidden states to parent offset locations
_, child_counts = torch.unique_consecutive(parent_indexes, return_counts=True)
child_counts = tuple(child_counts)
parent_children = torch.split(child_h, child_counts)
parent_list = [item.sum(0) for item in parent_children]
h_sum = torch.stack(parent_list)
iou = self.W_iou(x) + self.U_iou(h_sum)
# i, o and u are tensors of size n x M
i, o, u = torch.split(iou, iou.size(1) // 3, dim=1)
i = torch.sigmoid(i)
o = torch.sigmoid(o)
u = torch.tanh(u)
# At iteration 0 none of the nodes should have children
# Otherwise, calculate the forget states for each parent node and child node
# and sum over the child memory cell states
if iteration == 0:
c[node_mask, :] = i * u
else:
# f is a tensor of size e x M
f = self.W_f(features[parent_indexes, :]) + self.U_f(child_h)
f = torch.sigmoid(f)
# fc is a tensor of size e x M
fc = f * child_c
# Add the calculated f values to the parent's memory cell state
parent_children = torch.split(fc, child_counts)
parent_list = [item.sum(0) for item in parent_children]
c_sum = torch.stack(parent_list)
c[node_mask, :] = i * u + c_sum
h[node_mask, :] = o * torch.tanh(c[node_mask])