metapath2vec 异构网络表示学习

metapath2vec 异构网络表示学习

前言

周末立了个 Flag, 说要完成两篇博客的编写 (更精准的说法是至少两篇), 昨天完成了一篇 DIN 深度兴趣网络介绍以及源码浅析, 今天白天由于忙着买菜, 洗菜和做菜还有运动, 白天恍恍惚惚的过去了, 现在距离夜里 12 点还有 20 分钟左右, 水一篇~

距离 12 点还有 10 分钟时突然想到 … 可以先写一点, 留个坑, 以后再填, 这样的话, 只需要新立一个小小的 Flag, 不仅能完成我这个周末的 Flag, 还可以督促我未来用功, 一举两得, 一石二鸟, 我简直机智的一笔

(2020-08-16 补充) 上周立的 Flag, 现在终于终于来填坑了. 而且, 本周也完成了两篇博客的编写 (AFM 网络介绍与源码浅析 以及 Product-based Neural Network (PNN) 介绍与源码浅析) 简直 6 啊

广而告之

可以在微信中搜索 “珍妮的算法之路” 或者 “world4458” 关注我的微信公众号;另外可以看看知乎专栏 PoorMemory-机器学习, 以后文章也会发在知乎专栏中;

metapath2vec

文章信息

  • 论文标题: metapath2vec: Scalable Representation Learning for Heterogeneous Networks
  • 论文地址: https://ericdongyx.github.io/papers/KDD17-dong-chawla-swami-metapath2vec.pdf
  • 代码地址: https://ericdongyx.github.io/metapath2vec/m2v.html
  • 发表时间: KDD 2017
  • 论文作者: Yuxiao Dong, Nitesh V. Chawla, Ananthram Swami
  • 作者单位: Microsoft Research

核心观点

本文提出的算法主要用来处理异构图的表示学习问题. 异构网络指的是节点或边的类型大于 1 的网络, 即网络中有多种类型的节点或多种类型的边.

对于异构网络, 如果用 Random Walk 的方法来进行游走, 那么在学习时可能更偏向于那些高度可见的节点类型:

However, Sun et al. demonstrated that heterogeneous random walks are biased to highly visible types of nodes—those with a dominant number of paths—and concentrated nodes—those with a governing percentage of paths pointing to a small set of nodes

于是本文在游走时考虑了节点类型, 提出了基于 Meta-Path 的游走方法.

Meta-Path (元路径) 的意思是事先定义好节点类型的变化规律 (节点类型变化的顺序). 举个例子, 比如使用论文(Paper)、作者 (Author)、出版社 (Organization) 这些元素构建了一张图, 并设置 Meta-Path 为 “APA”, 那么就表示游走时一定是按照先走 Author 节点, 再走 Paper 节点, 最后再走 Author 节点; 此外, 一般 Meta-Path 设置为对称的, 即最开始的节点类型和最后的节点类型是一样的, 这样 Meta-Path 走到最后一个节点时又可以重新开始, 有些闭环的感觉.

总的来说, 就是 Meta-Path 相当于预定义了一种规则, 对于异构图来说, 需要按照这种特定的规则去游走, 选择下一个节点时需要考虑节点或边的类型. 其中就游走这个操作来说, 和 Random Walk 没有本质区别, 但是 Meta-Path 考虑了节点与边的类型.

按照 Meta-Path, 通过游走得到一系列的游走序列, 然后可以使用 word2vec 中的 skip-gram 来学习节点的 embedding. metapath2vec 直接使用 negative sampling 进行节点的采样和 embedding 的学习, 而 metapath2vec++ 则认为 metapath2vec 在负采样时没有考虑到节点的类型, 因此它在 negative sampling 时将节点类型考虑了进去, 只对同类型的节点使用 softmax 进行归一化, 这样的话, 每种类型的节点都会有一个分布.

在具体代码实现中, 其实很多借鉴了 Word2Vec 的 C++ 代码. LINE (详见 INE 图嵌入算法介绍与源码浅析) 也是在 Word2Vec 的代码上进行修改的.

核心观点解读

基于 Meta-Path 的随机游走

一个 meta-path scheme P \mathcal{P} P 形式化定义如下:

