Neural Collaborative Filtering 神經網絡協同過濾


論文的翻譯:https://www.cnblogs.com/HolyShine/p/6728999.html

一、MF協同過濾的局限性

The innerproduct, which simply combines the multiplication of latent features linearly, may not be sufficient to capture the complex structure of user interaction data.
簡單地將潛在特征的乘積線性組合的內積可能不足以捕捉用戶交互數據的復雜結構。
捕獲不到更高階的信息,本質上還是建模方式比較單一。


我們首先關注的圖 1(a) 中的前三行(用戶)。很容易可以計算出 \(s_{23}(0.66)>s_{12}(0.5)>s_{13}(0.4)\) 。這樣,\(p1\)\(p2\)\(p3\) 在潛在空間中的幾何關系可繪制成圖1(b)。現在,讓我們考慮一個新的用戶 \(u4\),它的輸入在圖1(a)中的用虛線框出。我們同樣可以計算出 \(s_{41}(0.6)>s_{43}(0.4)>s_{42}(0.2)\) ,表示 \(u4\) 最接近 \(u1\),接着是 \(u3\) ,最后是 \(u2\) 。然而,如果MF模型將 \(p4\) 放在了最接近 \(p1\) 的位置(圖1(b) 中的虛線展示了兩種不同的擺放 $p4 $的方式,結果一樣),那么會使得 \(p4\) 相比與 \(p3\) 更接近於 \(p2\) (顯然,根據圖1(a),\(u4\) 應該更接近 \(u3\),這會導致很大的排名誤差(ranking loss)。
  上面的示例顯示了MF因為使用一個簡單的和固定的內積,來估計在低維潛在空間中用戶-項目的復雜交互,從而所可能造成的限制。我們注意到,解決該問題的方法之一是使用大量的潛在因子 K (就是潛在空間向量的維度)。然而這可能對模型的泛化能力產生不利的影響(e.g. 數據的過擬合問題),特別是在稀疏的集合上。在論文的工作中,通過使用DNNs從數據中學習交互函數,突破了這個限制。

二、NCF的實現


輸入層:兩個特征向量:用戶特性向量 \({\bf v}_u^U\) 和 項目特征向量 \({\bf v}_i^I\),one-hot編碼的二值化稀疏向量
嵌入層:輸入層稀疏向量映射為稠密向量。輸入層向量和潛在因素矩陣相乘得到。
神經協同過濾層:多層神經網絡
輸出層:一個數值

\[\widehat{y}_{ui}=f({\bf P}^{T}{\bf v}_u^U,{\bf Q}^{T}{\bf v}_i^I|{\bf P},{\bf Q},\Theta_{f}),\ \ \ \ (1) \]

\({\bf Q}\in \mathbb{R}^{N\times K}\) 分別為用戶和項目的潛在因素矩陣。\(\Theta_{j}\)為神經網絡模型參數。
學習過程:

\[L_{sqr}=\sum_{(u,i)\in{\bf{y}\cup\bf{y^{-}}}}w_{ui}(y_{ui}-\widehat{y}_{ui})^{2},\ \ \ \ (2) \]

其中,\(\bf{y}\)表示訓練集數據(例如電影有明確的評分,評級(user,item,score)),作為正樣本,\(\bf{y^{-}}\) 表示負樣本。可以將為觀察到的樣本全體視為負樣本,或者采樣抽取的方式標記為負樣本。
\(w_{ui}\) 一個超參數,表示訓練集(user,item)的權重。

當評分數據為隱式數據,將輸出值作為一個標簽————1表示項目i和用戶u相關。此時,預測分數\(\widehat{y}_{ui}\)代表項目i和用戶u的相關性大小。
故,使用概率函數作為最后的激活函數。使用交叉熵作為損失函數:

\[L=-\sum_{(u,i)\in\bf{y}}\log\widehat{y}_{ui}-\sum_{(u,i)\in\bf{y}^{-}}\log\left(1-\widehat{y}_{ui}\right)=\sum_{(u,i)\in\bf{y}\cup\bf{y}^{-}}y_{ui}\log\widehat{y}_{ui}+\left(1-y_{ui}\right)\log\left(1-\widehat{y}_{ui}\right).\ \ \ \ (3) \]

2.2 廣義矩陣分解

\[\widehat{y}_{ui}=a_{out}\left({\bf h}^{T}\left({\bf{p}}_{u}\odot{\bf{q}}_{i} \right)\right),\ \ \ \ (4) \]

用戶潛在向量和項目潛在向量逐元素相乘,加權,經過激活函數得到輸出值。如果不考慮權值\({\bf h}\)和激活函數,原式為矩陣分解。這里稱其為GMF(Generalized Matrix Factorization,廣義矩陣分解)

2.3 多層感知機

即為一般的多層感知機網絡

\[{\bf{z}}_{1}=\phi_{1}\left({{\bf{p}}_{u}},{{\bf{q}}_{i}}\right)=\begin{bmatrix}{{{\bf{p}}_{u}}}\\{{{\bf{q}}_{i}}}\end{bmatrix} \\ \phi_{2}({\bf{z}}_{1})=a_{2}\left({\bf{W}}_2^T{\bf{z}}_{1}+{\bf b}_{2}\right), \\ \phi_{L}({\bf{z}}_{L-1})=a_{L}\left({\bf{W}}_L^T{\bf{z}}_{L-1}+{\bf b}_{L}\right), \\ \widehat{y}_{ui}=\sigma\left({\bf{h}}^{T}\phi_{L}\left({\bf{z}}_{L-1}\right)\right),\ \ \ \ \ \ \ \ \ \ (5)\]

2.4 結合GMF和MLP
GMF和MLP使用不同的嵌入矩陣得到不同的嵌入層。

\[\widehat{y}_{ui}=\sigma({\bf h}^{T}a({\bf p}_u\odot{\bf q}_i)+{\bf W}\begin{bmatrix}{{\bf p}_u}\\{{\bf q}_i}\end{bmatrix}+{\bf b}).\ \ \ \ (6) \]

三、數據預處理

主要參考:https://github.com/ZiyaoGeng/Recommender-System-with-TF2.0#1-neural-network-based-collaborative-filteringncf

3.1 數據集
MovieLens http://grouplens.org/datasets/movielens/1m/
Pinterest https://sites.google.com/site/xueatalphabeta/ academic-projects
隱式數據集,用於評估基於內容的圖像推薦。原始數據非常大但是很稀疏。 例如,超過20%的用戶只有一個pin(pin類似於贊一下),使得難以用來評估協同過濾算法。 因此,我們使用與MovieLens數據集相同的方式過濾數據集:僅保留至少有過20個pin的用戶。處理后得到了包含55,187個用戶和1,580,809個項目交互的數據的子集。 每個交互都表示用戶是否將圖像pin在自己的主頁上。

以MovieLens為例,原數據(1000209)處理為:
train.rating: 994169
test.rating: 6040
test.negative: 6040 每個測試集 包含 99個負例樣本

3.2 讀取數據

import scipy.sparse as sp
import numpy as np

# 1. 訓練集ml-1m.train.rating
def load_rating_file_as_matrix(filename):
    """
    讀取.rating文件,返回sp.dok_matrix矩陣
    """
    # Get number of users and items
    num_users, num_items = 0, 0
    with open(filename, "r") as f:
        line = f.readline()
        while line is not None and line != "":
            arr = line.split("\t")
            u, i = int(arr[0]), int(arr[1])
            num_users = max(num_users, u)
            num_items = max(num_items, i)
            line = f.readline()
    # 用戶數*電影數的矩陣,遍歷用戶-電影-評分數據,有評分的位置置1,其它位置置0。
    mat = sp.dok_matrix((num_users + 1, num_items + 1), dtype=np.float32)
    with open(filename, "r") as f:
        line = f.readline()
        while line is not None and line != "":
            arr = line.split("\t")
            user, item, rating = int(arr[0]), int(arr[1]), float(arr[2])
            if rating > 0:
                mat[user, item] = 1.0
            line = f.readline()
    return mat
# 2 測試集 ml-1m.test.rating
# 只有6040個樣本,保存為列表,其元素為[用戶ID,電影ID]
def load_rating_file_as_list(filename):
    ratingList = []
    with open(filename, "r") as f:
        line = f.readline()
        while line is not None and line != "":
            arr = line.split("\t")
            user, item = int(arr[0]), int(arr[1])
            ratingList.append([user, item])
            line = f.readline()
    return ratingList   
# 3 負采樣
# 保存到列表,列表元素為[負樣本電影ID,...]
def load_negative_file(filename):
    negativeList = []
    with open(filename, "r") as f:
        line = f.readline()
        while line is not None and line != "":
            arr = line.split("\t")
            negatives = []
            for x in arr[1:]:
                negatives.append(int(x))
            negativeList.append(negatives)
            line = f.readline()
    return negativeList 

四、構建模型(基於TF2.0)

class NeuMF(keras.Model):
    def __init__(self, num_users, num_items, mf_dim, layers, reg_layers, reg_mf):
        super(NeuMF, self).__init__()
        self.MF_Embedding_User = keras.layers.Embedding(
            input_dim=num_users, #用戶數
            output_dim=mf_dim,  # 嵌入維度8
            name='mf_embedding_user', # MF中的用戶嵌入層name
            embeddings_initializer='random_uniform', # 均勻分布初始化
            embeddings_regularizer=regularizers.l2(reg_mf[0]), # l2正則化
        )
        self.MF_Embedding_Item = keras.layers.Embedding(
            input_dim=num_items, # 電影數
            output_dim=mf_dim, # 嵌入維度 8
            name='mf_embedding_item', MF中的項目嵌入層name
            embeddings_initializer='random_uniform', # 均勻分布隨機初始化
            embeddings_regularizer=regularizers.l2(reg_mf[1]), # l2正則化
        )
        self.MLP_Embedding_User = keras.layers.Embedding(
            input_dim=num_users,
            output_dim=int(layers[0] / 2), # MLP嵌入層維度64/2 = 32
            name='mlp_embedding_user', # MLP中的用戶嵌入層
            embeddings_initializer='random_uniform',
            embeddings_regularizer=regularizers.l2(reg_layers[0]),
        )
        self.MLP_Embedding_Item = keras.layers.Embedding(
            input_dim=num_items,
            output_dim=int(layers[0] / 2), # MLP輸入層維度 64/2 = 32
            name='mlp_embedding_item', # MLP中的用戶嵌入層名稱
            embeddings_initializer='random_uniform',
            embeddings_regularizer=regularizers.l2(reg_layers[0]),
        )
        self.flatten = keras.layers.Flatten() #
        self.mf_vector = keras.layers.Dot(axes=1) # GMF層為用戶向量和項目向量點積。
        self.mlp_vector = keras.layers.Concatenate(axis=-1) 
        self.layer1 = keras.layers.Dense( 
            layers[1], # 第一層輸出維度32
            name='layer1',
            activation='relu',
            kernel_regularizer=regularizers.l2(reg_layers[1]),
        )
        self.layer2 = keras.layers.Dense(
            layers[2], # 第二層輸出維度16
            name='layer2',
            activation='relu',
            kernel_regularizer=regularizers.l2(reg_layers[2]),
        )
        self.layer3 = keras.layers.Dense(
            layers[3], # 第三層輸出維度8
            name='layer3',
            activation='relu',
            kernel_regularizer=regularizers.l2(reg_layers[3]),
        )
        self.predict_vector = keras.layers.Concatenate(axis=-1)
        self.layer4 = keras.layers.Dense(
            1, # 最好一層,即NeuMF層的輸出維度為1
            activation='sigmoid',
            kernel_initializer='lecun_uniform',
            name='prediction'
        )

    @tf.function
    def call(self, inputs):
        # Embedding,四個嵌入層,輸入皆為一個ID,然后轉換為到對應維度的嵌入向量
        MF_Embedding_User = self.MF_Embedding_User(inputs[0]) # (1,8)
        MF_Embedding_Item = self.MF_Embedding_Item(inputs[1]) # 1
        MLP_Embedding_User = self.MLP_Embedding_User(inputs[0]) # (1,32)
        MLP_Embedding_Item = self.MLP_Embedding_Item(inputs[1]) # 1

        # MF MF層輸出為用戶電影的點積
        mf_user_latent = self.flatten(MF_Embedding_User) #(1,8)
        mf_item_latent = self.flatten(MF_Embedding_Item)
        mf_vector = self.mf_vector([mf_user_latent, mf_item_latent]) # (1,1)

        # MLP 
        mlp_user_latent = self.flatten(MLP_Embedding_User) # (1,32)
        mlp_item_latent = self.flatten(MLP_Embedding_Item)
        mlp_vector = self.mlp_vector([mlp_user_latent, mlp_item_latent]) #兩個向量concatenate為一個長向量 (1,64)
        mlp_vector = self.layer1(mlp_vector)
        mlp_vector = self.layer2(mlp_vector)
        mlp_vector = self.layer3(mlp_vector) # 第三層的輸出維度為8

        # NeuMF
        vector = self.predict_vector([mf_vector, mlp_vector]) # MF層輸出和MLP層輸出合並得到NeuMF的輸入 (1,9)
        output = self.layer4(vector) # 輸出維度(1,1),[0,1]

        return output

五、build模型

model.build(input_shape=[1,1]) # 建立模型,並指明輸入的維度及其形狀
moodel.compile(optimizer=optimizers.Adam(lr=0.001), loss='binary_crossentropy') # 用於使用損失函數,優化,損失指標,損失重量等配置模型

六、訓練模型&保存模型

# 1. 輸入userID,輸入電影ID,標簽。正樣本 + 4個負樣本
user_input, item_input, labels = get_train_instances(train, configs.num_negatives)
# model.fit(x,y)
hist = model.fit([np.array(user_input), np.array(item_input)],
                         np.array(labels),
                         batch_size=256,
                         epochs=1,
                         verbose=1,
                         shuffle=True)

# 到達某些條件,保存模型
model_out_file = 'Save/%s_NeuMF_%d_%s_%d.h5' % (ml-1m, 8, [64,32,16,8], time())
model.save_weights(model_out_file, overwrite=True)

七、評估模型

對於測試集中的一個樣本即 (用戶ID,電影ID),[99個負例電影ID]組合為一個電影ID列表 [99個負例電影ID,正例電影ID]
輸入的用戶ID擴展為長度為100的數組

map_item_score = {}
gtItem # 正例
items = [99個負例itemID,gtItem]
users = np.full(100,userID,dtype='int32')
predictions = model.predict(x=[users,np.array(items)],batch_size=100,verbose=0)
for i in range(len(items)):
    item = items[i]
    map_item_score[item] = predictions[i] # 每個item 的預測得分。
items.pop() # 移除最后一個
ranklist = heapq.nlargest(_K, map_item_score, key=map_item_score.get) # 返回前10個最大值元素。

得到排序后的每個item的預測評分。計算其hit ratio 和 NDCG

# HitRatio
def getHitRatio(ranklist, gtItem):
    for item in ranklist:
        if item == gtItem:
            return 1
    return 0
# NDCG
def getNDCG(ranklist, gtItem):
    for i in range(len(ranklist)):
        item = ranklist[i]
        if item == gtItem:
            return np.log(2) / np.log(i+2)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM