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

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

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

依次训练的方法主要有: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

本文分享自微信公众号 - 小小挖掘机(wAIsjwj)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

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

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 推荐系统遇上深度学习(十一)--神经协同过滤NCF原理及实战

    好久没更新该系列了,最近看到了一篇关于神经协同过滤的论文,感觉还不错,跟大家分享下。

    石晓文
  • DQN三大改进(二)-Prioritised replay

    Prioritised replay原文:https://arxiv.org/pdf/1511.05952.pdf 代码地址:https://github.co...

    石晓文
  • 推荐系统遇上深度学习(二十三)--大一统信息检索模型IRGAN在推荐领域的应用

    信息检索领域的一个重要任务就是针对用户的一个请求query,返回一组排好序的召回列表。

    石晓文
  • 封装与扩展性

    封装在于明确区分内外,使得类实现者可以修改封装内的东西而不影响外部调用者的代码;而外部使用用者只知道一个接口(函数),只要接口(函数)名、参数不变,使用者的代码...

    超蛋lhy
  • 再议Python协程——从yield到asyncio

    协程,英文名Coroutine。 前面介绍Python的多线程,以及用多线程实现并发(参见这篇文章【浅析Python多线程】),今天介绍的协程也是常用的并发手段...

    用户1432189
  • What?废柴, 模拟登陆,代码控制滑动验证真的很难吗?Are you kidding???

    在前边的python接口自动化的时候,我们由于博客园的登录机制的改变,没有用博客园的登录测试接口。那么博客园现在变成了滑动验证登录,而且现在绝大多数的登录都变成...

    北京-宏哥
  • Selenium2+python自动化48-登录方法(参数化)

    前言 登录这个场景在写用例的时候经常会有,我们可以把登录封装成一个方法,然后把账号和密码参数化,这样以后用的登录的时候,只需调用这个方法就行了 一、登录方法 1...

    上海-悠悠
  • Django(三):HttpReques

      当一个请求连接进来时,django会创建一个HttpRequest对象来封装和保存所有请求相关的信息,并且会根据请求路由载入匹配的视图函数。每个请求的视图函...

    py3study
  • python unittest初探

    class Request:    url = ""    method = ""    paras = {}    def __init__(self...

    py3study
  • 昨晚家里停网后,我写了一段代码破解了隔壁小姐姐的wifi密码...

    昨晚,家里停网了,对于码农而言,停网了,这能忍?打电话给修网络的,说太晚了,要第二天才能过来...

    良月柒

扫码关注云+社区

领取腾讯云代金券