知識(shí)圖譜特征學(xué)習(xí)在推薦系統(tǒng)中的應(yīng)用步驟大致有以下三種方式:

依次訓(xùn)練的方法主要有:Deep Knowledge-aware Network(DKN)
聯(lián)合訓(xùn)練的方法主要有:Ripple Network
交替訓(xùn)練主要采用multi-task的思路,主要方法有:Multi-task Learning for KG enhanced Recommendation (MKR)
本文先來(lái)介紹聯(lián)合訓(xùn)練的方法Ripple Network。
論文下載地址為:https://arxiv.org/abs/1803.03467
1、RippleNet原理
1.1 RippleNet背景
在上一篇中我們介紹了Deep Knowledge-aware Network(DKN),在DKN中,我們需要首先學(xué)習(xí)到entity的向量和relation的向量,但是學(xué)習(xí)到的向量,其目的是為了還原知識(shí)圖譜中的三元組關(guān)系,而并非是為了我們的推薦任務(wù)而學(xué)習(xí)的。因此今天我們來(lái)介紹一下知識(shí)圖譜和推薦系統(tǒng)進(jìn)行聯(lián)合訓(xùn)練的一種網(wǎng)絡(luò)結(jié)構(gòu):RippleNet。
Ripple是波紋的意思,RippleNet就是模擬用戶(hù)興趣在知識(shí)圖譜上的一個(gè)傳播過(guò)程,如下圖所示:

如上圖,用戶(hù)的興趣以其歷史記錄為中心,在知識(shí)圖譜上逐層向外擴(kuò)散,而在擴(kuò)散過(guò)程中不斷的衰減,類(lèi)似于水中的波紋,因此稱(chēng)為RippleNet。
1.2 RippleNet網(wǎng)絡(luò)結(jié)構(gòu)
我們先來(lái)介紹兩個(gè)相關(guān)的定義:
Relevant Entity:在給定知識(shí)圖譜的情況下,用戶(hù)u的k-hop相關(guān)實(shí)體定義如下:

特別地,用戶(hù)u的0-hop相關(guān)實(shí)體即用戶(hù)的歷史記錄。
Ripple Set:用戶(hù)u的k-hop ripple set被定義為以k-1 Relevant Entity 為head的相關(guān)三元組:

這里,為避免Ripple Set過(guò)大,一般都會(huì)設(shè)定一個(gè)最大的長(zhǎng)度,進(jìn)行截?cái)?。另一方面,?gòu)建的知識(shí)圖譜都是有向圖,只考慮點(diǎn)的出度。
接下來(lái),我們來(lái)看看RippleNet的網(wǎng)絡(luò)結(jié)構(gòu):

可以看到,最終的預(yù)測(cè)值是通過(guò)item embedding和user embedding得到的,item embedding通過(guò)embedding 層可以直接得到,關(guān)鍵是user embedding的獲取。user embedding是通過(guò)圖中的綠色矩形表示的向量相加得到的,接下來(lái),我們以第一個(gè)綠色矩形表示的向量為例,來(lái)看一下具體是如何計(jì)算的。
第一個(gè)綠色矩形表示的向量,需要使用的是1-hop的ripple set,對(duì)于set中的每一個(gè)(h,r,t),會(huì)計(jì)算一個(gè)與item-embedding的相關(guān)性,相關(guān)性計(jì)算公式如下:

最后通過(guò)加權(quán)所有t對(duì)應(yīng)的embedding,就得到了第一個(gè)綠色矩形表示的向量,表示用戶(hù)興趣經(jīng)第一輪擴(kuò)散后的結(jié)果:

接下來(lái),我們重復(fù)上面的過(guò)程,假設(shè)一共H次,那么最終user embedding的結(jié)果為:

而最終的預(yù)測(cè)值計(jì)算如下:

1.3 RippleNet損失函數(shù)
在給定知識(shí)圖譜G,用戶(hù)的隱式反饋(即用戶(hù)的歷史記錄)Y時(shí),我們希望最大化后驗(yàn)概率:

