Graph
GraphSAGE
上图出自2017的《Inductive representation learning on large graphs》。首先GNN的学习方法可以分为两种:
而GraphSAGE则是归纳式学习的一个代表,不学习每个节点的固定表示,而是学习节点之间的聚合函数,即节点的信息是通过其邻居节点的特征聚合而来的,所以仅需邻域即可处理新节点了。那么如何学习聚合函数?
多种聚合方式:
#作者实验中 K=2,即聚合两跳内邻居特征;一跳采样邻居数S1=25,二跳采样邻居S2=10个节点:采样随机游走步长为 5 的且50 次,
负采样采样 20 个。
def aggregate(self, samples, input_features, dims, num_samples, support_sizes, batch_size=None,
aggregators=None, name=None, concat=False, model_size="small"):
if batch_size is None:
batch_size = self.batch_size
hidden = [tf.nn.embedding_lookup(input_features, node_samples) for node_samples in samples]
new_agg = aggregators is None
if new_agg:
aggregators = []
for layer in range(len(num_samples)):
if new_agg:
dim_mult = 2 if concat and (layer != 0) else 1
#聚合当前层
if layer == len(num_samples) - 1:
aggregator = self.aggregator_cls(dim_mult*dims[layer], dims[layer+1], act=lambda x : x,
dropout=self.placeholders['dropout'],
name=name, concat=concat, model_size=model_size)
else:
aggregator = self.aggregator_cls(dim_mult*dims[layer], dims[layer+1],
dropout=self.placeholders['dropout'],
name=name, concat=concat, model_size=model_size)
aggregators.append(aggregator)
else:
aggregator = aggregators[layer]
#多跳
next_hidden = []
#层数增加,采样邻居数逐渐减少
for hop in range(len(num_samples) - layer):
dim_mult = 2 if concat and (layer != 0) else 1
#support邻居
neigh_dims = [batch_size * support_sizes[hop],
num_samples[len(num_samples) - hop - 1],
dim_mult*dims[layer]]
h = aggregator((hidden[hop],
tf.reshape(hidden[hop + 1], neigh_dims)))
next_hidden.append(h)
#下一跳
hidden = next_hidden
return hidden[0], aggregators
完整的细节源代码逐行中文注释:https://github.com/nakaizura/Source-Code-Notebook/tree/master/GraphSAGE
GAT
Bengio ICLR2018年的《Graph Attention Network 》。按照Transductive learning的方式,需要对所有节点得到结果,所以训练好的GCN是无法用在不一样的图结构上的。GAT是另一种解决方案,即Attention。一般来说GCN分为两大类,一种是谱图卷积,是在傅里叶域进行卷积变换;另一种是非谱图卷积(也叫做“空间域卷积”),是直接在Graph上进行卷积,GAT就属于空间域卷积,而且放松了邻点个数和边的权重这两个限制条件图。
Attention的特点是自适应,所以即便图结构不一样,度数不相同,也可以通过动态计算邻域节点和该节点的强度关系weight来适应,所以GAT可移植到其他的图结构上,不需要与训练集完全相同。
对于节点特征向量 h = { h 1 , h 2 . . . h n } h=\{h_1,h_2...h_n\} h={h1,h2...hn},最后需要更新得到 h ′ = { h 1 ′ , h 2 ′ . . . h n ′ } h'=\{h'_1,h'_2...h'_n\} h′={h1′,h2′...hn′}。计算领域Attention使用MLP方法,即公式为: e i j = a T [ W h i ∣ ∣ W h j ] e_{ij}=a^T[Wh_i||Wh_j] eij=aT[Whi∣∣Whj] α i j = s o f t m a x ( e i j ) \alpha_{ij}=softmax(e_{ij}) αij=softmax(eij)该式计算,节点 j 对于节点 i 的重要性,a是MLP的权重,W是所有节点之间的权值(抽象特征,从h到h’的线性转换)。由上图左边就是W和a权重的区别和Attention的计算过程,清晰易懂。
由于使用的是masked attention,即只算邻居向量,所以不会关心整个图的结构问题,也就解决了问题(同时也是缺点)。最后乘以权重得到结果:
h i ′ = σ ( ∑ α i j W h j ) h'_i=\sigma(\sum \alpha_{ij}Wh_j) hi′=σ(∑αijWhj)
def attn_head(seq, out_sz, bias_mat, activation, in_drop=0.0, coef_drop=0.0, residual=False):
with tf.name_scope('my_attn'):
if in_drop != 0.0:
seq = tf.nn.dropout(seq, 1.0 - in_drop)#seq是原始的节点
seq_fts = tf.layers.conv1d(seq, out_sz, 1, use_bias=False)#一维卷积模拟nn投影
#自注意力self-attention
f_1 = tf.layers.conv1d(seq_fts, 1, 1) #Wh,一维卷积模拟投影
f_2 = tf.layers.conv1d(seq_fts, 1, 1) #Wh
logits = f_1 + tf.transpose(f_2, [0, 2, 1]) #拼接,注意力矩阵
#bias_mat是邻居mask矩阵,不是邻居的会有无限小(exp时就为0)mask
coefs = tf.nn.softmax(tf.nn.leaky_relu(logits) + bias_mat) #softmax分配权重
if coef_drop != 0.0:
coefs = tf.nn.dropout(coefs, 1.0 - coef_drop)
if in_drop != 0.0:
seq_fts = tf.nn.dropout(seq_fts, 1.0 - in_drop)
vals = tf.matmul(coefs, seq_fts) #分配值
ret = tf.contrib.layers.bias_add(vals)
# residual connection,残差连接保持特征一定的不变性
if residual:
if seq.shape[-1] != ret.shape[-1]: #维度不等就再投影一下
ret = ret + conv1d(seq, ret.shape[-1], 1) # activation
else:
ret = ret + seq #相加
return activation(ret) # activation
完整的细节源代码逐行中文注释:https://github.com/nakaizura/Source-Code-Notebook/tree/master/GAT
非对称的注意权重
比较以下两个注意力权重公式。
e i j = a ( W h i , W h j ) e_{ij}=a(Wh_i,Wh_j) eij=a(Whi,Whj) e i j = a T [ W h i ∣ ∣ W h j ] e_{ij}=a^T[Wh_i||Wh_j] eij=aT[Whi∣∣Whj]这里我之间奇怪了好久,为什么不直接算相似度计算注意力呢?非要拼接之后计算e。理由是这里拼接导致 e i j e_{ij} eij和 e i j e_{ij} eij是不等的 ,也就是说注意力值是非对称的!非对称的有点在于,我与大佬和大佬看我是完全不一样的东西,本来这种相互关系就是不对等。
LeakyReLU
这里同样也是为了非对称!首先按公式,拼接的部分是可以拆开的:
e x p ( a T [ W h i ∣ ∣ W h j ] ) = e x p ( a 1 T W h i ) e x p ( a 2 T W h j ) exp(a^T[Wh_i || Wh_j])=exp(a_1^TWh_i)exp(a_2^TWh_j) exp(aT[Whi∣∣Whj])=exp(a1TWhi)exp(a2TWhj)那么如果不用LeakyReLU激活,直接softmax归一化话:
α = e x p ( a 1 T W h i ) e x p ( a 2 T W h j ) ∑ k ∈ N i e x p ( a 1 T W h i ) e x p ( a 2 T W h k ) = e x p ( a 2 T W h j ) ∑ k ∈ N i e x p ( a 2 T W h k ) \alpha=\frac{exp(a_1^TWh_i)exp(a_2^TWh_j)}{\sum_{k \in N_i} exp(a_1^TWh_i)exp(a_2^TWh_k)}=\frac{exp(a_2^TWh_j)}{\sum_{k \in N_i} exp(a_2^TWh_k)} α=∑k∈Niexp(a1TWhi)exp(a2TWhk)exp(a1TWhi)exp(a2TWhj)=∑k∈Niexp(a2TWhk)exp(a2TWhj)可以看到a1被约掉了。节点(i,j)之间的注意力权重都将不会考虑节点i的表示,所以LeakyReLU这也为了防止被约掉。
Transformer 与 GAT
两者都是用了self-attention,都是找关联,算注意力权重,再通过对上下文或者节点邻居聚合,最后得到节点or词的表示。嘛,虽然说Transformer其实是隐式图的暴力解,还是会有一点点不同: