Mxnet (44): 使用神经协作过滤(NeuMF模型)进行个性化排名

ⅰ亾dé卋堺 提交于 2020-10-14 22:41:59

1.个性化排名

前面对电影数据集的处理只考虑了明确的反馈,通过观察到的等级进行了训练和测试。这种方法有两个缺点:

  1. 在实际应用中,大多数的反馈都是隐式的,显式的反馈往往需要更高的收集成本。
  2. 未被观察的哪些用户-项目交互可能用于预测用户的喜好但是被忽略了,当这些缺失不是随机导致的而是由于用户的喜好导致的这些方法将不在适用。很显然,未观测的这些用户-项目对是用户真实的负反馈(用户对这些不感兴趣才没看)和缺失值(正常随机的缺失,跟喜好无关,将来有可能会看)的结合体。如果简单的忽略其实是不对的。

为了解决这个问题,针对从隐式反馈生成排名推荐列表的一类推荐模型已获得普及。通常, 可以使用逐点、逐对以及逐列的方法优化个性化排名模型。逐点方法一次只考虑一次交互,并训练分类器或回归器来预测个人偏好。矩阵分解和AutoRec使用逐点目标进行了优化。 逐对方法为每一个用户考虑一对项目并且致力于为这对项目最优排序。通常, 逐对方法更适合于排序任务,因为预测一对的顺序会使人联想到排序。逐列方法将整列的项目近似排序, 如直接优化排名指标:Normalized Discounted Cumulative Gain (NDCG)。然而, 列表方法比点方法或成对方法更加复杂且计算量大。

1.1 贝叶斯个性化排序

贝叶斯个性化排序(BPR)是从最大后验估计量得出的成对个性化排序损失。它已在许多现有推荐模型中广泛使用。BPR的训练数据包含正对和负对(缺失值)。假设用户相较于那些为观察到的项目更加喜欢正对的项目。

形式上,训练的数据有 ( u , i , j ) (u, i, j) (u,i,j)这个形式的元祖组成, 代表用户 u u u喜欢项目 i i i的程度超过项目 j j j。 BPR的贝叶斯公式旨在最大程度上提高后验概率:

p ( Θ ∣ > u ) ∝ p ( > u ∣ Θ ) p ( Θ ) p(\Theta \mid >_u ) \propto p(>_u \mid \Theta) p(\Theta) p(Θ>u)p(>uΘ)p(Θ)

Θ \Theta Θ 是一个表示任意推荐模型的参数, > u >_u >u 代表用户 u u u对所有项目的期望排名。 我们可以制定最大后验估计量,以得出针对个性化排名任务的通用优化准则:

BPR-OPT : = ln ⁡ p ( Θ ∣ > u ) ∝ ln ⁡ p ( > u ∣ Θ ) p ( Θ ) = ln ⁡ ∏ ( u , i , j ∈ D ) σ ( y ^ u i − y ^ u j ) p ( Θ ) = ∑ ( u , i , j ∈ D ) ln ⁡ σ ( y ^ u i − y ^ u j ) + ln ⁡ p ( Θ ) = ∑ ( u , i , j ∈ D ) ln ⁡ σ ( y ^ u i − y ^ u j ) − λ Θ ∥ Θ ∥ 2 \begin{aligned} \text{BPR-OPT} : &= \ln p(\Theta \mid >_u) \\ & \propto \ln p(>_u \mid \Theta) p(\Theta) \\ &= \ln \prod_{(u, i, j \in D)} \sigma(\hat{y}_{ui} - \hat{y}_{uj}) p(\Theta) \\ &= \sum_{(u, i, j \in D)} \ln \sigma(\hat{y}_{ui} - \hat{y}_{uj}) + \ln p(\Theta) \\ &= \sum_{(u, i, j \in D)} \ln \sigma(\hat{y}_{ui} - \hat{y}_{uj}) - \lambda_\Theta \|\Theta \|^2 \end{aligned} BPR-OPT:=lnp(Θ>u)lnp(>uΘ)p(Θ)=ln(u,i,jD)σ(y^uiy^uj)p(Θ)=(u,i,jD)lnσ(y^uiy^uj)+lnp(Θ)=(u,i,jD)lnσ(y^uiy^uj)λΘΘ2

