\u200E
基于飞桨复现DMR模型,实现点击率预测
发布日期:2021-07-27T11:41:31.000+0000 浏览量:1129次




项目介绍




本项目是笔者参加百度AIStudio举办的飞桨论文复现挑战赛(第三期)( https://aistudio.baidu.com/aistudio/competition/detail/76 )的经验分享。本次比赛要求选手在指定时间内使用飞桨开源框架v2.0完成论文复现,考察复现模型的精度和速度。笔者复现了第一篇论文,推荐领域的DMR模型,取得了该模型复现的第一名。

笔者边查API边复现,中间踩了一些坑,正好能够借此机会与大家分享,希望对初学者有一定帮助。飞桨真的是简单易用,动态图模式调试起来丝滑流畅,对初学者非常友好,推荐大家使用。笔者水平有限,如有贻笑大方之处,还请大佬海涵,不吝指正。

闲话少叙,下面进入正题。



DMR论文简介




DMR全称Deep Match to Rank Model for Personalized Click-Through Rate Prediction,是阿里巴巴团队发表在AAAI 2020的论文,原文链接为 https://ojs.aaai.org//index.php/AAAI/article/view/5346 。据说该模型是首个联合训练Matching和Ranking的模型。



研究背景


推荐系统通常分为两个阶段,即召回Match和排序Rank阶段。在召回阶段会对用户和物品进行匹配,得到较小的一部分候选集进入到排序阶段。在召回阶段,协同过滤方法是最常用来计算用户和物品相关性的方法。在排序阶段,排序模型会对候选集的每个物品进行打分,然后选取得分最高的N个物品推荐给用户。而打分最为常用的方式是预测用户对物品的点击率。因此,点击率CTR预估也受到了学术界和工业界众多研究者的关注。而该论文也重点关注CTR预估问题。



现有问题


为了改善 CTR 的性能,目前很多模型的研究都集中在自动学习特征交互和增强模型的表达方面,如 Wide&Deep、PNN 等,也有一些模型想着去从用户行为序列中挖掘用户的潜在兴趣,如 DIN、DIEN、DSIN。但是这些Rank模型忽略了建模用户和物品之间的相关性User-to-Item (U2I),而U2I可以直接衡量用户对目标商品的偏好强度。



论文贡献


表征U2I相关性,主要有基于矩阵分解和基于深度学习的方法。基于深度学习的方法,有Youtube的DNN召回模型。那么能否将这种类似YoutbueDNN的Deep Match的思想融入到Rank阶段的模型呢?答案是可以的,也就是本项目要复现的DMR模型。自然,DMR论文的贡献点也就可以列出来了:
  • 指出了 CTR 领域捕获 U2I 相关性的重要性,并以此提出了 DMR 模型;

  • 设计了一个辅助召回网络去辅助 U2I 网络的训练,DMR 模型是一个将召回和排序联合起来训练的模型;

  • 引入注意力机制和位置编码来学习行为的权重;

  • 在公开数据集和工业数据集都取得了不错的成绩,并开放了源码。


开放源码这点对于本项目的完成尤为重要,没有这个,本项目的复现就极难完成了。所以要大力拥护开源,促进共同进步。



模型结构


下面来详细介绍一下DMR模型。DMR模型的网络结构如下图所示。仅仅依靠 MLP 隐式的特征交叉很难捕捉到 U2I 的相关性。对于输入到 MLP 中的 U2I 交叉特征,除了手工构建的 U2I 交叉特征,作者通过 User-to-Item 子网络和 Item-to-Item 子网络来表征 U2I 相关性,进一步提升模型的表达能力。





User-to-Item 网络


受到 MF 模型的启发,U2I 网络直接对用户表征和物品表征进行内积来表征 U2I 的相关性,这可以看成是一种显式的特征交叉。

用户行为可以折射出用户的潜在兴趣,所以可以通过对用户行为进行建模来获取用户表征。一种简单的方法是无视用户行为的顺序,直接对物品表征进行平均池化来获得用户表征。但考虑到用户最近行为更能代表用户兴趣,所以作者采用了带有位置编码的 Attention 来对不同行为进行加权作为用户表征:


其中, 为第 个 position embedding; 为第 个行为的特征向量; 为需要学习的参数, 为归一化的第 个行为的权重。

通过加权求和的池化操作和一个全连接层得到最终的用户表征

其中, 为一个非线性变化,对应网络中的 PReLU; 为第 个行为加权后的特征向量。

这里的 Attention 网络有三个被简化了的细节:
  • 可以加更多对的隐层以获得更好的表征;

  • 除了位置编码,我们还可以添加更多的上下文特征如行为类别、持续时间等;

  • 用户行为采用倒序,以使得最近行为在第一个位置。


然后用内积表示 U2I 的相关性:
其中 为目标 Item 的输出矩阵中对应的向量。

这里希望 越大,两者的相关性越强,从而对 CTR 预测有正向效果。但从反向传播的角度来看,仅仅通过点击标签很难学出这样的效果。为此,作者加入了辅助召回网络(Auxiliary Match Network)引入用户行为作为 label 来帮助原来的网络进行学习。

辅助网络作用是根据前 个行为来预测第 个行为,属于多分类问题,有多少候选商品就有多少个分类结果。我们很容易拿到用户前 次行为后的向量表示 ,那么用户第 次交互的物品为 的概率为:
我们以交叉熵作为损失函数:
其中, 为 label, 为预测概率, 为类别数。

当然,考虑计算量问题,所以使用 negative sampling 简化计算,损失函数如下:
其中, 为负样本数量,且数量远小于总数

DeepMatch 会把该 loss 加到原有网络中的 loss 中一起进行训练,辅助网络会促使内积与 CTR 正相关,从而帮助模型进行训练:
可以看到,U2I 网络是将排序和召回两者以统一的方式进行了联合训练。




Item-to-Item 网络


U2I 网络通过内积表达 U2I 的相关性,而 I2I 网络则通过计算 I2I 的相似性表征 U2I 对的相关性。参考 DIN 网络的做法,作者以目标商品为 query 对用户行为序列做 attention,从而区分不同行为的重要程度,最后对权重求和便得到了另一种 U2I 相关性表达:
通过加权求和的池化操作,用户行为的特征向量 可以转变为固定长度的特征向量
I2I 网络使用 Additive attention 形式进行计算,区别于 U2I 网络的 Dot-product Attention,可以增强表达能力。

除了 U2I 相关性表征,I2I 网络也将计算出的用户表征输入到 MLP 中。

更详细的模型设计细节可以查看原始论文。下面介绍核心代码的实现。



基于飞桨的代码




本项目是基于PaddleRec( https://github.com/PaddlePaddle/PaddleRec )开发,使用动态图模式。PaddleRec是基于飞桨进行二次开发的推荐模型库,内置了很多推荐算法,同时能够加速新模型的开发。



数据预处理


使用的原始数据集是阿里巴巴提供的一个淘宝展示点击率预估数据集。

数据集基本信息如下,更详细的说明请查看数据集介绍
( https://tianchi.aliyun.com/dataset/dataDetail?dataId=56 ):


从淘宝网站中随机抽样了114万用户8天内的广告展示/点击日志(2600万条记录),构成原始的样本骨架。用前面7天的做训练样本(20170506-20170512),用第8天的做测试样本(20170513)。

该数据集需要经过预处理才能成为模型的输入,但原论文及源代码中均未披露详细的数据预处理过程。经过仔细研究源代码和给出的sample数据,本项目分析出了数据预处理流程;但对于Price的归一化方法未做严格对齐,这个可能会对模型结果造成细微影响。

==> raw_sample.csv <== nonclk,clk对应alimama_sampled.txt最后一列(266),点击与否。用前面7天的做训练样本(20170506-20170512),用第8天的做测试样本(20170513),time_stamp 1494032110 stands for 2017-05-06 08:55:10。pid要编码为类别数字。
user,time_stamp,adgroup_id,pid,nonclk,clk
581738,1494137644,1,430548_1007,1,0

==> behavior_log.csv <== 对应alimama_sampled.txt中[0:150]列(列号从0开始),需要根据raw_sample.csv每行记录查找对应的50条历史数据,btag要编码为类别数字
user,time_stamp,btag,cate,brand
558157,1493741625,pv,6250,91286

==> user_profile.csv <== 对应alimama_sampled.txt中[250:259]列(列号从0开始)
userid,cms_segid,cms_group_id,final_gender_code,age_level,pvalue_level,shopping_level,occupation,new_user_class_level 
234,0,5,2,5,,3,0,3

==> ad_feature.csv <== 对应alimama_sampled.txt中[259:264]列(列号从0开始),price需要标准化到0~1
adgroup_id,cate_id,campaign_id,customer,brand,price
63133,6406,83237,1,95471,170.0


数据处理中更难的是数据集非常大,会爆内存,这里笔者费了一些功夫做索引、分治,最后终于得到了需要的数据集。详细的数据预处理流程请查看DMR data  preprocess( https://aistudio.baidu.com/aistudio/projectdetail/1805731 ),预处理完成的数据集请下载DMR Clean Data( https://aistudio.baidu.com/aistudio/datasetdetail/81892 )。

预处理好的数据集需要通过以下Data Loader进行读取并输入模型。

class RecDataset(IterableDataset):
    def __init__(self, file_list, config):
        super(RecDataset, self).__init__()
        self.file_list = file_list

    def __iter__(self):
        # 遍历所有的输入文件,文件中的每一行表示一条训练数据
        for file in self.file_list:
            with open(file, "r") as rf:
                for line in rf:
                    line = line.strip().split(",")
                    line = ['0' if i == '' or i.upper() == 'NULL' else i for i in line]  # handle missing values
                    # 需要转为numpy arrary方便后续转为Tensor
                    output_list = np.array(line, dtype='float32')
                    yield output_list



核心组网


组网代码的复现过程基本就是翻译TF源码(https://github.com/lvze92/DMR),不清楚的API就查文档。需要注意的是deep_match函数中softmax,tf源码是采样的,本项目最终的paddle代码没有采样,这点的影响应该是paddle会更准确,当然计算会慢些(不过实测发现在这个例子中这部分速度并不慢,完全可以接受)。

class DMRLayer(nn.Layer):
# 模型的超参数比较多,大部分是与数据集本身相关的,比如user_size是用户数,age_level_size是用户年龄分组数;lr, main_embedding_size, other_embedding_size是模型本身的超参数。详细的参数信息可以查看config.yaml文件,模型(而非数据集)相关的超参数信息在下文会详细介绍。
    def __init__(self, user_size, lr, global_step,
                 cms_segid_size, cms_group_id_size, final_gender_code_size,
                 age_level_size, pvalue_level_size, shopping_level_size,
                 occupation_size, new_user_class_level_size, adgroup_id_size,
                 cate_size, campaign_id_size, customer_size,
                 brand_size, btag_size, pid_size,
                 main_embedding_size, other_embedding_size)
:
        super(DMRLayer, self).__init__()

                  # 下面是参数的初始化,这里只贴出一部分
        self.user_size = user_size
        ……
        self.main_embedding_size = main_embedding_size
        self.other_embedding_size = other_embedding_size
        self.history_length = 50

                  # 下面是定义模型需要用到的各个embedding,必须在init里定义,才会被训练,这里只贴出几个示例
        self.uid_embeddings_var = paddle.nn.Embedding(
            self.user_size,
            self.main_embedding_size,
            sparse=True,
            weight_attr=paddle.ParamAttr(
                name="UidSparseFeatFactors",
                initializer=paddle.nn.initializer.Uniform()))
                  ……
        # 下面是定义User-to-Item网络的Attention layers
        self.query_layer = paddle.nn.Linear(self.other_embedding_size*2self.main_embedding_size*2, name='dm_align')
        self.query_prelu = paddle.nn.PReLU(num_parameters=self.history_length, init=0.1, name='dm_prelu')
        self.att_layer1_layer = paddle.nn.Linear(self.main_embedding_size*880, name='dm_att_1')
        self.att_layer2_layer = paddle.nn.Linear(8040, name='dm_att_2')
        self.att_layer3_layer = paddle.nn.Linear(401, name='dm_att_3')
        self.dnn_layer1_layer = paddle.nn.Linear(self.main_embedding_size*2self.main_embedding_size, name='dm_fcn_1')
        self.dnn_layer1_prelu = paddle.nn.PReLU(num_parameters=self.history_length, init=0.1, name='dm_fcn_1')

                  # 下面是定义Item-to-Item网络的 Attention layers
        self.query_layer2 = paddle.nn.Linear((self.other_embedding_size + self.main_embedding_size)*2self.main_embedding_size*2, name='dmr_align')
        self.query_prelu2 = paddle.nn.PReLU(num_parameters=self.history_length, init=0.1, name='dmr_prelu')
        self.att_layer1_layer2 = paddle.nn.Linear(self.main_embedding_size*880, name='tg_att_1')
        self.att_layer2_layer2 = paddle.nn.Linear(8040, name='tg_att_2')
        self.att_layer3_layer2 = paddle.nn.Linear(401, name='tg_att_3')

        self.logits_layer = paddle.nn.Linear(self.main_embedding_size, self.cate_size)

                  # 下面定义User-to-Item网络模块
        def deep_match(item_his_eb, context_his_eb, mask, match_mask, mid_his_batch, EMBEDDING_DIM, item_vectors, item_biases, n_mid):
            query = context_his_eb
            query = self.query_layer(query)  # [1, 50, 64] 
            query = self.query_prelu(query)

            inputs = paddle.concat([query, item_his_eb, query-item_his_eb, query*item_his_eb], axis=-1)  # B,T,E
            att_layer1 = self.att_layer1_layer(inputs)
            att_layer1 = F.sigmoid(att_layer1)
            att_layer2 = self.att_layer2_layer(att_layer1)
            att_layer2 = F.sigmoid(att_layer2)
            att_layer3 = self.att_layer3_layer(att_layer2)  # B,T,1
            scores = paddle.transpose(att_layer3, [021])  # B,1,T

            # mask
            bool_mask = paddle.equal(mask, paddle.ones_like(mask))  # B,T
            key_masks = paddle.unsqueeze(bool_mask, axis=1)  # B,1,T
            paddings = paddle.ones_like(scores) * (-2 ** 32 + 1)
            scores = paddle.where(key_masks, scores, paddings)

            # tril
            scores_tile = paddle.tile(paddle.sum(scores, 1), [1, paddle.shape(scores)[-1]])  # B, T*T
            scores_tile = paddle.reshape(scores_tile, [-1, paddle.shape(scores)[-1], paddle.shape(scores)[-1]])  # B, T, T
            diag_vals = paddle.ones_like(scores_tile)  # B, T, T
            tril = paddle.tril(diag_vals)
            paddings = paddle.ones_like(tril) * (-2 ** 32 + 1)
            scores_tile = paddle.where(paddle.equal(tril, paddle.to_tensor(0.0)), paddings, scores_tile)  # B, T, T
            scores_tile = F.softmax(scores_tile)  # B, T, T
            att_dm_item_his_eb = paddle.matmul(scores_tile, item_his_eb)  # B, T, E

            dnn_layer1 = self.dnn_layer1_layer(att_dm_item_his_eb)
            dnn_layer1 = self.dnn_layer1_prelu(dnn_layer1)

            # target mask
            user_vector = dnn_layer1[:, -1:]  # B, E
            user_vector2 = dnn_layer1[:, -2:] * paddle.reshape(match_mask, [-1, paddle.shape(match_mask)[1], 1])[:, -2:]  # B, E
            num_sampled = 2000
            labels = paddle.reshape(mid_his_batch[:, -1], [-11])  # B, 1

            # not sample
            # [B, E] * [E_size, cate_size]
            logits = paddle.matmul(user_vector2, item_vectors, transpose_y=True)
            logits = paddle.add(logits, item_biases)
            loss = F.cross_entropy(input=logits, label=labels)
            return loss, user_vector, scores

                  # 下面定义Item-to-Item网络模块
        def dmr_fcn_attention(item_eb, item_his_eb, context_his_eb, mask, mode='SUM'):
            mask = paddle.equal(mask, paddle.ones_like(mask))
            item_eb_tile = paddle.tile(item_eb, [1, paddle.shape(mask)[1]]) # B, T*E
            item_eb_tile = paddle.reshape(item_eb_tile, [-1, paddle.shape(mask)[1], item_eb.shape[-1]]) # B, T, E
            if context_his_eb is None:
                query = item_eb_tile
            else:
                query = paddle.concat([item_eb_tile, context_his_eb], axis=-1)
            query = self.query_layer2(query) 
            query = self.query_prelu2(query)
            dmr_all = paddle.concat([query, item_his_eb, query-item_his_eb, query*item_his_eb], axis=-1)
            att_layer_1 = self.att_layer1_layer2(dmr_all)
            att_layer_1 = F.sigmoid(att_layer_1)
            att_layer_2 = self.att_layer2_layer2(att_layer_1)
            att_layer_2 = F.sigmoid(att_layer_2)
            att_layer_3 = self.att_layer3_layer2(att_layer_2)  # B, T, 1
            att_layer_3 = paddle.reshape(att_layer_3, [-11, paddle.shape(item_his_eb)[1]])  # B,1,T
            scores = att_layer_3

            # Mask
            key_masks = paddle.unsqueeze(mask, 1)  # B,1,T
            paddings = paddle.ones_like(scores) * (-2 ** 32 + 1)
            paddings_no_softmax = paddle.zeros_like(scores)
            scores = paddle.where(key_masks, scores, paddings)  # [B, 1, T]
            scores_no_softmax = paddle.where(key_masks, scores, paddings_no_softmax)

            scores = F.softmax(scores)

            if mode == 'SUM':
                output = paddle.matmul(scores, item_his_eb)  # [B, 1, H]
                output = paddle.sum(output, 1)  # B,E
            else:
                scores = paddle.reshape(scores, [-1, paddle.shape(item_his_eb)[1]])
                output = item_his_eb * paddle.unsqueeze(scores, -1)
                output = paddle.reshape(output, paddle.shape(item_his_eb))

            return output, scores, scores_no_softmax

        self._deep_match = deep_match
        self._dmr_fcn_attention = dmr_fcn_attention
        ……
                  # 计算输入MLP的concat向量的维度,tf源码中这部分是在forward里自动计算的,但是Paddle必须在init里提前计算好代数表达式,这里还是稍微有点麻烦,需要细心计算
        self.inp_length = self.main_embedding_size + self.other_embedding_size*8 + self.main_embedding_size*5 + 1 + \
                          self.other_embedding_size + self.main_embedding_size*2 + self.main_embedding_size*2 + 1 + 1 \
                          + self.main_embedding_size*2
        # 下面是定义MLP,这里展示部分代码
        self.inp_layer = paddle.nn.BatchNorm(self.inp_length, momentum=0.99, epsilon=1e-03)
        self.dnn0_layer = paddle.nn.Linear(self.inp_length, 512, name='f0')
        self.dnn0_prelu = paddle.nn.PReLU(num_parameters=512, init=0.1, name='prelu0')
        ……
         # 下面定义前向计算,将init里定义的各个模块、layer串起来
    def forward(self, inputs_tensor, with_label=0):
        # 输入数据的预处理,展示部分代码
        inputs = inputs_tensor[0]  # sparse_tensor
        dense_tensor = inputs_tensor[1]
        self.btag_his = inputs[:0:self.history_length]
        ……
        self.price = dense_tensor.astype('float32')

        self.pid = inputs[:self.history_length*5+15]

        if with_label == 1:
            self.labels = inputs[:self.history_length*5+16]

        # embedding layer的查询,这里只列出部分
        self.uid_batch_embedded = self.uid_embeddings_var(self.uid)
                   ……
        self.pid_batch_embedded = self.pid_embeddings_var(self.pid)
                  # 下面是User特征的拼接
        self.user_feat = paddle.concat([self.uid_batch_embedded, self.cms_segid_batch_embedded, self.cms_group_id_batch_embedded, self.final_gender_code_batch_embedded, self.age_level_batch_embedded, self.pvalue_level_batch_embedded, self.shopping_level_batch_embedded, self.occupation_batch_embedded, self.new_user_class_level_batch_embedded], -1)
       ……
        # Item特征的拼接
        self.item_feat = paddle.concat([self.mid_batch_embedded, self.cat_batch_embedded, self.brand_batch_embedded, self.campaign_id_batch_embedded, self.customer_batch_embedded, self.price], -1)
        ……
        # Position特征的拼接
        self.position_his_eb = paddle.concat([self.position_his_eb, self.btag_his_batch_embedded], -1)
        self.dm_position_his_eb = paddle.concat([self.dm_position_his_eb, self.dm_btag_his_batch_embedded], -1)

        # User-to-Item Network
        # Auxiliary Match Network
        self.aux_loss, self.dm_user_vector, scores = self._deep_match(self.item_his_eb, self.dm_position_his_eb, self.mask, paddle.cast(self.match_mask, 'float32'), self.cate_his, self.main_embedding_size, self.dm_item_vectors_var.weight, self.dm_item_biases, self.cate_size)
        self.aux_loss *= 0.1
        self.dm_item_vec = self.dm_item_vectors_var(self.cate_id)
        rel_u2i = paddle.sum(self.dm_user_vector * self.dm_item_vec, -1, keepdim=True)  # B,1
        self.rel_u2i = rel_u2i

        # Item-to-Item Network
        att_outputs, alphas, scores_unnorm = self._dmr_fcn_attention(self.item_eb, self.item_his_eb, self.position_his_eb, self.mask)
        rel_i2i = paddle.unsqueeze(paddle.sum(scores_unnorm, [12]), -1)
        self.rel_i2i = rel_i2i
        self.scores = paddle.sum(alphas, 1)

                   # Concat向量的拼接
        inp = paddle.concat([self.user_feat, self.item_feat, self.context_feat, self.item_his_eb_sum, self.item_eb * self.item_his_eb_sum, rel_u2i, rel_i2i, att_outputs], -1

        # MLP网络全连接层,部分代码
        inp = self.inp_layer(inp)
        dnn0 = self.dnn0_layer(inp) 
        ……

        # prediction
        self.y_hat = F.sigmoid(dnn3)

                  # 计算loss
        if with_label == 1:
            # Cross-entropy loss and optimizer initialization
            x = paddle.sum(dnn3, 1)
            BCE = paddle.nn.BCEWithLogitsLoss()
            ctr_loss = paddle.mean(BCE(x, label=self.labels.astype('float32'))) 
            self.ctr_loss = ctr_loss
            self.loss = self.ctr_loss + self.aux_loss

            return self.y_hat, self.loss
        else:
            return self.y_hat



训练和评估


PaddleRec原来的trainer.py只能处理一组超参数,对于超参数优化并不方便,因此对其进行一定的修改,使之可以遍历搜索超参数。

if __name__ == '__main__':
    import os
    import shutil

    def f(best_auc, best_lr, current_lr, args):
        auc, current_lr, train_batch_size, model_save_path = main(args, current_lr)
        print(f'Trying Current_lr: {current_lr}, AUC: {auc}')
        if auc > best_auc:
            best_auc = auc
            best_lr = current_lr
            shutil.rmtree(f'{model_save_path}/1000', ignore_errors=True)
            shutil.copytree(f'{model_save_path}/0'f'{model_save_path}/1000')
            os.rename(src=f'{model_save_path}/0',
                      dst=f'{model_save_path}/b{train_batch_size}l{str(lr)[2:]}auc{str(auc)[2:]}')
            print(f'rename 0 to b{train_batch_size}l{str(lr)[2:]}auc{str(auc)[2:]}')
        return best_auc, best_lr

    def reset_graph():
        paddle.enable_static()
        paddle.disable_static()

    args = parse_args()
    best_auc = 0.0
    best_lr = -1

    # # if you want to try different learning_rate in one running, set try_lrs as below:
    # try_lrs = [0.006, 0.007, 0.008, 0.009, 0.01] * 2
    # # else if you want use learning_rate as set in config file, set try_lrs as below:
    try_lrs = [None]

    for lr in try_lrs:
        best_auc, best_lr = f(best_auc, best_lr, lr, args)
        reset_graph()
        if best_auc >= 0.6447:  # 0.6447 is the metric in the original paper
            break

    print(f'Best AUC: {best_auc}, Best learning_rate: {best_lr}')

启动训练的代码如下:
python -u ../../../tools/trainer.py -m config.yaml

启动推理和评估的代码如下:
python -u ../../../tools/infer.py -m config.yaml

其中config.yaml为配置文件,其中各参数含义如下:


这里仅展示了部分关键代码,详细实现请参考AI Studio项目:
https://aistudio.baidu.com/aistudio/projectdetail/1770964




复现效果



  • 复现参数设置
- lr:0.008
- batch_size: 5120
- optimizer:Adam

未尝试更小的学习率,有可能0.006~0.0075会取得更好结果



踩坑经历





飞桨用法


这个模型有很多Tensor的拼接,导致维度信息计算稍微有点麻烦。笔者一开始按照TF源码的写法,存在很多通过输入数据的维度来实时计算输入输出维度的layer,结果模型训练和测试流程虽然可以跑起来,但是模型训练集AUC一直在0.53以下忽高忽低,测试集就是0.50左右,说明模型完全没有得到训练。后来通过summary查看模型参数,这才发现飞桨在forward里定义的layer是不会得到训练的,需要训练的参数、layer必须在 init 里定义。最后费了一些功夫推导各个Tensor的维度信息,使之可以在 init 里进行定义,修改好这个后终于可以正常训练了。



过拟合问题


这个模型极易过拟合,第一个epoch,训练集AUC 0.6+,第2个epoch结束就 0.8+,4个epoch后就0.95+了;与此同时,测试集的AUC在第一个epoch结束前后效果最好,之后越来越差。后来我看了一篇文章讲在自己的数据上对比一些算法的情况,里头提到只能训练一个epoch效果就最好,多训练少训练都不行;另外,最优超参数与原论文的也不一样。笔者恍然大悟,之前保存的模型都不是刚刚好训练完成一个epoch的模型,因为在验证集上是batch level的数据,有一定的随机性。另外,笔者虽然试了很多参数组合,但其实学习率调试的范围不够广,数量不够多。意识这个问题后,模型终于取得了更好的结果。



数据预处理问题


如前面所说,原论文和代码中都没有说明具体的数据预处理方法,笔者一开始也是一头雾水。最后处理完了还有一个问题,那就是Price的归一化方法。一开始笔者处理完后Price大多数都是1e-6,取得了0.6438的复现精度,后来发现该问题后优化了归一化方法,使得Price在0.01~1之间,达到了0.6441的复现精度。也许再优化Price归一化方法,还能进一步提高精度。



总结与思考



本项目基于飞桨开源框架v2.0对DMR模型进行了复现,取得了不错的效果。通过基于PaddleRec进行开发,大大提高了开发效率。DMR模型创新地结合Match和Rank,进一步提高了Rank阶段的性能,对于点击率预估场景是值得一试的。

本模型已经合并到PaddleRec master分支:
https://github.com/PaddlePaddle/PaddleRec/tree/master/models/rank/dmr

另外也整理了无PaddleRec依赖的版本:
https://github.com/thinkall/Contrib/tree/master/DMR

本项目的AIStudio链接为:
https://aistudio.baidu.com/aistudio/projectdetail/1770964

欢迎大家Fork并使用AIStudio提供的数据集和GPU资源直接尝试。


如有飞桨相关技术有问题,欢迎在飞桨论坛中提问交流:
http://discuss.paddlepaddle.org.cn/

欢迎加入官方QQ群获取最新活动资讯:793866180

如果您想详细了解更多飞桨的相关内容,请参阅以下文档。

·飞桨官网地址·
https://www.paddlepaddle.org.cn/

·飞桨开源框架项目地址·
GitHub: https://github.com/PaddlePaddle/Paddle 
Gitee: https://gitee.com/paddlepaddle/Paddle

长按上方二维码立即star!


飞桨(PaddlePaddle)以百度多年的深度学习技术研究和业务应用为基础,集深度学习核心训练和推理框架、基础模型库、端到端开发套件和丰富的工具组件于一体,是中国首个自主研发、功能丰富、开源开放的产业级深度学习平台。飞桨企业版针对企业级需求增强了相应特性,包含零门槛AI开发平台EasyDL和全功能AI开发平台BML。EasyDL主要面向中小企业,提供零门槛、预置丰富网络和模型、便捷高效的开发平台;BML是为大型企业提供的功能全面、可灵活定制和被深度集成的开发平台。


END