周末立了个 Flag, 说要完成两篇博客的编写 (更精准的说法是至少两篇), 昨天完成了一篇 DIN 深度兴趣网络介绍以及源码浅析, 今天白天由于忙着买菜, 洗菜和做菜还有运动, 白天恍恍惚惚的过去了, 现在距离夜里 12 点还有 20 分钟左右, 水一篇~
距离 12 点还有 10 分钟时突然想到 … 可以先写一点, 留个坑, 以后再填, 这样的话, 只需要新立一个小小的 Flag, 不仅能完成我这个周末的 Flag, 还可以督促我未来用功, 一举两得, 一石二鸟, 我简直机智的一笔
(2020-08-16 补充) 上周立的 Flag, 现在终于终于来填坑了. 而且, 本周也完成了两篇博客的编写 (AFM 网络介绍与源码浅析 以及 Product-based Neural Network (PNN) 介绍与源码浅析) 简直 6 啊
可以在微信中搜索 “珍妮的算法之路” 或者 “world4458” 关注我的微信公众号;另外可以看看知乎专栏 PoorMemory-机器学习, 以后文章也会发在知乎专栏中;
本文提出的算法主要用来处理异构图的表示学习问题. 异构网络指的是节点或边的类型大于 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 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} V1⟶R1V2⟶R2⋯Vt⟶RtVt+1⋯⟶Rl−1Vl
其中 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=R1∘R2∘⋯∘Rl−1 表示节点类型 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+1∣vti,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} vti∈Vt, 说明节点 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+1∈Vt+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])
在上一步产生基于 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(ct∣v;θ)=∑u∈VeXu⋅XveXct⋅Xv
其中 c t c_{t} ct 为 v v v 的上下文节点 (正负样本节点).
但注意由于 metapath2vec 在进行负采样的时候, 并没有对节点的类型进行区分 (注意看上式分母中的 u ∈ V u\in V u∈V 是任何类型的节点). 但作者认为即使在负采样时也需要考虑节点的类型, 因此提出了 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(ct∣v;θ)=∑ut∈VteXut⋅XveXct⋅Xv
注意看此时分母 u t ∈ V t u_t\in V_{t} ut∈Vt, 即负采样时考虑了节点的类型. 另外结合上面论文中的图 2© 和图 2(b) 来看, 更容易体会. 代码不多说了~~, 没有太细看, 咳咳, 重点是了解思想!
终于填完坑了, 心情真好~ OK, 继续挖坑