推薦系統(tǒng)遇上深度學(xué)習(xí)(十一)--神經(jīng)協(xié)同過濾NCF原理及實(shí)戰(zhàn)

好久沒更新該系列了,最近看到了一篇關(guān)于神經(jīng)協(xié)同過濾的論文,感覺還不錯(cuò),跟大家分享下。

論文地址:https://www.comp.nus.edu.sg/~xiangnan/papers/ncf.pdf

1、Neural Collaborative Filtering

1.1 背景

本文討論的主要是隱性反饋協(xié)同過濾解決方案,先來明確兩個(gè)概念:顯性反饋和隱性反饋:

顯性反饋行為包括用戶明確表示對(duì)物品喜好的行為
隱性反饋行為指的是那些不能明確反應(yīng)用戶喜好

舉例來說:

很多應(yīng)用場(chǎng)景,并沒有顯性反饋的存在。因?yàn)榇蟛糠钟脩羰浅聊挠脩簦⒉粫?huì)明確給系統(tǒng)反饋“我對(duì)這個(gè)物品的偏好值是多少”。因此,推薦系統(tǒng)可以根據(jù)大量的隱性反饋來推斷用戶的偏好值。

根據(jù)已得到的隱性反饋數(shù)據(jù),我們將用戶-條目交互矩陣Y定義為:

但是,Yui為1僅代表二者有交互記錄,并不代表用戶u真的喜歡項(xiàng)目i,同理,u和i沒有交互記錄也不能代表u不喜歡i。這對(duì)隱性反饋的學(xué)習(xí)提出了挑戰(zhàn),因?yàn)樗峁┝岁P(guān)于用戶偏好的噪聲信號(hào)。雖然觀察到的條目至少反映了用戶對(duì)項(xiàng)目的興趣,但是未查看的條目可能只是丟失數(shù)據(jù),并且這其中存在自然稀疏的負(fù)反饋。

在隱性反饋上的推薦問題可以表達(dá)為估算矩陣 Y中未觀察到的條目的分?jǐn)?shù)問題(這個(gè)分?jǐn)?shù)被用來評(píng)估項(xiàng)目的排名)。形式上它可以被抽象為學(xué)習(xí)函數(shù):

為了處理缺失數(shù)據(jù),有兩種常見的做法:要么將所有未觀察到的條目視作負(fù)反饋,要么從沒有觀察到條目中抽樣作為負(fù)反饋實(shí)例。

1.2 矩陣分解及其缺陷

傳統(tǒng)的求解方法是矩陣分解(MF,Matrix Factorization),為每個(gè)user和item找到一個(gè)隱向量,問題變?yōu)椋?/p>

這里的 K表示隱式空間(latent space)的維度。正如我們所看到的,MF模型是用戶和項(xiàng)目的潛在因素的雙向互動(dòng),它假設(shè)潛在空間的每一維都是相互獨(dú)立的并且用相同的權(quán)重將它們線性結(jié)合。因此,MF可視為隱向量(latent factor)的線性模型。

論文中給出了一個(gè)例子來說明這種算法的局限性:

1(a)是user-item交互矩陣,1(b)是用戶的隱式空間,論文中強(qiáng)調(diào)了兩點(diǎn)來理解這張圖片:
1)MF將user和item分布到同樣的隱式空間中,那么兩個(gè)用戶之間的相似性也可以用二者在隱式空間中的向量夾角來確定。
2)使用Jaccard系數(shù)來作為真實(shí)的用戶相似性。
通過MF計(jì)算的相似性與Jaccard系數(shù)計(jì)算的相似性也可以用來評(píng)判MF的性能。我們先來看看Jaccard系數(shù)

上面的示例顯示了MF因?yàn)槭褂靡粋€(gè)簡(jiǎn)單的和固定的內(nèi)積,來估計(jì)在低維潛在空間中用戶-項(xiàng)目的復(fù)雜交互,從而所可能造成的限制。解決該問題的方法之一是使用大量的潛在因子 K (就是隱式空間向量的維度)。然而這可能對(duì)模型的泛化能力產(chǎn)生不利的影響(e.g. 數(shù)據(jù)的過擬合問題),特別是在稀疏的集合上。論文通過使用DNNs從數(shù)據(jù)中學(xué)習(xí)交互函數(shù),突破了這個(gè)限制。

