LINE代码

概述

LINE的代码包括以下4个文件:

  • reconstruct.cpp:将稀疏网络重建为密集网络的代码
  • line.cpp:实现LINE算法的代码
  • normalize.cpp:将节点的向量表示归一化的代码
  • concatenate.cpp:用于连接一阶和二阶embedding的代码

下载地址:LINE代码
其中有两行代码需要修改,分别为
reconstruct.cpp 142行 改为:printf("Number of vertices: %d \n", num_vertices);
reconstruct.cpp 315行 改为:if ((i = ArgPos((char *)"-k-max", argc, argv)) > 0) max_k = atoi(argv[i + 1]);

reconstruct.cpp

LINE代码_第1张图片
图1 reconstruct.cpp的输入和输出。图(b)中每个点处都有自环,没有画出,各个自环的权值已经列出
目的:

通过对节点的邻居进行扩展,将稀疏网络(a)重构为比较密集的网络(b)。

思路:

  以上图为例。图中节点2、3、4、5是1的邻居;节点6、7、8、9、10是2、3、4、5的邻居,即节点1邻居的邻居。定义节点1为节点1的零阶邻居,2、3、4、5为节点1的一阶邻居,6、7、8、9、10是节点1的二阶邻居,类比可以定义n阶邻居。
  对节点1进行最大深度为2(扩展到二阶邻居)的扩展时,将节点1的零阶邻居和二阶邻居扩展到1的邻居中,即在图中为节点1添加自环和节点1到其二阶邻居的边。图1(b)为图1(a)的扩展图(扩展了(a)中所有的节点,除了节点1外,其他节点没有二阶邻居,因此只添加了自环),扩展的深度为2阶。
  新添加的边的权值的计算方法为:
1)自环:某节点处自环的权值为该节点的度(入度+出度)/10。如节点2的自环权值为(5+3+6)/10=1.4。
2)节点和高阶邻居之间的边:若对节点a进行扩展,节点m是a的 i 阶邻居,节点n是m的邻居、a的i+1阶邻居,则w(a,n)=w(a,m)*(w(m,n)/w(m)),w(m)是所有起点为m的边的权值的和。如节点1、6之间的边的权值为5*(3/9)=1.167,节点1,7之间的边的权值为5*(6/9)+3*(8/8)。

代码:
  1. 代码中先定义了节点ClassVertex和邻居Neighbor两个结构。
    ClassVertex的属性有三个,分别为节点的名称name,节点的度degree(出度+入度),从节点出发的边的权重和sum_weight。
    Neighbor的属性有两个,分别为邻居的编号vid和对应的边的权值weight;还定义了neighbor比较大小的方法,weight属性大的neighbor小。
  2. 定义了一个int类型的数组vertex_hash_table,数组中存储的是节点的编号。vertex_hash_table实现的是Hash表作用,在节点的名字和编号之间建立一个一一映射关系。为了构造Hash表的功能,实现了4个函数,分别实现初始化Hash表、Hash运算、增加节点、查找节点的功能。
    void InitHashTable():将vertex_hash_table中的所有元素都初始化为-1,代表null。
    unsigned int Hash(char *key):将节点的name作为参数key传进来,经过Hash运算,得到对应的节点编号在数组vertex_hash_table中的下标。
    InsertHashTable(char *key, int value):参数是新加入的节点的名字和节点编号,作用是将新加入的节点存入Hash表。
    int SearchHashTable(char *key):参数是节点的名字,根据的节点的名字,在Hash表中找到节点的编号并返回。
  3. 代码中还定义了节点类型的数组vertex用来存储图中所有的节点,和一个向量类型的数组neighbor,每一个向量存储了一个节点的所有邻居。通过这两个数组,就可以完整保存图的结构。
  4. 代码中定义了工具函数int AddVertex(char *name),在从文件中读取图时使用。参数是节点的名字,返回节点的编号。在算法中,将节点存入数组vertex和Hash表,同时统计节点的数目,存入num_vertices
  5. 为了从文件中读取图,定义了函数void ReadData()。函数中,共读取了3遍文件:
    第一遍,得到图中边的数目,并存入num_edges
    第二遍,得到每个节点的name和degree属性,将图中的节点存入数组vertex;
    第三遍,得到每个邻居的vid属性和weight属性,将图中的邻接信息存入数组neighbor。
    最后,再根据数组neighbor计算出各个节点的sum_weight信息。
  6. 算法的主要函数是void Reconstruct(),作用就是对图进行重构。
    因为在对节点的邻居进行扩展时,是一层层的进行扩展,因此用到了队列这个数据结构(扩展过程和广度优先搜索类似)。队列中存储的是节点的编号,根据编号可以在数组vextex中找到对应的节点。
    当节点的邻居的数目大于max_k时,不需要对节点进行扩展。
    对一个节点进行扩展时,首先队列中首先要只有这个节点,因此要先清空队列,将要扩展的节点放入队列中。然后从队列中取出一个节点,将节点的邻居加入队列中,重复这一过程,直至扩展的层数达到max_depth;然后不再将节点的邻居加入队列,但仍要将节点从队列中取出,直至节点为空。从队列中取出的节点都是重构的图中要扩展的节点的邻居,先将这些节点的编号和计算得到权值的暂时保存在vid2weight中,然后复制到rank_list中并输出。