D : = { ( u , i , j ) ∣ i ∈ I u + ∧ j ∈ I \ I u + } D := \{(u, i, j) \mid i \in I^+_u \wedge j \in I \backslash I^+_u \} D:={ (u,i,j)iIu+jI\Iu+}是训练集, I u + I^+_u Iu+ 表示 用户 u u u 喜欢的项目, I I I 代表所有项目, 以及 I \ I u + I \backslash I^+_u I\Iu+ 代表用户不喜欢的项目。 y ^ u i \hat{y}_{ui} y^ui y ^ u j \hat{y}_{uj} y^uj 分别为用户 u u u 对于 i i i j j j的预测分数。 先验 p ( Θ ) p(\Theta) p(Θ) 是一个均值为0并且方差-协方差矩阵为的 Σ Θ \Sigma_\Theta ΣΘ的正太分布。这里我们让 Σ Θ = λ Θ I \Sigma_\Theta = \lambda_\Theta I ΣΘ=λΘI.

image-20201012192346363

我们将实现基类mxnet.gluon.loss.Loss并重写该forward方法以构造贝叶斯个性化排名损失。BPR损失的实现如下。

from mxnet import gluon, np, npx
npx.set_np()

class BPRLoss(gluon.loss.Loss):
    def __init__(self, weight=None, batch_axis=0, **kwargs):
        super(BPRLoss, self).__init__(weight=None, batch_axis=0, **kwargs)

    def forward(self, positive, negative):
        distances = positive - negative
        loss = - np.sum(np.log(npx.sigmoid(distances)), 0, keepdims=True)
        return loss

1.2 铰链损失

排名的铰链损失与gluon库中提供的 铰链损失不同,后者经常用于分类器,例如SVMs。 在推荐系统中用于排名的损失具有以下形式。

∑ ( u , i , j ∈ D ) max ⁡ ( m − y ^ u i + y ^ u j , 0 ) \sum_{(u, i, j \in D)} \max( m - \hat{y}_{ui} + \hat{y}_{uj}, 0) (u,i,jD)max(my^ui+y^uj,0)

m m m 是安全边际的大小。它目的是将负项目推向正项目。与BPR相似,它旨在优化正负样本之间的相关距离,而不是绝对输出,使其非常适合推荐系统。

class HingeLossbRec(gluon.loss.Loss):
    def __init__(self, weight=None, batch_axis=0, **kwargs):
        super(HingeLossbRec, self).__init__(weight=None, batch_axis=0,
                                            **kwargs)

    def forward(self, positive, negative, margin=1):
        distances = positive - negative
        loss = np.sum(np.maximum(- distances + margin, 0))
        return loss

2. 神经协作过滤

通过神经协作过滤(NCF)框架,用于带有隐式反馈的推荐。隐式反馈在推荐系统中无处不在。点击,购买和观看等操作是常见的隐式反馈,易于收集并指示用户的偏好。这里我们将介绍的模型称为NeuMF,是神经矩阵分解(neural matrix factorization)的缩写,旨在通过隐式反馈解决个性化排名任务。该模型利用神经网络的灵活性和非线性来代替矩阵分解的点积,旨在增强模型的表达能力。具体而言,该模型有两个子网络构成,包括广义矩阵分解(GMF)和MLP,并通过两个路径(而非简单的内部乘积)对相互作用进行建模。将这两个网络的输出连接起来以进行最终的预测分数计算。与AutoRec中的评分预测任务不同,此模型基于隐式反馈为每个用户生成一个排名推荐列表。我们将使用个性化排序损失来训练此模型。

3. NeuMF模型

如前所述, NeuMF 将两个子网结合在一起。 GMF是矩阵分解的通用神经网络版本,其中输入是用户和项目潜在因子的元素乘积。它由两个神经层组成:

x = p u ⊙ q i y ^ u i = α ( h ⊤ x ) , \mathbf{x} = \mathbf{p}_u \odot \mathbf{q}_i \\ \hat{y}_{ui} = \alpha(\mathbf{h}^\top \mathbf{x}), x=puqiy^ui=α(hx),

⊙ \odot 表示向量的 Hadamard乘积,就是元素乘积。 P ∈ R m × k \mathbf{P} \in \mathbb{R}^{m \times k} PRm×k 并且 Q ∈ R n × k \mathbf{Q} \in \mathbb{R}^{n \times k} QRn×k分别对应于用户和项目的潜在矩阵。 p u ∈ R k \mathbf{p}_u \in \mathbb{R}^{ k} puRk P P P u t h u^\mathrm{th} uth 行以及 q i ∈ R k \mathbf{q}_i \in \mathbb{R}^{ k} qiRk Q Q Q的第 i t h i^\mathrm{th} ith行。 α \alpha α h h h 表示激活功能和输出层的权重。 y ^ u i \hat{y}_{ui} y^ui 是预测的用户 u u u给项目 i i i的评分。