1.3 NCF

本文先提出了一種通用框架:

針對(duì)這個(gè)通用框架,論文提出了三種不同的實(shí)現(xiàn),三種實(shí)現(xiàn)可以用一張圖來說明:

GMF
上圖中僅使用GMF layer,就得到了第一種實(shí)現(xiàn)方式GMF,GMF被稱為廣義矩陣分解,輸出層的計(jì)算公式為:

MLP
上圖中僅使用右側(cè)的MLP Layers,就得到了第二種學(xué)習(xí)方式,通過多層神經(jīng)網(wǎng)絡(luò)來學(xué)習(xí)user和item的隱向量。這樣,輸出層的計(jì)算公式為:

NeuMF
結(jié)合GMF和MLP,得到的就是第三種實(shí)現(xiàn)方式,上圖是該方式的完整實(shí)現(xiàn),輸出層的計(jì)算公式為:

1.4 模型實(shí)驗(yàn)

論文通過三個(gè)角度進(jìn)行了試驗(yàn):

RQ1 我們提出的NCF方法是否勝過 state-of-the-art 的隱性協(xié)同過濾方法?
RQ2 我們提出的優(yōu)化框架(消極樣本抽樣的logloss)怎樣為推薦任務(wù)服務(wù)?
RQ3 更深的隱藏單元是不是有助于對(duì)用戶項(xiàng)目交互數(shù)據(jù)的學(xué)習(xí)?

使用的數(shù)據(jù)集:MovieLens 和 Pinterest 兩個(gè)數(shù)據(jù)集

評(píng)估方案:為了評(píng)價(jià)項(xiàng)目推薦的性能,論文采用了leave-one-out方法評(píng)估,即:對(duì)于每個(gè)用戶,我們將其最近的一次交互作為測(cè)試集(數(shù)據(jù)集一般都有時(shí)間戳),并利用余下的培訓(xùn)作為訓(xùn)練集。由于在評(píng)估過程中為每個(gè)用戶排列所有項(xiàng)目花費(fèi)的時(shí)間太多,所以遵循一般的策略,隨機(jī)抽取100個(gè)不與用戶進(jìn)行交互的項(xiàng)目,將測(cè)試項(xiàng)目排列在這100個(gè)項(xiàng)目中。排名列表的性能由命中率(HR)歸一化折扣累積增益(NDCG)來衡量。同時(shí),論文將這兩個(gè)指標(biāo)的排名列表截?cái)酁?0。如此一來,HR直觀地衡量測(cè)試項(xiàng)目是否存在于前10名列表中,而NDCG通過將較高分?jǐn)?shù)指定為頂級(jí)排名來計(jì)算命中的位置。本文計(jì)算每個(gè)測(cè)試用戶的這兩個(gè)指標(biāo),并求取了平均分。

Baselines,論文將NCF方法與下列方法進(jìn)行了比較:ItemPop,ItemKNN,BPR,eALS。

以下是三個(gè)結(jié)果的貼圖,關(guān)于試驗(yàn)結(jié)果的解讀,由于篇幅的原因,大家可以查看原論文。

RQ1試驗(yàn)結(jié)果

簡(jiǎn)單的結(jié)論,即NCF效果好于BaseLine模型,如果不好的話論文也不用寫了,哈哈。

RQ2試驗(yàn)結(jié)果

Figure 6 表示將模型看作一個(gè)二分類任務(wù)并使用logloss作為損失函數(shù)時(shí)的訓(xùn)練效果。
Figure7 表示采樣率對(duì)模型性能的影響(橫軸是采樣率,即負(fù)樣本與正樣本的比例)。

RQ3試驗(yàn)結(jié)果

上面的表格設(shè)置了兩個(gè)變量,分別是Embedding的長(zhǎng)度K和神經(jīng)網(wǎng)絡(luò)的層數(shù),使用類似網(wǎng)格搜索的方式展示了在兩個(gè)數(shù)據(jù)集上的結(jié)果。增加Embedding的長(zhǎng)度和神經(jīng)網(wǎng)絡(luò)的層數(shù)是可以提升訓(xùn)練效果的。

