推荐系统遇上深度学习 (二十七)-- 知识图谱与推荐系统结合之 RippleNet 模型原理及实现


本文地址:http://www.6aiq.com/article/1547907828825
知乎专栏 点击关注
本文版权归作者和AIQ共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出

原文发布于微信公众号 - 小小挖掘机(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 = 0
    for hop in range(self.n_hop):
        self.l2_loss += tf.reduce_mean(tf.reduce_sum(self.h_emb_list[hop] * self.h_emb_list[hop]))
        self.l2_loss += tf.reduce_mean(tf.reduce_sum(self.t_emb_list[hop] * self.t_emb_list[hop]))
        self.l2_loss += tf.reduce_mean(tf.reduce_sum(self.r_emb_list[hop] * self.r_emb_list[hop]))
        if self.item_update_mode == "replace nonlinear" or self.item_update_mode == "plus nonlinear":
            self.l2_loss += tf.nn.l2_loss(self.transform_matrix)
    self.l2_loss = self.l2_weight * self.l2_loss

    self.loss = self.base_loss + self.kge_loss + self.l2_loss

好了,代码的部分我们就介绍完了,如果大家感兴趣,可以下载相应的代码和数据,进行相应的编写和调试哟!

参考文献:
1、论文:https://arxiv.org/abs/1803.03467


本文地址:http://www.6aiq.com/article/1547907828825
知乎专栏 点击关注
本文版权归作者和AIQ共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出