文章目录

  • 前言
  • 组合优化
  • 社区检测
  • 端到端的学习与优化
    • 作者介绍
    • 核心思想
    • 技术手段
    • 方法创新
  • 代码复现
    • 导入包
    • 数据转换
    • ClusterNet模型
    • 创建网络
    • 参数设置和数据导入
    • 训练网络

前言

最近一直在研究组合优化问题,上周看到2019年NeurIPS会议上有篇文章提出了一种端到端的学习和优化框架,并且开源了代码,于是复现了一下,发现在社区检测任务上的效果真的不错,而且方法非常简单。

NeurIPS 2019:图上端到端的学习和优化
End to end learning and optimization on graphs
GitHub源码


组合优化

图中的很多问题都是组合优化问题,比如最大独立集、最小覆盖集、图分割、最短路等等。很多组合优化问题都是NP难问题,不存在多项式时间复杂度的求解算法,所以传统多是用贪婪算法或者启发式算法(比如遗传算法、粒子群算法等等)来求解。

最近,很多研究人员尝试用深度学习或者强化学习来解决组合优化问题,这几年相关研究也经常出现在IAAA、NIPS这样的顶会上。我在之前的一篇博客中整理一些代表性研究(包含文章及代码链接),感兴趣的可以移步看看。


社区检测

社区检测是网络科学中的一个经典问题了,其目的是发现网络中的社区结构。社区结构一般是指网络中一些内部联系非常紧密的子图,这些子图往往具有一些特定的功能或者属性。当然,社区结构有很多种,比如层次社区结构、重叠社区结构等等,这方面的文章有不少是发表在《Nature》《Science》这样级别的期刊上的。想深入了解社区检测问题可以看看下面几篇文章。

Finding and evaluating community structure in networks (2003)
Modularity and community structure in networks (2006)
Community detection in graphs (2009)
Community detection algorithms: a comparative analysis (2009)
Community detection in networks: A user guide (2016)

我之前也写了一篇关于社区检测的文章,里面给出了社区可视化的代码,用python3和networkx包实现的。

大多数社区检测算法都将模块度作为优化函数,其目的就是寻找一种最优划分将所有节点分配到不同的社区中,使得模块度值最大。因此,社区检测问题本质上也是一个组合优化问题。

下面就逐步介绍一下这篇文章的主要研究内容。


端到端的学习与优化

作者介绍

这篇文章发表在2019年的NeurIPS会议上。四位作者分别来自哈佛大学和南加州大学,其中一作Bryan Wilder是即将毕业的博士生,非常厉害,博士期间发表了很多高质量文章,其个人主页上有详细介绍。Bistra Dilkina是南加州大学的副教授,发表过很多关于组合优化问题的文章,如经典文章 Learning combinatorial optimization algorithms over graphs。

核心思想

许多实际应用同时涉及到图上的学习问题与优化问题,传统的做法是先解决学习问题然后再解决优化问题,这样有个缺点就是下游优化的结果无法反过来指导学习过程,实现不了学习与优化的协同改善。文章的目的就是提出一种端到端的框架,将学习过程和优化过程合并在一个网络中,这样最终优化任务的误差可以一直反向传播到学习任务上,网络参数就可以一起优化,改善模型在优化任务上的性能。

技术手段

这篇文章以链路预测作为学习问题的代表,以社区检测为优化问题的代表来开展研究。具体上,文章假定在进行社区检测之前,网络结构不是完全已知的,只有部分(40%)网络结构是能够观察到的,所以要先用链路预测来找出出网络中那些没有被观察到的连边,然后再在这种“复原”后的网络上进行社区检测,利用模块度指标来评估社区检测的效果。同时,文章还设立了对照实验,即在原始网络(不隐藏任何连边)上执行社区检测任务,通过观察两组实验的结果来分析他们提出的模型的有效性。

方法创新

文章提出了一种新的端到端的网络模型(ClusterNet),其中主要包含四个步骤:

  1. 基于GCN的节点嵌入
  2. 基于K-means的节点聚类
  3. 作出决策并计算当前的解的损失
  4. 误差的反向传播和参数优化

整个模型框架如下图所示,上面是ClusterNet,下面是两阶段优化模型。


其中关键在于决策和误差反向传播。

