原文发布于微信公众号 - 小小挖掘机(wAIsjwj)

原文发表时间:2018-11-15

知识图谱特征学习在推荐系统中的应用步骤大致有以下三种方式:

依次训练的方法主要有:Deep Knowledge-aware Network(DKN)

联合训练的方法主要有:Ripple Network

交替训练主要采用multi-task的思路,主要方法有:Multi-task Learning for KG enhanced Recommendation (MKR)

本文先来介绍联合训练的方法Ripple Network。

论文下载地址为: https://arxiv.org/abs/1803.03467

1、RippleNet原理

1.1 RippleNet背景

在上一篇中我们介绍了Deep Knowledge-aware Network(DKN),在DKN中,我们需要首先学习到entity的向量和relation的向量,但是学习到的向量,其目的是为了还原知识图谱中的三元组关系,而并非是为了我们的推荐任务而学习的。因此今天我们来介绍一下知识图谱和推荐系统进行联合训练的一种网络结构:RippleNet。

Ripple是波纹的意思,RippleNet就是模拟用户兴趣在知识图谱上的一个传播过程,如下图所示:

如上图,用户的兴趣以其历史记录为中心,在知识图谱上逐层向外扩散,而在扩散过程中不断的衰减,类似于水中的波纹,因此称为RippleNet。

1.2 RippleNet网络结构

我们先来介绍两个相关的定义:

Relevant Entity:在给定知识图谱的情况下,用户u的k-hop相关实体定义如下:

特别地,用户u的0-hop相关实体即用户的历史记录。

Ripple Set:用户u的k-hop ripple set被定义为以k-1 Relevant Entity 为head的相关三元组:

这里,为避免Ripple Set过大,一般都会设定一个最大的长度,进行截断。另一方面,构建的知识图谱都是有向图,只考虑点的出度。

接下来,我们来看看RippleNet的网络结构:

可以看到,最终的预测值是通过item embedding和user embedding得到的,item embedding通过embedding 层可以直接得到,关键是user embedding的获取。user embedding是通过图中的绿色矩形表示的向量相加得到的,接下来,我们以第一个绿色矩形表示的向量为例,来看一下具体是如何计算的。

第一个绿色矩形表示的向量,需要使用的是1-hop的ripple set,对于set中的每一个(h,r,t),会计算一个与item-embedding的相关性,相关性计算公式如下:

最后通过加权所有t对应的embedding,就得到了第一个绿色矩形表示的向量,表示用户兴趣经第一轮扩散后的结果:

接下来,我们重复上面的过程,假设一共H次,那么最终user embedding的结果为:

而最终的预测值计算如下:

1.3 RippleNet损失函数

在给定知识图谱G,用户的隐式反馈(即用户的历史记录)Y时,我们希望最大化后验概率:

后验概率展开如下:

其中,我们认为参数的先验概率服从0均值的正态分布:

第二项的似然函数形式如下:

上面的式子搞得我有点懵,后面应该是一个具体的概率值而不是一个正态分布,G在θ条件下的分布也是一个0均值的正态分布,后面应该是取得Ih,r,t-hTRt的一个概率,由于我们希望我们得到的指数图谱特征表示能够更好的还原三元组关系,因此希望Ih,r,t-hTRt越接近0越好。

第三项没什么问题,即我们常用的二分类似然函数:

因此,我们可以得到RippleNet的损失函数形式如下:

2、RippleNet的Tensorflow实现

本文的代码地址如下: https://github.com/princewen/tensorflow_practice/tree/master/recommendation/Basic-RippleNet-Demo

参考的代码地址为: https://github.com/hwwang55/RippleNet

数据下载地址为:: https://pan.baidu.com/s/13vL-z5Wk3jQFfmVIPXDovw 密码:infx

在对数据进行预处理后,我们得到了两个文件:kg_final.txt和rating_final.txt

rating_final.txt数据形式如下,三列分别是user-id,item-id以及label(0是通过负采样得到的,正负样本比例为1:1)。

kg_final.txt格式如下,三类分别代表h,r,t(这里entity和item用的是同一套id):

好了,接下来我们重点介绍一下我们的RippleNet网络的构建。

网络输入

网络输入主要有item的id,label以及对应的用户的ripple set:

def _build_inputs(self):
    self.items = tf.placeholder(dtype=tf.int32, shape=[None], name="items")
    self.labels = tf.placeholder(dtype=tf.float64, shape=[None], name="labels")
    self.memories_h = []
    self.memories_r = []
    self.memories_t = []

    for hop in range(self.n_hop):
        self.memories_h.append(
            tf.placeholder(dtype=tf.int32, shape=[None, self.n_memory], name="memories_h_" + str(hop)))
        self.memories_r.append(
            tf.placeholder(dtype=tf.int32, shape=[None, self.n_memory], name="memories_r_" + str(hop)))
        self.memories_t.append(
            tf.placeholder(dtype=tf.int32, shape=[None, self.n_memory], name="memories_t_" + str(hop)))

embedding层构建

这里需要的embedding主要有entity的embedding(与item 的embedding共用)和relation的embedding,假设embedding的长度为dim,那么注意到由于relation是要用来链接head和tail的,所以它的embedding的维度为dim * dim:

