NCF知識總結代碼實作
- 1. NeuralCF 模型的結構
-
- 1.1 回顧CF和MF
- 1.2 NCF 模型結構
- 1.3 NeuralCF 模型的擴充---雙塔模型
- 2. NCF代碼實作
-
- 2.1 tensorflow
- 2.2 pytorch
NeuralCF:如何用深度學習改造協同過濾?
随着技術的發展,協同過濾相比深度學習模型的弊端就日益顯現,因為它是通過直接利用非常稀疏的共現矩陣進行預測的,是以模型的泛化能力非常弱,遇到曆史行為非常少的使用者,就沒法産生準确的推薦結果。
雖然,可以通過矩陣分解算法增強它的泛化能力,但因為矩陣分解是利用非常簡單的内積方式來處理使用者向量和物品向量的交叉問題的,是以,它的拟合能力也比較弱。
2017 年,新加坡國立的研究者就使用深度學習網絡來改進了傳統的協同過濾算法,取名 NeuralCF(神經網絡協同過濾)。NeuralCF 大大提高了協同過濾算法的泛化能力和拟合能力,讓這個經典的推薦算法又重新在深度學習時代煥發生機。
1. NeuralCF 模型的結構
1.1 回顧CF和MF
先來簡單回顧一下協同過濾和矩陣分解的原理。協同過濾是利用使用者和物品之間的互動行為曆史,建構出一個像圖左一樣的共現矩陣。在共現矩陣的基礎上,利用每一行的使用者向量相似性,找到相似使用者,再利用相似使用者喜歡的物品進行推薦。
矩陣分解則進一步加強了協同過濾的泛化能力,它把協同過濾中的共現矩陣分解成了使用者矩陣和物品矩陣,從使用者矩陣中提取出使用者隐向量,從物品矩陣中提取出物品隐向量,再利用它們之間的内積相似性進行推薦排序。
如果用神經網絡的思路來了解矩陣分解,它的結構圖就是圖 2 這樣的。
圖 2 中的輸入層是由使用者 ID 和物品 ID 生成的 One-hot 向量,Embedding 層是把 One-hot 向量轉化成稠密的 Embedding 向量表達,這部分就是矩陣分解中的使用者隐向量和物品隐向量。輸出層使用了使用者隐向量和物品隐向量的内積作為最終預測得分,之後通過跟目标得分對比,進行反向梯度傳播,更新整個網絡。
把矩陣分解神經網絡化之後,把它跟 Embedding+MLP 以及 Wide&Deep 模型做對比,我們可以一眼看出網絡中的薄弱環節:矩陣分解在 Embedding 層之上的操作好像過于簡單了,就是直接利用内積得出最終結果。這會導緻特征之間還沒有充分交叉就直接輸出結果,模型會有欠拟合的風險。
1.2 NCF 模型結構
針對矩陣分解的弱點,NeuralCF 對矩陣分解進行了改進,它的結構圖是圖 3 這樣的。
NeuralCF 用一個多層的神經網絡替代掉了原來簡單的點積操作。這樣就可以讓使用者和物品隐向量之間進行充分的交叉,提高模型整體的拟合能力。
1.3 NeuralCF 模型的擴充—雙塔模型
NeuralCF 的模型結構之中,蘊含了一個非常有價值的思想,就是我們可以把模型分成使用者側模型和物品側模型兩部分,然後用互操作層把這兩部分聯合起來,産生最後的預測得分。
這裡的使用者側模型結構和物品側模型結構,可以是簡單的 Embedding 層,也可以是複雜的神經網絡結構,最後的互操作層可以是簡單的點積操作,也可以是比較複雜的 MLP 結構。但隻要是這種物品側模型 + 使用者側模型 + 互操作層的模型結構,我們把它統稱為“雙塔模型”結構。
對于 NerualCF 來說,它隻利用了使用者 ID 作為“使用者塔”的輸入特征,用物品 ID 作為“物品塔”的輸入特征。事實上,我們完全可以把其他使用者和物品相關的特征也分别放入使用者塔和物品塔,讓模型能夠學到的資訊更全面。比如說,YouTube 在建構用于召回層的雙塔模型時,就分别在使用者側和物品側輸入了多種不同的特征。
YouTube 召回雙塔模型的使用者側特征包括了使用者正在觀看的視訊 ID、頻道 ID(圖中的 seed features)、該視訊的觀看數、被喜歡的次數,以及使用者曆史觀看過的視訊 ID 等等。物品側的特征包括了候選視訊的 ID、頻道 ID、被觀看次數、被喜歡次數等等。在經過了多層 ReLU 神經網絡的學習之後,雙塔模型最終通過 softmax 輸出層連接配接兩部分,輸出最終預測分數。
這個雙塔模型相比Embedding MLP 和 Wide&Deep 有優勢:在實際工作中,雙塔模型最重要的優勢就在于它易上線、易服務。
注意看物品塔和使用者塔最頂端的那層神經元,那層神經元的輸出其實就是一個全新的物品 Embedding 和使用者 Embedding。拿圖 4 來說,物品塔的輸入特征向量是 x,經過物品塔的一系列變換,生成了向量 u(x),那麼這個 u(x) 就是這個物品的 Embedding 向量。同理,v(y) 是使用者 y 的 Embedding 向量,這時,我們就可以把 u(x) 和 v(y) 存入特征資料庫,這樣一來,線上服務的時候,我們隻要把 u(x) 和 v(y) 取出來,再對它們做簡單的互操作層運算就可以得出最後的模型預估結果了。
是以使用雙塔模型,不用把整個模型都部署上線,隻需要預存物品塔和使用者塔的輸出,以及線上上實作互操作層就可以了。如果這個互操作層是點積操作,那麼這個實作可以說沒有任何難度,這是實際應用中非常容易落地的,這也正是雙塔模型在業界巨大的優勢所在。
2. NCF代碼實作
2.1 tensorflow
NeuralCF模型部分的實作
# neural cf model arch two. only embedding in each tower, then MLP as the interaction layers
def neural_cf_model_1(feature_inputs, item_feature_columns, user_feature_columns, hidden_units):
# 物品側特征層
item_tower = tf.keras.layers.DenseFeatures(item_feature_columns)(feature_inputs)
# 使用者側特征層
user_tower = tf.keras.layers.DenseFeatures(user_feature_columns)(feature_inputs)
# 連接配接層及後續多層神經網絡
interact_layer = tf.keras.layers.concatenate([item_tower, user_tower])
for num_nodes in hidden_units:
interact_layer = tf.keras.layers.Dense(num_nodes, activation='relu')(interact_layer)
# sigmoid單神經元輸出層
output_layer = tf.keras.layers.Dense(1, activation='sigmoid')(interact_layer)
# 定義keras模型
neural_cf_model = tf.keras.Model(feature_inputs, output_layer)
return neural_cf_model
代碼中定義的生成 NeuralCF 模型的函數,接收了四個輸入變量。其中 feature_inputs 代表着所有的模型輸入, item_feature_columns 和 user_feature_columns 分别包含了物品側和使用者側的特征。在訓練時,如果隻在 item_feature_columns 中放入 movie_id ,在 user_feature_columns 放入 user_id, 就是NeuralCF的經典實作了。
通過 DenseFeatures 層建立好使用者側和物品側輸入層之後,再利用 concatenate 層将二者連接配接起來,然後輸入多層神經網絡進行訓練。如果想要定義多層神經網絡的層數和神經元數量,可以通過設定 hidden_units 數組來實作。
2.2 pytorch
#GMF層
class GMF(nn.Module):
def __init__(self,embedding_dim):
super(GMF, self).__init__()
self.embedding_dim = embedding_dim
self.fc = nn.Linear(self.embedding_dim,self.embedding_dim)
def forward(self, user_emb, item_emb):
out = self.fc(user_emb*item_emb).sigmoid()
return out
#MLP
class MLP_Layer(nn.Module):
def __init__(self,
input_dim,
output_dim=None,
hidden_units=[],
hidden_activations="ReLU",
final_activation=None,
dropout_rates=0,
batch_norm=False,
use_bias=True):
super(MLP_Layer, self).__init__()
dense_layers = []
if not isinstance(dropout_rates, list):
dropout_rates = [dropout_rates] * len(hidden_units)
if not isinstance(hidden_activations, list):
hidden_activations = [hidden_activations] * len(hidden_units)
hidden_activations = [set_activation(x) for x in hidden_activations]
hidden_units = [input_dim] + hidden_units
for idx in range(len(hidden_units) - 1):
dense_layers.append(nn.Linear(hidden_units[idx], hidden_units[idx + 1], bias=use_bias))
if batch_norm:
dense_layers.append(nn.BatchNorm1d(hidden_units[idx + 1]))
if hidden_activations[idx]:
dense_layers.append(hidden_activations[idx])
if dropout_rates[idx] > 0:
dense_layers.append(nn.Dropout(p=dropout_rates[idx]))
if output_dim is not None:
dense_layers.append(nn.Linear(hidden_units[-1], output_dim, bias=use_bias))
if final_activation is not None:
dense_layers.append(set_activation(final_activation))
self.dnn = nn.Sequential(*dense_layers) # * used to unpack list
def forward(self, inputs):
return self.dnn(inputs)
def set_device(gpu=-1):
if gpu >= 0 and torch.cuda.is_available():
os.environ["CUDA_VISIBLE_DEVICES"] = str(gpu)
device = torch.device(f"cuda:{gpu}")
else:
device = torch.device("cpu")
return device
def set_activation(activation):
if isinstance(activation, str):
if activation.lower() == "relu":
return nn.ReLU()
elif activation.lower() == "sigmoid":
return nn.Sigmoid()
elif activation.lower() == "tanh":
return nn.Tanh()
else:
return getattr(nn, activation)()
else:
return activation
def get_dnn_input_dim(enc_dict,embedding_dim):
num_sparse = 0
num_dense = 0
for col in enc_dict.keys():
if 'min' in enc_dict[col].keys():
num_dense+=1
elif 'vocab_size' in enc_dict[col].keys():
num_sparse+=1
return num_sparse*embedding_dim+num_dense
def get_linear_input(enc_dict,data):
res_data = []
for col in enc_dict.keys():
if 'min' in enc_dict[col].keys():
res_data.append(data[col])
res_data = torch.stack(res_data,axis=1)
return res_data
# NCF 模型
class NCF(nn.Module):
def __init__(self,
embedding_dim1=16, # GMF 對應的Embedding層
embedding_dim2=32, # MLP 對應的Embedding層
hidden_units=[64, 32, 16],
loss_fun = 'torch.nn.BCELoss()',
enc_dict=None):
super(NCF, self).__init__()
self.embedding_dim1 = embedding_dim1 # GMF Emb
self.embedding_dim2 = embedding_dim2 # MLP Emb
self.hidden_units = hidden_units
self.loss_fun = eval(loss_fun)
self.enc_dict = enc_dict
# GMF
self.user_emb_layer1 = nn.Embedding(self.enc_dict['user_id']['vocab_size'],
self.embedding_dim1)
self.item_emb_layer1 = nn.Embedding(self.enc_dict['item_id']['vocab_size'],
self.embedding_dim1)
# MLP
self.user_emb_layer2 = nn.Embedding(self.enc_dict['user_id']['vocab_size'],
self.embedding_dim2)
self.item_emb_layer2 = nn.Embedding(self.enc_dict['item_id']['vocab_size'],
self.embedding_dim2)
self.gmf = GMF(self.embedding_dim1)
self.mlp = MLP_Layer(input_dim=self.embedding_dim2*2, hidden_units=self.hidden_units,
hidden_activations='relu', dropout_rates=0)
# GMF:[batch,Emb1] MLP:[batch,hidden_units[-1]]-> FC的輸入次元:self.embedding_dim1 + self.hidden_units[-1]
self.fc = nn.Linear(self.embedding_dim1 + self.hidden_units[-1],1)
def forward(self, data):
# GMF
user_emb1 = self.user_emb_layer1(data['user_id'])
item_emb1 = self.item_emb_layer1(data['item_id'])
# MLP
user_emb2 = self.user_emb_layer2(data['user_id'])
item_emb2 = self.item_emb_layer2(data['item_id'])
# GMF
gmf_out = self.gmf(user_emb1, item_emb1)
# MLP
mlp_input = torch.cat([user_emb2,item_emb2],axis=-1)
mlp_out = self.mlp(mlp_input)
#輸出
final_input = torch.cat([gmf_out,mlp_out],axis=-1)
y_pred = self.fc(final_input).sigmoid()
loss = self.loss_fun(y_pred.squeeze(-1),data['label'])
output_dict = {'pred':y_pred,'loss':loss}
return output_dict