针对不同的优化任务,决策函数是不一样的,而且训练阶段和测试阶段也有些不同。本文只介绍社区检测这一任务,在训练阶段,节点聚类的结果是概率值,被当做社区的软划分,这样计算梯度更准确,有利于参数优化;而在测试阶段(推理过程),对节点聚类的结果进行softmax操作就可以得到社区的硬划分(二值化),这样可以计算出最终的模块度值。

在误差反向传播过程中,有两个影响优化效果的重要参数,一个是β\betaβ,即聚类分配的严格程度(hardness),δ\deltaδ,即类别之间的区分程度。这两个参数的乘积决定了社区划分的效果,一般情况下,大一些比较好。在代码中,这两个参数只用其乘积一个参数来代表了。

作者们还证明了该模型可以通过梯度下降来寻优,并推导出了参数的梯度计算公式。对公式感兴趣的可以去看原文。

除了决策和参数优化之外,K-means聚类的作用也是极为重要的。如果没有中间聚类这一步的话,效果是要大打折扣的。对于社区检测任务来说,如果去掉中间的聚类层,那么最后的结果基本上都是将所有节点都分配到同一个社区,这样网络中全部边都在社区内部,也算是最优了,但是没有任何意义。文中也特意设计了一种直接优化的方法(不含聚类层),也就是GCN-e2e方法,可以看出其效果比ClusterNet要差很多。


代码复现

下面,就一步一步复现一下文中的代码。

导入包

import numpy as np
import sklearn
import sklearn.cluster
import scipy.sparse as sp
import math
import torch
import torch.optim as optim
import torch.nn as nn
import torch.nn.functional as F
from torch.nn.parameter import Parameter

数据转换

将networkx中的graph对象转换为网络要求的输入,输入数据有两个,一个是归一化后的邻接矩阵(稀疏矩阵),一个是节点的特征矩阵(没有特征的图默认为单位矩阵)。

## Data handling
def normalize(mx):"""Row-normalize sparse matrix"""rowsum = np.array(mx.sum(1), dtype=np.float32)r_inv = np.power(rowsum, -1).flatten()r_inv[np.isinf(r_inv)] = 0.r_mat_inv = sp.diags(r_inv)mx = r_mat_inv.dot(mx)return mxdef mx_to_sparse_tensor(mx):"""Convert a scipy sparse matrix to a torch sparse tensor."""mx = mx.tocoo().astype(np.float32)indices = torch.from_numpy(np.vstack((mx.row, mx.col)).astype(np.int64))values = torch.from_numpy(mx.data)shape = torch.Size(mx.shape)return torch.sparse.FloatTensor(indices, values, shape)def load_data(G):"""Load network (graph)"""adj = nx.to_scipy_sparse_matrix(G).tocoo()adj = normalize(adj+sp.eye(adj.shape[0]))adj = mx_to_sparse_tensor(adj)features = torch.eye(len(G.nodes())).to_sparse()return adj, features

ClusterNet模型

实际上,ClusterNet网络仅仅包含两个模块,第一个模块是经典的图卷积网络,第二个模块就是kmeans聚类,只不过聚类分成了两步,第一步先得到各聚类中心的初始向量,第二步再优化节点的聚类结果。得到聚类结果后就可以通过softmax操作进行社区的硬化分。