2、NCF實(shí)戰(zhàn)

本文的github地址為:https://github.com/princewen/tensorflow_practice/tree/master/recommendation/Basic-NCF-Demo

本文僅介紹模型相關(guān)細(xì)節(jié),數(shù)據(jù)處理部分就不介紹啦。

項(xiàng)目結(jié)構(gòu)如下:

數(shù)據(jù)輸入
本文使用了一種新的數(shù)據(jù)處理方式,不過我們的輸入就是三個(gè):userid,itemid以及l(fā)abel,對(duì)訓(xùn)練集來說,label是0-1值,對(duì)測(cè)試集來說,是具體的itemid

def get_data(self):
    sample = self.iterator.get_next()
    self.user = sample['user']
    self.item = sample['item']
    self.label = tf.cast(sample['label'],tf.float32)

定義初始化方式、損失函數(shù)、優(yōu)化器

def inference(self):
    """ Initialize important settings """
    self.regularizer = tf.contrib.layers.l2_regularizer(self.regularizer_rate)

    if self.initializer == 'Normal':
        self.initializer = tf.truncated_normal_initializer(stddev=0.01)
    elif self.initializer == 'Xavier_Normal':
        self.initializer = tf.contrib.layers.xavier_initializer()
    else:
        self.initializer = tf.glorot_uniform_initializer()

    if self.activation_func == 'ReLU':
        self.activation_func = tf.nn.relu
    elif self.activation_func == 'Leaky_ReLU':
        self.activation_func = tf.nn.leaky_relu
    elif self.activation_func == 'ELU':
        self.activation_func = tf.nn.elu

    if self.loss_func == 'cross_entropy':
        # self.loss_func = lambda labels, logits: -tf.reduce_sum(
        #       (labels * tf.log(logits) + (
        #       tf.ones_like(labels, dtype=tf.float32) - labels) *
        #       tf.log(tf.ones_like(logits, dtype=tf.float32) - logits)), 1)
        self.loss_func = tf.nn.sigmoid_cross_entropy_with_logits

    if self.optim == 'SGD':
        self.optim = tf.train.GradientDescentOptimizer(self.lr,
                                                       name='SGD')
    elif self.optim == 'RMSProp':
        self.optim = tf.train.RMSPropOptimizer(self.lr, decay=0.9,
                                               momentum=0.0, name='RMSProp')
    elif self.optim == 'Adam':
        self.optim = tf.train.AdamOptimizer(self.lr, name='Adam')

得到embedding值
分別得到GMF和MLP的embedding向量,當(dāng)然也可以使用embedding_lookup方法:

with tf.name_scope('input'):
    self.user_onehot = tf.one_hot(self.user,self.user_size,name='user_onehot')
    self.item_onehot = tf.one_hot(self.item,self.item_size,name='item_onehot')

with tf.name_scope('embed'):
    self.user_embed_GMF = tf.layers.dense(inputs = self.user_onehot,
                                          units = self.embed_size,
                                          activation = self.activation_func,
                                          kernel_initializer=self.initializer,
                                          kernel_regularizer=self.regularizer,
                                          name='user_embed_GMF')

    self.item_embed_GMF = tf.layers.dense(inputs=self.item_onehot,
                                          units=self.embed_size,
                                          activation=self.activation_func,
                                          kernel_initializer=self.initializer,
                                          kernel_regularizer=self.regularizer,
                                          name='item_embed_GMF')

    self.user_embed_MLP = tf.layers.dense(inputs=self.user_onehot,
                                          units=self.embed_size,
                                          activation=self.activation_func,
                                          kernel_initializer=self.initializer,
                                          kernel_regularizer=self.regularizer,
                                          name='user_embed_MLP')
    self.item_embed_MLP = tf.layers.dense(inputs=self.item_onehot,
                                          units=self.embed_size,
                                          activation=self.activation_func,
                                          kernel_initializer=self.initializer,
                                          kernel_regularizer=self.regularizer,
                                          name='item_embed_MLP')