该模型的另一个组件是MLP。为了丰富模型的灵活性, MLP子网不与GMF分享用户和项目的嵌入。它使用用户和项目嵌入的串联作为输入。通过复杂的连接和非线性转换,它能够估计用户和项目之间的复杂交互。更准确地说,MLP子网定义为:

z ( 1 ) = ϕ 1 ( U u , V i ) = [ U u , V i ] ϕ ( 2 ) ( z ( 1 ) ) = α 1 ( W ( 2 ) z ( 1 ) + b ( 2 ) ) . . . ϕ ( L ) ( z ( L − 1 ) ) = α L ( W ( L ) z ( L − 1 ) + b ( L ) ) ) y ^ u i = α ( h ⊤ ϕ L ( z ( L − 1 ) ) ) \begin{aligned} z^{(1)} &= \phi_1(\mathbf{U}_u, \mathbf{V}_i) = \left[ \mathbf{U}_u, \mathbf{V}_i \right] \\ \phi^{(2)}(z^{(1)}) &= \alpha^1(\mathbf{W}^{(2)} z^{(1)} + b^{(2)}) \\ &... \\ \phi^{(L)}(z^{(L-1)}) &= \alpha^L(\mathbf{W}^{(L)} z^{(L-1)} + b^{(L)})) \\ \hat{y}_{ui} &= \alpha(\mathbf{h}^\top\phi^L(z^{(L-1)})) \end{aligned} z(1)ϕ(2)(z(1))ϕ(L)(z(L1))y^ui=ϕ1(Uu,Vi)=[Uu,Vi]=α1(W(2)z(1)+b(2))...=αL(W(L)z(L1)+b(L)))=α(hϕL(z(L1)))

W ∗ , b ∗ \mathbf{W}^*, \mathbf{b}^* W,b α ∗ \alpha^* α 分别表示权重矩阵,偏差向量和激活函数。 ϕ ∗ \phi^* ϕ 表示相应层的功能。 z ∗ \mathbf{z}^* z表示相应层的输出。

为了融合GMF和MLP的结果,NeuMF串联了两个子网的倒数第二层,以创建一个可以传递到其他层的特征向量,而不是简单地相加。 然而,用矩阵投影输出 h \mathbf{h} h以及 sigmoid 激活函数。预测层的公式为:

y ^ u i = σ ( h ⊤ [ x , ϕ L ( z ( L − 1 ) ) ] ) . \hat{y}_{ui} = \sigma(\mathbf{h}^\top[\mathbf{x}, \phi^L(z^{(L-1)})]). y^ui=σ(h[x,ϕL(z(L1))]).

下图说明了NeuMF的模型架构。
image-20201012173007689

from d2l import mxnet as d2l
from mxnet import autograd, gluon, np, npx
from mxnet.gluon import nn
import mxnet as mx
from plotly import express as px
import pandas as pd
import random
import sys
npx.set_np()

4. 模型定义

以下代码实现了NeuMF模型。它由广义矩阵分解模型和具有不同用户和项目嵌入向量的多层感知器组成。MLP的结构由参数控制nums_hiddens。ReLU用作默认激活功能。

class NeuMF(nn.Block):
    def __init__(self, num_factors, num_users, num_items, nums_hiddens,
                 **kwargs):
        super(NeuMF, self).__init__(**kwargs)
        self.P = nn.Embedding(num_users, num_factors)
        self.Q = nn.Embedding(num_items, num_factors)
        self.U = nn.Embedding(num_users, num_factors)
        self.V = nn.Embedding(num_items, num_factors)
        self.mlp = nn.Sequential()
        for num_hiddens in nums_hiddens:
            self.mlp.add(nn.Dense(num_hiddens, activation='relu', use_bias=True))
        self.prediction_layer = nn.Dense(1, activation='sigmoid', use_bias=False)

    def forward(self, user_id, item_id):
        p_mf = self.P(user_id)
        q_mf = self.Q(item_id)
        gmf = p_mf * q_mf
        p_mlp = self.U(user_id)
        q_mlp = self.V(item_id)
        mlp = self.mlp(np.concatenate([p_mlp, q_mlp], axis=1))
        con_res = np.concatenate([gmf, mlp], axis=1)
        return self.prediction_layer(con_res)