## Model
class GraphConvolution(nn.Module):'''Simple GCN layer, similar to https://arxiv.org/abs/1609.02907'''def __init__(self, in_features, out_features, bias=True):super(GraphConvolution, self).__init__()self.in_features = in_featuresself.out_features = out_featuresself.weight = Parameter(torch.FloatTensor(in_features, out_features))if bias:self.bias = Parameter(torch.FloatTensor(out_features))else:self.register_parameter('bias', None)self.reset_parameters()def reset_parameters(self):stdv = 1. / math.sqrt(self.weight.size(1))self.weight.data.uniform_(-stdv, stdv)if self.bias is not None:self.bias.data.uniform_(-stdv, stdv)def forward(self, input, adj):support = torch.mm(input, self.weight)output = torch.spmm(adj, support)if self.bias is not None:return output + self.biaselse:return outputdef __repr__(self):return self.__class__.__name__ + ' (' \+ str(self.in_features) + ' -> ' \+ str(self.out_features) + ')'class GCN(nn.Module):'''2-layer GCN with dropout'''def __init__(self, nfeat, nhid, nout, dropout):super(GCN, self).__init__()self.gc1 = GraphConvolution(nfeat, nhid)self.gc2 = GraphConvolution(nhid, nout)self.dropout = dropoutdef forward(self, x, adj):x = F.relu(self.gc1(x, adj))x = F.dropout(x, self.dropout, training=self.training)x = self.gc2(x, adj)return xdef cluster(data, k, num_iter, init=None, cluster_temp=5):'''pytorch (differentiable) implementation of soft k-means clustering.'''# normalize x so it lies on the unit spheredata = torch.diag(1./torch.norm(data, p=2, dim=1)) @ data# use kmeans++ initialization if nothing is providedif init is None:data_np = data.detach().numpy()norm = (data_np**2).sum(axis=1)init = sklearn.cluster.k_means_._k_init(data_np, k, norm, sklearn.utils.check_random_state(None))init = torch.tensor(init, requires_grad=True)if num_iter == 0: return initmu = initfor t in range(num_iter):# get distances between all data points and cluster centersdist = data @ mu.t()# cluster responsibilities via softmaxr = torch.softmax(cluster_temp*dist, 1)# total responsibility of each clustercluster_r = r.sum(dim=0)# mean of points in each cluster weighted by responsibilitycluster_mean = (r.t().unsqueeze(1) @ data.expand(k, *data.shape)).squeeze(1)# update cluster meansnew_mu = torch.diag(1/cluster_r) @ cluster_meanmu = new_mudist = data @ mu.t()r = torch.softmax(cluster_temp*dist, 1)return mu, r, distclass GCNClusterNet(nn.Module):'''The ClusterNet architecture. The first step is a 2-layer GCN to generate embeddings.The output is the cluster means mu and soft assignments r, along with the embeddings and the the node similarities (just output for debugging purposes).The forward pass inputs are x, a feature matrix for the nodes, and adj, a sparseadjacency matrix. The optional parameter num_iter determines how many steps to run the k-means updates for.'''def __init__(self, nfeat, nhid, nout, dropout, K, cluster_temp):super(GCNClusterNet, self).__init__()self.GCN = GCN(nfeat, nhid, nout, dropout)self.distmult = nn.Parameter(torch.rand(nout))self.sigmoid = nn.Sigmoid()self.K = Kself.cluster_temp = cluster_tempself.init =  torch.rand(self.K, nout)def forward(self, x, adj, num_iter=1):embeds = self.GCN(x, adj)mu_init, _, _ = cluster(embeds, self.K, num_iter, init = self.init, cluster_temp = self.cluster_temp)mu, r, dist = cluster(embeds, self.K, num_iter, init = mu_init.detach().clone(), cluster_temp = self.cluster_temp)return mu, r, embeds, dist# 损失函数
def loss_modularity(mu, r, embeds, dist, bin_adj, mod, args):bin_adj_nodiag = bin_adj*(torch.ones(bin_adj.shape[0], bin_adj.shape[0]) - torch.eye(bin_adj.shape[0]))return (1./bin_adj_nodiag.sum()) * (r.t() @ mod @ r).trace()# 获得模块度矩阵
def make_modularity_matrix(adj):adj = adj*(torch.ones(adj.shape[0], adj.shape[0]) - torch.eye(adj.shape[0]))degrees = adj.sum(dim=0).unsqueeze(1)mod = adj - degrees@degrees.t()/adj.sum()return mod

创建网络

创建一个demo网络,用于演示社区检测结果,该网络包含三个社区,每个社区10个节点。

import networkx as nx
import matplotlib.pyplot as plt# create a graph
G = nx.random_partition_graph([10, 10, 10], 0.8, 0.1)# plot the graph
fig, ax = plt.subplots(figsize=(5,5))
option = {'font_family':'serif', 'font_size':'15', 'font_weight':'semibold'}
nx.draw_networkx(G, node_size=400, **option)  # pos=nx.spring_layout(G)
plt.axis('off')
plt.show()

网络可视化效果如下

参数设置和数据导入

设置网络参数,并导入数据。