后驗(yàn)概率展開(kāi)如下:

其中,我們認(rèn)為參數(shù)的先驗(yàn)概率服從0均值的正態(tài)分布:

第二項(xiàng)的似然函數(shù)形式如下:

上面的式子搞得我有點(diǎn)懵,后面應(yīng)該是一個(gè)具體的概率值而不是一個(gè)正態(tài)分布,G在θ條件下的分布也是一個(gè)0均值的正態(tài)分布,后面應(yīng)該是取得Ih,r,t-hTRt的一個(gè)概率,由于我們希望我們得到的指數(shù)圖譜特征表示能夠更好的還原三元組關(guān)系,因此希望Ih,r,t-hTRt越接近0越好。
第三項(xiàng)沒(méi)什么問(wèn)題,即我們常用的二分類(lèi)似然函數(shù):

因此,我們可以得到RippleNet的損失函數(shù)形式如下:

2、RippleNet的Tensorflow實(shí)現(xiàn)
本文的代碼地址如下:https://github.com/princewen/tensorflow_practice/tree/master/recommendation/Basic-RippleNet-Demo
參考的代碼地址為:https://github.com/hwwang55/RippleNet
數(shù)據(jù)下載地址為::https://pan.baidu.com/s/13vL-z5Wk3jQFfmVIPXDovw 密碼:infx
在對(duì)數(shù)據(jù)進(jìn)行預(yù)處理后,我們得到了兩個(gè)文件:kg_final.txt和rating_final.txt
rating_final.txt數(shù)據(jù)形式如下,三列分別是user-id,item-id以及l(fā)abel(0是通過(guò)負(fù)采樣得到的,正負(fù)樣本比例為1:1)。

kg_final.txt格式如下,三類(lèi)分別代表h,r,t(這里entity和item用的是同一套id):

好了,接下來(lái)我們重點(diǎn)介紹一下我們的RippleNet網(wǎng)絡(luò)的構(gòu)建。
網(wǎng)絡(luò)輸入
網(wǎng)絡(luò)輸入主要有item的id,label以及對(duì)應(yīng)的用戶(hù)的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層構(gòu)建
這里需要的embedding主要有entity的embedding(與item 的embedding共用)和relation的embedding,假設(shè)embedding的長(zhǎng)度為dim,那么注意到由于relation是要用來(lái)鏈接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())
模型構(gòu)建
模型構(gòu)建的代碼如下,可以看到我們建立了一個(gè)transform_matrix的tensor,這個(gè)tensor就是用來(lái)更新計(jì)算過(guò)程中的item-embedding的,我們后面會(huì)詳細(xì)介紹:
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)
上面用到了兩個(gè)函數(shù),分別是_key_addressing()和predict(),接下來(lái),我們來(lái)介紹這兩個(gè)函數(shù)。
_key_addressing()是用來(lái)的到我們的olist的,即我們?cè)赗ippleNet中的綠色矩形表示的向量:
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
可以看到,在上面的代碼中,我們計(jì)算的是ripple set中每一個(gè)(h,r,t)和item-embedding的相關(guān)性,再每一個(gè)hop計(jì)算完成后,有一個(gè)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里面最后一個(gè)向量,也可以選擇相加所有的向量,來(lái)代表user-embedding,并最終計(jì)算得到預(yù)測(cè)值:
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
計(jì)算損失
我們前面提到了,模型的loss最終由三部分組成,在取對(duì)數(shù)后,三部分損失分別表示對(duì)數(shù)損失、知識(shí)圖譜特征表示的損失,正則化損失:
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
好了,代碼的部分我們就介紹完了,如果大家感興趣,可以下載相應(yīng)的代碼和數(shù)據(jù),進(jìn)行相應(yīng)的編寫(xiě)和調(diào)試喲!
參考文獻(xiàn):
1、論文:https://arxiv.org/abs/1803.03467