5. 带有负采样的自定义数据集

对于成对排名损失,重要的一步是负采样。对于每个用户,用户未与之交互的项目是候选项目(未观察到的条目)。以下功能将用户身份和候选项目作为输入,并从该用户的候选集中为每个用户随机抽取否定项目。在训练阶段,该模型可确保用户喜欢的商品的排名高于他不喜欢或未与之互动的商品。

class PRDataset(gluon.data.Dataset):
    def __init__(self, users, items, candidates, num_items):
        self.users = users
        self.items = items
        self.cand = candidates
        self.num_items = num_items

    def __len__(self):
        return len(self.users)

    def __getitem__(self, idx):
        pos_items = set(self.cand[int(self.users[idx])])
        while True:
            indices = random.randint(0, num_items - 1)
            if (indices not in pos_items):
                break
        return self.users[idx], self.items[idx], indices

6. 评估器

在本节中,我们采用按时间分割策略来构造训练和测试集。使用两种评估方法来评估模型的有效性:在给定截止时间的点击率 ℓ \ell ( Hit @ ℓ \text{Hit}@\ell Hit@)以及 ROC曲线下的面积 (AUC)。为每一个用户指定位置的点击率 ℓ \ell 表示推荐的项目是否在前 ℓ \ell 排名列表中。正式定义如下:

Hit @ ℓ = 1 m ∑ u ∈ U 1 ( r a n k u , g u < = ℓ ) , \text{Hit}@\ell = \frac{1}{m} \sum_{u \in \mathcal{U}} \textbf{1}(rank_{u, g_u} <= \ell), Hit@=m1uU1(ranku,gu<=),

1 \textbf{1} 1 表示一种指标函数:如果给定项目在前 ℓ \ell 列表中则返回1否则返回0。 r a n k u , g u rank_{u, g_u} ranku,gu表示项目在用户 u u u推荐列表中的排名 g u g_u gu(理想排名为 1)。 m m m 使用用户数。 U \mathcal{U} U 是用户集。

AUC的定义如下:

AUC = 1 m ∑ u ∈ U 1 ∣ I \ S u ∣ ∑ j ∈ I \ S u 1 ( r a n k u , g u < r a n k u , j ) , \text{AUC} = \frac{1}{m} \sum_{u \in \mathcal{U}} \frac{1}{|\mathcal{I} \backslash S_u|} \sum_{j \in I \backslash S_u} \textbf{1}(rank_{u, g_u} < rank_{u, j}), AUC=m1uUI\Su1jI\Su1(ranku,gu<ranku,j),

I \mathcal{I} I 是项目集。 S u S_u Su 是用户 u u u的候选项目。注意,还可以使用许多其他评估协议,例如精度,召回率和归一化折现累积增益(NDCG)。

以下函数计算每个用户的点击数和AUC。

def hit_and_auc(rankedlist, test_matrix, k):
    hits_k = [(idx, val) for idx, val in enumerate(rankedlist[:k])  if val in set(test_matrix)]
    hits_all = [(idx, val) for idx, val in enumerate(rankedlist) if val in set(test_matrix)]
    max_num = len(rankedlist) - 1
    auc = 1.0 * (max_num - hits_all[0][0]) / max_num if len(hits_all) > 0 else 0
    return len(hits_k), auc

然后,总命中率和AUC的计算如下。

def evaluate_ranking(net, test_input, seq, candidates, num_users, num_items,
                     devices):
    ranked_list, ranked_items, hit_rate, auc = {
   
   }, {
   
   }, [], []
    all_items = set([i for i in range(num_users)])
    for u in range(num_users):
        neg_items = list(all_items - set(candidates[int(u)]))
        x, scores = [], []
        item_ids = neg_items
        user_ids = [u] * len(neg_items)
        x.extend([np.array(user_ids)])
        if seq is not None:
            x.append(seq[user_ids, :])
        x.extend([np.array(item_ids)])
        test_data_iter = gluon.data.DataLoader(gluon.data.ArrayDataset(*x), 
                                               shuffle=False, last_batch="keep",batch_size=1024)
        for index, values in enumerate(test_data_iter):
            x = [gluon.utils.split_and_load(v, devices, even_split=False)  for v in values]
            scores.extend([list(net(*t).asnumpy()) for t in zip(*x)])
        scores = [item for sublist in scores for item in sublist]
        item_scores = list(zip(item_ids, scores))
        ranked_list[u] = sorted(item_scores, key=lambda t: t[1], reverse=True)
        ranked_items[u] = [r[0] for r in ranked_list[u]]
        temp = hit_and_auc(ranked_items[u], test_input[u], 50)
        hit_rate.append(temp[0])
        auc.append(temp[1])
    return np.mean(np.array(hit_rate)), np.mean(np.array(auc))

