在本系列的上一篇中,我們大致介紹了一下知識圖譜在推薦系統中的一些應用,我們最後講到知識圖譜特徵學習(Knowledge Graph Embedding)是最常見的與推薦系統結合的方式,知識圖譜特徵學習爲知識圖譜中的每個實體和關係學習到一個低維向量,同時保持圖中原有的結構或語義信息,最常見的得到低維向量的方式主要有基於距離的翻譯模型和基於語義的匹配模型。
知識圖譜特徵學習在推薦系統中的應用步驟大致有以下三種方式:
依次訓練的方法主要有:Deep Knowledge-aware Network(DKN) 聯合訓練的方法主要有:Ripple Network 交替訓練主要採用multi-task的思路,主要方法有:Multi-task Learning for KG enhanced Recommendation (MKR)
本文先來介紹依次訓練的方法Deep Knowledge-aware Network(DKN)。
論文下載地址爲:https://arxiv.org/abs/1801.08284v1
1、DKN原理
1.1 背景
推薦系統最初是爲了解決互聯網信息過載的問題,給用戶推薦其感興趣的內容。在新聞推薦領域,有三個突出的問題需要解決: 1.新聞文章具有高度的時間敏感性,它們的相關性很快就會在短時間內失效。 過時的新聞經常被較新的新聞所取代。 導致傳統的基於ID的協同過濾算法失效。 2.用戶在閱讀新聞的時候是帶有明顯的傾向性的,一般一個用戶閱讀過的文章會屬於某些特定的主題,如何利用用戶的閱讀歷史記錄去預測其對於候選文章的興趣是新聞推薦系統的關鍵 。 3.新聞類文章的語言都是高度濃縮的,包含了大量的知識實體與常識。用戶極有可能選擇閱讀與曾經看過的文章具有緊密的知識層面的關聯的文章。以往的模型只停留在衡量新聞的語義和詞共現層面的關聯上,很難考慮隱藏的知識層面的聯繫。
因此,Deep Knowledge-aware Network(DKN)模型中加入新聞之間知識層面的相似度量,來給用戶更精確地推薦可能感興趣的新聞。
1.2 基礎概念
1.2.1 知識圖譜特徵學習(Knowledge Graph Embedding)
知識圖譜特徵學習(Knowledge Graph Embedding)爲知識圖譜中的每個實體和關係學習得到一個低維向量,同時保持圖中原有的結構或語義信息。一般而言,知識圖譜特徵學習的模型分類兩類:基於距離的翻譯模型和基於語義的匹配模型。
基於距離的翻譯模型(distance-based translational models)
這類模型使用基於距離的評分函數評估三元組的概率,將尾節點視爲頭結點和關係翻譯得到的結果。這類方法的代表有TransE、TransH、TransR等;
上面三個方法的基本思想都是一樣的,我們以TransE爲例來介紹一下這些方法的核心思想。在空間中,三元組的頭節點h、關係r、尾節點t都有對應的向量,我們希望的是h + r = t,如果h + r的結果和t越接近,那麼我們認爲這些向量能夠很好的表示知識圖譜中的實體和關係。
基於語義的匹配模型(semantic-based matching models)
類模型使用基於相似度的評分函數評估三元組的概率,將實體和關係映射到隱語義空間中進行相似度度量。這類方法的代表有SME、NTN、MLP、NAM等。
上述方法的核心是構造一個二分類模型,將h、r和t輸入到網絡中,如果(h,r,t)在知識圖譜中真實存在,則應該得到接近1的概率,如果不存在,應該得到接近0的概率。
1.2.2 基於CNN的句子特徵提取
DKN中提取句子特徵的CNN源自於Kim CNN,用句子所包含詞的詞向量組成的二維矩陣,經過一層卷積操作之後再做一次max-over-time的pooling操作得到句子向量,如下圖所示:
1.3 問題定義
給定義一個用戶useri,他的點擊歷史記爲{t1,t2,t3,....,tN}是該用戶過去一段時間內層點擊過的新聞的標題,N代表用戶點擊過新聞的總數。每個標題都是一個詞序列t={w1,w2,w3,....,wn},標題中的單詞有的對應知識圖譜中的一個實體 。舉例來說,標題《Trump praises Las Vegas medical team》其中Trump與知識圖譜中的實體“Donald Trump”對應,Las和Vegas與實體Las Vegas對應。本文要解決的問題就是給定用戶的點擊歷史,以及標題單詞和知識圖譜中實體的關聯,我們要預測的是:一個用戶i是否會點擊一個特定的新聞tj。
1.4 模型框架
DKN模型的整體框架如下:
可以看到,DKN的網絡輸入有兩個:候選新聞集合,用戶點擊過的新聞標題序列。輸入數據通過KCNN來提取特徵,之上是一個attention層,計算候選新聞向量與用戶點擊歷史向量之間的attention權重,在頂層拼接兩部分向量之後,用DNN計算用戶點擊此新聞的概率。接下來,我們介紹一下DKN模型中的一些細節。
1.4.1 知識提取(Knowledge Distillation)
知識提取過程有三方面,一是得到標題中每個單詞的embedding,二是得到標題中每個單詞對應的實體的embedding。三是得到每個單詞對應實體的上下文embedding。每個單詞對應的embedding可以通過word2vec預訓練的模型得到。這裏我們主要講後兩部分。
實體embedding 實體特徵即標題中每個單詞對應的實體的特徵表示,通過下面四個步驟得到:
- 識別出標題中的實體並利用實體鏈接技術消除歧義
- 根據已有知識圖譜,得到與標題中涉及的實體鏈接在一個step之內的所有實體所形成的子圖。
- 構建好知識子圖以後,利用基於距離的翻譯模型得到子圖中每個實體embedding。
- 得到標題中每個單詞對應的實體embedding。
過程圖示如下:
上下文embedding
儘管目前現有的知識圖譜特徵學習方法得到的向量保存了絕大多數的結構信息,但還有一定的信息損失,爲了更好地利用一個實體在原知識圖譜的位置信息,文中還提到了利用一個實體的上下文來進一步的刻畫每個實體,具體來說,即用每個實體相連的實體embedding的平均值來進一步刻畫每個實體,計算公式如下:
圖示如下:
1.4.2 新聞特徵提取KCNN(Knowledge-aware CNN)
在知識抽取部分,我們得到了三部分的embedding,一種最簡單的使用方式就是直接將其拼接:
但這樣做存在幾方面的限制:
- 連接策略打破了單詞和相關實體之間的聯繫,並且不知道它們的對齊方式。
- 單詞的embedding和對應實體的embedding是通過不同的方法學習的,這意味着它們不適合在單個向量空間中將它們一起進行卷積操作。
- 連接策略需要單詞的embedding和實體的embedding具有相同的維度,這在實際設置中可能不是最優的,因爲詞和實體embedding的最佳維度可能彼此不同。
因此本文使用的是multi-channel和word-entity-aligned KCNN。具體做法是先把實體的embedding和實體上下文embedding映射到一個空間裏,映射的方式可以選擇線性方式g(e) = Me,也可以選擇非線性方式g(e) = tanh(Me + b),這樣我們就可以拼接三部分作爲KCNN的輸入:
KCNN的過程我們之前已經介紹過了,這裏就不再贅述。
1.4.3 基於注意力機制的用戶興趣預測
獲取到用戶點擊過的每篇新聞的向量表示以後,作者並沒有簡單地作加和來代表該用戶,而是計算候選文檔對於用戶每篇點擊文檔的attention,再做加權求和,計算attention:
1.5 實驗結果
本文的數據來自bing新聞的用戶點擊日誌,包含用戶id,新聞url,新聞標題,點擊與否(0未點擊,1點擊)。蒐集了2016年10月16日到2017年7月11號的數據作爲訓練集。2017年7月12號到8月11日的數據作爲測試集合。使用的知識圖譜數據是Microsoft Satori。以下是一些基本的統計數據以及分佈:
本文將DKN與FM、KPCNN、DSSM、Wide&Deep、DeepFM等模型進行對比試驗,結果如下:
隨後,本文根據DKN中是否使用上下文實體embedding、使用哪種實體embedding計算方法、是否對實體embedding進行變換、是否使用attention機制等進行了對比試驗,結果如下:
實驗表明,在使用DKN模型時,同時使用實體embedding和上下文embedding、使用TransD方法、使用非線性變換、使用attention機制可以獲得更好的預測效果。
2、DKN模型tensorflow實現
接下來我們就來看一下DKN模型的tensorflow實現。本文的代碼地址:https://github.com/princewen/tensorflow_practice/tree/master/recommendation/Basic-DKN-Demo
參考的代碼地址爲:https://github.com/hwwang55/DKN
目錄的結構如下:
可以看到,除代碼外,還有news和kg兩個文件夾,按照如下的步驟運行代碼,就可以得到我們的訓練數據、測試數據、單詞對應的embedding、實體對應的embedding、實體對應的上下文embedding:
$ cd news $ python news_preprocess.py $ cd ../kg $ python prepare_data_for_transx.py $ cd Fast-TransX/transE/ (note: you can also choose other KGE methods) $ g++ transE.cpp -o transE -pthread -O3 -march=native $ ./transE $ cd ../.. $ python kg_preprocess.py
目錄中共4個python文件,含義分別爲: data_loader.py:加載數據的代碼,主要是產生模型的輸入數據 dkn.py:定義DKN模型 main.py:程序的入口 trian.py: 訓練DKN模型的代碼
代碼整體還是比較好理解的,這裏我們主要介紹的是DKN模型相關的代碼,這裏大家需要注意的主要是各個變量轉換的維度,當然,我在代碼裏都有對應的註釋,大家可以跟着代碼的節奏來體會DKN中變量維度的變換。
定義輸入
模型的輸入有五個部分:用戶點擊過的新聞的標題對應單詞、用戶點擊過的實體、候選集新聞的單詞、候選集新聞的實體、label。
def _build_inputs(self,args): with tf.name_scope('input'): self.clicked_words = tf.placeholder(dtype=tf.int32,shape=[None,args.max_click_history,args.max_title_length],name='clicked_words') self.clicked_entities = tf.placeholder(dtype=tf.int32,shape=[None,args.max_click_history,args.max_title_length],name='clicked_entities') self.news_words = tf.placeholder(dtype=tf.int32,shape=[None,args.max_title_length],name='news_words') self.news_entities = tf.placeholder(dtype=tf.int32,shape=[None,args.max_title_length],name='news_entities') self.labels = tf.placeholder(dtype=tf.float32,shape=[None],name='labels')
得到Embeddings
得到所有單詞、實體的embedding、實體的上下文embedding,注意這裏實體的embedding和上下文embedding進行了一次非線性變換:
with tf.name_scope('embedding'): word_embs = np.load('news/word_embeddings_' + str(args.word_dim) + '.npy') entity_embs = np.load('kg/entity_embeddings_' + args.KGE + '_' + str(args.entity_dim) + '.npy') self.word_embeddings = tf.Variable(word_embs,dtype=np.float32,name='word') self.entity_embeddings = tf.Variable(entity_embs,dtype=np.float32,name='entity') self.params.append(self.word_embeddings) self.params.append(self.entity_embeddings) if args.use_context: context_embs = np.load( 'kg/context_embeddings_' + args.KGE + '_' + str(args.entity_dim) + '.npy') self.context_embeddings = tf.Variable(context_embs, dtype=np.float32, name='context') self.params.append(self.context_embeddings) if args.transform: self.entity_embeddings = tf.layers.dense(self.entity_embeddings,units = args.entity_dim,activation=tf.nn.tanh,name='transformed_entity', kernel_regularizer=tf.contrib.layers.l2_regularizer(args.l2_weight)) if args.use_context: self.context_embeddings = tf.layers.dense( self.context_embeddings, units=args.entity_dim, activation=tf.nn.tanh, name='transformed_context', kernel_regularizer=tf.contrib.layers.l2_regularizer(args.l2_weight))
KCNN
KCNN這裏需要注意的是變量維度的變換,首先是輸入數據的維度,對用戶向量來說:(batch_size * max_click_history, max_title_length, full_dim),對新聞向量來說:(batch_size, max_title_length, full_dim):
# (batch_size * max_click_history, max_title_length, word_dim) for users # (batch_size, max_title_length, word_dim) for news embedded_words = tf.nn.embedding_lookup(self.word_embeddings,words) embedded_entities = tf.nn.embedding_lookup(self.entity_embeddings,entities) # (batch_size * max_click_history, max_title_length, full_dim) for users # (batch_size, max_title_length, full_dim) for news if args.use_context: embedded_contexts = tf.nn.embedding_lookup(self.context_embeddings,entities) concat_input = tf.concat([embedded_words,embedded_entities,embedded_contexts],axis=-1) full_dim = args.word_dim + args.entity_dim * 2 else: concat_input = tf.concat([embedded_words,embedded_entities],axis=-1) full_dim = args.word_dim + args.entity_dim
接下來是卷積和池化操作:
卷積:這裏我們設定了不同大小的卷積核,卷積核的的大小爲filter_size * full_dim,輸入的信道有1個,卷積核的大小爲n_filters: 因此對user向量來說,卷積後的大小變爲:(batch_size * max_click_history, max_title_length - filter_size + 1, 1, n_filters), 對新聞向量來說,大小變爲:(batch_size, max_title_length - filter_size + 1, 1, n_filters)。
池化:池化操作是max-over-time的,池化後維度爲: 對用戶向量來說:(batch_size * max_click_history, 1, 1, n_filters), 對新聞向量來說:(batch_size, 1, 1, n_filters):
for filter_size in args.filter_sizes: filter_shape = [filter_size, full_dim, 1, args.n_filters] w = tf.get_variable(name='w_' + str(filter_size), shape=filter_shape, dtype=tf.float32) b = tf.get_variable(name='b_' + str(filter_size), shape=[args.n_filters], dtype=tf.float32) if w not in self.params: self.params.append(w) # (batch_size * max_click_history, max_title_length - filter_size + 1, 1, n_filters_for_each_size) for users # (batch_size, max_title_length - filter_size + 1, 1, n_filters_for_each_size) for news conv = tf.nn.conv2d(concat_input, w, strides=[1, 1, 1, 1], padding='VALID', name='conv') relu = tf.nn.relu(tf.nn.bias_add(conv, b), name='relu') # (batch_size * max_click_history, 1, 1, n_filters_for_each_size) for users # (batch_size, 1, 1, n_filters_for_each_size) for news pool = tf.nn.max_pool(relu, ksize=[1, args.max_title_length - filter_size + 1, 1, 1], strides=[1, 1, 1, 1], padding='VALID', name='pool') outputs.append(pool) # (batch_size * max_click_history, 1, 1, n_filters_for_each_size * n_filter_sizes) for users # (batch_size, 1, 1, n_filters_for_each_size * n_filter_sizes) for news output = tf.concat(outputs, axis=-1) # (batch_size * max_click_history, n_filters_for_each_size * n_filter_sizes) for users # (batch_size, n_filters_for_each_size * n_filter_sizes) for news output = tf.reshape(output, [-1, args.n_filters * len(args.filter_sizes)]) return output
Attention機制
接下來,我們要通過attention 機制得到user embeddings:
with tf.variable_scope('kcnn', reuse=tf.AUTO_REUSE): # reuse the variables of KCNN # (batch_size * max_click_history, title_embedding_length) # title_embedding_length = n_filters_for_each_size * n_filter_sizes clicked_embeddings = self._kcnn(clicked_words, clicked_entities, args) # (batch_size, title_embedding_length) news_embeddings = self._kcnn(self.news_words, self.news_entities, args) # (batch_size, max_click_history, title_embedding_length) clicked_embeddings = tf.reshape( clicked_embeddings, shape=[-1, args.max_click_history, args.n_filters * len(args.filter_sizes)]) # (batch_size, 1, title_embedding_length) news_embeddings_expanded = tf.expand_dims(news_embeddings, 1) # (batch_size, max_click_history) attention_weights = tf.reduce_sum(clicked_embeddings * news_embeddings_expanded, axis=-1) # (batch_size, max_click_history) attention_weights = tf.nn.softmax(attention_weights, dim=-1) # (batch_size, max_click_history, 1) attention_weights_expanded = tf.expand_dims(attention_weights, axis=-1) # (batch_size, title_embedding_length) user_embeddings = tf.reduce_sum(clicked_embeddings * attention_weights_expanded, axis=1) return user_embeddings, news_embeddings
得到輸出
最終我們可以得到我們的輸出,作爲點擊的概率值:
self.scores_unnormalized = tf.reduce_sum(user_embeddings * news_embeddings,axis=1) self.scores = tf.sigmoid(self.scores_unnormalized)
參考文獻
1、原文:https://arxiv.org/abs/1801.08284v1 2、https://www.zuanbi8.com/talk/16467.html