MIND:Multi-Interest Network with Dynamic Routing for Recommendation at Tmall

1.论文解读

改论文是阿里巴巴2019年在CIKM上发表的一篇关于多兴趣召回的论文,改论文属于U2I召回的一种,传统的召回方法往往针对User的行为序列只生产单一的向量,但是在用户行为序列中往往蕴含着多种兴趣,生产单一的用户的嵌入向量无法很好的对用户的行为进行建模,所以阿里巴巴提出了对用户的行为序列生产多个向量以表示用户的多个兴趣表征,从而大大的提高了用户的行为表征能力,我们今天阅读学习的这篇论文就是主要来讨论多兴趣建模相关的一篇工作

1.1论文核心方法

在这里插入图片描述

1.2 问题定义

在了解一篇论文的具体做法之前,我们有必要了解一下论文所解决问题的定义,这里我们就是根据User的历史行为序列来预测User下一个会点击的Item,所以我们针对User的行为序列的标签就是User下一个点击的Item。我们将 I u I_{u} Iu计作是User u u u的行为序列, P u P_{u} Pu计作是User u u u的基本特征(例如年龄,性别等), F i F_{i} Fi为Item i i i 的基本特征(例如item_id,cate_id等),则我们的目标就是学习一个函数 f u s e r f_{user} fuser使得:
V u = f u s e r ( I u , P u ) V_{u}=f_{user}(I_{u},P_{u}) Vu=fuser(Iu,Pu)
这里的 V u = ( v u 1 → , . . . , v u K → ) ∈ R d × K V_{u}=(\mathop{v_{u}^{1}}\limits ^{\rightarrow},...,\mathop{v_{u}^{K}}\limits ^{\rightarrow}) \in \mathbb{R}^{d \times K} Vu=(vu1,...,vuK)Rd×K,其中d表示User的Embedding向量的维度,K代表着所提取出的用户的兴趣个数,当 K = 1 K=1 K=1的时候,其退化为正常的U2I序列召回,对于Item我们也需要对其学习一个函数 f i t e m f_{item} fitem
e i → = f i t e m ( F i ) \mathop{e_{i}}\limits^{\rightarrow}=f_{item}(F_{i}) ei=fitem(Fi)
这里的 e i → ∈ R d × 1 \mathop{e_{i}}\limits^{\rightarrow} \in \mathbb{R}^{d \times 1} eiRd×1,表示每个Item的Embedding向量表征。在获取了User和Item的向量表征之后,那我们该如何对User的多兴趣表征与指定的Item进行打分呢,我们对其采用以下公式进行打分:
f s c o r e ( V u , e i → ) = m a x 1 ≤ k ≤ K e i → T v u k → f_{score}(V_{u},\mathop{e_{i}}\limits^{\rightarrow})=\mathop{max}\limits_{1\leq k \leq K} {\mathop{e_{i}}\limits^{\rightarrow}}^\mathrm{T} \mathop{v_{u}^{k}}\limits ^{\rightarrow} fscore(Vu,ei)=1kKmaxeiTvuk

实际上可以看出,这里只是对User的所有兴趣向量挨个与Item的向量进行内积,从所有的内积结果中挑出最大的得分作为最终的得分

1.3 Embedding & Pooling Layer

从上图可以看出,我们模型的输入有三部分组成,也就是我们在上一小节中的 I u , P u , F i I_{u},P_{u},F_{i} Iu,Pu,Fi,这里对所有的离散特征都使用Embedding Layer获取其Embedding向量,但是对于User/Item而言都是由一个或者多个Embedding向量组成,这里如果User/Item的向量表征有多个的话,我们对这些向量进行mean pooling操作,将其变成单一的Embedding向量

1.4 Multi-Interest Extractor Layer

