基于MindSpore详解推荐模型的原理与实践

在这里插入图片描述

​相信大家都在淘宝购买过相关的商品。
比如某一天用户购买了一台台式主机后,淘宝会立刻给该用户推送与电脑相关的商品,例如电脑显示屏,键盘和鼠标等等。
这些推荐的物品完全是淘宝根据大量的点击数据去学习到的。


比如,在100个人中,有99个人在买了电脑之后,还会去购买鼠标,键盘,无线网卡等等。 也就是说,用户最近是否买了电脑是一个非常强的信号,暗示着如果用户最近买了电脑,那么用户再次购买电脑显示屏,键盘等物品时的概率非常高,那么这个时候推荐系统及时的去推荐这些物品给用户,用户可能很可能会购买这些物品,这些商铺也能提高自身的销量。 因此,可以通过特征工程生成很多个这样强关联的信号,提升推荐系统的性能。

除了电商领域,推荐系统还广泛的应用在电影推荐,兴趣推荐等等。 相信最近大家都看过大火的<隐秘的角落>,可以堪称是今年最好看的国产剧。 在豆瓣上的评分如下:

在这里插入图片描述

看完这部剧之后,还觉得不过瘾怎么办?
往下翻,你就会惊喜的发现一系列的推荐:

在这里插入图片描述

是不是感觉推荐的电影都很符合自己的口味?
<隐秘的角落>属于悬疑类的连续剧,而下面推荐的相关剧集也大多属于这个风格。


这个例子也同样引出了推荐系统中一个重要的分支——协同过滤

它主要可以分为基于用户的,基于物品的和基于模型的协同过滤算法。 基于用户的协同过滤算法的核心思想是: 给当前用户推荐相似的用户看过的物品。

例如,用户A看过了<白夜追凶>和<无证之罪>,而资深悬疑剧爱好者不仅看过这两部剧,还看过了<非自然死亡>和<我们与恶的距离>。 那么根据一定的相似度算法,在所有的用户中找到和用户A最相似的用户,比如我们找到了用户B,将用户B看过的另外两部剧推荐给了用户A,这样一次推荐过程就完成了。

推荐系统的另一个典型场景是广告点击率预估。 在我们浏览手机的应用商店时,经常可以看到商店会给我们推荐某些应用,某些推荐的应用往往是广告主通过付费来进行推广。

为了提高广告投放的准确度,广告点击率预估模型需要评价用户点击某些应用的概率,将用户最可能点击的应用进行推送,达到准确的投放广告的目的。 一旦我们点击了其中的一个应用,商店就可以成功的向广告投放商进行收费,而广告主们也达到了应用推广的目的。

在这里插入图片描述

除了上述领域,推荐系统还广泛的应用在生活中的方方面面,可以说是机器学习在工业界中最成功的落地场景之一了。
美国著名的电影和电视节目提供商Netflix曾经发起了奖金为百万美元的推荐系统比赛,旨在提升推荐系统的准确度。
在广告点击率预估的场景,性能提高了1%的模型往往可以为公司带来巨大的收入。

算法原理介绍

传统的推荐系统以协同过滤为代表,而随着深度学习的快速发展,越来越多的研究者尝试应用深度学习技术到推荐系统中。
在此我们主要介绍其中的Wide&Deep网络。


总体流程

在此我们以APP商店中的推荐系统为例,其整体流程可以如下图所示

在这里插入图片描述

推荐系统流程图


给定一个查询,这个查询可能是用户相关的特征,推荐系统首先会从数据库中检索到查询相关的APP,由于APP的数量非常巨大,因此我们可以取最相关的100个检索结果作为候选APP,这一过程通常叫作 粗排 。


然后将候选的100个APP送入排序模型中,此处的排序模型就是我们下面将要介绍的 Wide&Deep模型 ,这一过程也被叫作 精排 。


排序完成后,我们可以将点击概率最高的APP放置于用户最容易注意到的地方。 无论用户是否点击了我们的推荐结果,我们都可以构造一个新的日志文件。 在累积了一定数量的日志文件后,就可以继续微调排序模型,提高模型的准确程度

Wide&Deep模型

Wide&Deep模型自从被提出后就引起来非常大的关注。
正如其名,
此模型主要可以分成两个部分,Wide部分和Deep部分


Wide模型

Wide部分就是一个线性网络,和我们在初中学的多元线性方程是一样的,只不过其中的参数求解采用梯度下降的方式。 例如下式

y=wx


