PyTorch图神经网络实践(七)社区检测
文章目錄
- 前言
- 組合優化
- 社區檢測
- 端到端的學習與優化
- 作者介紹
- 核心思想
- 技術手段
- 方法創新
- 代碼復現
- 導入包
- 數據轉換
- 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),其中主要包含四個步驟:
整個模型框架如下圖所示,上面是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, featuresClusterNet模型
實際上,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图神经网络实践(七)社区检测的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 会计软件遭黑客攻击,QuickBooks
- 下一篇: 深眸分享——机器视觉光源基础知识