AI上推荐 之 AutoRec与Deep Crossing模型(改变神经网络的复杂程度)

    科技2024-01-27  110

    1. 前言

    随着信息技术和互联网的发展, 我们已经步入了一个信息过载的时代,这个时代,无论是信息消费者还是信息生产者都遇到了很大的挑战:

    信息消费者:如何从大量的信息中找到自己感兴趣的信息?信息生产者:如何让自己生产的信息脱颖而出, 受到广大用户的关注?

    为了解决这个矛盾, 推荐系统应时而生, 并飞速前进,在用户和信息之间架起了一道桥梁,一方面帮助用户发现对自己有价值的信息, 一方面让信息能够展现在对它感兴趣的用户前面。 推荐系统近几年有了深度学习的助推发展之势迅猛, 从前深度学习的传统推荐模型(协同过滤,矩阵分解,LR, FM, FFM, GBDT)到深度学习的浪潮之巅(DNN, Deep Crossing, DIN, DIEN, Wide&Deep, Deep&Cross, DeepFM, AFM, NFM, PNN, FNN, DRN), 现在正无时无刻不影响着大众的生活。

    推荐系统通过分析用户的历史行为给用户的兴趣建模, 从而主动给用户推荐给能够满足他们兴趣和需求的信息, 能够真正的“懂你”。 想上网购物的时候, 推荐系统在帮我们挑选商品, 想看资讯的时候, 推荐系统为我们准备了感兴趣的新闻, 想学习充电的时候, 推荐系统为我们提供最合适的课程, 想消遣放松的时候, 推荐系统为我们奉上欲罢不能的短视频…, 所以当我们淹没在信息的海洋时, 推荐系统正在拨开一层层波浪, 为我们追寻多姿多彩的生活!

    这段时间刚好开始学习推荐系统, 通过王喆老师的《深度学习推荐系统》已经梳理好了知识体系, 了解了当前推荐系统领域各种主流的模型架构和技术。 所以接下来的时间就开始对这棵大树开枝散叶,对每一块知识点进行学习总结。 所以接下来一块目睹推荐系统的风采吧!

    这次整理重点放在推荐系统的模型方面, 前面已经把传统的推荐模型梳理完毕, 下面正式进入深度学习的浪潮之巅。在2016年, 随着微软的Deep Crossing, 谷歌的Wide&Deep以及FNN、PNN等一大批优秀的深度学习模型被提出, 推挤系统和计算广告领域全面进入了深度学习时代, 时至今日, 依然是主流。 在进入深度学习时代, 推荐模型主要有下面两个进展:

    与传统的机器学习模型相比, 深度学习模型的表达能力更强, 能够挖掘更多数据中隐藏的模式深度学习模型结构非常灵活, 能够根据业务场景和数据特点, 灵活调整模型结构, 使模型与应用场景完美契合

    所以, 后面开始尝试整理深度学习推荐模型,它们以多层感知机(MLP)为核心, 通过改变神经网络结构进行演化,它们的演化关系依然拿书上的一张图片, 便于梳理关系脉络, 对知识有个宏观的把握:

    今天是推荐系统深度学习模型的第一篇,会整理两个比较简单的神经网络模型AutoRec和DeepCrossing模型, 这两个模型的进化方式是增加了深度神经网络的层数和结构复杂度。 前者是将深度学习的思想用于推荐系统的初步尝试, 后者是一次深度学习框架在推荐系统中的完整应用。我们会先从最简单的神经网络模型AutoRec开始, 介绍其相关原理和局限性, 这个模型2015年由澳大利亚国立大学提出, 是个单隐层的神经网络推荐模型,网络结构简单,非常适合深度学习推荐系统的入门模型,但由于比较简单, 表达能力不足, 并没有真正的被应用。 在2016年,微软基于ResNet的经典DNN结构, 提出了DeepCrossing模型, 该模型完整的解决了从特征工程、稀疏向量稠密化, 多层神经网络进行优化目标拟合等一系列深度学习在推荐系统中的应用问题, 为后面研究打下了良好的基础, 这个模型也是本篇内容的重点部分, 首先会介绍该模型的原理, 结构特点, 然后进行网络的复现, 完成一个点击率预测的任务。

    大纲如下:

    AutoRec的结构及推荐原理DeepCrossing模型原理部分DeepCrossing模型的代码实践总结

    Ok, let’s go!

    2. AutoRec的结构及推荐原理

    这是2015年由澳大利亚国立大学提出来的将自编码器的思想和协同过滤结合起来的一种单隐层的神经网络推荐模型。 它的基本原理是利用协同过滤中的共现矩阵, 完成物品向量或者用户向量的自编码。 再利用自编码的结果得到用户对物品的预估评分, 进而进行的推荐排序。

    关于自编码器的知识, 这里不详细整理, 可以参考王喆老师的书, 简单说一下重点, 自编码器指能够完成数据“自编码”的模型。 这貌似是句废话。 这是一个单隐层的神经网络。长下面这个样子: 自编码器输出层的节点数与输入层相等,训练这个网络以期望得到近似恒等函数, 假设数据向量是 r \bold r r, 自编码器的作用是将向量 r \bold r r作为输入, 通过自编码器, 得到的输出向量尽量接近其本身。

    假设自编码器的重建函数为 h ( r ; θ ) h(\bold r; \theta) h(r;θ), 那么自编码器的目标函数: min ⁡ θ ∑ r ∈ S ∥ r − h ( r ; θ ) ∥ 2 2 \min _{\theta} \sum_{\boldsymbol{r} \in S}\|\boldsymbol{r}-h(\boldsymbol{r} ; \theta)\|_{2}^{2} θminrSrh(r;θ)22

    S S S表示所有向量的集合, 这个式子也很好理解, 输出向量尽量接近其本身。完成自编码器的训练后, 相当于在 h ( r ; θ ) h(\bold r; \theta) h(r;θ)中存储了所有数据向量的“精华”。经过自编码器生成的输出向量, 经过了自编码器的“泛化”过程, 具备了一定的缺失维度的预测能力, 这也是自编码器能用于推荐系统的原因。

    那么关键是怎么用呢?

    假设有 m m m个用户, n n n个物品, 用户会对物品中的一个或者几个评分, 这样就会得到一个 m × n m \times n m×n的评分矩阵, 为评分的可用默认值或者均值表示。 这个就是共现矩阵了。 这个在协同过滤那里说的很详细了, 就是这样的一个矩阵: 对于一个物品 i i i来说, 所有 m m m个用户对它的评分就会形成一个 m m m维的向量 r ( i ) = ( R 1 i , R 2 i , . . R m i ) \bold r^{(i)}=(R_{1i}, R_{2i}, ..R_{mi}) r(i)=(R1i,R2i,..Rmi), AutoRec也是要通过自编码器学习一个重建函数 h ( r ; θ ) h(\bold r;\theta) h(r;θ), 使得所有该重建函数生成的评分向量与原评分向量的平方残差和最小。

    那么AutoRec模型的结构以及如何利用重建函数得到最终推荐列表呢? 这就是AutoRec的核心内容了。

    2.1 AutoRec模型的结构

    AutoRec使用单隐层神经网络结构来解决重建函数的问题。 模型的结构图如下(该图来自《深度学习推荐系统》:

    如果把这个图竖起来, 和自编码器那个一模一样。图中的 V \bold V V W \bold W W分别表示输入层到隐层, 隐层到输出层的参数矩阵, 如果熟悉神经网络的计算, 这个模型的重构函数表示: h ( r ; θ ) = f ( W ⋅ g ( V r + μ ) + b ) h(\boldsymbol{r} ; \theta)=f(\boldsymbol{W} \cdot g(\boldsymbol{V} \boldsymbol{r}+\mu)+b) h(r;θ)=f(Wg(Vr+μ)+b) 这里的 f , g f, g f,g都是激活函数。

    为防止过拟合, 还加入了L2正则, 即最终的目标函数为: min ⁡ θ ∑ i = 1 n ∥ r ( i ) − h ( r ( i ) ; θ ) ∥ 0 2 + λ 2 ⋅ ( ∥ W ∥ F 2 + ∥ V ∥ F 2 ) \min _{\theta} \sum_{i=1}^{n}\left\|\boldsymbol{r}^{(i)}-h\left(\boldsymbol{r}^{(i)} ; \theta\right)\right\|_{0}^{2}+\frac{\lambda}{2} \cdot\left(\|\boldsymbol{W}\|_{F}^{2}+\|\boldsymbol{V}\|_{F}^{2}\right) θmini=1nr(i)h(r(i);θ)02+2λ(WF2+VF2) 这个其实不用多解释了, 这个就是一个普通的三层神经网络, 然后训练的过程就是先初始化两个参数矩阵 V , W \bold V, \bold W V,W, 然后正向传播得到 h ( r ( i ) , θ ) h(r^{(i)},\theta) h(r(i),θ), 求出损失, 然后反向传播更新参数即可。 这个由于结构比较简单, 也不实现了。 如果用Pytorch的话, 两层nn.Linear即可搞定。而关于神经元, 神经网络和反向传播这些东西, 这里就先不整理了, 具体可以看王喆老师的书里面的基础知识补充。

    但我们要明白, 这个东西训练完了之后, 当我输入一个物品向量的时候, 经过这三层网络, 可以得到一个和输入向量维度相同的向量, 但是和输入向量又不太一样, 毕竟这个输出是衡量了所有的输入, 尽可能的使得对于所有的输入, 都进行重建, 这样模型有一定的泛化能力了。

    那么我们得到了这个向量, 怎么用呢?

    2.2 基于AutoRec模型的推荐过程

    这个推荐过程其实很简单, 因为当我们输入物品 i i i的评分向量 r ( i ) r^{(i)} r(i)的时候, 模型的输出向量 h ( r ( i ) , θ ) h(r^{(i)},\theta) h(r(i),θ)就是所有用户对物品 i i i的评分预测。 那么其中的第 u u u维就是用户 u u u对物品 i i i的评分 R ^ u i \widehat{R}_{\mathrm{ui}} R ui: R ^ u i = ( h ( r ( i ) ; θ ^ ) ) u \widehat{R}_{\mathrm{ui}}=\left(h\left(\boldsymbol{r}^{(\mathrm{i})} ; \hat{\theta}\right)\right)_{u} R ui=(h(r(i);θ^))u 通过遍历输入物品向量就可以得到用户 u u u对所有物品的评分预测, 进而根据评分预测排序得到推荐列表。

    和协同过滤一样, AutoRec也分为基于物品的和基于用户的, 上面这个就是基于物品的, 称为I-AutoRec。 如果把用户的评分向量作为输入, 就得到了U-AutoRec。 这个输入之后得到的输出, 就是用户对所有物品的一个评分。 这样就直接可以根据这个评分做推荐了。 但是往往用户向量会比较稀疏, 可能会影响模型效果。

    AutoRec模型由于结构比较简单, 表达能力不足,并且往往共现矩阵非常的稀疏, 更加加大了模型的预测难度。 所以这个东西是将深度学习思想应用于推荐系统的初步尝试, 拉开了使用深度学习思想解决推荐问题的序幕。But, 没有真正的投入到实践。

    3. DeepCrossing模型原理

    这个模型就是一个真正的把深度学习架构应用于推荐系统中的模型了, 2016年由微软提出, 完整的解决了特征工程、稀疏向量稠密化, 多层神经网络进行优化目标拟合等一系列深度学习再推荐系统的应用问题。 这个模型涉及到的技术比较基础,在传统神经网络的基础上加入了embedding, 残差连接等思想, 且结构比较简单, 对初学者复现和学习都比较友好。

    DeepCrossing模型应用场景是微软搜索引擎Bing中的搜索广告推荐, 用户在输入搜索词之后, 搜索引擎除了返回相关结果, 还返回与搜索词相关的广告,Deep Crossing的优化目标就是预测对于某一广告, 用户是否会点击, 依然是点击率预测的一个问题。

    这种场景下, 我们的输入一般会有类别型特征, 比如广告id, 和数值型特征, 比如广告预算,两种情况。 对于类别型特征, 我们需要进行one-hot编码处理, 而数值型特征, 一般需要进行归一化处理, 这样算是把数据进行了一个简单清洗。 DeepCrossing模型就是利用这些特征向量进行CRT预估,那么它的结构长啥样, 又是怎么做CTR预估的呢? 这又是DeepCrossing的核心内容。

    3.1 DeepCrossing模型的网络结构

    为了完成端到端的训练, DeepCrossing模型要在内部网络结构中解决如下问题:

    离散类特征编码后过于稀疏, 不利于直接输入神经网络训练, 需要解决稀疏特征向量稠密化的问题如何解决特征自动交叉组合的问题如何在输出层中达成问题设定的优化目标

    DeepCrossing分别设置了不同神经网络层解决上述问题。 模型结构如下:

    下面分别介绍一下各层的作用:

    Embedding层: 将稀疏的类别型特征转成稠密的Embedding向量,Embedding的维度会远小于原始的系数特征向量。 Embedding是NLP里面常用的一种技术, 之前主要是用来把单词进行embedding, 这里我是第一次看到和在普通的类别数据上使用embedding。待会代码实践的时候, 会发现有些代码实践上的差别。这里的Feature #1表示的类别特征(one-hot编码后的稀疏特征向量), Feature #2是数值型特征, 不用embedding, 直接到了Stacking层。 关于Embedding层的实现, 往往一个全连接层即可, Pytorch中有实现好的层可以直接用。 而关于推荐系统中Embedding的不同方法, 后面会结合王喆老师书上的内容单独整理一下。但和NLP里面的embedding技术异曲同工, 比如Word2Vec, 语言模型等。Stacking层:这个层是把不同的Embedding特征和数值型特征拼接在一起, 形成新的包含全部特征的特征向量, 该层通常也称为连接层Multiple Residual Units层: 该层的主要结构是多层感知机, 但DeepCrossing采用了残差网络进行的连接, 关于残差网络, 这里不做过多的介绍, 具体实现的时候, 会提下过程。通过多层残差网络对特征向量各个维度充分的交叉组合, 使得模型能够抓取更多的非线性特征和组合特征信息, 增加模型的表达能力。Scoring层: 这个作为输出层, 为了拟合优化目标存在。 对于CTR预估二分类问题, Scoring往往采用逻辑回归, 对于多分类, 往往采用Softmax模型

    这就是DeepCrossing的结构了, 比较清晰和简单, 没有引入特殊的模型结构, 只是常规的Embedding+多层神经网络。但这个网络模型的出现, 有革命意义。 DeepCrossing模型中没有任何人工特征工程的参与, 只需要清洗一下, 原始特征经Embedding后输入神经网络层, 自主交叉和学习。 相比于FM, FFM只具备二阶特征交叉能力的模型, DeepCrossing可以通过调整神经网络的深度进行特征之间的“深度交叉”, 这也是Deep Crossing名称的由来。 了解一下。

    顺便说一下, 这个模型由于都是常规的结构, 对于小白复现起来比较友好, 所以这是一个适合复现练手的模型,后面就是复现这个模型, 然后完成一个点击率预测的任务。

    4. DeepCrossing代码实践

    这里是尝试把这个模型复现一下, 来完成一个简单的任务, 为了和前面保持统一, 这里实验依然用的Criteo数据集, 但由于数据量太大, 做了采样, 取了一部分。 关于数据的预处理, 就是读入, 做了简单的缺失值填充。 具体的可以参考下面的GitHub链接, 这里主要是看看这个模型的复现过程, 我这里是参考了下面子耀TF2.0的代码用Pytorch写的, 一方面是刚刚学习完Pytorch, 想找个机会练手, 而这个模型刚好对小白友好。 另一方面也是为了自己动手走一遍, 熟悉上面的处理流程。

    Pytorch进行模型的训练分为四个步骤: 准备数据, 建立模型, 训练模型,使用和保存。

    关于数据准备, 导入数据清洗, 然后封装成DataLoader结构即可。这里主要是模型建立部分的一些细节解释。首先, 如果建立这模型, 模型结构图再放一遍:

    我们下面看看如何构建这个模型, 首先, 会有embedding层, 这个由于不同的类别特征会通过不同的embedding, 且每个类别的取值个数不一样, 所以有多少个类别特征就需要多少次embedding。 所以这里就有了第一个细节, 类别特征的编码其实用的LabelEncoder而不是One-hotEncoder, 我觉得原因就是LabelEncoder就类似于把每个类别下的不同取值映射到了一个字典里面去。 通过这个LabelEncoder的取值, 就可以直接拿出对应的embedding向量来。Pytorch中, 实现这一层, 需要用一个ModuleDict来实现, 里面的每个值都是embedding层。这里出现了第二个细节, 就是由于每个类别的取值不一样, 需要实现先把每个特征的取值个数记录下来, 这样是embedding的输入维度。在NLP中, 我们是先将单词建立成一个字典, 字典的维度就是embedding的输入维度, 而这里, 我们用LabelEncoder的方式代替了字典, 而类别的取值个数就是输入维度, 异曲同工的。

    然后就是残差层, 因为Stacking不需要特殊的层, 只需要把特征拼接即可。 残差层其实就是实现了神经网络的运算过程, 只不过稍微有点不同的是残差网络加了跳远链接:

    所以我们需要先定义一个这样的残差块网络结构, 看这个结构, 两层线性可以用两个nn.Linear来搞定, 剩下的就是跳远那部分, 在前向传播的时候加过去即可。 代码如下:

    class Residual_block(nn.Module): """ Define Residual_block """ def __init__(self, hidden_unit, dim_stack): super(Residual_block, self).__init__() # 两个线性层 注意维度, 输出的时候和输入的那个维度一致, 这样才能保证后面的相加 self.linear1 = nn.Linear(dim_stack, hidden_unit) self.linear2 = nn.Linear(hidden_unit, dim_stack) self.relu = nn.ReLU() def forward(self, x): orig_x = x.clone() x = self.linear1(x) x = self.linear2(x) outputs = self.relu(x + orig_x) # 跳远链接 return outputs

    这是上面残差块的实现, 而实际应用中, 我们可能很多个这样的残差块结构, 下面我们用了三个, 每一个里面神经单元的个数不一样。 所以用了ModuleList,然后加了一层Dropout层缓解过拟合, 最后一个线性层加sigmoid完成scoring层的实现。 具体代码如下:

    # 定义deep Crossing 网络 class DeepCrossing(nn.Module): def __init__(self, feature_info, hidden_units, dropout=0., embed_dim=10, output_dim=1): """ DeepCrossing: feature_info: 特征信息(数值特征, 类别特征, 类别特征embedding映射) hidden_units: 列表, 隐藏单元的个数(多层残差那里的) dropout: Dropout层的失活比例 embed_reg: embedding正则系数 """ super(DeepCrossing, self).__init__() self.dense_feas, self.sparse_feas, self.sparse_feas_map = feature_info # embedding层, 这里需要一个列表的形式, 因为每个类别特征都需要embedding self.embed_layers = nn.ModuleDict({ 'embed_' + str(key): nn.Embedding(num_embeddings=val, embedding_dim=embed_dim) for key, val in self.sparse_feas_map.items() }) # 统计embedding_dim的总维度 embed_dim_sum = sum([embed_dim]*len(self.sparse_feas)) # stack layers的总维度 dim_stack = len(self.dense_feas) + embed_dim_sum # 残差层 self.res_layers = nn.ModuleList([ Residual_block(unit, dim_stack) for unit in hidden_units ]) # dropout层 self.res_dropout = nn.Dropout(dropout) # 线性层 self.linear = nn.Linear(dim_stack, output_dim) def forward(self, x): dense_inputs, sparse_inputs = x[:, :13], x[:, 13:] sparse_inputs = sparse_inputs.long() # 需要转成长张量, 这个是embedding的输入要求格式 sparse_embeds = [self.embed_layers['embed_'+key](sparse_inputs[:, i]) for key, i in zip(self.sparse_feas_map.keys(), range(sparse_inputs.shape[1]))] sparse_embed = torch.cat(sparse_embeds, axis=-1) stack = torch.cat([sparse_embed, dense_inputs], axis=-1) r = stack for res in self.res_layers: r = res(r) r = self.res_dropout(r) outputs = F.sigmoid(self.linear(r)) return outputs

    应该挺好理解的, 前向传播部分的逻辑, 首先得先把输入向量X分成两部分处理, 因为数值型和类别型的处理方式不一样, 类别型经过embedding, 数值型是直接进入stacking。 所以这是前向传播的第一行代码。 而由于Pytorch中, embedding层输入要求是long类型, 转一下, 这是第二行代码。 第三行代码就是不同的类别特征分别embedding。 第四行代码是把类别型特征进行拼接, 第五行代码是数值型和类别型拼接, 这就是stacking层的任务。 后面就是经过残差网络, sigmoid激活输出。

    这样就完成了DeepCrossing模型的搭建。 关于训练过程, 我这里用了脚本式的训练风格, 顺便复习了一下Pytorch的训练的整个流程。具体细节参考链接吧。如果对TF2.0感兴趣, 也可以参考下面2.0的复现链接, 这里面是各个推荐系统的TF2.0实现, 现在正尝试跟着大佬的步伐用Pytorch走一遍这些模型, 并结合博客进行整理理论知识。

    5. 总结

    这里简单的总结一下, 由于这篇文章是深度学习模型的第一篇文章, 所以相对来说比较简单, 不是太复杂,这篇文章主要整理了两个最基本的模型AutoRec和DeepCrossing模型。 前者是一个简单的自编码器神经网络, 但表达能力不强, 没有被使用。 后者是深度学习框架在推荐系统的一次完整应用, 用的是比较传统的模块, 但自主完成了稀疏变稠密, 深度交叉特征学习等, 为后面的深度学习的研究奠定了基础, 这两个模型都是从改变神经网络的复杂程度上进行了进化。 本篇文章首先从AutoRec开始, 介绍了其基本原理, 在推荐中如何使用, 然后分析了局限, 引出了DeepCrossing模型, 介绍原理和结构并尝试复现, 具体实验代码可以参考下面的链接。

    下一次的两个模型, 从改变特征交叉方式方式的角度完成了进化, 一个是改变了用户向量和物品向量互操作方式的神经网络协同过滤(NeuralCF), 一个是定义了多种特征向量交叉操作的PNN(Product-based Neural Network, 基于积操作的神经网络)模型。这个模型还是采用了DeepCrossing的模型堆叠结构, 只不过把Stacking层换成了Product 层。 具体的下一篇文章进行整理了。

    参考:

    王喆 - 《深度学习推荐系统》Deep Crossing: Web-Scale Modeling without Manually Crafted Combinatorial Features论文原文Autoencoders meet collaborative filtering论文原文DeepCrossing模型的论文导读DeepCrossing模型的TF2.0复现

    整理这篇文章的同时, 也刚建立了一个GitHub项目, 准备后面把各种主流的推荐模型复现一遍,并用通俗易懂的语言进行注释和逻辑整理, 今天的DeepCrossing模型已经上传, 参考的TF2.0的复现过程, 写成了Pytorch代码, 感兴趣的可以看一下 😉

    筋斗云:https://github.com/zhongqiangwu960812/AI-RecommenderSystem

    Processed: 0.021, SQL: 9