它的设计目的是 为了记住数据中特定的特征组合方式 。 例如前面介绍购买电脑的场景,购买了电脑主机的用户,再次购买显示器,键盘等物件的概率特别高。 因此可以将用户最近是否购买了电脑作为输入模型的输入,也就是特征。


假设 当前wide网络只有一个特征 ,当该特征取1时,y=wx可以得到y=0.9(假设w的值是0.9),当该特征取0时,y=0。 y输出值越大,会增大模型对应用户点击概率的估计。


回到我们APP应用推荐的场景中,可以看到图右侧的Cross Product Transformation,就是我们的wide部分的输入。 Cross Product Transformation是指特征交叉,即将User Installed App和Impression APP进行组合。


例如用户手机已经安装了微信,且当前的待估计的APP为QQ,那么这个组合特征就是(User Installed App=’微信’,  Impression APP=’QQ’)。



Deep模型


介绍完Wide部分,我们重新回来再看下Deep部分。 Deep部分的设计是为了模型具有较好的泛化能力 ,在输入的数据没有在训练集中出现时,它依然能够保持相关性较好的输出 。


在下图中, Deep模型输入都是一些含义不是非常明显的特征 ,例如设备类型,用户统计数据等类别特征(Categorical Features)。 类别特征一般属于高维特征。


例如手机的种类可能存在成千上万个,因此我们通常把这些类别特征通过 嵌入(Embedding) 的方式, 映射成低维空间的参数向量 。 这个向量可以被认为表示了原先这个类别特征的信息。 对于连续特征,其数值本身就具备一定的含义,因此可以直接将和其他嵌入向量进行拼接。

在这里插入图片描述

2Wide&Deep模型图


在拼接完成后,可以得到大致为1200维度的向量 。 将其作为三层全连接层网络的输入,并且选择Relu作为激活函数,其中每层的输出维度分别为[1024, 512, 256]。

输出层

在广告点击率预估的场景中, 模型的输出是一个0~1之间的值 , 表示当前候选APP被点击的概率 。 因此可以采用逻辑回归函数,将Wide&Deep部分的输出压缩到0~1在之间。


首先, Deep部分的输出是一个256维度的向量 ,可以通过一个线性变换将其映射为维度为1的值,然后 和Wide部分的输出进行求和,将求和后的结果输入到逻辑回归函数中。

代码实践

在介绍完模型之后,现在开始动手实践了,由于谷歌公司并没有将其在论文中使用的APP商店数据集进行公开,因此我们现在
采用Criteo公司发布在Kaggle的广告点击率预估数据集Criteo


对应的Mindpsore代码可以在此处找到:
https://gitee.com/mindspore/mindspore/tree/master/model_zoo/wide_and_deep
其中包含了模型的定义、训练以及数据的预处理过程。

数据处理

在模型定义前,我们需要对数据进行预处理。

数据处理的目的是将数据集中的特征取值映射为数值id,并且去除一些出现次数过少的特征值
,避免特征值出现次数过少,导致当特征值对应的参数向量的更新次数过少,影响模型的精度。
Criteo数据集由13类连续特征和26列类别特征,已经通过哈希方式映射为了32位数值。
对应的标签(label)的取值为0和1。


数据处理的核心思路如下图所示,针对类别特征,建立一个词表,里面记录着每次出现的特征取值的编号。 然后再遍历每一列的特征取值,将原始的特征值根据词表映射为对应的id。

在这里插入图片描述

还记得之前提到过的低维嵌入过程吗?
映射操作是为了模型中的嵌入向量查找而准备的。
例如,输入数据中的A,B,C被映射为了0,1,2。
而0,1,2分别表示在嵌入矩阵中(Embedding Table)的第0行,第1行和第2行。


将Criteo数据集下载后解压 ,可以看到train.txt和test.txt文件。 查看train.txt中的文件, 可以看到其中某些行存在缺失 。

在这里插入图片描述

我们可以调用


model_zoo/wide_and_deep/src/preprocess_data.py


进行数据的下载和处理, 对于缺失值,可以将其标记为OOV(Out Of Vocabulary)对应的id

模型定义

介绍完数据处理后,我们在此开始 定义模型 。 模型的核心代码在mindspore仓库下


model_zoo/wide_and_deep/src/wide_and_deep.py


在MindSpore中,网络的定义方式和Pytorch比较接近,先定义定义的操作,然后再construct函数中对调用对应的操作对输入进行处理。