7. 模型训练和评估

训练功能定义如下。我们以成对方式训练模型。

def train_ranking(net, train_iter, test_iter, loss, trainer, test_seq_iter,
                  num_users, num_items, num_epochs, devices, evaluator, candidates, eval_step=1):
    timer, hit_rate, auc, data = d2l.Timer(), 0, 0, []
    for epoch in range(num_epochs):
        metric, l = d2l.Accumulator(3), 0.
        for i, values in enumerate(train_iter):
            input_data = []
            for v in values:
                input_data.append(gluon.utils.split_and_load(v, devices))
            with autograd.record():
                p_pos = [net(*t) for t in zip(*input_data[0:-1])]
                p_neg = [net(*t) for t in zip(*input_data[0:-2], input_data[-1])]
                ls = [loss(p, n) for p, n in zip(p_pos, p_neg)]
            for l in ls:
                l.backward(retain_graph=False)
            l += sum([l.asnumpy() for l in ls]).mean()/len(devices)
            trainer.step(values[0].shape[0])
            metric.add(l, values[0].shape[0], values[0].size)
            timer.stop()
        with autograd.predict_mode():
            if (epoch + 1) % eval_step == 0:
                hit_rate, auc = evaluator(net, test_iter, test_seq_iter,
                                          candidates, num_users, num_items, devices)
                data.append((epoch + 1, hit_rate, auc))
                print(f'[epoch {epoch + 1:>2}]  test hit rate: {float(hit_rate):.3f};  test AUC: {float(auc):.3f}')
    print(f'train loss {metric[0] / metric[1]:.3f},  test hit rate {float(hit_rate):.3f}, test AUC {float(auc):.3f}')
    print(f'{metric[2] * num_epochs / timer.sum():.1f} examples/sec on {str(devices)}')
    fig = px.line(pd.DataFrame(data, columns=['epoch', 'test hit rate', 'test AUC']), x='epoch', 
    			y=[ 'test hit rate', 'test AUC'],  width=580, height=400)
    fig.show()

我们可以加载MovieLens 100k数据集并训练模型。由于MovieLens数据集中只有等级,但准确性有所下降,因此我们将这些等级二进制化为零和一。如果用户对某项进行评分,我们会将隐式反馈视为1,否则视为零。对项目进行评分的动作可以视为提供隐式反馈的一种形式。在这里,我们将数据集拆分为以下seq-aware模式:用户最新互动的项目被排除在测试之外。

batch_size = 1024
df, num_users, num_items = d2l.read_data_ml100k()
train_data, test_data = d2l.split_data_ml100k(df, num_users, num_items, 'seq-aware')
users_train, items_train, ratings_train, candidates = d2l.load_data_ml100k(
    train_data, num_users, num_items, feedback="implicit")
users_test, items_test, ratings_test, test_iter = d2l.load_data_ml100k(
    test_data, num_users, num_items, feedback="implicit")
train_iter = gluon.data.DataLoader(
    PRDataset(users_train, items_train, candidates, num_items ), 
    batch_size, True, last_batch="rollover", num_workers=d2l.get_dataloader_workers()
)

然后,我们创建并初始化模型。我们使用具有恒定隐藏大小10的三层MLP。以下代码训练模型。

devices = d2l.try_all_gpus()
net = NeuMF(10, num_users, num_items, nums_hiddens=[10, 10, 10])
net.initialize(ctx=devices, force_reinit=True, init=mx.init.Normal(0.01))

lr, num_epochs, wd, optimizer = 0.01, 10, 1e-5, 'adam'
loss = d2l.BPRLoss()
trainer = gluon.Trainer(net.collect_params(), optimizer, {
   
   "learning_rate": lr, 'wd': wd})
train_ranking(net, train_iter, test_iter, loss, trainer, None, num_users,
              num_items, num_epochs, devices, evaluate_ranking, candidates)

image-20201012192920970

image-20201012181322719

8. 参考

https://d2l.ai/chapter_recommender-systems/neumf.html#the-neumf-model

9.代码

github

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!