推荐算法之Wide&Deep模型

前言

推荐系统进入深度学习领域以来,一共讲解了四个使用深度学习的模型了,今天来讲一下Wide&Deep模型,也是具有着里程碑意义的,前面讲解的AutoRec和Deep Crossing模型是在神经网络的复杂度和层数方面进行的进化,而这两个模型也是使用深度学习从用户和物品相似度的角度来进行系统推荐的。其中对AutoRec和DeepCrossing这两个模型有些遗忘的小伙伴可以看看我这篇文章,推荐算法之AutoRec与Deep Crossing模型,但是这两个模型在特征交叉这个方面并没有进行很合理的设计,更多的是使用了全连接层来增加了模型的复杂度和使得所有特征进行了一个统一的交叉。这样存在着很大的问题,即两个毫不相关的特征也较交叉在了一起,这就使得对模型权重有所影响。所以为了更好地使两两特征更好的交叉,于是新加披国立大学在2017年提出了基于深度学习的协同过滤模型NeuralCF。我在前面也进行了讲解,文章推荐系统之NerualCF。但是NeuralCF也存在这很多问题,比如这个模型是基于协同过滤的思想来构造的,所以在这个模型中并没有引入更多的其它类型的特征,这在实际应用中也是浪费了很多的有用信息。而PNN模型是在加入多组特征的基础上研究的特征交叉, 这个模型和Deep Crossing模型的架构类似, 只不过把Deep Crossing的stacking层换成了Product层, 也就是不同特征的embedding向量不再是简单的堆叠, 而是两两交互, 更有针对性的获取特征之间的交叉信息。同样的PNN模型也存在一些局限性,比如在我们进行复现的时候就明显感觉到这些特征虽然进行了很好的交叉,但是也同样是无差别交叉,这样会使得在一定程度上忽略了原始特征向量中包含的有价值信息。它们的演化关系依然拿书上的一张图片, 便于梳理关系脉络, 对知识有个宏观的把握:

推荐算法之Wide&Deep模型_第1张图片

从上面图里面也会看到Wide&Deep的重要性, 是处于一个核心的地位, 后面的很多深度学习模型都是基于此模型的架构进行的改进。 所以该模型的提出在业界是有非常大的影响力的。 在这个模型中小伙伴们主要还是学习这个模型的思想。

一、模型的记忆能力和泛化能力

在CTR预估任务中,线性模型仍然很重要,主要是因为利用手工构造的交叉组合特征来使线性模型具有“记忆性”,模型记住共现频率较高的特征组合,往往也能达到一个不错的baseline,且可解释性强。但这种方式有着较为明显的缺点:首先,特征工程需要耗费太多精力。其次,因为模型是强行记住这些组合特征的,所以对于未曾出现过的特征组合,权重系数为0,无法进行泛化。为了加强模型的泛化能力,研究者引入了DNN结构,将高维稀疏特征编码为低维稠密的Embedding vector,这种基于Embedding的方式能够有效提高模型的泛化能力。但是,现实世界是没有银弹的。基于Embedding的方式可能因为数据长尾分布,导致长尾的一些特征值无法被充分学习,其对应的Embedding vector是不准确的,这便会造成模型泛化过度。

简单解释一下就是embedding的这种思路, 如果碰到了共现矩阵高度稀疏且高秩(比如user有特殊的爱好, 或者item比较小众) ,很难非常效率的学习出低维度的表示。这种情况下,大部分的query-item都没有什么关系。但是dense embedding会导致几乎所有的query-item预测值都是非0的,这就导致了推荐过度泛化,会推荐一些不那么相关的物品。相反,简单的linear model却可以通过cross-product transformation来记住这些excption rules。所以根据上面的分析, 我们发现简单的模型, 比如协同过滤, 逻辑回归等,能够从历史数据中学习到高频共现的特征组合能力, 但是泛化能力不足;而像矩阵分解, embedding再加上深度学习网络, 能够利用相关性的传递性去探索历史数据中未出现的特征组合, 挖掘数据潜在的关联模式, 但是对于某些特定的场景(数据分布长尾, 共现矩阵稀疏高秩)很难有效学习低纬度的表示, 造成推荐的过渡泛化。 既然这两种模型的优缺点都这么的互补, 为啥不组合一下子他俩呢?2016年,Google提出Wide&Deep模型,将线性模型与DNN很好的结合起来,在提高模型泛化能力的同时,兼顾模型的记忆性。Wide&Deep这种线性模型与DNN的并行连接模式,后来成为推荐领域的经典模式, 奠定了后面深度学习模型的基础。 这个是一个里程碑式的改变, 但仔细看看人家的模型架构, 会发现并没有多复杂, 甚至比之前的PNN, Neural CF等都简单。但是人家的这种思想却是有着重大意义的。 后面我们会具体学习这个结构, 但是在这之前, 想借着王喆老师书上的例子聊聊“Memorization"和”Generalization"。