首先我们再回忆下 Wide&Deep网络 ,它由一个Wide部分和Deep部分。 其中 Wide部分是一个线性网络 。


在实现上,我们将线性网络中的权重视为维度为1,通过嵌入矩阵查找的方式即可获得输入x对应的权重,然后将其和输入的mask相乘,将结果求和。


值得注意的是, 我们将连续特征和类别特征进行等同处理,因此这里的mask是为了将连续特征和类别特征进行区而设计的 。 连续特征mask中的值即为连续特征值,类别特征mask中的值为1 。 Wide部分的核心代码如下所示,我们定义个名为self.wide_w的权重,它的形状为[词表大小,1]。

class WideDeepModel(nn.Cell):
    def __init__(self, config):
        super(WideDeepModel, self).__init__()
        …
        init_acts = [('Wide_w', [self.vocab_size, 1], self.emb_init),
                     ('V_l2', [self.vocab_size, self.emb_dim], self.emb_init),
                     ('Wide_b', [1], self.emb_init)]
        var_map = init_var_dict(self.init_args, init_acts)
        self.wide_w = var_map["Wide_w"]
        self.wide_b = var_map["Wide_b"]
        self.embeddinglookup = nn.EmbeddingLookup()
        self.mul = P.Mul()
        self.reduce_sum = P.ReduceSum(keep_dims=False)
        self.reshape = P.Reshape()
        self.square = P.Square()

    def construct(self, id_hldr, wt_hldr):
        mask = self.reshape(wt_hldr, (self.batch_size, self.field_size, 1))
        # Wide layer
        wide_id_weight = self.embeddinglookup(self.wide_w, id_hldr, 0)
        wx = self.mul(wide_id_weight, mask)
        wide_out = self.reshape(self.reduce_sum(wx, 1) + self.wide_b, (-1, 1))
        out = wide_out

现在我们开始定义Deep部分。

Deep部分同样有一个嵌入矩阵查找
,以及5层的全连接层构成。<mpchecktext contenteditable=“false”


class WideDeepModel(nn.Cell):
    def __init__(self, config):
        super(WideDeepModel, self).__init__()
        …
        init_acts = [('Wide_w', [self.vocab_size, 1], self.emb_init),
                     ('V_l2', [self.vocab_size, self.emb_dim], self.emb_init),
                     ('Wide_b', [1], self.emb_init)]
        var_map = init_var_dict(self.init_args, init_acts)
        self.wide_w = var_map["Wide_w"]
        self.wide_b = var_map["Wide_b"]
        self.embedding_table = var_map["V_l2"]
        self.dense_layer_1 = DenseLayer(self.all_dim_list[0],
                                        self.all_dim_list[1],
                                        self.weight_bias_init,
                                        self.deep_layer_act,
                                        convert_dtype=True, drop_out=config.dropout_flag)
        self.dense_layer_2 = DenseLayer(self.all_dim_list[1],
                                        self.all_dim_list[2],
                                        self.weight_bias_init,
                                        self.deep_layer_act,
                                        convert_dtype=True, drop_out=config.dropout_flag)
        self.dense_layer_3 = DenseLayer(self.all_dim_list[2],
                                        self.all_dim_list[3],
                                        self.weight_bias_init,
                                        self.deep_layer_act,
                                        convert_dtype=True, drop_out=config.dropout_flag)
        self.dense_layer_4 = DenseLayer(self.all_dim_list[3],
                                        self.all_dim_list[4],
                                        self.weight_bias_init,
                                        self.deep_layer_act,
                                        convert_dtype=True, drop_out=config.dropout_flag)
        self.dense_layer_5 = DenseLayer(self.all_dim_list[4],
                                        self.all_dim_list[5],
                                        self.weight_bias_init,
                                        self.deep_layer_act,
                                        use_activation=False, convert_dtype=True, drop_out=config.dropout_flag)

        self.embeddinglookup = nn.EmbeddingLookup()
        self.mul = P.Mul()
        self.reduce_sum = P.ReduceSum(keep_dims=False)
        self.reshape = P.Reshape()
        self.square = P.Square()
        self.shape = P.Shape()
        self.tile = P.Tile()
        self.concat = P.Concat(axis=1)
        self.cast = P.Cast()

    def construct(self, id_hldr, wt_hldr):
        """
        Args:
            id_hldr: batch ids;
            wt_hldr: batch weights;
        """
        mask = self.reshape(wt_hldr, (self.batch_size, self.field_size, 1))
        # Wide layer
        wide_id_weight = self.embeddinglookup(self.wide_w, id_hldr, 0)
        wx = self.mul(wide_id_weight, mask)
        wide_out = self.reshape(self.reduce_sum(wx, 1) + self.wide_b, (-1, 1))
        # Deep layer
        deep_id_embs = self.embeddinglookup(self.embedding_table, id_hldr, 0)
        vx = self.mul(deep_id_embs, mask)
        deep_in = self.reshape(vx, (-1, self.field_size * self.emb_dim))
        deep_in = self.dense_layer_1(deep_in)
        deep_in = self.dense_layer_2(deep_in)
        deep_in = self.dense_layer_3(deep_in)
        deep_in = self.dense_layer_4(deep_in)
        deep_out = self.dense_layer_5(deep_in)
        out = wide_out + deep_out
        return out, self.embedding_table

