推薦模型NeuralCF:原理介紹與TensorFlow2.0實現
- 2021 年 3 月 27 日
- 筆記
1. 簡介
NCF是協同過濾在神經網路上的實現——神經網路協同過濾。由新加坡國立大學與2017年提出。
我們知道,在協同過濾的基礎上發展來的矩陣分解取得了巨大的成就,但是矩陣分解得到低維隱向量求內積是線性的,而神經網路模型能帶來非線性的效果,非線性可以更好地捕捉用戶和物品空間的交互特徵。因此可以極大地提高協同過濾的效果。
另外,NCF處理的是隱式回饋數據,而不是顯式回饋,這具有更大的意義,在實際生產環境中隱式回饋數據更容易得到。
本篇論文展示了NCF的架構原理,以及實驗過程和效果。
2. 網路架構和原理
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),然後進行回歸預測:
\]
隱式回饋數據,MSE不好用,因此隱式回饋數據的標記不是分值而是用戶是否觀測過物品,即1 or 0.其中,1不代表喜歡,0也不代表不喜歡,僅僅是否有交互行為。
因此,預測分數就可以表示為用戶和物品是否相關,表徵相關的數學定義為概率,因此要限制網路輸出為[0,1]
,則使用概率函數如,sigmoid函數。
目的是求得最後一層輸出的概率最大,即使用似然估計的方式來進行推導:
\]
連乘無法光滑求導,且容易導致數值下溢,因此兩邊取對數,得到對數損失取負數可以最小化 損失函數,
\]
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
定義稀疏向量的關係,如下
from collections import namedtuple
# 使用具名元組定義特徵標記:由名字 和 域組成,類似字典但是不可更改,輕量便捷
SparseFeat = namedtuple('SparseFeat', ['name', 'vocabulary_size', 'embedding_dim'])
2. 定義Embedding層
與上篇Deep Crossing使用ReLu激活函數自定義單層神經網路作為Embedding不同的是,使用TF自帶的Embedding模組。
好處是:自定義的Embedding需要自己對類別變數進行onehot後才能輸入,而自帶Embedding只需要定義好輸入輸入的格式,就能自動實現降維效果,簡單方便。
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整個網路
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個未觀測樣本,所以需要訓練測試集的處理
# 數據處理
# 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數據)
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())
- 模型驗證
# 這裡沒特意設置驗證集,因此直接使用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
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的損失。