def _build_embeddings(self):
    self.entity_emb_matrix = tf.get_variable(name="entity_emb_matrix", dtype=tf.float64,
                                             shape=[self.n_entity, self.dim],
                                             initializer=tf.contrib.layers.xavier_initializer())
    self.relation_emb_matrix = tf.get_variable(name="relation_emb_matrix", dtype=tf.float64,
                                               shape=[self.n_relation, self.dim, self.dim],
                                               initializer=tf.contrib.layers.xavier_initializer())

模型构建

模型构建的代码如下,可以看到我们建立了一个transform_matrix的tensor,这个tensor就是用来更新计算过程中的item-embedding的,我们后面会详细介绍:

def _build_model(self):
    # transformation matrix for updating item embeddings at the end of each hop
    self.transform_matrix = tf.get_variable(name="transform_matrix", shape=[self.dim, self.dim], dtype=tf.float64,
                                            initializer=tf.contrib.layers.xavier_initializer())

    # [batch size, dim]
    self.item_embeddings = tf.nn.embedding_lookup(self.entity_emb_matrix, self.items)

    self.h_emb_list = []
    self.r_emb_list = []
    self.t_emb_list = []
    for i in range(self.n_hop):
        # [batch size, n_memory, dim]
        self.h_emb_list.append(tf.nn.embedding_lookup(self.entity_emb_matrix, self.memories_h[i]))

        # [batch size, n_memory, dim, dim]
        self.r_emb_list.append(tf.nn.embedding_lookup(self.relation_emb_matrix, self.memories_r[i]))

        # [batch size, n_memory, dim]
        self.t_emb_list.append(tf.nn.embedding_lookup(self.entity_emb_matrix, self.memories_t[i]))

    o_list = self._key_addressing()

    self.scores = tf.squeeze(self.predict(self.item_embeddings, o_list))
    self.scores_normalized = tf.sigmoid(self.scores)

上面用到了两个函数,分别是_key_addressing()和predict(),接下来,我们来介绍这两个函数。

_key_addressing()是用来的到我们的olist的,即我们在RippleNet中的绿色矩形表示的向量:

def _key_addressing(self):
    o_list = []
    for hop in range(self.n_hop):
        # [batch_size, n_memory, dim, 1]
        h_expanded = tf.expand_dims(self.h_emb_list[hop], axis=3)
        # [batch_size, n_memory, dim]
        Rh = tf.squeeze(tf.matmul(self.r_emb_list[hop], h_expanded), axis=3)
        # [batch_size, dim, 1]
        v = tf.expand_dims(self.item_embeddings, axis=2)
        # [batch_size, n_memory]
        probs = tf.squeeze(tf.matmul(Rh, v), axis=2)
        # [batch_size, n_memory]
        probs_normalized = tf.nn.softmax(probs)
        # [batch_size, n_memory, 1]
        probs_expanded = tf.expand_dims(probs_normalized, axis=2)
        # [batch_size, dim]
        o = tf.reduce_sum(self.t_emb_list[hop] * probs_expanded, axis=1)

        self.item_embeddings = self.update_item_embedding(self.item_embeddings, o)
        o_list.append(o)
    return o_list

可以看到,在上面的代码中,我们计算的是ripple set中每一个(h,r,t)和item-embedding的相关性,再每一个hop计算完成后,有一个update_item_embedding的操作,在这里面,我们可以选择不同的替换策略:

def update_item_embedding(self, item_embeddings, o):
    if self.item_update_mode == "replace":
        item_embeddings = o
    elif self.item_update_mode == "plus":
        item_embeddings = item_embeddings + o
    elif self.item_update_mode == "replace_transform":
        item_embeddings = tf.matmul(o, self.transform_matrix)
    elif self.item_update_mode == "plus_transform":
        item_embeddings = tf.matmul(item_embeddings + o, self.transform_matrix)
    else:
        raise Exception("Unknown item updating mode: " + self.item_update_mode)
    return item_embeddings

在得到olist之后,我们可以只用olist里面最后一个向量,也可以选择相加所有的向量,来代表user-embedding,并最终计算得到预测值:

def predict(self, item_embeddings, o_list):
    y = o_list[-1]
    if self.using_all_hops:
        for i in range(self.n_hop - 1):
            y += o_list[i]

    # [batch_size]
    scores = tf.reduce_sum(item_embeddings * y, axis=1)
    return scores

计算损失

我们前面提到了,模型的loss最终由三部分组成,在取对数后,三部分损失分别表示对数损失、知识图谱特征表示的损失,正则化损失:

def _build_loss(self):
    self.base_loss = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(labels=self.labels, logits=self.scores))

    self.kge_loss = 0
    for hop in range(self.n_hop):
        h_expanded = tf.expand_dims(self.h_emb_list[hop], axis=2)
        t_expanded = tf.expand_dims(self.t_emb_list[hop], axis=3)
        hRt = tf.squeeze(tf.matmul(tf.matmul(h_expanded, self.r_emb_list[hop]), t_expanded))
        self.kge_loss += tf.reduce_mean(tf.sigmoid(hRt))
    self.kge_loss = -self.kge_weight * self.kge_loss

    self.l2_loss =