推薦系統遇上深度學習(二十七)--知識圖譜與推薦系統結合之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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章