NeuMF 的 gluon 实现

  Posted by Mr.Zhang on 30 Jul, 2019

   MXNET   

Xiangnan He et al.,2017,Neural Collaborative Filtering论文的gluon复现,原作的代码github。NeuMF模型分为两块:GMF与MLP,具体架构如下图:

NeuMF

本文作二分类问题,user与item分别embedding后丢入GMF作内积;丢入MLP学习后的结果与前面结果拼接后丢入一激活函数为sigmoid的层后得到预测值,与真实值作交叉熵损失函数。这篇文章的实现有两个tricks:负采样的样本数是训练样本数的4倍,而不是用户数的4倍;每个epoch重新负采样,让模型看到更多的负类型样本。

生成训练集合测试集的类:

class NCFData(gluon.data.Dataset):
    """构建NCF模型数据
       如果想每个epoch都重新采样,在构建训练集迭代前要调用process_tr_data函数
            ncf = NCFData(tr_raw, te_raw)
            ncf.process_tr_data()
            tr_iter = gluon.data.DataLoader(ncf)
    """
    def __init__(self, tr_raw, te_raw, tr_num_ng=4, te_num_ng=99):
        super(NCFData, self).__init__()
        self.tr_num_ng = tr_num_ng
        self.te_num_ng = te_num_ng

        self.tr_raw_users = tr_raw[0]
        self.tr_raw_items = tr_raw[1]
        self.tr_raw_rates = tr_raw[2]

        self.te_raw_users = te_raw[0]
        self.te_raw_items = te_raw[1]
        self.te_raw_rates = te_raw[2]

        self.max_user = None
        self.max_item = None
        self.mat = None

        self.tr_users = None
        self.tr_items = None
        self.tr_rates = None

        self.test = None

        self.get_mat()

    def process_tr_data(self):

        self.tr_users, self.tr_items, self.tr_rates = [], [], []

        self.tr_users[:] = self.tr_raw_users[:]
        self.tr_items[:] = self.tr_raw_items[:]
        self.tr_rates[:] = self.tr_raw_rates[:]
        logging.info('训练集负采样中...')

        for u in self.tr_raw_users:
            for t in range(self.tr_num_ng):
                j = np.random.randint(self.max_item)
                while (u,j) in self.mat.keys():
                    j = np.random.randint(self.max_item)
                self.tr_users.append(u)
                self.tr_items.append(j)
                self.tr_rates.append(0)

    def get_tr_data(self):
        self.process_tr_data()
        return self.tr_users, self.tr_items, self.tr_rates

    def get_te_data(self):
        test = dict()
        logging.info('测试集负采样中...')
        for u,i in zip(self.te_raw_users, self.te_raw_items):
            test[u] =[i]
            temp = set()
            temp.add(i)
            for t in range(self.te_num_ng):
                j = np.random.randint(self.max_item)
                while (u,j) in self.mat.keys() or (j in temp):
                    j = np.random.randint(self.max_item)
                temp.add(j)
                test[u].append(j)
        return test

    def get_mat(self):
        logging.info('构建稀疏矩阵...')
        self.max_user = max(self.tr_raw_users) + 1
        self.max_item = max(self.tr_raw_items) + 1
        self.mat = sparse.dok_matrix((self.max_user, self.max_item), 
                                     dtype=np.float32)
        for u,i,r in zip(self.tr_raw_users, self.tr_raw_items, self.tr_raw_rates):
            self.mat[u, i] = r

    def __len__(self):
        return (self.tr_num_ng + 1) * len(self.tr_raw_users)

    def __getitem__(self, idx):
        if not self.tr_users:
            self.process_tr_data()
        return self.tr_users[idx], self.tr_items[idx], self.tr_rates[idx]

模型的类:

class NeuCF(gluon.HybridBlock):
    def __init__(self, num_factors, num_layers, max_user, max_item, model_name):
        super(NeuCF, self).__init__()

        self.model_name = model_name

        if not self.model_name == 'MLP':
            self.gmf_embed_user = gluon.nn.Embedding(max_user, num_factors)
            self.gmf_embed_item = gluon.nn.Embedding(max_item, num_factors)

        if not self.model_name == 'GMF':
            self.mlp_embed_user = gluon.nn.Embedding(max_user,
                                   num_factors * (2 ** (num_layers -1)))

            self.mlp_embed_item = gluon.nn.Embedding(max_item,
                                   num_factors * (2 ** (num_layers -1)))

            self.mlp_layers = gluon.nn.HybridSequential()
            for i in range(num_layers):
                output_size = num_factors * (2 ** (num_layers - i - 1))
                self.mlp_layers.add(gluon.nn.Dense(output_size, 
                                                   activation='relu'))
        if self.model_name in ['MLP', 'GMF']:    
            self.pred = gluon.nn.Dense(1, activation='sigmoid')
        else:
            self.pred_ncf = gluon.nn.Dense(1, activation='sigmoid')

    def hybrid_forward(self, F, user, item):
        if not self.model_name == 'MLP':
            gmf_embed_user = self.gmf_embed_user(user)
            gmf_embed_item = self.gmf_embed_item(item)
            gmf_output = gmf_embed_user * gmf_embed_item

        if not self.model_name == 'GMF':
            mlp_embed_user = self.mlp_embed_user(user)
            mlp_embed_item = self.mlp_embed_item(item)
            interaction = F.concat(mlp_embed_user, mlp_embed_item, dim=1)
            mlp_output = self.mlp_layers(interaction)

        if self.model_name == 'GMF':
            concat = gmf_output
        elif self.model_name == 'MLP':
            concat = mlp_output
        else:
            concat = F.concat(gmf_output, mlp_output, dim=1)
            return self.pred_ncf(concat)

        return self.pred(concat)

如果想导入预训练的参数,可以试试:

net.load_parameters('GMF.params', allow_missing=True, ignore_extra=True)
net.load_parameters('MLP.params', allow_missing=True, ignore_extra=True)
net_pred_weight = nd.concat(net_gmf.pred.weight.data(), 
                            net_mlp.pred.weight.data(), dim=1)
net_pred_bias = net_gmf.pred.bias.data() + net_mlp.pred.bias.data()
net.pred_ncf.weight.set_data(0.5 * net_pred_weight)
net.pred_ncf.bias.set_data(0.5 * net_pred_bias)