推荐系统遇上深度学习(二十七)--知识图谱与推荐系统结合之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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容