推薦系統實踐 0x11 NeuralCF

NoMornings發表於2020-12-18

前言

這一篇文章我們來談一下2017年新加坡國立大學提出的基於深度學習的協同過濾模型NeuralCF。我們在之前講過矩陣分解技術,將協同過濾中的共現矩陣分解成使用者向量矩陣以及物品向量矩陣。那麼Embedding的思路也是一樣的,只不過不是通過矩陣分解的形式,而是通過多層神經網路使用Embedding構造使用者隱向量以及物品隱向量。使用者隱向量與物品隱向量的內積,就是使用者對物品的評分。這個內積的操作,同樣可以使用神經網路層來實現,也就是前一篇文章所用的Scoring層,得到所謂的“相似度”,也就得到了評分。在實際使用中,我們往往會發現矩陣分解的模型會得到欠擬合的結果,所以深度學習可以彌補矩陣分解的結構簡單,擬合不充分的弊端。論文原文以及我找到的一篇全文翻譯的博文我貼在參考裡面了,有興趣的讀者可以閱讀一下。

網路結構

NeuralCF的網路結構如下圖所示:

這裡使用一個使用者和一個物品作為輸入特徵,它使用one-hot編碼將它們轉化為二值化稀疏向量。注意到,對輸入使用這樣的通用特徵表示,可以很容易地使用的內容特徵來表示使用者和物品,以調整解決冷啟動問題。文章使用了多層感知機(Layer1-X)來代替了之前的內機操作,最終得到了評分。這樣做的原因:

  1. 可以使得使用者向量和物品向量充分交叉,得到更多有價值的特徵組合
  2. 可以引入更多的非線性特徵讓模型的表達能力更強

輸入層上面是嵌入層(Embedding Layer);它是一個全連線層,用來將輸入層的稀疏表示對映為一個稠密向量(dense vector)。所獲得的使用者(物品)的Embedding(就是一個稠密向量)可以被看作是在潛在因素模型的上下文中用於描述使用者(專案)的潛在向量。然後我們將使用者Embedding和物品Embedding送入多層神經網路結構,我們把這個結構稱為神經協作過濾層,它將潛在向量對映為預測分數。NCF層的每一層可以被定製,用以發現使用者-物品互動的某些潛在結構。最後一個隱含層Layer X的維度大小決定了模型的能力。

實際上,使用者和商品的互操作可以使用任意的互操作形式,這也就是廣義矩陣分解(GMF)。那麼,這篇文章使用了元素積,也就是逐元素點乘(element-wise product)的形式,將使用者向量和物品向量對映到同等維度大小的空間當中,然後對應維度相乘,這樣就實現了互操作,最後再送入邏輯迴歸層等輸出層,擬合最終的預測目標。GMF,它應用了一個線性核心來模擬潛在的特徵互動;MLP,使用非線性核心從資料中學習互動函式。接下來的問題是:我們如何能夠在NCF框架下融合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}) \]

整體的框架中的公式可以如下:

\[\phi^{GMF}={\bf p}_u^G\odot{\bf q}_i^G,\\\phi^{MLP}=a_{L}(W_L^T(a_{L-1}(...a_{2}(W_2^T\begin{bmatrix}{{\bf p}_u^M}\\{{\bf q}_i^M}\end{bmatrix}+{\bf b}_2)...))+{\bf b}_L),\\\widehat{y}_{ui}=\sigma({\bf h}^T\begin{bmatrix}{\phi^{GMF}}\\{\phi^{MLP}}\end{bmatrix}) \]

這裡的\(p^G_u\)\(p^M_u\)分別表示GMF部分和MLP部分的使用者嵌入(user embedding);同樣的,\(q^G_i\)\(q^M_i\)分別表示物品的Embedding。

程式碼部分

# 廣義矩陣分解
class GMF(torch.nn.Module):
    def __init__(self, config):
        super(GMF, self).__init__()
        self.num_users = config['num_users']
        self.num_items = config['num_items']
        self.latent_dim = config['latent_dim']

        self.embedding_user = torch.nn.Embedding(num_embeddings=self.num_users, embedding_dim=self.latent_dim)
        self.embedding_item = torch.nn.Embedding(num_embeddings=self.num_items, embedding_dim=self.latent_dim)

        self.affine_output = torch.nn.Linear(in_features=self.latent_dim, out_features=1)
        self.logistic = torch.nn.Sigmoid()

    def forward(self, user_indices, item_indices):
        user_embedding = self.embedding_user(user_indices)
        item_embedding = self.embedding_item(item_indices)
        element_product = torch.mul(user_embedding, item_embedding)
        logits = self.affine_output(element_product)
        rating = self.logistic(logits)
        return rating

    def init_weight(self):
        pass


