CANE是发表在ACL2017上的一篇network embedding的论文,文章假设一个节点和不同的邻居节点相连有不同的embedding,提出考虑上下文的embedding方法CANE.
1. Problem Formulation
假设存在信息网络G=(V,E,T),V是顶点集,是边集,T是节点的文本信息。一个节点
的文本信息由一个词序列
表示。
Context-free Embeddings: 一个节点的embedding是固定的,不会根据它的context(与它相互影响的另一个节点)改变。
Context-aware Embeddings: CANE根据一个节点不同的context学习不同的embedding。
2. Framework
对一个节点v有两种embedding,一种是基于结构的embedding ,一种是基于文本的
(可以是context-free或context-aware),然后concatenate拼接它们得到
。
CANE希望最大化边的目标函数如下:
Ls是基于结构的目标函数,Lt是基于文本的。
2.1 structure-based objective
基于结构的目标函数希望衡量一个有向边的对数似然:
(3)
继承LINE,节点v被u generate 的条件概率如下:
(4)
2.2 text-based objective
基于文本的目标函数为了与结构目标函数兼容,定义如下:
条件概率的计算如公式4所示。
structure-based embedding作为参数,text-based可以通过Context-free或Context-aware方法获得。
2.3 Context-free text embedding
将一个节点的词序列作为CNN的输入,通过一下三层得到embedding:
Looking-up. 将词序列中每个词转化为对应的词向量,从而得到序列embedding S.
Convolution. 从序列embedding S中提取局部特征,使用卷积矩阵在一个长度为l(序列S中取l个单词)的滑动窗口上进行卷积操作:
Max-pooling.
2.4 Context-aware text embedding
给定一条边以及对应的两个文本序列Su,Sv,通过卷积层得到
和
(m,n分别代表Su,Sv的长度).引入注意力矩阵A
,相关性矩阵F计算如下:
行池化和列池化分别如下:
对于公式(3)和(6),使用spftmax优化条件概率计算代价过大,使用负采样将目标方程变为如下所示:
使用Adam算法优化该目标。
关键代码讲解:
实验中使用的数据data.txt包含每个节点的文本描述,每行一个节点;graph.txt每行(例[440, 50] 两个节点编号)对应一条边。
#数据处理
class dataSet:
def __init__(self,text_path,graph_path):
text_file,graph_file=self.load(text_path,graph_path)
self.edges=self.load_edges(graph_file)
self.text,self.num_vocab,self.num_nodes=self.load_text(text_file)
self.negative_table=InitNegTable(self.edges)
def load(self,text_path,graph_path):
text_file=open(text_path,'rb').readlines()
graph_file=open(graph_path,'rb').readlines()
return text_file,graph_file
#将边准化成数字列表[[440, 50], [440, 103], [440, 105], [440, 116], [440, 135], [440, #235],...]
def load_edges(self,graph_file):
edges=[]
for i in graph_file:
edges.append(map(int,i.strip().split('\t')))
return edges
#节点文本中每一单词对应一个index,每个节点文本对应一个300维的list
#[[ 1 2 3 ..., 0 0 0]
#[ 38 39 40 ..., 0 0 0]
# ...,
# [100 256 55 ..., 0 0 0]]
def load_text(self,text_file):
vocab=learn.preprocessing.VocabularyProcessor(config.MAX_LEN)
text=np.array(list(vocab.fit_transform(text_file)))
num_vocab=len(vocab.vocabulary_)
num_nodes=len(text)
return text,num_vocab,num_nodes
#负采样
def negative_sample(self,edges):
node1,node2=zip(*edges)
sample_edges=[]
func=lambda : self.negative_table[random.randint(0,config.neg_table_size-1)]
for i in range(len(edges)):
neg_node=func()
while node1[i] == neg_node or node2[i] == neg_node:
neg_node=func()
sample_edges.append([node1[i],node2[i],neg_node])
return sample_edges
def generate_batches(self,mode=None):
num_batch=len(self.edges)/config.batch_size
edges=self.edges
if mode=='add':
num_batch+=1
edges.extend(edges[:(config.batch_size-len(self.edges)//config.batch_size)])
if mode != 'add':
random.shuffle(edges)
sample_edges=edges[:num_batch*config.batch_size]
sample_edges=self.negative_sample(sample_edges)
batches=[]
for i in range(num_batch):
batches.append(sample_edges[i*config.batch_size:(i+1)*config.batch_size])
# print sample_edges[0]
return batches
模型(context-aware text embedding方法)
class Model:
def __init__(self,vocab_size,num_nodes):
# '''hyperparameter'''
with tf.name_scope('read_inputs') as scope:
self.Text_a=tf.placeholder(tf.int32,[config.batch_size,config.MAX_LEN],name='Ta')
self.Text_b=tf.placeholder(tf.int32,[config.batch_size,config.MAX_LEN],name='Tb')
self.Text_neg=tf.placeholder(tf.int32,[config.batch_size,config.MAX_LEN],name='Tneg')
self.Node_a=tf.placeholder(tf.int32,[config.batch_size],name='n1')
self.Node_b=tf.placeholder(tf.int32,[config.batch_size],name='n2')
self.Node_neg=tf.placeholder(tf.int32,[config.batch_size],name='n3')
with tf.name_scope('initialize_embedding') as scope:
self.text_embed=tf.Variable(tf.truncated_normal([vocab_size,config.embed_size/2],stddev=0.3))
self.node_embed=tf.Variable(tf.truncated_normal([num_nodes,config.embed_size/2],stddev=0.3))
self.node_embed=tf.clip_by_norm(self.node_embed,clip_norm=1,axes=1)
with tf.name_scope('lookup_embeddings') as scope:
self.TA=tf.nn.embedding_lookup(self.text_embed,self.Text_a)
self.T_A=tf.expand_dims(self.TA,-1)
self.TB=tf.nn.embedding_lookup(self.text_embed,self.Text_b)
self.T_B=tf.expand_dims(self.TB,-1)
self.TNEG=tf.nn.embedding_lookup(self.text_embed,self.Text_neg)
self.T_NEG=tf.expand_dims(self.TNEG,-1)
self.N_A=tf.nn.embedding_lookup(self.node_embed,self.Node_a)
self.N_B=tf.nn.embedding_lookup(self.node_embed,self.Node_b)
self.N_NEG=tf.nn.embedding_lookup(self.node_embed,self.Node_neg)
self.convA,self.convB,self.convNeg=self.conv()
self.loss=self.compute_loss()
def conv(self):
W2=tf.Variable(tf.truncated_normal([2,config.embed_size/2,1,100],stddev=0.3))
rand_matrix=tf.Variable(tf.truncated_normal([100,100],stddev=0.3))
convA=tf.nn.conv2d(self.T_A,W2,strides=[1,1,1,1],padding='VALID')
convB=tf.nn.conv2d(self.T_B,W2,strides=[1,1,1,1],padding='VALID')
convNEG=tf.nn.conv2d(self.T_NEG,W2,strides=[1,1,1,1],padding='VALID')
hA=tf.tanh(tf.squeeze(convA))
hB=tf.tanh(tf.squeeze(convB))
hNEG=tf.tanh(tf.squeeze(convNEG))
tmphA=tf.reshape(hA,[config.batch_size*(config.MAX_LEN-1),config.embed_size/2])
ha_mul_rand=tf.reshape(tf.matmul(tmphA,rand_matrix),[config.batch_size,config.MAX_LEN-1,config.embed_size/2])
#-----------------------need to be revise
r1=tf.matmul(ha_mul_rand,hB,adjoint_b=True)
r3=tf.matmul(ha_mul_rand,hNEG,adjoint_b=True)
att1=tf.expand_dims(tf.stack(r1),-1)
att3=tf.expand_dims(tf.stack(r3),-1)
att1=tf.tanh(att1)
att3=tf.tanh(att3)
pooled_A=tf.reduce_mean(att1,2)
pooled_B=tf.reduce_mean(att1,1)
pooled_NEG=tf.reduce_mean(att3,1)
a_flat=tf.squeeze(pooled_A)
b_flat=tf.squeeze(pooled_B)
neg_flat=tf.squeeze(pooled_NEG)
w_A=tf.nn.softmax(a_flat)
w_B=tf.nn.softmax(b_flat)
w_NEG=tf.nn.softmax(neg_flat)
rep_A=tf.expand_dims(w_A,-1)
rep_B=tf.expand_dims(w_B,-1)
rep_NEG=tf.expand_dims(w_NEG,-1)
hA=tf.transpose(hA,perm=[0,2,1])
hB=tf.transpose(hB,perm=[0,2,1])
hNEG=tf.transpose(hNEG,perm=[0,2,1])
rep1=tf.matmul(hA,rep_A)
rep2=tf.matmul(hB,rep_B)
rep3=tf.matmul(hNEG,rep_NEG)
attA=tf.squeeze(rep1)
attB=tf.squeeze(rep2)
attNEG=tf.squeeze(rep3)
return attA,attB,attNEG
def compute_loss(self):
p1=tf.reduce_sum(tf.multiply(self.convA,self.convB),1)
p1=tf.log(tf.sigmoid(p1)+0.001)
p2=tf.reduce_sum(tf.multiply(self.convA,self.convNeg),1)
p2=tf.log(tf.sigmoid(-p2)+0.001)
p3=tf.reduce_sum(tf.multiply(self.N_A,self.N_B),1)
p3=tf.log(tf.sigmoid(p3)+0.001)
#后面的mul()函数依次改成multiply
p4=tf.reduce_sum(tf.mul(self.N_A,self.N_NEG),1)
p4=tf.log(tf.sigmoid(-p4)+0.001)
p5=tf.reduce_sum(tf.mul(self.convB,self.N_A),1)
p5=tf.log(tf.sigmoid(p5)+0.001)
p6=tf.reduce_sum(tf.mul(self.convNeg,self.N_A),1)
p6=tf.log(tf.sigmoid(-p6)+0.001)
p7=tf.reduce_sum(tf.mul(self.N_B,self.convA),1)
p7=tf.log(tf.sigmoid(p7)+0.001)
p8=tf.reduce_sum(tf.mul(self.N_B,self.convNeg),1)
p8=tf.log(tf.sigmoid(-p8)+0.001)
rho1=0.7
rho2=1.0
rho3=0.1
temp_loss=rho1*(p1+p2)+rho2*(p3+p4)+rho3*(p5+p6)+rho3*(p7+p8)
loss=-tf.reduce_sum(temp_loss)
return loss