前面几次的整理GCN,GAT,GraphSAGE等等都适合在半监督,监督的场景下,而有没有图方法可以使用于在无监督的场景下使用呢?去发现节点的内在结果,挖掘隐藏关系如链接预测等等任务。
答案是:自编码器(AE) /变分自编码器(VAE)+Graph
Graph Auto-Encoders (GAE)
GAE的目的是通过encoder-decoder 的结构去获取到图中节点的 embedding,然后再去做具体的下游任务比如链接预测。
首先回顾一下自编码器,它是利用神经网络将数据逐层降维压缩,相当于让每层神经网络之间的激活函数就起到了将"线性"转化为"非线性"的作用,然后尝试还原输入即让输出==输出以捕捉到数据的隐含信息。整体的结构分编码器Decoder和解码器Encoder,编码器负责压缩,解码器负责还原。同样的,想要在Graph上也完成这种操作,也是使用encoder-decoder 的结构,具体操作如上图:
-
Encoder。直接使用 GCN 作为 encoder,来得到节点的 latent representations(即关于每个节点的 embedding)
Z = G C N ( A ) Z=GCN(A) Z=GCN(A) 其中A是邻接矩阵,Z代表的就是所有节点的表示,如果接下来要做下游任务也是直接使用这个表示就可以。 -
Decoder。这里和原来的AE不一样,不是对称结构的网络,而是直接采用内积 inner-product 作为 decoder 来重构(reconstruct)原始的Graph
A ′ = σ ( Z T Z ) A'=\sigma (Z^TZ) A′=σ(ZTZ)这里的A’就是重构(reconstruct)出来的邻接矩阵,可以被理解为两个节点的独立事件概率相乘。 -
最后的目标是使重构出的邻接矩阵与原始的邻接矩阵尽可能的相似,因为邻接矩阵决定了图的结构。所以直接采用交叉熵作为损失函数衡量A和A’就可以了
L = − 1 N ∑ y l o g y ′ + ( 1 − y ) l o g ( 1 − y ′ ) L=-\frac{1}{N}\sum ylogy'+(1-y)log(1-y') L=−N1∑ylogy′+(1−y)log(1−y′)
其中y代表邻接矩阵 A 中某个元素的值(0 或 1),y’ 代表重构的邻接矩阵A’中相应元素的值(概率值)。
pytorch_geomatric的实现:
class EncoderGCN(nn.Module): #编码器
def __init__(self, n_total_features, n_latent, p_drop=0.):
super(EncoderGCN, self).__init__()
self.n_total_features = n_total_features
self.conv1 = GCNConv(self.n_total_features, 11)
self.act1=nn.Sequential(nn.ReLU(),
nn.Dropout(p_drop))
self.conv2 = GCNConv(11, 11)
self.act2 = nn.Sequential(nn.ReLU(),
nn.Dropout(p_drop))
self.conv3 = GCNConv(11, n_latent)
def forward(self, data): #实践中一般采取多层的GCN来编码
x, edge_index = data.x, data.edge_index
x = self.act1(self.conv1(x, edge_index))
x = self.act2(self.conv2(x, edge_index))
x = self.conv3(x, edge_index) #经过三层GCN后得到节点的表示
return x
class DecoderGCN(nn.Module): #解码器
def __init__(self):
super(DecoderGCN, self).__init__()
def forward(self, z):
A = torch.mm(z, torch.t(z)) #直接算点积
A = torch.sigmoid(A) #映射成分数
return A
完整逐行的中文源码阅读笔记可以参考:https://github.com/nakaizura/Source-Code-Notebook/tree/master/GAE
GAE和AE的区别
- GAE在encoder过程中使用了 n∗n 矩阵的卷积核
- GAE在decoder部分实际上没有解码,直接计算内积算邻接矩阵的相似度,然后用loss来约束
GVAE
上面的 GAE 用于重建(数据压缩和还原)效果还不错,但是如果用于直接的图生成就不够了,所以同样的AE不行,那VAE来试一下。VGAE 的思想和变分自编码器(VAE)很像,博主已经仔细推导过就不再赘述,大致的想法是:利用隐变量(latent variables),让模型学习出一些分布(distribution),再从这些分布中采样得到z,通过这样的z就会有多样化的结果,而不仅仅是还原,重建。
如上图,其与GAE的不同只在Encoder的部分,后面的Decoder还是用内积基本是一样的,对于编码器即在GAE中是直接使用GCN作为编码器,它是一个确定的函数所以只能得到确定的结果。而在VGAE中,不再使用这样的函数得到Z,而是从一个多维的高斯分布中采样得到,即用GCN确定分布,再从分布中采样Z。
- Encoder。而这样的分布,使用两个GCN来分别得到高斯分布的均值和方差就行了,即VGAE 利用GCN来分别计算均值和方差:
u = G C N u ( X , A ) u=GCN_u(X,A) u=GCNu(X,A) σ = G C N σ ( X , A ) \sigma=GCN_{\sigma}(X,A) σ=GCNσ(X,A) 再将使其与 noise(随机生成的变量)相乘,相加,便得到高斯分布上采样到的一个Z z = u + ϵ × σ z=u+\epsilon \times \sigma z=u+ϵ×σ - Decoder和GAE是一样的,只是由于使用了变分的思想,所以损失函数变成了: l o s s = E q ( Z ∣ X , A ) [ l o g p ( A ∣ Z ) ] − K L [ Q ( Z ∣ X , A ) ∣ ∣ p ( Z ) ] loss=E_{q(Z|X,A)} [log p(A|Z)]-KL[Q(Z|X,A)||p(Z)] loss=Eq(Z∣X,A)[logp(A∣Z)]−KL[Q(Z∣X,A)∣∣p(Z)]变分下界写出的优化目标,第一项是期望,第二项是 KL 散度,详细推导在这里。
class EncoderGCN(nn.Module): #和GAE的一样
def __init__(self, n_total_features, n_latent, p_drop=0.):
super(EncoderGCN, self).__init__()
self.n_total_features = n_total_features
self.conv1 = GCNConv(self.n_total_features, 11)
self.act1=nn.Sequential(nn.ReLU(),
nn.Dropout(p_drop))
self.conv2 = GCNConv(11, 11)
self.act2 = nn.Sequential(nn.ReLU(),
nn.Dropout(p_drop))
self.conv3 = GCNConv(11, n_latent)
def forward(self, data):
x, edge_index = data.x, data.edge_index
x = self.act1(self.conv1(x, edge_index))
x = self.act2(self.conv2(x, edge_index))
x = self.conv3(x, edge_index)
return x
def reparametrize(self, mean, log_std): #通过mean和std得到z
if self.training:
return mean + torch.randn_like(log_std) * torch.exp(log_std)
else:
return mean
def encode(self, *args, **kwargs):
self.mean, self.log_std = self.encoder(*args, **kwargs) #两个GCN分别得到mean和std
z = self.reparametrize(self.mean, self.log_std) #得到z
return z
ARGA
编码器是否真的能够习得一个高斯分布?上GAN吧…即把模型分为生成器和判别器两部分,让两者对抗训练。
- 生成器直接使用GAE,输出还原的图A’
- 判别器使用成对比较咯,即将正例和负例同时输入到神经网络中,由判别器直接尝试区分这两者
class Discriminator(nn.Module):
def __init__(self, n_input):
super(Discriminator, self).__init__()
# 判别器是3层FC
self.fcs = nn.Sequential(nn.Linear(n_input,40),
nn.ReLU(),
nn.Linear(40,30),
nn.ReLU(),
nn.Linear(30,1),
nn.Sigmoid())
def forward(self, z):
return self.fcs(z)
class ARGA(nn.Module):
def __init__(self, n_total_features, n_latent):
super(ARGA, self).__init__()
#这里的encode和decoder都是GAE的东西,一起作为生成器
self.encoder = EncoderGCN(n_total_features, n_latent)
self.decoder = DecoderGCN()
self.discriminator = Discriminator(n_latent)
def forward(self, x):
z_fake = self.encoder(x)
z_real=torch.randn(z_fake.size())
A = self.decoder(z_fake) #得到生成的A
# 让判别器区分真 与 假。
d_real=self.discriminator(z_real)
d_fake=self.discriminator(z_fake)
return A, d_real, d_fake
def simulate(self, x): #训练完成后可以用来生成更好的A
z_fake = self.encoder(x)
A = self.decoder(z_fake)
return A
其实除了这几份工作外,可以无监督的表示学习方法还有SDNE这种,而使用GAN的思想,其实也有做的更充分的工作比如GraphGAN,所以好像相比较之下,这三个算法更适合做链接预测,用于推荐系统之类的,比如GCMC。