我们采用交叉熵作为损失函数,Wide部分采用FTRL作为优化器

FTRL可以产生较好的稀疏权重,可以帮助筛选有价值的特征,并且可以压缩模型权重。
Deep采用Adam优化器。<mpchecktext contenteditable=“false”

模型训练

在train_and_eval.py中的定义了数据,模型的初始化以及训练过程,在定义完模型中,可以将初始化的网络送入Model类中,这个类和Tensorflow中Estimator比较接近,可以通过简单的接口实现网络的训练(model.train)和评估(model.eval)。

model_zoo/wide_and_deep/train_and_eval.py


另外,此函数中定义了一些回调函数
https://www.mindspore.cn/tutorial/-zhCN/master/advanced_use/customized_debugging_information.html
可以打印损失值等相关信息。

def test_train_eval(config):
    """
    test_train_eval
    """
    data_path = config.data_path
    batch_size = config.batch_size
    epochs = config.epochs
    ds_train = create_dataset(data_path, train_mode=True, epochs=epochs, batch_size=batch_size)
    ds_eval = create_dataset(data_path, train_mode=False, epochs=epochs + 1, batch_size=batch_size)
    print("ds_train.size: {}".format(ds_train.get_dataset_size()))
    print("ds_eval.size: {}".format(ds_eval.get_dataset_size()))

    net_builder = ModelBuilder()

    train_net, eval_net = net_builder.get_net(config)
    train_net.set_train()
    auc_metric = AUCMetric()

    model = Model(train_net, eval_network=eval_net, metrics={"auc": auc_metric})

    eval_callback = EvalCallBack(model, ds_eval, auc_metric, config)

    callback = LossCallBack(config=config)
    ckptconfig = CheckpointConfig(save_checkpoint_steps=ds_train.get_dataset_size(), keep_checkpoint_max=5)
    ckpoint_cb = ModelCheckpoint(prefix='widedeep_train', directory=config.ckpt_path, config=ckptconfig)

    out = model.eval(ds_eval)
    print("=====" * 5 + "model.eval() initialized: {}".format(out))
    model.train(epochs, ds_train,
                callbacks=[TimeMonitor(ds_train.get_dataset_size()), eval_callback, callback, ckpoint_cb])

模型评估

一旦训练完成后
,就可以装载模型参数进行评估。

评估网络和训练网络类似,只不过输出经过了一个Sigmoid层。<mpchecktext contenteditable=“false”

class PredictWithSigmoid(nn.Cell):
    def __init__(self, network):
        super(PredictWithSigmoid, self).__init__()
        self.network = network
        self.sigmoid = P.Sigmoid()

    def construct(self, batch_ids, batch_wts, labels):
        logits, _, _, = self.network(batch_ids, batch_wts)
        pred_probs = self.sigmoid(logits)
        return logits, pred_probs, labels

采用AUC作为评价指标

AUC广泛的应用在分类模型的评估中,
可以较好的反映模型学习的好坏,其值在0~1之间,值越高,模型的性能越好。

总结

本文内容介绍了推荐系统的原理和实践代码。
首先讲述了推荐系统在我们生活中的应用场景,并且介绍了推荐系统的核心原理。
然后详细介绍了Wide&Deep网络以及相关的代码实践,期望可以帮助大家入门<

参考文献

[1] Cheng H T, Koc L, Harmsen J, et al. Wide & deep learning for recommender systems[C]//Proceedings of the 1st workshop on deep learning for recommender systems. 2016: 7-10.

[2]https://www.mindspore.cn/tutorial/-zhCN/master/advanced_use/customized_debugging_information.html

长按下方二维码关注↓


  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值