class GMFEngine(Engine):
    """Engine for training & evaluating GMF model"""
    def __init__(self, config):
        self.model = GMF(config)
        if config['use_cuda'] is True:
            use_cuda(True, config['device_id'])
            self.model.cuda()
        super(GMFEngine, self).__init__(config)

# 多層感知機
class NeuMF(torch.nn.Module):
    def __init__(self, config):
        super(NeuMF, self).__init__()
        self.config = config
        self.num_users = config['num_users']
        self.num_items = config['num_items']
        self.latent_dim_mf = config['latent_dim_mf']
        self.latent_dim_mlp = config['latent_dim_mlp']

        self.embedding_user_mlp = torch.nn.Embedding(num_embeddings=self.num_users, embedding_dim=self.latent_dim_mlp)
        self.embedding_item_mlp = torch.nn.Embedding(num_embeddings=self.num_items, embedding_dim=self.latent_dim_mlp)
        self.embedding_user_mf = torch.nn.Embedding(num_embeddings=self.num_users, embedding_dim=self.latent_dim_mf)
        self.embedding_item_mf = torch.nn.Embedding(num_embeddings=self.num_items, embedding_dim=self.latent_dim_mf)

        self.fc_layers = torch.nn.ModuleList()
        for idx, (in_size, out_size) in enumerate(zip(config['layers'][:-1], config['layers'][1:])):
            self.fc_layers.append(torch.nn.Linear(in_size, out_size))

        self.affine_output = torch.nn.Linear(in_features=config['layers'][-1] + config['latent_dim_mf'], out_features=1)
        self.logistic = torch.nn.Sigmoid()

    def forward(self, user_indices, item_indices):
        user_embedding_mlp = self.embedding_user_mlp(user_indices)
        item_embedding_mlp = self.embedding_item_mlp(item_indices)
        user_embedding_mf = self.embedding_user_mf(user_indices)
        item_embedding_mf = self.embedding_item_mf(item_indices)

        mlp_vector = torch.cat([user_embedding_mlp, item_embedding_mlp], dim=-1)  # the concat latent vector
        mf_vector =torch.mul(user_embedding_mf, item_embedding_mf)

        for idx, _ in enumerate(range(len(self.fc_layers))):
            mlp_vector = self.fc_layers[idx](mlp_vector)
            mlp_vector = torch.nn.ReLU()(mlp_vector)

        vector = torch.cat([mlp_vector, mf_vector], dim=-1)
        logits = self.affine_output(vector)
        rating = self.logistic(logits)
        return rating

    def init_weight(self):
        pass

    def load_pretrain_weights(self):
        """Loading weights from trained MLP model & GMF model"""
        config = self.config
        config['latent_dim'] = config['latent_dim_mlp']
        mlp_model = MLP(config)
        if config['use_cuda'] is True:
            mlp_model.cuda()
        resume_checkpoint(mlp_model, model_dir=config['pretrain_mlp'], device_id=config['device_id'])

        self.embedding_user_mlp.weight.data = mlp_model.embedding_user.weight.data
        self.embedding_item_mlp.weight.data = mlp_model.embedding_item.weight.data
        for idx in range(len(self.fc_layers)):
            self.fc_layers[idx].weight.data = mlp_model.fc_layers[idx].weight.data

        config['latent_dim'] = config['latent_dim_mf']
        gmf_model = GMF(config)
        if config['use_cuda'] is True:
            gmf_model.cuda()
        resume_checkpoint(gmf_model, model_dir=config['pretrain_mf'], device_id=config['device_id'])
        self.embedding_user_mf.weight.data = gmf_model.embedding_user.weight.data
        self.embedding_item_mf.weight.data = gmf_model.embedding_item.weight.data

        self.affine_output.weight.data = 0.5 * torch.cat([mlp_model.affine_output.weight.data, gmf_model.affine_output.weight.data], dim=-1)
        self.affine_output.bias.data = 0.5 * (mlp_model.affine_output.bias.data + gmf_model.affine_output.bias.data)


class NeuMFEngine(Engine):
    """Engine for training & evaluating GMF model"""
    def __init__(self, config):
        self.model = NeuMF(config)
        if config['use_cuda'] is True:
            use_cuda(True, config['device_id'])
            self.model.cuda()
        super(NeuMFEngine, self).__init__(config)
        print(self.model)

        if config['pretrain']:
            self.model.load_pretrain_weights()

小結

多種使用者向量、物品向量的Embedding,以及多種互操作形式進行特徵的交叉組合,可以靈活的進行拼接,同時也利用了神經網路對任意函式的擬合能力,按需增加複雜度或者減小複雜度。但是NeuralCF並沒有引入其他型別的特徵,使得很多有價值的資訊浪費。同時,互操作的選取也沒有給出更多說明,只有在實踐中進行探討了。

參考

Neural Collaborative Filtering
【翻譯】Neural Collaborative Filtering--神經協同過濾
Github:yihong-chen/neural-collaborative-filtering

相關文章