V 1 ⟶ R 1 V 2 ⟶ R 2 ⋯ V t ⟶ R t V t + 1 ⋯ ⟶ R l − 1 V l V_{1} \stackrel{R_{1}}{\longrightarrow} V_{2} \stackrel{R_{2}}{\longrightarrow} \cdots V_{t} \stackrel{R_{t}}{\longrightarrow} V_{t+1} \cdots \stackrel{R_{l-1}}{\longrightarrow} V_{l} V1R1V2R2VtRtVt+1Rl1Vl

其中 V i V_{i} Vi 表示节点类型, 而 R = R 1 ∘ R 2 ∘ ⋯ ∘ R l − 1 R=R_{1} \circ R_{2} \circ \cdots \circ R_{l-1} R=R1R2Rl1 表示节点类型 V 1 V_1 V1 V l V_{l} Vl 之间的构成关系. Meta-Path 相当于预定义了一种规则, 对于异构图来说, 需要按照这种特定的规则去游走, 选择下一个节点时需要考虑节点或边的类型. 之后进行游走时, 在第 i 步的转移概率定义为:

p ( v i + 1 ∣ v t i , P ) = { 1 ∣ N t + 1 ( v t i ) ∣ ( v i + 1 , v t i ) ∈ E , ϕ ( v i + 1 ) = t + 1 0 ( v i + 1 , v t i ) ∈ E , ϕ ( v i + 1 ) ≠ t + 1 0 ( v i + 1 , v t i ) ∉ E p\left(v^{i+1} \mid v_{t}^{i}, \mathcal{P}\right)=\left\{\begin{array}{cl}\frac{1}{\left|N_{t+1}\left(v_{t}^{i}\right)\right|} & \left(v^{i+1}, v_{t}^{i}\right) \in E, \phi\left(v^{i+1}\right)=t+1 \\ 0 & \left(v^{i+1}, v_{t}^{i}\right) \in E, \phi\left(v^{i+1}\right) \neq t+1 \\ 0 & \left(v^{i+1}, v_{t}^{i}\right) \notin E\end{array}\right. p(vi+1vti,P)=Nt+1(vti)100(vi+1,vti)E,ϕ(vi+1)=t+1(vi+1,vti)E,ϕ(vi+1)=t+1(vi+1,vti)/E

其中节点 v t i ∈ V t v_{t}^{i} \in V_{t} vtiVt, 说明节点 v t i v_{t}^{i} vti 的类型为 V t V_{t} Vt; N t + 1 ( v t i ) N_{t+1}\left(v_{t}^{i}\right) Nt+1(vti) 表示节点 v t i v_{t}^{i} vti 的邻居, 它们的类型为 V t + 1 V_{t+1} Vt+1, 即 v i + 1 ∈ V t + 1 v^{i+1}\in V_{t + 1} vi+1Vt+1.

可以看到, 和 Random Walk 不同的是, 进行下一步游走的时候, 需要考虑下一个节点的类型是否满足 Meta-Path Scheme 中的定义.

作者在 https://ericdongyx.github.io/metapath2vec/m2v.html 给出了产生 Meta-Path 的代码. 不过多介绍.

import sys
import os
import random
from collections import Counter

class MetaPathGenerator:
	def __init__(self):
		self.id_author = dict()
		self.id_conf = dict()
		self.author_coauthorlist = dict()
		self.conf_authorlist = dict()
		self.author_conflist = dict()
		self.paper_author = dict()
		self.author_paper = dict()
		self.conf_paper = dict()
		self.paper_conf = dict()

	def read_data(self, dirpath):
		with open(dirpath + "/id_author.txt") as adictfile:
			for line in adictfile:
				toks = line.strip().split("\t")
				if len(toks) == 2:
					self.id_author[toks[0]] = toks[1].replace(" ", "")

		#print "#authors", len(self.id_author)

		with open(dirpath + "/id_conf.txt") as cdictfile:
			for line in cdictfile:
				toks = line.strip().split("\t")
				if len(toks) == 2:
					newconf = toks[1].replace(" ", "")
					self.id_conf[toks[0]] = newconf

		#print "#conf", len(self.id_conf)

		with open(dirpath + "/paper_author.txt") as pafile:
			for line in pafile:
				toks = line.strip().split("\t")
				if len(toks) == 2:
					p, a = toks[0], toks[1]
					if p not in self.paper_author:
						self.paper_author[p] = []
					self.paper_author[p].append(a)
					if a not in self.author_paper:
						self.author_paper[a] = []
					self.author_paper[a].append(p)

		with open(dirpath + "/paper_conf.txt") as pcfile:
			for line in pcfile:
				toks = line.strip().split("\t")
				if len(toks) == 2:
					p, c = toks[0], toks[1]
					self.paper_conf[p] = c 
					if c not in self.conf_paper:
						self.conf_paper[c] = []
					self.conf_paper[c].append(p)

		sumpapersconf, sumauthorsconf = 0, 0
		conf_authors = dict()
		for conf in self.conf_paper:
			papers = self.conf_paper[conf]
			sumpapersconf += len(papers)
			for paper in papers:
				if paper in self.paper_author:
					authors = self.paper_author[paper]
					sumauthorsconf += len(authors)

		print "#confs  ", len(self.conf_paper)
		print "#papers ", sumpapersconf,  "#papers per conf ", sumpapersconf / len(self.conf_paper)
		print "#authors", sumauthorsconf, "#authors per conf", sumauthorsconf / len(self.conf_paper)


	def generate_random_aca(self, outfilename, numwalks, walklength):
		for conf in self.conf_paper:
			self.conf_authorlist[conf] = []
			for paper in self.conf_paper[conf]:
				if paper not in self.paper_author: continue
				for author in self.paper_author[paper]:
					self.conf_authorlist[conf].append(author)
					if author not in self.author_conflist:
						self.author_conflist[author] = []
					self.author_conflist[author].append(conf)
		#print "author-conf list done"

		outfile = open(outfilename, 'w')
		for conf in self.conf_authorlist:
			conf0 = conf
			for j in xrange(0, numwalks ): #wnum walks
				outline = self.id_conf[conf0]
				for i in xrange(0, walklength):
					authors = self.conf_authorlist[conf]
					numa = len(authors)
					authorid = random.randrange(numa)
					author = authors[authorid]
					outline += " " + self.id_author[author]
					confs = self.author_conflist[author]
					numc = len(confs)
					confid = random.randrange(numc)
					conf = confs[confid]
					outline += " " + self.id_conf[conf]
				outfile.write(outline + "\n")
		outfile.close()


#python py4genMetaPaths.py 1000 100 net_aminer output.aminer.w1000.l100.txt
#python py4genMetaPaths.py 1000 100 net_dbis   output.dbis.w1000.l100.txt

dirpath = "net_aminer" 
# OR 
dirpath = "net_dbis"

numwalks = int(sys.argv[1])
walklength = int(sys.argv[2])

metapath2vec 与 metapath2vec++

在上一步产生基于 meta-path 的序列后, 使用 word2vec 的 skip-gram 来处理序列, 并采用 Negative Sampling 来进行优化,

p ( c t ∣ v ; θ ) = e X c t ⋅ X v ∑ u ∈ V e X u ⋅ X v p\left(c_{t} \mid v ; \theta\right)=\frac{e^{X_{c_{t}} \cdot X_{v}}}{\sum_{u \in V} e^{X_{u} \cdot X_{v}}} p(ctv;θ)=uVeXuXveXctXv

其中 c t c_{t} ct v v v 的上下文节点 (正负样本节点).

但注意由于 metapath2vec 在进行负采样的时候, 并没有对节点的类型进行区分 (注意看上式分母中的 u ∈ V u\in V uV 是任何类型的节点). 但作者认为即使在负采样时也需要考虑节点的类型, 因此提出了 Heterogeneous Negative Sampling (异构负采样), 即:

p ( c t ∣ v ; θ ) = e X c t ⋅ X v ∑ u t ∈ V t e X u t ⋅ X v p\left(c_{t} \mid v ; \theta\right)=\frac{e^{X_{c_{t}} \cdot X_{v}}}{\sum_{u_{t} \in V_{t}} e^{X_{u_{t}} \cdot X_{v}}} p(ctv;θ)=utVteXutXveXctXv

注意看此时分母 u t ∈ V t u_t\in V_{t} utVt, 即负采样时考虑了节点的类型. 另外结合上面论文中的图 2© 和图 2(b) 来看, 更容易体会. 代码不多说了~~, 没有太细看, 咳咳, 重点是了解思想!

总结

终于填完坑了, 心情真好~ OK, 继续挖坑

推荐资料

  • 【Graph Embedding】: metapath2vec算法

你可能感兴趣的:(Graph,Embedding,metapath,metapath2vec,图网络学习,异构图学习,word2vec)