GMF
GMF部分就是求兩個(gè)embedding的內(nèi)積:

with tf.name_scope("GMF"):
    self.GMF = tf.multiply(self.user_embed_GMF,self.item_embed_GMF,name='GMF')

MLP

with tf.name_scope("MLP"):
    self.interaction = tf.concat([self.user_embed_MLP, self.item_embed_MLP],
                                 axis=-1, name='interaction')

    self.layer1_MLP = tf.layers.dense(inputs=self.interaction,
                                      units=self.embed_size * 2,
                                      activation=self.activation_func,
                                      kernel_initializer=self.initializer,
                                      kernel_regularizer=self.regularizer,
                                      name='layer1_MLP')
    self.layer1_MLP = tf.layers.dropout(self.layer1_MLP, rate=self.dropout)

    self.layer2_MLP = tf.layers.dense(inputs=self.layer1_MLP,
                                      units=self.embed_size,
                                      activation=self.activation_func,
                                      kernel_initializer=self.initializer,
                                      kernel_regularizer=self.regularizer,
                                      name='layer2_MLP')
    self.layer2_MLP = tf.layers.dropout(self.layer2_MLP, rate=self.dropout)

    self.layer3_MLP = tf.layers.dense(inputs=self.layer2_MLP,
                                      units=self.embed_size // 2,
                                      activation=self.activation_func,
                                      kernel_initializer=self.initializer,
                                      kernel_regularizer=self.regularizer,
                                      name='layer3_MLP')
    self.layer3_MLP = tf.layers.dropout(self.layer3_MLP, rate=self.dropout)

得到預(yù)測(cè)值

with tf.name_scope('concatenation'):
    self.concatenation = tf.concat([self.GMF,self.layer3_MLP],axis=-1,name='concatenation')


    self.logits = tf.layers.dense(inputs= self.concatenation,
                                  units = 1,
                                  activation=None,
                                  kernel_initializer=self.initializer,
                                  kernel_regularizer=self.regularizer,
                                  name='predict')

    self.logits_dense = tf.reshape(self.logits,[-1])

測(cè)試集構(gòu)建
這里只介紹幾行關(guān)鍵的測(cè)試集構(gòu)建代碼,整個(gè)流程希望大家可以看一下完整的代碼。
需要明確的一點(diǎn)是,對(duì)于測(cè)試集,我們的評(píng)價(jià)不只是對(duì)錯(cuò),還要關(guān)注排名,所以測(cè)試集的label不是0-1,而是具體的itemid
首先,對(duì)每個(gè)user取最后一行作為測(cè)試集的正樣本:

split_train_test = []

for i in range(len(user_set)):
    for _ in range(user_length[i] - 1):
        split_train_test.append('train')
    split_train_test.append('test')

full_data['split'] = split_train_test

train_data = full_data[full_data['split'] == 'train'].reset_index(drop=True)
test_data = full_data[full_data['split'] == 'test'].reset_index(drop=True)

添加一些負(fù)采樣的樣本, 這里順序是,1正樣本-n負(fù)樣本-1正樣本-n負(fù)樣本....,每個(gè)用戶有n+1條數(shù)據(jù),便于計(jì)算HR和NDCG:

feature_user.append(user)
feature_item.append(item)
labels_add.append(label)

for k in neg_samples:
    feature_user.append(user)
    feature_item.append(k)
    labels_add.append(k)

不打亂測(cè)試集的順序,設(shè)置batch的大小為1+n:

dataset = tf.data.Dataset.from_tensor_slices(data)
dataset = dataset.batch(test_neg + 1)

計(jì)算HR和NDCG

def hr(gt_item, pred_items):
    if gt_item in pred_items:
        return 1
    return 0


def ndcg(gt_item, pred_items):
    if gt_item in pred_items:
        index = np.where(pred_items == gt_item)[0][0]
        return np.reciprocal(np.log2(index + 2))
    return 0

更詳細(xì)的代碼可以參考github,最好能夠手敲一遍來理解其原理喲!

參考文章

https://www.comp.nus.edu.sg/~xiangnan/papers/ncf.pdf
https://www.cnblogs.com/HolyShine/p/6728999.html

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容