1. 程式人生 > >推薦模型NeuralCF:原理介紹與TensorFlow2.0實現

推薦模型NeuralCF:原理介紹與TensorFlow2.0實現

### 1. 簡介 NCF是協同過濾在神經網路上的實現——**神經網路協同過濾**。由新加坡國立大學與2017年提出。 我們知道,在協同過濾的基礎上發展來的矩陣分解取得了巨大的成就,但是矩陣分解得到低維隱向量求**內積是線性的**,而神經網路模型能帶來**非線性的效果,非線性可以更好地捕捉使用者和物品空間的互動特徵**。因此可以極大地提高協同過濾的效果。 另外,NCF處理的是隱式反饋資料,而不是顯式反饋,這具有更大的意義,在實際生產環境中隱式反饋資料更容易得到。 本篇論文展示了NCF的架構原理,以及實驗過程和效果。 ### 2. 網路架構和原理 ![image](https://img2020.cnblogs.com/blog/690773/202103/690773-20210327113449474-1682294339.png) #### 2.1 輸入 由於這篇文章的主要目的是協同過濾,因此輸入為user和item的id,把他們進行onehot編碼,然後使用單層神經網路進行降維即Embedding化。**作為通用框架,其輸入應該不限制與id類資訊,可以是上下文環境,可以是基於內容的特徵,基於鄰居的特徵等輔助資訊。** *為啥圖中使用兩組user和item的向量?一組走向GMF一組走向MLP?——後續分析* #### 2.2 MLP部分 可以發現MLP部分為多層感知機的堆疊,每一層的輸出就作為下一層的輸入,文中描述**最後一層Layer X表示模型的容量能力,所以越大容量就越大。** 這部分可以捕獲使用者-物品的互動非線性關係,增強模型的表達能力。 每層的非線性通過ReLu(符合生物學特徵;能帶來稀疏性;符合稀疏資料,比tanh效果好一點)來啟用,可以防止sigmoid帶來的梯度消失問題 #### 2.3 GMF部分 NCF其實是MF的一個通用化框架,去掉MLP部分,如果新增一層element-product(上圖左側),就是使用者-物品隱向量的內積。同時NeuMF Layer僅僅使用線性啟用函式,則最終的結果 就是MF的一個輸出。如果啟用函式是一般的函式,那麼MF可以被稱為GMF,Generalized Matrix Factorization廣義矩陣分解。 #### 2.4目標函式 如果是矩陣分解模型,常處理顯式反饋資料,這樣可以將目標函式定義為**平方誤差損失(MSE)**,然後進行迴歸預測: $$ L_{sqr}=\sum_{(u,i)\in y\cup y^-}w_{ui}(y_{ui}-\hat{y}_{ui})^2 $$ **隱式反饋資料,MSE不好用,因此隱式反饋資料的標記不是分值而是使用者是否觀測過物品,即1 or 0.**其中,1不代表喜歡,0也不代表不喜歡,僅僅是否有互動行為。 因此,預測分數就可以表示為使用者和物品是否相關,表徵相關的數學定義為概率,因此要限制網路輸出為`[0,1]`,則使用概率函式如,`sigmoid函式。`目的是求得最後一層輸出的概率最大,即使用似然估計的方式來進行推導: $$ p(y,y^-|P,Q,\Theta_f)=\prod_{(u,i)\in{y}}\hat{y}_{ui}\prod_{(u,j)\in{y^-}}(1-\hat{y}_{uj}) $$ **連乘無法光滑求導,且容易導致數值下溢**,因此兩邊取對數,得到對數損失取負數可以最小化 損失函式, $$ L=-\sum_{(u,i)\in{y}}log\hat{y}_{ui}-\sum_{(u,j)\in{y^-}}log(1-\hat{y}_{uj})=-\sum_{(u,i)\in{y}\cup{y}^-}y_{ui}log \hat{y}_{ui}+(1-y_{ui})log(1-\hat{y}_{ui}) $$ #### 2.5 GMF和MLP的結合 GMF,它應用了一個線性核心來模擬潛在的特徵互動;MLP,使用非線性核心從資料中學習互動函式。接下來的問題是:我們如何能夠在NCF框架下融合GMF和MLP,使他們能夠相互強化,以更好地對複雜的使用者-專案互動建模?一個直接的解決方法是讓GMF和MLP共享相同的嵌入層(Embedding Layer),然後再結合它們分別對相互作用的函式輸出。這種方式和著名的神經網路張量(NTN,Neural Tensor Network)有點相似。然而,**共享GMF和MLP的嵌入層可能會限制融合模型的效能。例如,它意味著,GMF和MLP必須使用的大小相同的嵌入;對於資料集,兩個模型的最佳嵌入尺寸差異很大,使得這種解決方案可能無法獲得最佳的組合**。為了使得融合模型具有更大的靈活性,我們允許GMF和MLP學習獨立的嵌入,並結合兩種模型通過連線他們最後的隱層輸出。 黑體字部分解釋了輸入部分使用兩組Embedding的作用。 ### 3. 程式碼實現 使用TensorFlow2.0和Keras API 構造各個模組層,通過繼承**Layer和Model**的方式來實現。 **1. 輸入資料** 為了簡化模型輸入過程中的引數,使用一個`namedtuple`定義稀疏向量的關係,如下 ```python from collections import namedtuple # 使用具名元組定義特徵標記:由名字 和 域組成,類似字典但是不可更改,輕量便捷 SparseFeat = namedtuple('SparseFeat', ['name', 'vocabulary_size', 'embedding_dim']) ``` **2. 定義Embedding層** 與[上篇Deep Crossing](https://www.cnblogs.com/sxzhou/p/14532111.html)使用ReLu啟用函式自定義單層神經網路作為Embedding不同的是,使用TF自帶的Embedding模組。 好處是:自定義的Embedding需要自己對類別變數進行onehot後才能輸入,而自帶Embedding只需要定義好輸入輸入的格式,就能自動實現降維效果,簡單方便。 ```python class SingleEmb(keras.layers.Layer): def __init__(self, emb_type, sparse_feature_column): super().__init__() # 取出sparse columns self.sparse_feature_column = sparse_feature_column self.embedding_layer = keras.layers.Embedding(sparse_feature_column.vocabulary_size, sparse_feature_column.embedding_dim, name=emb_type + "_" + sparse_feature_column.name) def call(self, inputs): return self.embedding_layer(inputs) ``` **3. 定義NCF整個網路** ```python class NearalCF(keras.models.Model): def __init__(self, sparse_feature_dict, MLP_layers_units): super().__init__() self.sparse_feature_dict = sparse_feature_dict self.MLP_layers_units = MLP_layers_units self.GML_emb_user = SingleEmb('GML', sparse_feature_dict['user_id']) self.GML_emb_item = SingleEmb('GML', sparse_feature_dict['item_id']) self.MLP_emb_user = SingleEmb('MLP', sparse_feature_dict['user_id']) self.MLP_emb_item = SingleEmb('MLP', sparse_feature_dict['item_id']) self.MLP_layers = [] for units in MLP_layers_units: self.MLP_layers.append(keras.layers.Dense(units, activation='relu')) # input_shape=自己猜 self.NeuMF_layer = keras.layers.Dense(1, activation='sigmoid') def call(self, X): #輸入X為n行兩列的資料,第一列為user,第二列為item GML_user = keras.layers.Flatten()(self.GML_emb_user(X[:,0])) GML_item = keras.layers.Flatten()( self.GML_emb_item(X[:,1])) GML_out = tf.multiply(GML_user, GML_item) MLP_user = keras.layers.Flatten()(self.MLP_emb_user(X[:,0])) MLP_item = keras.layers.Flatten()(self.MLP_emb_item(X[:,1])) MLP_out = tf.concat([MLP_user, MLP_item],axis=1) for layer in self.MLP_layers: MLP_out = layer(MLP_out) # emb的型別為int64,而dnn之後的型別為float32,否則報錯 GML_out = tf.cast(GML_out, tf.float32) MLP_out = tf.cast(MLP_out, tf.float32) concat_out = tf.concat([GML_out, MLP_out], axis=1) return self.NeuMF_layer(concat_out) ``` **3. 模型驗證** * *資料處理* 按照論文正負樣本標記為1一個觀測樣本,4個未觀測樣本,所以需要訓練測試集的處理 ```python # 資料處理 # train是字典形式,不然不太容易判斷是否包含u,i對 def get_data_instances(train, num_negatives, num_items): user_input, item_input, labels = [],[],[] for (u, i) in train.keys(): # positive instance user_input.append(u) item_input.append(i) labels.append(1) # negative instances for t in range(num_negatives): j = np.random.randint(num_items) while train.__contains__((u, j)): # python3沒有has_key方法 j = np.random.randint(num_items) user_input.append(u) item_input.append(j) labels.append(0) return user_input, item_input, labels # 這個字典,當資料量較大時,可以使用scipy.sparse 的dok_matrix:sparse.dok_matrix def get_data_dict(data, lst=['userId', 'movieId']): d = dict() for idx, row in data[lst].iterrows(): d[(row[0], row[1])] = 1 return d ``` 得到資料(可使用movielen資料) ```python train, test = train_test_split(data, test_size=0.1,random_state=13) train_dict, test_dict = get_data_dict(train), get_data_dict(test) train_set, test_set = get_data_instances(train_dict, 4, train['movieId'].max()), get_data_instances(test_dict, 4, test['movieId'].max()) ``` * *模型驗證* ```python # 這裡沒特意設定驗證集,因此直接使用array來餵給模型 BATCH = 128 X = np.array([train_set[0], train_set[1]]).T # 根據模型的輸入為兩列,因此轉置 # 模型驗證 feature_columns_dict = {'user_id': SparseFeat('user_id', data.userId.nunique(), 8), 'item_id': SparseFeat('item_id', data.movieId.nunique(), 8)} # 模型 model = NearalCF(feature_columns_dict, [16, 8, 4]) model.compile(loss=keras.losses.binary_crossentropy, optimizer=keras.optimizers.Adam(0.001), metrics=['acc']) model.fit(X, np.array(train_set[2]), batch_size=BATCH, epochs=5, verbose=2, validation_split=0.1) ``` out: ``` Train on 408384 samples, validate on 45376 samples Epoch 5/5 408384/408384 - 10s - loss: 1708.5975 - acc: 0.8515 - val_loss: 277.9610 - val_acc: 0.8635 ``` ```python X_test = np.array([test_set[0], test_set[1]]).T loss, acc = model.evaluate(X_test, np.array(test_set[2]),batch_size=BATCH, verbose=0) print(loss, acc) # 276.6405882021682 0.86309004 ``` **4. 說明** * `tf.data.Dataset`的資料處理方式已經在前面文章提到了,這裡換種思路和方式,在劃分資料集的時候不劃分驗證集,而是使用array的形式輸入後,在fit階段劃分。如果是Dataset的格式則不能進行fit階段劃分,詳情見官網fit的函式說明。 * 文章中所計算的評估指標是HR@10和NDCG@10,並對BPR,ALS等經典的傳統方法進行了比較發現最終的NCF的效果是最好的; * 文章中高斯分佈初始化引數,推薦使用的是pre-training的GMF和MLP模型,預訓練過程優化方法為Adam方法,在合併為NCF過程後,由於未儲存引數之外的動量資訊,所以使用SGD方法優化; * 在合併為NCF時,還有一個可調節超引數是GML_out和MLP_out的係數$\alpha$,pre-training時為0.5,本篇部落格直接使用了0.5且沒有使用預訓練方式,僅僅展示了使用tf構造NCF模型的過程。 * MLP的Layer X越大模型的容量越大,越容易導致過擬合,至於使用多少 視實驗情況而定。文章中使用了`[8, 16, 32, 64]`來測試。 * 與DeepCrossing和AutoRec的深層一樣,越深效果越好。 ### 4. 小結 本篇文章介紹了**神經協同過濾**的網路架構和程式碼實踐,並對文章實驗中的細節部分加以說明。 NCF模型混合了MLP和GML二者的特性,具有更強的特徵組合以及非線性表達的能力。 要注意的是模型結構不是越複雜越好,要防止過擬合,這部分並沒有使用Dropout和引數初始化的正則化,因為模型相對簡單。 NCF模型的侷限性在於協同過濾思想中只用使用者-物品的id資訊,儘管可以新增輔助資訊,這些需要後續的研究人員進行擴充套件,同時文章中說損失是基於pointwise的損失 可能也可以嘗試pairwise的