"记忆能力"可以被理解为模型直接学习并利用历史数据中物品和特征的“共现频率”的能力。 一般来说, 协同过滤、逻辑回归这种都具有较强的“记忆能力”, 由于这类模型比较简单, 原始数据往往可以直接影响推荐结果, 产生类似于“如果点击A, 就推荐B”这类规则的推荐, 相当于模型直接记住了历史数据的分布特点, 并利用这些记忆进行推荐。

以谷歌APP推荐场景为例理解一下:
假设在Google Play推荐模型训练过程中, 设置如下组合特征: AND(user_installed_app=netflix, impression_app=pandora), 它代表了用户安装了netflix这款应用, 而且曾在应用商店中看到过pandora这款应用。 如果以“最终是否安装pandora”为标签,可以轻而易举的统计netfilx&pandora这个特征与安装pandora标签之间的共现频率。 比如二者的共现频率高达10%, 那么在设计模型的时候, 就希望模型只要发现这一特征,就推荐pandora这款应用(像一个深刻记忆点一样印在脑海), 这就是所谓的“记忆能力”。 像逻辑回归这样的模型, 发现这样的强特, 就会加大权重, 对这种特征直接记忆。但是对于神经网络这样的模型来说, 特征会被多层处理, 不断与其他特征进行交叉, 因此模型这个强特记忆反而没有简单模型的深刻。

"泛化能力“可以被理解为模型传递特征的相关性, 以及发掘稀疏甚至从未出现过的稀有特征与最终标签相关性的能力。比如矩阵分解, embedding等, 使得数据稀少的用户或者物品也能生成隐向量, 从而获得由数据支撑的推荐得分, 将全局数据传递到了稀疏物品上, 提高泛化能力。再比如神经网络, 通过特征自动组合, 可以深度发掘数据中的潜在模式,提高泛化等。所以, Wide&Deep模型的直接动机就是将两者进行融合, 使得模型既有了简单模型的这种“记忆能力”, 也有了神经网络的这种“泛化能力”, 这也是记忆与泛化结合的伟大模式的初始尝试。下面就来具体看一下W&D模型的结构。

二、Wide&Deep模型原理

W&D的模型如下面中间的图所示(左边的是wide部分, 也就是一个简单的线性模型, 右边是deep部分, 一个经典的DNN模型)

推荐算法之Wide&Deep模型_第2张图片

 W&D模型把单输入层的Wide部分和Embedding+多层的全连接的部分连接起来, 一起输入最终的输出层得到预测结果。 单层的wide层善于处理大量的稀疏的id类特征, Deep部分利用深层的特征交叉, 挖掘在特征背后的数据模式。 最终, 利用逻辑回归, 输出层部分和Deep组合起来, 形成统一的模型。

这里写一下自己的一点感悟,不知道小伙伴们还记不记得NerualCF模型,也是由两个模型,一个神经网络,一个矩阵分解来进行了组合,也是和W&D一样的组合,那么为什么效果就不一样呢?当时我看到这里的时候的确是有些混淆了,然后又重新翻看了之前的博客。其实这两个模型还是有很大区别的,在NeuralCF上,两个分块都是基于矩阵分解来进行的下一步,也就是说并没有对特征进行明显的划分,而是将其全部进行了无差别交叉,这样就和前面说的一样,损失了很多信息。而且都是将特征分别进行了embedding变成了稠密向量,而这样子就不能够使得线性模型拥有着很强的记忆能力,对高维稀疏向量处理也显得乏力。

2.1 wide部分

Wide部分是一个广义的线性模型, 公式如下:

 输入的X特征包括原始特征和转换的特征, 还有一些离散的id类特征给它(神经网络那边是不喜欢这种高稀疏的离散id特征的, 巧了,wide这边喜欢)。 其中一种比较重要的转换操作就是cross-product transformation(原始特征的交互特征),公式如下:

如果两个特征同时为1的时候, 这个特征就是1, 否则就是0, 这是一种特征组合, 往往我们在特征工程的时候常常会做一些这种特征。 比如 And(gender=female, language=en)=1, 当且仅当gender=female, language=en的时候, 否则就是0。

对于wide部分训练时候使用的优化器是带正则的FTRL算法(Follow-the-regularized-leader),我们可以把FTRL当作一个稀疏性很好,精度又不错的随机梯度下降方法, 该算法是非常注重模型稀疏性质的,也就是说W&D模型采用L1 FTRL是想让Wide部分变得更加的稀疏,即Wide部分的大部分参数都为0,这就大大压缩了模型权重及特征向量的维度。Wide部分模型训练完之后留下来的特征都是非常重要的,那么模型的“记忆能力”就可以理解为发现"直接的",“暴力的”,“显然的”关联规则的能力。 例如, Google W&D期望wide部分发现这样的规则:用户安装了应用A,此时曝光应用B,用户安装应用B的概率大。 所以对于稀疏性的规则的考量, 和具体的业务场景有关。同样, 这部分的输入特征也是有讲究的, 后面会看谷歌的那个例子。

这里再补充一点上面wide部分使用FTRL算法的原因, 也就是为啥这边要注重模型稀疏性质的原因, 上面的压缩模型权重,减少服务的时候存储压力是其一, 还有一个是来自工业上的经验, 就是对模型的实时更新更加有利, 能够在实时更新的时候, 尽量的加大实时的那部分数据对于参数更新的响应速度,不至于用实时数据更新好久也没有更新动原来的大模型。毕竟它注重稀疏性质,一旦察觉到新来的这部分数据某些特征变了, 就立即加大权重或者直接置为0, 这样就能只记住实时数据的关键特征了, 使得模型能够更好的实时服务。 deep端的这种普通梯度下降的方式是不行的,这种都是一般使用类似L2正则的方式, 更新参数的时候尽量的慢慢减小所有的w参数, 更新起来是会很慢的, 不太能反应实时变化。 像FTRL这种, 用的类似于L1正则的方式, 这俩的区别是显然的了。

 2.2、Deep部分

该部分主要是一个Embedding+MLP的神经网络模型。大规模稀疏特征通过embedding转化为低维密集型特征。然后特征进行拼接输入到MLP中,挖掘藏在特征背后的数据模式。

 输入的特征有两类, 一类是数值型特征, 一类是类别型特征(会经embedding)。我们知道DNN模型随着层数的增加,中间的特征就越抽象,也就提高了模型的泛化能力。 对于Deep部分的DNN模型作者使用了深度学习常用的优化器AdaGrad,这也是为了使得模型可以得到更精确的解。

2.3、联合训练

W&D模型是将两部分输出的结果结合起来联合训练,将deep和wide部分的输出重新使用一个逻辑回归模型做最终的预测,输出概率值。联合训练的数学形式如下:

 wide&deep模型本身的结构非常简单的,但是如何根据自己的场景去选择那些特征放在Wide部分,哪些特征放在Deep部分是用好该模型的一个前提。总体模型如下图所示:

推荐算法之Wide&Deep模型_第3张图片

 

我们重点看看这两部分的输入特征:

Deep部分: 全量的特征向量, 包括用户年龄(age), 已安装应用数量(#app installs), 设备类型(device class), 已安装应用(installed app), 曝光应用impression app)等特征。 其中, 已安装应用, 曝光应用等类别型特征, 需要经过embedding层输入连接层, 而数值型的特征和前面的特征拼接起来直接输入连接层, 经过3层的Relu全连接层。
Wide部分:输入仅仅是已安装应用和曝光应用两类特征。 其中已安装应用代表用户的历史行为, 而曝光应用代表当前待推荐应用。 选择这两部分是想发现当前曝光APP和用户已安装APP之间的关联关系, 以充分发挥Wide的记忆能力, 影响最终的得分。 这部分是L1正则化的FTRL优化器, 可能是因为这两个id类特征向量组合, 在维度爆炸的同时, 会让原本已经非常稀疏的multihot特征向量变得更加稀疏。 因此采用FTRL过滤掉那些稀疏特征是非常好的工程经验。
两者结合: 最后将两部分的特征再进行一个拼接, 输出到logistics Loss层进行输出。
这个就是W&D模型的全貌了, 该模型开创了组合模型的构造方法, 对深度学习推荐模型的发展产生了重大的影响。 下面就用pytorch搭建一个Wide&Deep模型, 完成一个电子商品推荐的任务。

这是工业上常用的一个模型, 下面整理一些工业上使用的经验:

像上面说的,这个模型的wide和deep端接收的特征是不一样的, wide端一般会接收一些重要的交互特征,高维的稀疏离散特征, 而deep端接收的是一些连续特征
这两端用的梯度下降的方式不一样, wide段用的是那种带有L1正则的那种方式,L1有特征选择的作用, 注重稀疏性些, deep端用的就是普通的梯度下降方式,带L2正则
wide部分是直接与输出连着的, 这个其实和ResNet的那种原理有点像, 知识在某种程度上都是想通的。
wide & deep是一种架构,不是说一定非得是这样的形式, 具体要跟着具体业务来, 还得进行扩展,比如某些特征,既不适合wide也不适合deep,而是适合FM,那就把这部分特征过一个FM, 和wide deep端的输出拼起来得到最后的输出,其实是可以任意改造的。

三、模型复现

同样的贴一下模型复现结果。

import torch.nn as nn
import torch

class Linear(nn.Module):
    """
    Linear part
    """

    def __init__(self, input_dim):
        super(Linear, self).__init__()
        self.linear = nn.Linear(in_features=input_dim, out_features=1)

    def forward(self, x):
        return self.linear(x)


class Dnn(nn.Module):
    """
    Dnn part
    """

    def __init__(self, hidden_units, dropout=0.):
        """
        hidden_units: 列表, 每个元素表示每一层的神经单元个数, 比如[256, 128, 64], 两层网络, 第一层神经单元128, 第二层64, 第一个维度是输入维度
        dropout: 失活率
        """
        super(Dnn, self).__init__()

        self.dnn_network = nn.ModuleList(
            [nn.Linear(layer[0], layer[1]) for layer in list(zip(hidden_units[:-1], hidden_units[1:]))])
        self.dropout = nn.Dropout(p=dropout)

    def forward(self, x):
        for linear in self.dnn_network:
            x = linear(x)
            x = F.relu(x)

        x = self.dropout(x)
        return x


class WideDeep(nn.Module):
    def __init__(self, feature_columns, hidden_units, dnn_dropout=0.):
        super(WideDeep, self).__init__()
        self.dense_feature_cols, self.sparse_feature_cols = feature_columns

        # embedding
        self.embed_layers = nn.ModuleDict({
            'embed_' + str(i): nn.Embedding(num_embeddings=feat['feat_num'], embedding_dim=feat['embed_dim'])
            for i, feat in enumerate(self.sparse_feature_cols)
        })

        hidden_units.insert(0,
                            len(self.dense_feature_cols) + len(self.sparse_feature_cols) * self.sparse_feature_cols[0][
                                'embed_dim'])
        self.dnn_network = Dnn(hidden_units)
        self.linear = Linear(len(self.dense_feature_cols))
        self.final_linear = nn.Linear(hidden_units[-1], 1)

    def forward(self, x):
        dense_input, sparse_inputs = x[:, :len(self.dense_feature_cols)], x[:, len(self.dense_feature_cols):]
        sparse_inputs = sparse_inputs.long()
        sparse_embeds = [self.embed_layers['embed_' + str(i)](sparse_inputs[:, i]) for i in
                         range(sparse_inputs.shape[1])]
        sparse_embeds = torch.cat(sparse_embeds, axis=-1)

        dnn_input = torch.cat([sparse_embeds, dense_input], axis=-1)

        # Wide
        wide_out = self.linear(dense_input)

        # Deep
        deep_out = self.dnn_network(dnn_input)
        deep_out = self.final_linear(deep_out)

        # out
        outputs = F.sigmoid(0.5 * (wide_out + deep_out))

        return outputs

注:本文很多内容都是采自于AI上推荐 之 Wide&Deep与Deep&Cross模型(记忆与泛化并存的华丽转身) 因为我自己觉得这篇博客写的很全,也是我要写的内容,所以很多内容就没有自己写一遍,但是其中添加了自己的一些感悟,望周知。谢谢!

你可能感兴趣的:(推荐算法,机器学习,深度学习)