class arguments():def __init__(self):self.no_cuda = True        # Disables CUDA trainingself.seed = 24             # Random seedself.lr = 0.001            # Initial learning rateself.weight_decay = 5e-4   # Weight decay (L2 loss on parameters)self.hidden = 50           # Number of hidden unitsself.embed_dim = 50        # Dimensionality of node embeddingsself.dropout = 0.5         # Dropout rate (1 - keep probability)self.K = 3                 # How many partitionsself.clustertemp = 100     # how hard to make the softmax for the cluster assignmentsself.train_iters = 301     # number of training iterationsself.num_cluster_iter = 1  # number of iterations for clusteringargs = arguments()
args.cuda = not args.no_cuda and torch.cuda.is_available()## Load data
adj_all, features = load_data(G=G)   # normalized adjacency matrix
bin_adj_all = (adj_all.to_dense() > 0).float()   # standard adjacency matrix
test_object = make_modularity_matrix(bin_adj_all)
nfeat = features.shape[1]
num_cluster_iter = args.num_cluster_iter
K = args.K

训练网络

创建一个ClusterNet网络,在CPU上训练,不分割数据,直接在原图上进行测试。

%%time
## INITIALIZE MODELS
model_cluster = GCNClusterNet(nfeat=nfeat,nhid=args.hidden,nout=args.embed_dim,dropout=args.dropout,K = args.K,cluster_temp = args.clustertemp)optimizer = optim.Adam(model_cluster.parameters(),lr=args.lr, weight_decay=args.weight_decay)if args.cuda:model_cluster.cuda()adj_all = adj_all.cuda()features = features.cuda()bin_adj_all = bin_adj_all.cuda()test_object = test_object.cuda()losses = []
losses_test = []## Decision-focused training
best_train_val = 100
for t in range(args.train_iters):# optimization setting: get loss with respect to the full graphmu, r, embeds, dist = model_cluster(features, adj_all, num_cluster_iter)loss = loss_modularity(r, bin_adj_all, test_object)loss = -lossoptimizer.zero_grad()loss.backward()# increase number of clustering iterations after 200 updates to fine-tune solutionif t == 200:num_cluster_iter = 5# every 100 iterations, look and see if we've improved on the best training loss# seen so far. Keep the solution with best training value.if t % 100 == 0:# round solution to discrete partitioningr = torch.softmax(100*r, dim=1)# evalaute test loss -- note that the best solution is# chosen with respect training loss. Here, we store the test loss# of the currently best training solutionloss_test = loss_modularity(r, bin_adj_all, test_object)# training loss, to do rounding afterif loss.item() < best_train_val:best_train_val = loss.item()curr_test_loss = loss_test.item()log = 'Iterations: {:03d}, ClusterNet modularity: {:.4f}'print(log.format(t, curr_test_loss))losses.append(loss.item())optimizer.step()

输出结果如下:

Iterations: 000, ClusterNet modularity: 0.3532
Iterations: 100, ClusterNet modularity: 0.4813
Iterations: 200, ClusterNet modularity: 0.4813
Iterations: 300, ClusterNet modularity: 0.4813
CPU times: user 59.7 s, sys: 869 ms, total: 1min
Wall time: 2.31 s

可以看出,网络在100步后就收敛了,实际上收敛步骤更快,差不多50左右就收敛了。用时也很少。

再看看社区检测结果:

可以看出,这30个节点分类非常准确,1-10、11-20、21-30节点分别被分配到三个社区中,效果非常好。感兴趣的同学可以自己试试,这篇文章提出的方法我觉得是目前社区检测任务中最有效的图神经网络方法了,兼顾了性能与效率,而且实现起来也很简单。

