【前言】
继续我们本系列对复杂网络社区结构的方法探索,之前已经尝试过spark上标签传播算法、igraph 中随机游走算法、networkx中的clique渗透算法(见笔者相关文章),但一直局限于无向、无权重图的分析。本次,向前迈一步,引入权重。选用了igraph中的标签传播算法。
【方法讨论】
相比于spark上的标签传播算法,发现igraph中的接口增加了对权重的支持,同时不用事先指定迭代的次数,这是两点好的地方。当然,可以处理的数据量级上,igraph单机版本没法和spark集群版本相提并论了。但是相比于python版本networkx的性能瓶颈,igraph C library的计算效率领先几条街。(小小吐槽一下:igraph同时提供有python、R、C三种版本的接口,相比之下,C library的使用要繁琐很多,如果不是追求性能稍好些,真心推荐使用其它两种方式)
在对权重的处理上,官方文档如是说:
Weights are taken into account as follows: when the new label of node i is determined, the algorithm iterates over all edges incident on node i and calculate the total weight of edges leading to other nodes with label 0, 1, 2, ..., k-1 (where k is the number of possible labels). The new label of node i will then be the label whose edges (among the ones incident on node i) have the highest total weight.
算法接口也比较简洁明确(稍后咱们详细看如何使用):
int igraph_community_label_propagation(const igraph_t *graph,
igraph_vector_t *membership,
const igraph_vector_t *weights,
const igraph_vector_t *initial,
igraph_vector_bool_t *fixed,
igraph_real_t *modularity);
【不废话,上完整版代码】
#include <stdio.h>
#include <stdlib.h>
#include </usr/local/igraph-0.7.1/include/igraph.h>
#include <string.h>
int main(int argc,char *argv[])
{
printf("Hello world!\n");
FILE *edgeListFile;
FILE *communityWriteFile;
igraph_t wbNetwork;
long int i;
long int no_of_nodes;
long int no_of_edges;
int rstCode;
igraph_vector_t gtypes, vtypes, etypes;
igraph_strvector_t gnames, vnames, enames;
/* turn on attribute handling */
igraph_i_set_attribute_table(&igraph_cattribute_table);
//初始化对象
igraph_vector_t membership;
igraph_vector_init(&membership,0);
igraph_vector_t weights;
igraph_vector_init(&weights,0);
if(argc < 2){
printf("Usage: %s <inputRelationFile> \n", argv[0]);
exit(1);
}
//边存放的文件,空格分隔
edgeListFile = fopen(argv[1],"r");
//从文件中读入图
igraph_read_graph_ncol(&wbNetwork,
edgeListFile,
NULL, /*预定义的节点名称*/
1, /*读入节点名称*/
IGRAPH_ADD_WEIGHTS_YES , /*是否将边的权重也读入*/
0 /*有向图*/
);
fclose(edgeListFile);
//警惕:下面这个函数要慎用,第二个参数及最后一个参数有可能会把权重属性抹掉
//igraph_simplify(&wbNetwork, 1, 1, 0);
igraph_vector_init(>ypes, 0);
igraph_vector_init(&vtypes, 0);
igraph_vector_init(&etypes, 0);
igraph_strvector_init(&gnames, 0);
igraph_strvector_init(&vnames, 0);
igraph_strvector_init(&enames, 0);
igraph_cattribute_list(&wbNetwork, &gnames, >ypes, &vnames, &vtypes,
&enames, &etypes);
no_of_nodes = igraph_vcount(&wbNetwork);
no_of_edges = igraph_ecount(&wbNetwork);
printf("Graph node numbers: %d \n",no_of_nodes);
printf("Graph edge numbers: %d \n",no_of_edges);
printf("图属性个数: %d \n", igraph_strvector_size(&gnames));
printf("节点属性个数: %d \n", igraph_strvector_size(&vnames));
printf("边属性个数: %d \n", igraph_strvector_size(&enames));
if(igraph_cattribute_has_attr(&wbNetwork,IGRAPH_ATTRIBUTE_EDGE,"weight")){
//将权重提取到向量中
EANV(&wbNetwork,"weight",&weights);
//printf("边权重属性值的个数为:%d \n", igraph_vector_size(&weights));
printf("Edge weight: \n");
//这里打印2份是为了验证从属性集合中取值与上面所存权重向量的值是否一致
for(i=0; i<10; i++) {
printf("Edge weight: %g %g \n",igraph_cattribute_EAN(&wbNetwork,"weight",i),VECTOR(weights)[i]);
}
}else{
printf("The Graph does not have attribute of weight \n");
}
rstCode = igraph_community_label_propagation(&wbNetwork,
&membership, /*重要:存储最终每个节点被划分到的社区编号*/
&weights,
NULL, /*对节点分配的初始化标签*/
NULL, /*是否将一部分节点的标签固定*/
NULL /*最终社区划分结果的模块度*/
);
if(rstCode != 0){
printf("igraph_community_label_propagation 执行失败");
return -2;
}else{
printf("Success! \n");
printf("划分的社区总数为:%g\n", igraph_vector_max(&membership));
for(i=0;i<10;i++){
printf("节点: %d -> 社区:%g \n",i,VECTOR(membership)[i]);
}
}
//将节点划分的社区结构保存到文件中
communityWriteFile = fopen("/data/tmp/igraph_lp_result.txt","w");
for(i=0;i<no_of_nodes;i++){
fprintf(communityWriteFile,"%s\t%g\n",igraph_cattribute_VAS(&wbNetwork,"name",i),VECTOR(membership)[i]);
}
fclose(communityWriteFile);
igraph_vector_destroy(&membership);
igraph_vector_destroy(>ypes);
igraph_vector_destroy(&vtypes);
igraph_vector_destroy(&etypes);
igraph_strvector_destroy(&gnames);
igraph_strvector_destroy(&vnames);
igraph_strvector_destroy(&enames);
igraph_vector_destroy(&weights);
igraph_destroy(&wbNetwork);
return 0;
}
在我们的测试数据集上,上述代码运行结果示意如下,9万个节点,52万条边,瞬间完成。比之前尝试networkx时9万节点,30万条边就已经抗不住了,这里的计算性能令人十分满意。
【要点讨论】
在上面代码中,从一个存储边信息的文件中读入图结构,使用的方法是:
igraph_read_graph_ncol(&wbNetwork,
edgeListFile,
NULL, /*预定义的节点名称*/
1, /*读入节点名称*/
IGRAPH_ADD_WEIGHTS_YES , /*是否将边的权重也读入*/
0 /*有向图*/
);
注意其中 IGRAPH_ADD_WEIGHTS_YES 这个信息就表示将边的权重读入图的属性中,而且千万要注意接下来不要写 igraph_simplify(&wbNetwork, 1, 1, 0); 这样的方法,会把权重数据从边的属性中删除掉,我猜想是最后那个参数把权重抹掉了,暂时也没时间细究,但这一点真心花了俺好长时间才意识到,因为之前导入图时,喜欢使用这个方法再去除多重边及环啥的,现在先对边数据文件预处理好,可以实现简化。
同样一个功能,在R中只需要更加简单的一句话:
g <- read.graph("your_edge_file.ncol", format="ncol")
如果你认为像上面那样导入图后,就可以开心地处理各种属性值,你就错了! igraph C 版本中,图的属性处理是最最让人抓狂的。比如像上面那样导入一个图后,在R中,你就可以直接使用各种属性了,比如
但是,在c library中,你要至少操作以下几步:一、建立存储属性的各种变量,如下所示:
igraph_vector_t gtypes, vtypes, etypes;
igraph_strvector_t gnames, vnames, enames;
/* turn on attribute handling */
igraph_i_set_attribute_table(&igraph_cattribute_table);
二、导入图后,要初始化这些属性变量,如下所示:
igraph_vector_t gtypes, vtypes, etypes;
igraph_strvector_t gnames, vnames, enames;
/* turn on attribute handling */
igraph_i_set_attribute_table(&igraph_cattribute_table);
三、属性又分图属性、节点属性、边属性,各有各的方法,比如
查看各有哪些属性时,可如此这般:
printf("Graph attributes: ");
for (i=0; i<igraph_strvector_size(&gnames); i++) {
printf("%s (%i) ", STR(gnames, i), (int)VECTOR(gtypes)[i]);
}
printf("\n");
printf("Vertex attributes: ");
for (i=0; i<igraph_strvector_size(&vnames); i++) {
printf("%s (%i) ", STR(vnames, i), (int)VECTOR(vtypes)[i]);
}
printf("\n");
printf("Edge attributes: ");
for (i=0; i<igraph_strvector_size(&enames); i++) {
printf("%s (%i) ", STR(enames, i), (int)VECTOR(etypes)[i]);
}
printf("\n");
本文完整代码中查看是否存在边的权重属性时,可如此这般:
igraph_cattribute_has_attr(&wbNetwork,IGRAPH_ATTRIBUTE_EDGE,"weight")
【结语】
事实上,不同方法的使用都有相通的地方,上手未必困难,但个中细节又往往需要花费时间去研究。这也是为何我们坚持把最近尝试过的一些方法完完整整记录下相关经验,以便于同道中人相互交流,减少一些细节点对时间的消耗。如本文有此功效,则幸甚。