line.cpp

LINE代码_第2张图片
图2 使用一阶相似度的LINE算法的输出
LINE代码_第3张图片
图3 使用二阶相似度的LINE算法的输出
目的:

  根据图的结构信息,计算出图中各个节点的向量表示形式。

思路:
  1. 先利用别名抽样法选择一条边 uv,下面将计算u的向量表示。
  2. 利用负采样选择几个节点 v,将节点u、v代入下面的公式,利用随机梯度下降算法(SGD)更新节点的向量表示。
    image.png
  3. 过程1、2是由多个线程同时进行的,因此,LINE算中使用的是异步随机梯度下降算法。第一步边采样的数目和第二步节点采样的数目由参数决定。
代码
  1. 代码中先定义了节点ClassVertex,属性有两个,分别为节点的名称name,节点的度degree(出度+入度)。
  2. 代码中定义的int类型的数组vertex_hash_table及实现其功能需要的void InitHashTable()unsigned int Hash(char *key)InsertHashTable (char *key, int value)int SearchHashTable(char *key)函数 ,int AddVertex(char *name)函数均与reconstruct.cpp中相同,不再重复。
  3. 在reconstruct.cpp中,保存图的信息用到了vertex和neighbor两个数组,分别表示各个节点的信息和节点之间的邻接信息。但是在LINE.cpp中,是通过保存各个节点的信息和各条边的信息来保存图。保存节点信息用到的还是数组vertex;保存各条边用到了三个数组分别为:edge_source_idedge_target_idedge_weight,分别保存边的起始节点的编号、终止节点的编号和边的权值。
  4. 在函数void ReadData()中,共读取了2遍文件:
    第一遍,得到图中边的数目,并存入num_edges
    第二遍,得到每个节点的name和degree属性,将图中的节点存入数组vertex;将每条边的起点、终点、权值分别存入edge_source_id、edge_target_id、edge_weight三个数组。
  5. 函数void InitSigmoidTable()real FastSigmoid(real x)int Rand(unsigned long long &seed)都是简单的工具函数,前两个是为了能快速计算出sigmoid function的值,最后一个是为了得到随机整数。
  6. 为了实现别名采样法,代码中定义了long long类型的数组alias和double类型的数组prob。alias数组保存了别名抽样法中占同一块的两个边的组合,数组的下标是一条边的编号,数组的对应位置中存的值是另一条边的编号。prob数组中保存了,当在alias组合的两条边选其一时,选择编号为下标的边的概率。(边的编号即保存边的信息的三个数组的下标)
    函数void InitAliasTable()初始化了这两个数组。
    函数long long SampleAnEdge(double rand_value1, double rand_value2)的参数是两个随机数,函数根据这两个随机数和数组alias、prob,使用别名抽样法选择一条边,并返回被选中的边的编号。
  7. 为了实现对点进行负采样,代码中定义了int类型数组neg_table,数组中存的是节点的编号,节点的编号在数组中出现的概率和节点的度的大小成正比。函数void InitNegTable()初始化了数组neg_table。
  8. 代码中,将算法的运行结果,即各个节点的向量表示保存到float类型数组emb_vertex中,(节点的数目x向量表示的维度)作为数组的大小。因为当使用LINE(2nd)时,每个节点作为别的节点的邻居时,还有一个向量表示,因此声明了emb_context
  9. 函数void Update(real *vec_u, real *vec_v, real *vec_error, int label)实现的是利用梯度下降,根据论文中给出的目标函数,计算出需要对节点的向量表示进行的调整。参数分别为别名抽样法选中的边的起点、负采样选中的节点、需要对边的起点的向量进行的调整i、负采样选中的节点是正样本(1)还是负样本(0)。
  10. void *TrainLINEThread(void *id)是实现LINE过程的函数,函数内主要是一个循环,循环的执行次数为抽样的总边数/线程数。
    循环每执行10000次,更新计数器、输出进度、更新学习率。
    每次执行循环时,利用别名采样法选择一条边,再利用负采样选择几个节点。将选择的边的起点和选择的节点,送入Update函数进行更新。

normalize.cpp

LINE代码_第4张图片
图4 归一化后的向量表示(1st)
LINE代码_第5张图片
图5 归一化后的向量表示(2nd)
目的:

将表示各个节点的向量的模规范化为1。

思路:

将向量中各个维度的值除以向量的模。

代码

很简单,不再解释。

concatenate.cpp

LINE代码_第6张图片
image.png
目的:

结合一阶相似度和二阶相似度得到节点的向量表示形式。

思路

将在一阶相似度和二阶相似度下求出的节点的向量表示归一化后连接到一起。
代码:
很简单,不再解释。

你可能感兴趣的:(LINE代码)