PyTorch图神经网络实践(七)社区检测相关推荐

  1. PyTorch图神经网络实践(六)组合优化问题

    最近研究网络中的组合优化问题时,发现已经有研究人员将图神经网络用于求解此类问题,诸如寻找网络中的最大点独立集.最小点覆盖集.最大团等等,并找到了相关的源码,在这里整理一下这些资源,以后详细介绍. Ne ...

  2. PyTorch图神经网络实践(五)链路预测

    链路预测是网络科学里面的一个经典任务,其目的是利用当前已获取的网络数据(包含结构信息和属性信息)来预测网络中会出现哪些新的连边. 本文计划利用networkx包中的网络来进行链路预测,因为目前PyTo ...

  3. 基于图神经网络的代码漏洞检测方法

    文章结构 1. 引言 1.1 漏洞现状 1.2 漏洞研究传统特点 现有的研究方案大多把源代码转化为文本序列,使用自然语言处理中常用的循环神经网络进行自动化特征提取,但这忽略了代码中由于跳转.循环.判断 ...

  4. 图神经网络实践之图节点分类(一)

    本文主要以Deep Graph Library(DGL)为基础,利用图神经网络来进行图节点分类任务.本篇针对的图为同构图. 1. DGL 介绍 DGL是一个python包,用以在现有的深度学习框架上( ...

  5. 业界分享 | 百度图神经网络实践

    作者 | 黄正杰 来源 | DataFunTalk 图是一个复杂世界的通用语言,社交网络中人与人之间的连接.蛋白质分子.推荐系统中用户与物品之间的连接等等,都可以使用图来表达.图神经网络将神经网络运用 ...

  6. [论文阅读] (27) AAAI20 Order Matters: 基于图神经网络的二进制代码相似性检测(腾讯科恩实验室)

    <娜璋带你读论文>系列主要是督促自己阅读优秀论文及听取学术讲座,并分享给大家,希望您喜欢.由于作者的英文水平和学术能力不高,需要不断提升,所以还请大家批评指正,非常欢迎大家给我留言评论,学 ...

  7. 【图神经网络】Pytorch图神经网络库——PyG异构图学习

    PyG异构图学习 举个例子 创建异构图 Utility函数 异构图Transformations 创建异构图神经网络 自动转换GNN模型 使用异构卷积包装器 部署现有的异构算子 异构图采样 参考资料 ...

  8. 机器学习实践七----异常检测和推荐系统

    Anomaly detection 异常检测是机器学习中比较常见的应用,它主要用于非监督学习问题,从某些角度看, 它又类似于一些监督学习问题. 什么是异常检测?来看几个例子: 例1. 假设是飞机引擎制 ...

  9. 图神经网络/GNN(三)-基于图神经网络的节点表征学习

    Task3概览: 在图任务当中,首要任务就是要生成节点特征,同时高质量的节点表征也是用于下游机器学习任务的前提所在.本次任务通过GNN来生成节点表征,并通过基于监督学习对GNN的训练,使得GNN学会产 ...

最新文章

  1. SAP 物料主数据屏幕增强
  2. 奇怪吸引子---一个奇妙的四维混沌吸引子
  3. PyCairo 中的剪裁和屏蔽
  4. 网上借鉴及自己对git的认知(很早就写了,一直没有上传,如有误解,请指出)...
  5. airdrop 是 蓝牙吗_您可以在Windows PC或Android手机上使用AirDrop吗?
  6. 华为手机免root改mac_拿到华为手机,这4个默认设置一定要改,不然流量电量很快被耗光...
  7. 【拉普拉斯机制代码实现demo】差分隐私代码实现系列(四)
  8. java sha1hash 算法_javaweb使用sha1算法登录加密的整个过程
  9. 【零基础学Java】—Java 日期时间(三十一)
  10. dup和dup2详解
  11. ffmpeg推拉流优化方案
  12. 云计算机房架构图,云计算架构技术与实践
  13. 再见PDF提取收费!我用100行Python代码搞定!
  14. js两只手指控制div图片放大缩小功能
  15. 淘淘商城分布式电商系统项目总结
  16. Easyexcel·读取excel
  17. 漫威金刚狼男主弃影炒币了?
  18. PC机组成——内存储器
  19. Typora使用方法简单总结
  20. ZOJ 4062 Plants vs. Zombies 2018 ICPC 青岛站 E Plants vs. Zombies

热门文章

  1. SP7258 SUBLEX (后缀自动机)
  2. 计算2的N次方【高精度算法】
  3. 多附件添加功能的实现
  4. How to create subroutine
  5. 思科4500R系列交换机双引擎冗余讲解
  6. 为什么 Github 可以加分
  7. uni-app checkbox限制选中数量 移除选中样式
  8. 我的贝尔宾团队角色。
  9. java 影像处理_java图像处理的方法
  10. Anaconda 5.0.0 JupyterLab 0.27.0 中使用 matplotlib basemap 绘制世界地图