在获取了用户行为序列的Embedding之后,我们就需要对用户的行为序列的Embedding向量进行多兴趣建模了,我们在这一小节详细的介绍MIND论文对于多兴趣建模的相关内容
MIND中对于多兴趣建模师参考了胶囊网络中的动态路由的方法来做的,这里将输入的用户的历史行为序列中的每一个Item当中一个胶囊,将所提取出的多兴趣向量的每一个兴趣向量作为所提取出的胶囊,这里我们将用户的行为序列(胶囊)计作 { e i → , i ∈ I u } \{\mathop{e_{i}}\limits^{\rightarrow},i \in I_{u} \} {ei,iIu},将所得的用户的多兴趣向量(胶囊)计作 { u j →   j = 1 , . . . , K u ′ } \{\mathop{u_{j}}\limits^{\rightarrow}\,j=1,...,K_{u}^{'}\} {ujj=1,...,Ku} ,其主要的算法流程如下所示:
在这里插入图片描述

我们针对针对该流程进行逐一的解释,首先流程的第一行,我们需要根据输入的用户User的长度来动态的计算所提取的User的兴趣向量个数,可以简答的认为一个User的行为序列越长,其所包含的潜在兴趣的个数就越多,这里用户兴趣向量的计算公式如下:
K u ′ = m a x ( 1 , m i n ( K , l o g 2 I u ) ) K_{u}^{'} = max(1,min(K,{log}_{2}I_{u})) Ku=max(1,min(K,log2Iu))
这里的 K K K是我们设置的一个超参数,代表着最小兴趣的个数,当然了,我们也可以直接给所有用户都指定提取出固定个数的兴趣向量,这一部分在作者的实验部分会有体现。第2行就是我们需要给User的行为胶囊 i i i 和User的兴趣胶囊 j j j 之间声明一个系数 b i j b_{ij} bij ,这里我们对 b i j b_{ij} bij 使用高斯分布进行初始化。第三行开始是一个for循环,这里是对动态路由的次数进行for循环,在原论文中,这里是循环三次,即做三次动态路由。下面我们来具体分析一下每一次动态路由需要做什么操作

  • 1.首先对 b i j b_{ij} bij 进行softmax,这里要注意我们softmax的维度是-1,这也就是让每个User的行为胶囊i的所有兴趣胶囊j的和为1,即:
    w i j = e x p ( b i j ) ∑ i = 1 m e x p ( b i k ) w_{ij}=\frac{exp(b_{ij})}{\sum_{i=1}^{m}exp(b_{ik})} wij=i=1mexp(bik)exp(bij)
  • 2.在得到 w i j w_{ij} wij 之后,我们来生成兴趣胶囊 z j z_{j} zj ,其生成方法就是遍历的User行为序列,对每一个User的行为胶囊i执行第五行的公式,这样就可以将user行为序列的信息聚合到每一个兴趣胶囊中了。这里要注意 S ∈ R d × d S \in \mathbb{R}^{d \times d} SRd×d 是一个可学习参数, S S S 相当于是对User的行为胶囊和兴趣胶囊进行信息交互融合的一个权重。
  • 3.在得到兴趣胶囊 z j z_j zj 之后,我们对其使用squash激活函数,其公式如下:
    s q u a s h ( z j ) = ∣ ∣ z j ∣ ∣ 2 1 + ∣ ∣ z j ∣ ∣ 2 z j ∣ ∣ z j ∣ ∣ squash(z_j)=\frac{{||z_{j}||}^2}{1+{||z_{j}||}^2}\frac{z_{j}}{{||z_{j}||}} squash(zj)=1+∣∣zj∣∣2∣∣zj∣∣2∣∣zj∣∣zj
    这里的 ∣ ∣ z j ∣ ∣ ||z_{j}|| ∣∣zj∣∣ 代表向量的模长
  • 4.最后一步我们需要来根据第七行中的公式来更新所有的 b i j b_{ij} bij ,然后继续重复这一循环过程,直到达到预设的循环次数。
    在完成动态路由之后,我们就针对User的行为序列得到了他的多兴趣表征了,下面我们来看一下怎么根据用户的多兴趣表征来进行模型的训练

1.5 Label-aware Attention Layer

我们在得到了用户的多兴趣表征之后,我们就要对其进行Loss计算,以此来进行模型的反向传播了,那这个时候我们手里有的信息其实只有两个,第一个就是我们上一小节刚刚得到的用户的多兴趣向量 V u ∈ R d × K V_{u} \in \mathbb{R}^{d \times K} VuRd×K ,另一个就是我们User的“标签”,也就是User下一个点击的Item,我们同样也可以获取到这一个Item的Embedding向量 e i → ∈ R d \mathop{e_{i}}\limits^{\rightarrow} \in \mathbb{R}^{d} eiRd ,对于传统的序列召回,我们得到的只有单一的User/Item的Embedding向量,这时我们可以通过Sample Softmax,Softmax或者是一些基于Pair-Wise的方法来对其进行Loss的计算,但是我们这里对于User的Embedding表征的向量是有多个,这里就涉及到了怎么把User的多兴趣表征重新整合成单一的向量了,这里作者进行了详细的实验与讨论。

首先作者通过attention计算出各个兴趣向量对目标item的权重,用于将多兴趣向量融合为用户向量,其中p为可调节的参数
v u → = A t t e n t i o n ( e i → , V u , V u ) = V u s o f t m a x ( p o w ( V u T e i → , p ) ) \mathop{v_{u}}\limits^{\rightarrow} = Attention(\mathop{e_{i}}\limits^{\rightarrow},V_{u},V_{u})=V_{u}softmax(pow(V_{u}^{T}\mathop{e_{i}}\limits^{\rightarrow},p)) vu=Attention(ei,Vu,Vu)=Vusoftmax(pow(VuTei,p))
可以看出,上述式子中只有 p p p 为超参数,下面作者对 p p p 的不同取值进行了讨论

  • 1.当p=0的时候,每个用户兴趣向量有相同的权重,相当于对所有的User兴趣向量做了一个Mean Pooling
  • 2.当p>1的时候,p越大,与目标商品向量点积更大的用户兴趣向量会有更大的权重
  • 3.当p趋于无穷大的时候,实际上就相当于只使用与目标商品向量点积最大的用户兴趣向量,忽略其他用户向量,可以理解是每次只激活一个兴趣向量

在作者的实际实验中发现,当p趋于无穷大的时候模型的训练效果是最好的,即在进行Loss计算的时候,我们只对和目标Item最相似的那个兴趣向量进行Loss计算,这样我们就将多兴趣模型的Loss计算重新转换为一个一般的序列召回的问题,这里作者采用了Sample-Softmax当作损失函数,这里的Sample-Softmax可以认为是Softmax的简化版,其核心也是一个多分类任务,但是当Item的个数很大的时候,直接当作多分类做会有很大的计算压力,所以这里引出了Sample-Softmax,这里的Sample-Softmax是对负样本进行了随机采样,这样就可以极大的缩小多分类的总类别数,也就可以在大规模的推荐数据上进行训练了,在后面的代码实践中,由于我们使用的是公开数据集,其Item的个数比较小,所以我们就没有使用Sample-Softmax而是直接使用了Softmax

2.代码实践

!pip install faiss
Looking in indexes: https://pypi.tuna.tsinghua.edu.cn/simple
Requirement already satisfied: faiss in /opt/conda/envs/python35-paddle120-env/lib/python3.7/site-packages (1.5.3)
Requirement already satisfied: numpy in /opt/conda/envs/python35-paddle120-env/lib/python3.7/site-packages (from faiss) (1.19.5)

[1m[[0m[34;49mnotice[0m[1;39;49m][0m[39;49m A new release of pip available: [0m[31;49m22.1.2[0m[39;49m -> [0m[32;49m22.3[0m
[1m[[0m[34;49mnotice[0m[1;39;49m][0m[39;49m To update, run: [0m[32;49mpip install --upgrade pip[0m
import paddle
from paddle import nn
from paddle.io import DataLoader, Dataset
import paddle.nn.functional as F
import pandas as pd
import numpy as np
import copy
import os
import math
import random
from sklearn.metrics import roc_auc_score,log_loss
from sklearn.preprocessing import normalize
from tqdm import tqdm
from collections import defaultdict
from sklearn.manifold import TSNE
import seaborn as sns
from matplotlib import pyplot as plt
import faiss
paddle.device.set_device('gpu:0')
import warnings
warnings.filterwarnings("ignore")

2.1 Dataset

class SeqnenceDataset(Dataset):
    def __init__(self, config, df, phase='train'):
        self.config = config
        self.df = df
        self.max_length = self.config['max_length']
        self.df = self.df.sort_values(by=['user_id', 'timestamp'])
        self.user2item = self.df.groupby('user_id')['item_id'].apply(list).to_dict()
        self.user_list = self.df['user_id'].unique()
        self.phase = phase

    def __len__(self, ):
        return len(self.user2item)

    def __getitem__(self, index):
        if self.phase == 'train':
            user_id = self.user_list[index]
            item_list = self.user2item[user_id]
            hist_item_list = []
            hist_mask_list = []

            k = random.choice(range(4, len(item_list)))  # 从[8,len(item_list))中随机选择一个index
            # k = np.random.randint(2,len(item_list))
            item_id = item_list[k]  # 该index对应的item加入item_id_list

            if k >= self.max_length:  # 选取seq_len个物品
                hist_item_list.append(item_list[k - self.max_length: k])
                hist_mask_list.append([1.0] * self.max_length)
            else:
                hist_item_list.append(item_list[:k] + [0] * (self.max_length - k))
                hist_mask_list.append([1.0] * k + [0.0] * (self.max_length - k))

            return paddle.to_tensor(hist_item_list).squeeze(0), paddle.to_tensor(hist_mask_list).squeeze(
                0), paddle.to_tensor([item_id])
        else:
            user_id = self.user_list[index]
            item_list = self.user2item[user_id]
            hist_item_list = []
            hist_mask_list = []

            k = int(0.8 * len(item_list))
            # k = len(item_list)-1

            if k >= self.max_length:  # 选取seq_len个物品
                hist_item_list.append(item_list[k - self.max_length: k])
                hist_mask_list.append([1.0] * self.max_length)
            else:
                hist_item_list.append(item_list[:k] + [0] * (self.max_length - k))
                hist_mask_list.append([1.0] * k + [0.0] * (self.max_length - k))

            return paddle.to_tensor(hist_item_list).squeeze(0), paddle.to_tensor(hist_mask_list).squeeze(
                0), item_list[k:]

    def get_test_gd(self):
        self.test_gd = {}
        for user in self.user2item:
            item_list = self.user2item[user]
            test_item_index = int(0.8 * len(item_list))
            self.test_gd[user] = item_list[test_item_index:]
        return self.test_gd

2.2 MIND 模型定义


class CapsuleNetwork(nn.Layer):

    def __init__(self, hidden_size, seq_len, bilinear_type=2, interest_num=4, routing_times=3, hard_readout=True,
                 relu_layer=False):
        super(CapsuleNetwork, self).__init__()
        self.hidden_size = hidden_size  # h
        self.seq_len = seq_len  # s
        self.bilinear_type = bilinear_type
        self.interest_num = interest_num
        self.routing_times = routing_times
        self.hard_readout = hard_readout
        self.relu_layer = relu_layer
        self.stop_grad = True
        self.relu = nn.Sequential(
            nn.Linear(self.hidden_size, self.hidden_size, bias_attr=False),
            nn.ReLU()
        )
        if self.bilinear_type == 0:  # MIND
            self.linear = nn.Linear(self.hidden_size, self.hidden_size, bias_attr=False)
        elif self.bilinear_type == 1:
            self.linear = nn.Linear(self.hidden_size, self.hidden_size * self.interest_num, bias_attr=False)
        else:  # ComiRec_DR
            self.w = nn.Parameter(paddle.Tensor(1, self.seq_len, self.interest_num * self.hidden_size, self.hidden_size))

    def forward(self, item_eb, mask):
        if self.bilinear_type == 0:  # MIND
            item_eb_hat = self.linear(item_eb)  # [b, s, h]
            item_eb_hat = paddle.repeat_interleave(item_eb_hat, self.interest_num, 2) # [b, s, h*in]
        elif self.bilinear_type == 1:
            item_eb_hat = self.linear(item_eb)
        else:  # ComiRec_DR
            u = paddle.unsqueeze(item_eb, dim=2)  # shape=(batch_size, maxlen, 1, embedding_dim)
            item_eb_hat = paddle.sum(self.w[:, :self.seq_len, :, :] * u,
                                    dim=3)  # shape=(batch_size, maxlen, hidden_size*interest_num)

        item_eb_hat = paddle.reshape(item_eb_hat, (-1, self.seq_len, self.interest_num, self.hidden_size))
        item_eb_hat = paddle.transpose(item_eb_hat, perm=[0,2,1,3])
        # item_eb_hat = paddle.reshape(item_eb_hat, (-1, self.interest_num, self.seq_len, self.hidden_size))

        # [b, in, s, h]
        if self.stop_grad:  # 截断反向传播,item_emb_hat不计入梯度计算中
            item_eb_hat_iter = item_eb_hat.detach()
        else:
            item_eb_hat_iter = item_eb_hat

        # b的shape=(b, in, s)
        if self.bilinear_type > 0:  # b初始化为0(一般的胶囊网络算法)
            capsule_weight = paddle.zeros(item_eb_hat.shape[0], self.interest_num, self.seq_len)
        else:  # MIND使用高斯分布随机初始化b
            capsule_weight = paddle.randn((item_eb_hat.shape[0], self.interest_num, self.seq_len))

        for i in range(self.routing_times):  # 动态路由传播3次
            atten_mask = paddle.repeat_interleave(paddle.unsqueeze(mask, 1), self.interest_num, 1) # [b, in, s]
            paddings = paddle.zeros_like(atten_mask)

            # 计算c,进行mask,最后shape=[b, in, 1, s]
            capsule_softmax_weight = F.softmax(capsule_weight, axis=-1)
            capsule_softmax_weight = paddle.where(atten_mask==0, paddings, capsule_softmax_weight)  # mask
            capsule_softmax_weight = paddle.unsqueeze(capsule_softmax_weight, 2)

            if i < 2:
                # s=c*u_hat , (batch_size, interest_num, 1, seq_len) * (batch_size, interest_num, seq_len, hidden_size)
                interest_capsule = paddle.matmul(capsule_softmax_weight,
                                                item_eb_hat_iter)  # shape=(batch_size, interest_num, 1, hidden_size)
                cap_norm = paddle.sum(paddle.square(interest_capsule), -1, keepdim=True)  # shape=(batch_size, interest_num, 1, 1)
                scalar_factor = cap_norm / (1 + cap_norm) / paddle.sqrt(cap_norm + 1e-9)  # shape同上
                interest_capsule = scalar_factor * interest_capsule  # squash(s)->v,shape=(batch_size, interest_num, 1, hidden_size)

                # 更新b
                delta_weight = paddle.matmul(item_eb_hat_iter,  # shape=(batch_size, interest_num, seq_len, hidden_size)
                                            paddle.transpose(interest_capsule, perm=[0,1,3,2])
                                            # shape=(batch_size, interest_num, hidden_size, 1)
                                            )  # u_hat*v, shape=(batch_size, interest_num, seq_len, 1)
                delta_weight = paddle.reshape(delta_weight, (
                -1, self.interest_num, self.seq_len))  # shape=(batch_size, interest_num, seq_len)
                capsule_weight = capsule_weight + delta_weight  # 更新b
            else:
                interest_capsule = paddle.matmul(capsule_softmax_weight, item_eb_hat)
                cap_norm = paddle.sum(paddle.square(interest_capsule), -1, keepdim=True)
                scalar_factor = cap_norm / (1 + cap_norm) / paddle.sqrt(cap_norm + 1e-9)
                interest_capsule = scalar_factor * interest_capsule

        interest_capsule = paddle.reshape(interest_capsule, (-1, self.interest_num, self.hidden_size))

        if self.relu_layer:  # MIND模型使用book数据库时,使用relu_layer
            interest_capsule = self.relu(interest_capsule)

        return interest_capsule
class MIND(nn.Layer):
    def __init__(self, config):
        super(MIND, self).__init__()

        self.config = config
        self.embedding_dim = self.config['embedding_dim']
        self.max_length = self.config['max_length']
        self.n_items = self.config['n_items']

        self.item_emb = nn.Embedding(self.n_items, self.embedding_dim, padding_idx=0)
        self.capsule = CapsuleNetwork(self.embedding_dim, self.max_length, bilinear_type=0,
                                      interest_num=self.config['K'])
        self.loss_fun = nn.CrossEntropyLoss()
        self.reset_parameters()

    def calculate_loss(self,user_emb,pos_item):
        all_items = self.item_emb.weight
        scores = paddle.matmul(user_emb, all_items.transpose([1, 0]))
        return self.loss_fun(scores,pos_item)

    def output_items(self):
        return self.item_emb.weight

    def reset_parameters(self, initializer=None):
        for weight in self.parameters():
            paddle.nn.initializer.KaimingNormal(weight)

    def forward(self, item_seq, mask, item, train=True):

        if train:
            seq_emb = self.item_emb(item_seq)  # Batch,Seq,Emb
            item_e = self.item_emb(item).squeeze(1)

            multi_interest_emb = self.capsule(seq_emb, mask)  # Batch,K,Emb

            cos_res = paddle.bmm(multi_interest_emb, item_e.squeeze(1).unsqueeze(-1))
            k_index = paddle.argmax(cos_res, axis=1)

            best_interest_emb = paddle.rand((multi_interest_emb.shape[0], multi_interest_emb.shape[2]))
            for k in range(multi_interest_emb.shape[0]):
                best_interest_emb[k, :] = multi_interest_emb[k, k_index[k], :]

            loss = self.calculate_loss(best_interest_emb,item)
            output_dict = {
                'user_emb': multi_interest_emb,
                'loss': loss,
            }
        else:
            seq_emb = self.item_emb(item_seq)  # Batch,Seq,Emb
            multi_interest_emb = self.capsule(seq_emb, mask)  # Batch,K,Emb
            output_dict = {
                'user_emb': multi_interest_emb,
            }
        return output_dict

2.3 Pipeline

config = {
    'train_path':'/home/aistudio/data/data173799/train_enc.csv',
    'valid_path':'/home/aistudio/data/data173799/valid_enc.csv',
    'test_path':'/home/aistudio/data/data173799/test_enc.csv',
    'lr':1e-4,
    'Epoch':100,
    'batch_size':256,
    'embedding_dim':16,
    'max_length':20,
    'n_items':15406,
    'K':4
}
def my_collate(batch):
    hist_item, hist_mask, item_list = list(zip(*batch))

    hist_item = [x.unsqueeze(0) for x in hist_item]
    hist_mask = [x.unsqueeze(0) for x in hist_mask]

    hist_item = paddle.concat(hist_item,axis=0)
    hist_mask = paddle.concat(hist_mask,axis=0)
    return hist_item,hist_mask,item_list
def save_model(model, path):
    if not os.path.exists(path):
        os.makedirs(path)
    paddle.save(model.state_dict(), path + 'model.pdparams')


def load_model(model, path):
    state_dict = paddle.load(path + 'model.pdparams')
    model.set_state_dict(state_dict)
    print('model loaded from %s' % path)
    return model

2.4 基于Faiss的向量召回

def get_predict(model, test_data, hidden_size, topN=20):

    item_embs = model.output_items().cpu().detach().numpy()
    item_embs = normalize(item_embs, norm='l2')
    gpu_index = faiss.IndexFlatIP(hidden_size)
    gpu_index.add(item_embs)
    
    test_gd = dict()
    preds = dict()
    
    user_id = 0

    for (item_seq, mask, targets) in tqdm(test_data):

        # 获取用户嵌入
        # 多兴趣模型,shape=(batch_size, num_interest, embedding_dim)
        # 其他模型,shape=(batch_size, embedding_dim)
        user_embs = model(item_seq,mask,None,train=False)['user_emb']
        user_embs = user_embs.cpu().detach().numpy()

        # 用内积来近邻搜索,实际是内积的值越大,向量越近(越相似)
        if len(user_embs.shape) == 2:  # 非多兴趣模型评估
            user_embs = normalize(user_embs, norm='l2').astype('float32')
            D, I = gpu_index.search(user_embs, topN)  # Inner Product近邻搜索,D为distance,I是index
#             D,I = faiss.knn(user_embs, item_embs, topN,metric=faiss.METRIC_INNER_PRODUCT)
            for i, iid_list in enumerate(targets):  # 每个用户的label列表,此处item_id为一个二维list,验证和测试是多label的
                test_gd[user_id] = iid_list
                preds[user_id] = I[i,:]
                user_id +=1
        else:  # 多兴趣模型评估
            ni = user_embs.shape[1]  # num_interest
            user_embs = np.reshape(user_embs,
                                   [-1, user_embs.shape[-1]])  # shape=(batch_size*num_interest, embedding_dim)
            user_embs = normalize(user_embs, norm='l2').astype('float32')
            D, I = gpu_index.search(user_embs, topN)  # Inner Product近邻搜索,D为distance,I是index
#             D,I = faiss.knn(user_embs, item_embs, topN,metric=faiss.METRIC_INNER_PRODUCT)
            for i, iid_list in enumerate(targets):  # 每个用户的label列表,此处item_id为一个二维list,验证和测试是多label的
                recall = 0
                dcg = 0.0
                item_list_set = []

                # 将num_interest个兴趣向量的所有topN近邻物品(num_interest*topN个物品)集合起来按照距离重新排序
                item_list = list(
                    zip(np.reshape(I[i * ni:(i + 1) * ni], -1), np.reshape(D[i * ni:(i + 1) * ni], -1)))
                item_list.sort(key=lambda x: x[1], reverse=True)  # 降序排序,内积越大,向量越近
                for j in range(len(item_list)):  # 按距离由近到远遍历推荐物品列表,最后选出最近的topN个物品作为最终的推荐物品
                    if item_list[j][0] not in item_list_set and item_list[j][0] != 0:
                        item_list_set.append(item_list[j][0])
                        if len(item_list_set) >= topN:
                            break
                test_gd[user_id] = iid_list
                preds[user_id] = item_list_set
                user_id +=1
    return test_gd, preds

def evaluate(preds,test_gd, topN=50):
    total_recall = 0.0
    total_ndcg = 0.0
    total_hitrate = 0
    for user in test_gd.keys():
        recall = 0
        dcg = 0.0
        item_list = test_gd[user]
        for no, item_id in enumerate(item_list):
            if item_id in preds[user][:topN]:
                recall += 1
                dcg += 1.0 / math.log(no+2, 2)
            idcg = 0.0
            for no in range(recall):
                idcg += 1.0 / math.log(no+2, 2)
        total_recall += recall * 1.0 / len(item_list)
        if recall > 0:
            total_ndcg += dcg / idcg
            total_hitrate += 1
    total = len(test_gd)
    recall = total_recall / total
    ndcg = total_ndcg / total
    hitrate = total_hitrate * 1.0 / total
    return {f'recall@{topN}': recall, f'ndcg@{topN}': ndcg, f'hitrate@{topN}': hitrate}

# 指标计算
def evaluate_model(model, test_loader, embedding_dim,topN=20):
    test_gd, preds = get_predict(model, test_loader, embedding_dim, topN=topN)
    return evaluate(preds, test_gd, topN=topN)
# 读取数据
train_df = pd.read_csv(config['train_path'])
valid_df = pd.read_csv(config['valid_path'])
test_df = pd.read_csv(config['test_path'])
train_dataset = SeqnenceDataset(config, train_df, phase='train')
valid_dataset = SeqnenceDataset(config, valid_df, phase='test')
test_dataset = SeqnenceDataset(config, test_df, phase='test')
train_loader = DataLoader(dataset=train_dataset, batch_size=config['batch_size'], shuffle=True,num_workers=8)
valid_loader = DataLoader(dataset=valid_dataset, batch_size=config['batch_size'], shuffle=False,collate_fn=my_collate)
test_loader = DataLoader(dataset=test_dataset, batch_size=config['batch_size'], shuffle=False,collate_fn=my_collate)
model = MIND(config)
optimizer = paddle.optimizer.Adam(parameters=model.parameters(), learning_rate=config['lr'])
log_df = pd.DataFrame()
best_reacall = -1

exp_path = './exp/ml-20m_softmax/MIND_{}_{}_{}/'.format(config['lr'],config['batch_size'],config['embedding_dim'])
os.makedirs(exp_path,exist_ok=True,mode=0o777)
patience = 5
last_improve_epoch = 1
log_csv = exp_path+'log.csv'
# *****************************************************train*********************************************
for epoch in range(1, 1 + config['Epoch']):
    # try :
        pbar = tqdm(train_loader)
        model.train()
        loss_list = []
        acc_50_list = []
        print()
        print('Training:')
        print()
        for batch_data in pbar:
            (item_seq, mask, item) = batch_data

            output_dict = model(item_seq, mask, item)
            loss = output_dict['loss']

            loss.backward()
            optimizer.step()
            optimizer.clear_grad()

            loss_list.append(loss.item())

            pbar.set_description('Epoch [{}/{}]'.format(epoch,config['Epoch']))
            pbar.set_postfix(loss = np.mean(loss_list))
    # *****************************************************valid*********************************************

        print('Valid')
        recall_metric = evaluate_model(model, valid_loader, config['embedding_dim'], topN=50)
        print(recall_metric)
        recall_metric['phase'] = 'valid'
        log_df = log_df.append(recall_metric, ignore_index=True)
        log_df.to_csv(log_csv)

        if recall_metric['recall@50'] > best_reacall:
            save_model(model,exp_path)
            best_reacall = recall_metric['recall@50']
            last_improve_epoch = epoch

        if epoch - last_improve_epoch > patience:
            break

print('Testing')
model = load_model(model,exp_path)
recall_metric = evaluate_model(model, test_loader, config['embedding_dim'], topN=50)
print(recall_metric)
recall_metric['phase'] = 'test'
log_df = log_df.append(recall_metric, ignore_index=True)
log_df.to_csv(log_csv)

log_df
hitrate@50ndcg@50phaserecall@50
00.3736370.170178valid0.047637
10.3751640.178589valid0.054515
20.4042460.197123valid0.062555
30.4104260.201520valid0.064550
40.4307110.215622valid0.070258
50.4459070.224988valid0.074076
60.4538320.229855valid0.075559
70.4719350.239573valid0.078563
80.4928750.251977valid0.084322
90.5013810.257030valid0.086518
100.5160680.265742valid0.090916
110.5229020.269815valid0.092930
120.5261740.272822valid0.094782
130.5277010.273558valid0.095177
140.5274830.274433valid0.095636
150.5287920.274984valid0.095898
160.5302460.276017valid0.096185
170.5311910.276624valid0.096645
180.5334450.277648valid0.096800
190.5338810.277905valid0.096932
200.5350440.278058valid0.096865
210.5365710.278463valid0.096698
220.5366440.278655valid0.096998
230.5369350.278540valid0.096731
240.5320630.275862valid0.095614
250.5319910.275563valid0.095481
260.5294460.273832valid0.094594
270.5277740.272779valid0.093911
280.5186130.265859valid0.090243
290.5436760.281832test0.096097

2.5 基于TSNE的Item Embedding分布可视化

def plot_embedding(data, title):
    x_min, x_max = np.min(data, 0), np.max(data, 0)
    data = (data - x_min) / (x_max - x_min)
 
    fig = plt.figure(dpi=120)
    plt.scatter(data[:,0], data[:,1], marker='.')
    
    plt.xticks([])
    plt.yticks([])
    plt.title(title)
    plt.show()
item_emb = model.output_items().numpy()
plot_embedding(item_emb,'MIND Item Embedding')

在这里插入图片描述

3.总结

MIND模型是阿里巴巴在序列召回领域中一次重大的尝试,该工作创新性的引入了胶囊网络,将胶囊网络的动态路由算法引入到了用户的多兴趣建模上,通过B2I动态路由很好的从用户的原始行为序列中提取出用户的多种兴趣表征。并在离线训练阶段,通过提出Label-Aware Attention详细的探讨了多兴趣建模下的模型参数更新范式。

此文章为搬运
原项目链接

Logo

学大模型,用大模型上飞桨星河社区!每天8点V100G算力免费领!免费领取ERNIE 4.0 100w Token >>>

更多推荐