许久没有写博客记录笔记,最近在看推荐系统的相关模型,网站的资料以及非常完善了,这里用这篇博客来汇总推荐系统中一些经典的排序模型(推荐系统从算法层面上可以细分为多个阶段:物料库——召回——粗排——精排——重排)
这里介绍的模型就是一些排序模型。简单来说,就是将问题转化为CTR预测任务,以概率来对需要推荐不同的商品进行排序。
以下资料是根据参考资料的浓缩摘抄,只针对我个人以后的温故知新,想了解详细细节可以看原论文与相关资料。
1. 协同算法测试
1.1 基于用户的协同过滤(UserCF)
1.2 基于物品的协同过滤(ItemCF)
- 协同过滤代码测试
import numpy as np import pandas as pd def loadData(): items = [[3,1,2,3,3], [4,3,4,3,5], [3,3,1,5,4], [1,5,5,2,1], [5,3,4,4,0]] return items item_data = loadData() item_dataframe = pd.DataFrame(data=np.array(item_data), columns=['A','B','C','D','E'], index=['user1','user2','user3','user4','Alice']) item_dataframe
# 计算皮尔逊相关系数 similarity_matrix = item_dataframe.iloc[:4].corr() similarity_matrix
可以看出与E物品最相关的是A,D这两个物品
# 取E物品进行相似性排序,获取对应的索引 top_list = similarity_matrix['E'].sort_values(ascending=False).index.to_list() top_list
['E', 'A', 'D', 'C', 'B']
这里与E相似性的前两个就是AD,接下来就可以利用AD对E进行打分
# 提取Alice这行的均值 user_mean = item_dataframe['E'].sum() / 4 # 获取A,D的皮尔逊相关系数 other_corr = (similarity_matrix['E']['A'], similarity_matrix['E']['D']) # 获取物品AD的评分均值 other_mean = (item_dataframe[['A']].mean().values, item_dataframe[['D']].mean().values) # 获取用户Alice对AD的评分值 user_value = (item_dataframe[item_dataframe.index == 'Alice']['A'].values, item_dataframe[item_dataframe.index == 'Alice']['D'].values) # 根据公式计算最后结果 result = user_mean + \ (other_corr[0]*(user_value[0]-other_mean[0])+other_corr[1]*(user_value[1]-other_mean[1])) / np.array(other_corr).sum() result # 输出: array([4.6])
最后根据物品协同,应该给alice的物品E打分为4.6分
2. GBDT+LR
- GBDT模型
- 手动实现one-hot编码处理
item_data = [['A','apple'], ['B','apple'], ['A','peach'], ['C','banana']]
item_dataframe = pd.DataFrame(data=item_data, columns=['f1','f2']) item_dataframe
feature = 'f1' pd.get_dummies(item_dataframe[feature], prefix=feature)
feature = 'f2' pd.get_dummies(item_dataframe[feature], prefix=feature)
# 获取特征列表 item_dataframe.columns
Index(['f1', 'f2'], dtype='object')
# 下面进行ohe-hot编码合并 feature_names = item_dataframe.columns for feature in feature_names: onehot_feats = pd.get_dummies(item_dataframe[feature], prefix = feature) # 设置前缀 item_dataframe.drop([feature], axis=1, inplace=True) item_dataframe = pd.concat([item_dataframe, onehot_feats], axis=1) item_dataframe
- 使用GBDT+LR进行分类
data = pd.read_csv('ctr_data/criteo_dataset/train.csv') test = pd.read_csv('ctr_data/criteo_dataset/test.csv') # 缺失值处理 data = data.replace(np.nan, 0) sparse_feature = ['C{}'.format(i+1) for i in range(26)] dense_feature = ['I{}'.format(i+1) for i in range(13)] # one-hot编码 for col in sparse_feature: onehot_feats = pd.get_dummies(data[col], prefix = col) data.drop([col], axis = 1, inplace = True) data = pd.concat([data, onehot_feats], axis = 1) # 切分数据集 from sklearn.model_selection import train_test_split train = data[data['Label'] != -1] target = train.pop('Label') test = data[data['Label'] == -1] test.drop(['Label'], axis = 1, inplace = True) x_train, x_val, y_train, y_val = train_test_split(train, target, test_size = 0.2, random_state = 2018) # len(x_train), len(x_val), len(y_train), len(y_val) # (1279, 320, 1279, 320) # 模型训练 import lightgbm as lgb gbm = lgb.LGBMRegressor(objective='binary', subsample= 0.8, min_child_weight= 0.5, colsample_bytree= 0.7, num_leaves=100, max_depth = 12, learning_rate=0.05, n_estimators=20, # 控制叶子节点数 ) gbm.fit(x_train, y_train, eval_set = [(x_train, y_train), (x_val, y_val)], eval_names = ['train', 'val'], eval_metric = 'binary_logloss', # early_stopping_rounds = 100, ) model = gbm.booster_
[1] train's binary_logloss: 0.48792 val's binary_logloss: 0.561825 [2] train's binary_logloss: 0.476001 val's binary_logloss: 0.557582 [3] train's binary_logloss: 0.464712 val's binary_logloss: 0.555256 [4] train's binary_logloss: 0.455389 val's binary_logloss: 0.551913 [5] train's binary_logloss: 0.447175 val's binary_logloss: 0.550257 [6] train's binary_logloss: 0.439437 val's binary_logloss: 0.549616 [7] train's binary_logloss: 0.431481 val's binary_logloss: 0.546767 [8] train's binary_logloss: 0.423676 val's binary_logloss: 0.546433 [9] train's binary_logloss: 0.416871 val's binary_logloss: 0.545065 [10] train's binary_logloss: 0.409206 val's binary_logloss: 0.543259 [11] train's binary_logloss: 0.402514 val's binary_logloss: 0.541918 [12] train's binary_logloss: 0.396308 val's binary_logloss: 0.54192 [13] train's binary_logloss: 0.389801 val's binary_logloss: 0.541947 [14] train's binary_logloss: 0.384084 val's binary_logloss: 0.541799 [15] train's binary_logloss: 0.378239 val's binary_logloss: 0.541707 [16] train's binary_logloss: 0.3721 val's binary_logloss: 0.541957 [17] train's binary_logloss: 0.366702 val's binary_logloss: 0.542125 [18] train's binary_logloss: 0.362069 val's binary_logloss: 0.542602 [19] train's binary_logloss: 0.357859 val's binary_logloss: 0.541112 [20] train's binary_logloss: 0.353107 val's binary_logloss: 0.541495
gbdt_feats_train = model.predict(x_train, pred_leaf = True) gbdt_feats_test = model.predict(x_val, pred_leaf = True) gbdt_feats_train.shape, gbdt_feats_test.shape # 输出: ((1279, 20), (320, 20))
可以看见, 对于稀疏特征用GDBT也是一种编码方式,这个叶子节点可以与原来的密集特征进行拼接,再丢进去逻辑回归进行分类处理
3. 特征交叉系列
3.1 FM
FM考虑了低阶的特征交互,考虑了所有的二阶交叉项
公式:
3.2 PNN
- PNN模型结构
主要是Product层主要有线性部分和非线性部分组成:
1)线性模块,一阶特征(未经过显示特征交叉处理)——内积
2)非线性模块,高阶特征(经过显示特征交叉处理)——外积
3.3 DCN
- DCN模型结构
3.4 AutoInt
- AutoInt模型结构
输入层这里, 用到的特征主要是离散型特征和连续性特征, 这里不管是哪一类特征,都会过embedding层转成低维稠密的向量。是的, 连续性特征,这里并没有经过分桶离散化,而是直接走embedding。这个是怎么做到的呢?就是就是类似于预训练时候的思路,先通过item_id把连续型特征与类别特征关联起来,最简单的,就是把item_id拿过来,过完embedding层取出对应的embedding之后,再乘上连续值即可, 所以这个连续值事先一定要是归一化的。
这样,不管是连续特征,离散特征还是变长的离散特征,经过embedding之后,都能得到等长的embedding向量。 我们把这个向量拼接到一块,就得到了交互层的输入。
这样,每一个embedding就可以看成是一个序列信息,做一个self-attention处理,可以在不同的序列信息之间进行交互,一般来说,self-attention操作是不改变序列维度大小的,最后就可以进行逻辑回归来输出ctr概率。
- 总结:
这个模型的重点是引入了transformer来实现特征之间的高阶显性交互, 而transformer的魅力就是多头的注意力机制,相当于在多个子空间中, 根据不同的相关性策略去让特征交互然后融合,在这个交互过程中,特征之间计算相关性得到权重,并加权汇总,使得最终每个特征上都有了其它特征的信息,且其它特征的信息重要性还有了权重标识。 这个过程的自注意力计算以及汇总是一个自动的过程,这是很powerful的。所以这篇文章的重要意义是又给我们传授了一个特征交互时候的新思路,就是transformer的多头注意力机制
3.5 FiBiNET
- FiBiNET模型结构
推荐领域里面的特征有个特点,就是海量稀疏,意思是大量长尾特征是低频的,而这些低频特征,去学一个靠谱的Embedding是基本没希望的,但是你又不能把低频的特征全抛掉,因为有一些又是有效的。既然这样,如果我们把SENet用在特征Embedding上,类似于做了个对特征的Attention,弱化那些不靠谱低频特征Embedding的负面影响,强化靠谱低频特征以及重要中高频特征的作用,从道理上是讲得通的
把SENet放在Embedding层之上,通过SENet网络,动态地学习这些特征的重要性。对于每个特征学会一个特征权重,然后再把学习到的权重乘到对应特征的Embedding里,这样就可以动态学习特征权重,通过小权重抑制噪音或者无效低频特征,通过大权重放大重要特征影响的目的。
- Bilinear-Interaction Layer
所谓的双线性,其实就是组合了内积和哈达玛积的操作,参考看法如下:
1)如果我们单独先看内积操作,特征交互如果是两个向量直接内积,这时候, 结果大的,说明两个向量相似或者特征相似, 但向量内积,其实是相当于向量的各个维度先对应位置元素相乘再相加求和。 这个过程中认为的是向量的各个维度信息的重要性是一致的。但真的一致吗? — 内积操作没有考虑向量各个维度的重要性;
2)如果我们单独看哈达玛积操作, 特征交互如果是两个向量哈达玛积,这时候,是各个维度对应位置元素相乘得到一个向量, 而这个向量往往后面会进行线性或者非线性交叉的操作, 最后可能也会得到具体某个数值,但是这里经过了线性或者非线性交叉操作之后, 有没有感觉把向量各个维度信息的重要性考虑了进来?如果模型认为重要性相同,那么哈达玛积还有希望退化成内积,所以哈达玛积感觉考虑的比内积就多了一些。 — 哈达玛积操作自身也没有考虑各个维度重要性,但通过后面的线性或者非线性操作,有一定的维度重要性在里面;
3)再看看这个双线性, 是先内积再哈达玛积。这个内积操作不是直接v i 与v j 内积,而是中间引入了个W矩阵,参数可学习。如果这里的W 是个对角矩阵,那么这里就退化成了哈达玛积。 所以双线性感觉考虑的又比哈达玛积多了一些。如果后面再走一个非线性操作的话,就会发现这里同时考虑了两个交互向量各个维度上的重要性。—双线性操作同时可以考虑交互的向量各自的各个维度上的重要性信息, 这应该是作者所说的细粒度,各个维度上的重要性
当然思路是思路,双线性并不一定见得一定比哈达玛积有效, SENET也不一定就会比原始embedding要好。两路embedding都两两交互完事, Flatten展平,和连续特征拼在一块过DNN输出。结构后面就没什么可以说的了,就是拼接起来丢给cnn作高阶特征交互,然后进行逻辑回归输出ctr预估概率。
- 补充
一个可以看见的缺点就是,这个模型同样只是考虑了两两特征之间的交互,并没有考虑多个特征的交互,在这一点上,上诉的AutoInt使用self-attention机制就可以弥补这一点。
4. Wide&Deep系列
4. 1 Wide&Deep
- Wide&Deep模型结构
模型的总体结构比较简单,分为两部分——Deep与Wide。重点是如何理解Wide部分有利于增强模型的“记忆能力”,Deep部分有利于增强模型的“泛化能力”?
1)wide部分是一个广义的线性模型,输入的特征主要有两部分组成,一部分是原始的部分特征,另一部分是原始特征的交叉特征(大体意思就是两个特征都同时为1这个新的特征才能为1,否则就是0,说白了就是一个特征组合);W&D模型采用L1 FTRL是想让Wide部分变得更加的稀疏,即Wide部分的大部分参数都为0;ide部分模型训练完之后留下来的特征都是非常重要的,那么模型的“记忆能力”就可以理解为发现"直接的",“暴力的”,“显然的”关联规则的能力。ps:Wide侧的数据是高维稀疏的。
2)Deep部分是一个DNN模型,输入的特征主要分为两大类,一类是数值特征(可直接输入DNN),一类是类别特征(需要经过Embedding之后才能输入到DNN中)。DNN模型随着层数的增加,中间的特征就越抽象,也就提高了模型的泛化能力。
最后,W&D模型是将两部分输出的结果结合起来联合训练,将deep和wide部分的输出重新使用一个逻辑回归模型做最终的预测,输出概率值。
4.2 NFM
NFM(Neural Factorization Machines)。传统的FM模型仅局限于线性表达和二阶交互, 无法胜任生活中各种具有复杂结构和规律性的真实数据, 针对FM的这点不足, 作者提出了一种将FM融合进DNN的策略,通过引进了一个特征交叉池化层的结构,使得FM与DNN进行了完美衔接,这样就组合了FM的建模低阶特征交互能力和DNN学习高阶特征交互和非线性的能力,形成了深度学习时代的神经FM模型(NFM)。
FM公式:
NFM公式:
FM的一个问题,就是只能到二阶交叉, 且是线性模型, 这是他本身的一个局限性, 而如果想突破这个局限性, 就需要从他的公式本身下点功夫。所以可以看见,NFM用一个表达能力更强的函数来替代原FM中二阶隐向量内积的部分。而这个表达能力更强的函数呢, 我们很容易就可以想到神经网络来充当,因为神经网络理论上可以拟合任何复杂能力的函数,作者这里也确实吧f ( x ) 换成了一个神经网络。
- NFM的f(x)结构
ps:这里注意, 这个结构中,忽略了一阶部分,只可视化出来了f ( x )
- Bi-Interaction Pooling layer
简单来说,就是进行两两Embedding元素积之后,对交叉特征向量取和, 得到该层的输出向量, 很显然, 输出是一个k维的向量。
- 最后说明
4.3 AFM
- AFM模型结构
AFM的全称是Attentional Factorization Machines, 从模型的名称上来看是在FM的基础上加上了注意力机制,如何让不同的交叉特征具有不同的重要性就是AFM核心的贡献。
- Pair-wise Interaction Layer
将输入的非零特征的隐向量两两计算element-wise product(哈达玛积,两个向量对应元素相乘,得到的还是一个向量),假如输入的特征中的非零向量的数量为m,那么经过Pair-wise Interaction Layer之后输出的就是1 2 m ( m − 1 )个向量
- Attention-based Pooling
将前面Pair-wise Interaction Layer得到的交叉特征向量组输入到Attention-based Pooling,该pooling层会先计算出每个特征组合的自适应权重(通过Attention Net进行计算),通过加权求和的方式将向量组压缩成一个向量,由于最终需要输出的是一个数值,所以还需要将前一步得到的向量通过另外一个向量将其映射成一个值,得到最终的基于注意力加权的二阶交叉特征的输出。
具体的attention实现方式有很多,这里说明一个。假设现在有n个交叉特征(假如维度是k),将nxk的数据输入到一个kx1的全连接网络中,输出的张量维度为nx1,使用softmax函数将nx1的向量的每个维度进行归一化,得到一个新的nx1的向量,这个向量所有维度加起来的和为1,每个维度上的值就可以表示原nxk数据每一行(即1xk的数据)的权重。
- 最后说明
在代码实现上,这里也是有linear part(在deepctr中通过get_linear_logits函数实现)与dnn part,只是这里的dnn part就是这里的attention最后的输出结果。最终将linear部分与attention部分相加之后,通过sigmoid激活得到最终的输出。
4.4 DeepFM
回归PNN, FNN和W&D的缺点:
- FNN模型: 预训练的方式增加了开销,模型能力受限于FM表征能力的上限,且只考虑了高阶交互
- PNN模型:IPNN的内积计算非常复杂, OPNN的外积近似计算损失了很多信息,结果不稳定, 且同样忽视了低阶交互
- W&D模型:虽然是考虑到了低阶和高阶交互,兼顾了模型的泛化和记忆,但是Wide部分输入需要专业的特征工程经验
- DeepFM模型结构:
1)FM部分:依然是一个标准的FM模型,负责特征之间的低阶交互过程,FM的输出是Addition单元和Inner Product units的加和, Addition单元反映1阶特征各自的影响, 而Inner product代表2阶特征交互的影响。
2)Deep部分:原始的特征通过Embedding层之后,会从高维稀疏性转成低维稠密性的特征, 这时候,就可以直接作为DNN的输入
- 论文的其他细节:
1)CTR预测任务中, 高阶特征和低阶特征的学习都非常的重要。 推荐模型我们也学习了很多,基本上是从最简单的线性模型(LR), 到考虑低阶特征交叉的FM, 到考虑高度交叉的神经网络,再到两者都考虑的W&D组合模型。 这样一串联就会发现前面这些模型存在的问题了, 简单盘点一下:
简单的线性模型虽然简单,同样这样是它的不足,就是限制了模型的表达能力,随着数据的大且复杂,这种模型并不能充分挖掘数据中的隐含信息,且忽略了特征间的交互,如果想交互,需要复杂的特征工程;
FM模型考虑了特征的二阶交叉,但是这种交叉仅停留在了二阶层次,虽然说能够进行高阶,但是计算量和复杂性一下子随着阶数的增加一下子就上来了。所以二阶是最常见的情况,会忽略高阶特征交叉的信息;
DNN,适合天然的高阶交叉信息的学习,但是低阶的交叉会忽略掉。
DeepFM就是把上面的几种结构组合在一起
2)在CTR预测中, 学习用户点击行为背后的特征隐式交互非常重要。这人工的进行特征工程是有难度的,一些特征工程比较容易理解,这时候往往我们都能很容易的设计或者组合那样的特征。 然而,其他大部分特征交互都隐藏在数据中,难以先验识别(比如经典的关联规则 "尿布和啤酒 "就是从数据中挖掘出来的,而不是由专家发现的),只能由机器学习自动捕捉,即使是对于容易理解的交互,专家们似乎也不可能详尽地对它们进行建模,特别是当特征的数量很大的时候.
所以,尽量的避免人工特征工程, 构建端到端的推荐系统时作者研究该篇论文的另一动机所在(改进了W&D)
3)DeepFM用FM换掉了W&D的LR,并Wide部分和Deep部分通过低阶和高阶特征交互来影响特征表示,从而更精确地对特征表示进行建模的策略共享了特征Embedding,解决了FNN,PNN,W&D的问题。
4.5 xDeepFM
xDeepFM这个模型的改进出发点依然是如何更好的学习特征之间的高阶交互作用,从而挖掘更多的交互信息。而基于这样的动机,作者提出了又一个更powerful的网络来完成特征间的高阶显性交互(DCN的话是一个交叉网络), 这个网络叫做CIN(Compressed Interaction Network)。xDeepFM的模型架构依然是w&D结构,更好的理解方式就是用这个CIN网络代替了DCN里面的Cross Network, 这样使得该网络同时能够显性和隐性的学习特征的高阶交互(显性由CIN完成,隐性由DNN完成)。xDeepFM的动机,正是将FM的vector-wise的思想引入到了Cross部分
- Embedding Layer
这里再介绍一下推荐系统中的Embedding方式,各个模型中其实都是类似的。我们拿到的数据往往会有连续型数据和离散型或者叫类别型数据之分。 如果是连续型数据,那个不用多说,一般会归一化或者标准化处理,当然还可能进行一定的非线性化操作,就算处理完了。 而类别型数据,一般需要先LabelEncoder,转成类别型编码,然后再one-hot转成0或者1的编码格式。这样的数据往往是高维稀疏的,不利于模型的学习,所以往往在这样数据之后,加一个embedding层,把数据转成低维稠密的向量表示。 关于embedding的原理这里不说了,但这里要注意一个细节,就是如果某个特征每个样本只有一种取值,也就是one-hot里面只有一个地方是1,比如前面3个field。这时候,可以直接拿1所在位置的embedding当做此时类别特征的embedding向量。 但是如果某个特征域每个样本好多种取值,比如interests这个,有好几个1的这种,那么就拿到1所在位置的embedding向量之后求和来代表该类别特征的embedding。
解释:为什么特征交互的时候vector-wise要比bit-wise要好? 因为bit-wise最大的问题其实在于违背了特征交互的初衷, 我们本意上其实是让模型学习特征之间的交互,放到embedding的角度,也理应是embedding与embedding的相关作用交互, 但bit-wise已经没有了embedding的概念,以bit为最细粒度进行学习, 这里面既有不同embedding的bit交互,也有同一embedding的bit交互,已经意识不到Field vector的概念。bit-wise具体到了元素级别上,虽然可能学习的更加细致,但这样应该会增加过拟合的风险,失去一定的泛化能力。
- DNN的隐性高阶交互
1)DNN 是一种隐性的方式学习特征交互, 但这种交互是不可解释性的, 没法看出究竟是学习了几阶的特征交互
2)DNN是在bit-wise层级上学习的特征交互, 这个不同于传统的FM的vector-wise
3)DNN是否能有效的学习高阶特征交互是个迷,其实不知道学习了多少重要的高阶交互,哪些高阶交互会有作用,高阶到了几阶等, 如果用的话,只能靠玄学二字来解释
所以,DNN是否能够真正的有效学习特征之间的高阶交互是个谜
- xDeepFM结构
最后的计算公式为:
下面再放CIN网络的两个处理图,就可以大概知道他做了什么样的工作了 类似与RNN,前面的输出还作为后面的输入,层级递进处理。(下图是第一层特征交互的过程,我觉得博主画的挺明了的了)
- CIN完整结构图
5. 序列模型
5.1 DIN
- 特征表示
工业上的CTR预测数据集一般都是multi-group categorial form的形式,就是类别型特征最为常见,这种数据集一般长这样:
这里的亮点就是框出来的那个特征,这个包含着丰富的用户兴趣信息。对于特征编码,作者这里举了个例子:[weekday=Friday, gender=Female, visited_cate_ids={Bag,Book}, ad_cate_id=Book], 这种情况我们知道一般是通过one-hot的形式对其编码, 转成系数的二值特征的形式。但是这里我们会发现一个visted_cate_ids, 也就是用户的历史商品列表, 对于某个用户来讲,这个值是个多值型的特征, 而且还要知道这个特征的长度不一样长,也就是用户购买的历史商品个数不一样多,这个显然。这个特征的话,我们一般是用到multi-hot编码,也就是可能不止1个1了,有哪个商品,对应位置就是1, 所以经过编码后的数据长下面这个样子:
这个就是喂入模型的数据格式了,这里还要注意一点 就是上面的特征里面没有任何的交互组合,也就是没有做特征交叉。这个交互信息交给后面的神经网络去学习。
- base model
- din model
这个在base model上加了一个注意力机制来学习用户兴趣与当前候选广告间的关联程度,引入了一个新的local activation unit, 这个东西用在了用户历史行为特征上面, 能够根据用户历史行为特征和当前广告的相关性给用户历史行为特征embedding进行加权。local activation unit里面是一个前馈神经网络,输入是用户历史行为商品和当前的候选商品, 输出是它俩之间的相关性, 这个相关性相当于每个历史商品的权重,把这个权重与原来的历史行为embedding相乘求和就得到了用户的兴趣表示
- DIN的数据处理
DIN模型的输入特征大致上分为了三类: Dense(连续型), Sparse(离散型), VarlenSparse(变长离散型),也就是指的上面的历史行为数据。而不同的类型特征也就决定了后面处理的方式会不同:
1)Dense型特征:由于是数值型了,这里为每个这样的特征建立Input层接收这种输入, 然后拼接起来先放着,等离散的那边处理好之后,和离散的拼接起来进DNN。
2)Sparse型特征,为离散型特征建立Input层接收输入,然后需要先通过embedding层转成低维稠密向量,然后拼接起来放着,等变长离散那边处理好之后, 一块拼起来进DNN, 但是这里面要注意有个特征的embedding向量还得拿出来用,就是候选商品的embedding向量,这个还得和后面的计算相关性,对历史行为序列加权。
3)VarlenSparse型特征:这个一般指的用户的历史行为特征,变长数据, 首先会进行padding操作成等长, 然后建立Input层接收输入,然后通过embedding层得到各自历史行为的embedding向量, 拿着这些向量与上面的候选商品embedding向量进入AttentionPoolingLayer去对这些历史行为特征加权合并,最后得到输出。
通过上面的三种处理, 就得到了处理好的连续特征,离散特征和变长离散特征, 接下来把这三种特征拼接,进DNN网络,得到最后的输出结果即可。
5.2 DIEN
DIN直接将用户的行为当做用户的兴趣(因为DIN模型只是在行为序列上做了简单的特征处理),但是用户潜在兴趣一般很难直接通过用户的行为直接表示,大多模型都没有挖掘用户行为背后真实的兴趣,捕捉用户兴趣的动态变化对用户兴趣的表示非常重要。DIEN相比于之前的模型,即对用户的兴趣进行建模,又对建模出来的用户兴趣继续建模得到用户的兴趣变化过程。
所以DIEN模型的重点就是如何将用户的行为序列转换成与用户兴趣相关的向量,在DIN中是直接通过与target item计算序列中每个元素的注意力分数,然后加权求和得到最终的兴趣表示向量。在DIEN中使用了两层结构(兴趣提取层及兴趣演化层)来建模用户兴趣相关的向量。
- DIEN模型结构
- Interest Exterator Layer
作者并没有直接完全使用原始的GRU来提取用户的兴趣,而是引入了一个辅助函数来指导用户兴趣的提取。作者认为如果直接使用GRU提取用户的兴趣,只能得到用户行为之间的依赖关系,不能有效的表示用户的兴趣。因为是用户的兴趣导致了用户的点击,用户的最后一次点击与用户点击之前的兴趣相关性就很强,但是直接使用行为序列训练GRU的话,只有用户最后一次点击的物品(也就是label,在这里可以认为是Target Ad), 那么最多就是能够捕捉到用户最后一次点击时的兴趣,而最后一次的兴趣又和前面点击过的物品在兴趣上是相关的,而前面点击的物品中并没有target item进行监督。所以作者提出的辅助损失就是为了让行为序列中的每一个时刻都有一个target item进行监督训练,也就是使用下一个行为来监督兴趣状态的学习,相当于是行为序列中的第t+1个物品与用户第t时刻的兴趣表示之间的损失。
当然,如果只计算用户点击物品与其点击前一次的兴趣之间的损失,只能认为是正样本之间的损失,那么用户第t时刻的兴趣其实还有很多其他的未点击的商品,这些未点击的商品就是负样本,负样本一般通过从用户点击序列中采样得到,这样一来辅助损失中就包含了用户某个时刻下的兴趣及与该时刻兴趣相关的正负物品。辅助损失会加到最终的目标损失(ctr损失)中一起进行优化,并且通过α \alphaα参数来平衡点击率和兴趣的关系: L = L t a r g e t + α L a u x
- Interest Evolving Layer
Attention模块就是DIN中介绍的局部激活单元。当得到了用户历史兴趣序列及兴趣序列与target item之间的相关性(注意力分数)之后,就需要再次对注意力序列进行建模得到用户注意力的演化过程,进一步表示用户最终的兴趣向量。下面就是考虑如何使用这个注意力权重来一起优化序列建模的结果了。作者提出了三种注意力结合的GRU模型快:
1)AIGRU: 将注意力分数直接与输入的序列进行相乘,也就是权重越大的向量对应的值也越大。这种方式的弊端是即使是零输入也会改变GRU的隐藏状态,所以相对较少的兴趣值也会影响兴趣的学习进化(根据GRU门的更新公式就可以知道,下一个隐藏状态的计算会用到上一个隐藏状态的信息,所以即使当前输入为0,最终隐藏状态也不会直接等于0,所以即使兴趣较少,也会影响到最终兴趣的演化)。
2)AGRU: 将注意力分数直接作为GRU模块中更新门的值,但是这种方式的弊端是弱化了兴趣之间的相关性,因为最终兴趣的更新前后是没关系的,只取决于输入的注意力分数。
3)AUGRU: 将注意力分数作为更新门的权重,这样既兼顾了注意力分数很低时的状态更新值,也利用了兴趣之间的相关性
5.3 DSIN
DIEN只关注了如何去改进网络,而忽略了用户历史行为序列本身的特点, 其实我们仔细想想的话,用户过去可能有很多历史点击行为,比如[item3, item45, item69, item21, …], 这个按照用户的点击时间排好序了,既然我们说用户的兴趣是非常广泛且多变的,那么这一大串序列的商品中,往往出现的一个规律就是在比较短的时间间隔内的商品往往会很相似,时间间隔长了之后,商品之间就会出现很大的差别,这个是很容易理解的,一个用户在半个小时之内的浏览点击的几个商品的相似度和一个用户上午点击和晚上点击的商品的相似度很可能是不一样的。这其实就是作者说的homogeneous和heterogeneous。而DIEN模型呢? 它并没有考虑这个问题,而是会直接把这一大串行为序列放入GRU让它自己去学(当然我们其实可以人工考虑这个问题,然后如果发现序列很长的话我们也可以分成多个样本哈,当然这里不考虑这个问题),如果一大串序列一块让GRU学习的话,往往用户的行为快速改变和突然终止的序列会有很多噪声点,不利于模型的学习。
所以,作者这里就是从序列本身的特点出发, 把一个用户的行为序列分成了多个会话。所谓会话,其实就是按照时间间隔把序列分段,每一段的商品列表就是一个会话,这其实也是DSIN改进的动机了, DSIN,这次的关键就是在S上。
- DSIN模型结构
整个模型主要分为4步:
1)首先, 分段这个是必须的了吧,也就是在用户行为序列输入到模型之前,要按照固定的时间间隔(比如30分钟)给他分开段,每一段里面的商品序列称为一个会话Session。 这个叫做会话划分层(Session Divsion Layer)
2)然后呢,就是学习商品时间的依赖关系或者序列关系,由于上面把一个整的行为序列划分成了多段,那么在这里就是每一段的商品时间的序列关系要进行学习,当然我们说可以用GRU, 不过这里作者用了多头的注意力机制,这个东西是在多个角度研究一个会话里面各个商品的关联关系, 相比GRU来讲,没有啥梯度消失,并且可以并行计算,比GRU可强大多了。这个叫做会话兴趣提取层(session interset extractor layer)
3)上面研究了会话内各个商品之间的关联关系,接下来就是研究会话与会话之间的关系了,虽然我们说各个会话之间的关联性貌似不太大,但是可别忘了会话可是能够表示一段时间内用户兴趣的, 所以研究会话与会话的关系其实就是在学习用户兴趣的演化规律,这里用了双向的LSTM,不仅看从现在到未来的兴趣演化,还能学习未来到现在的变化规律, 这个叫做会话交互层(session interset interacting layer)
4)既然会话内各个商品之间的关系学到了,会话与会话之间的关系学到了,然后呢? 当然也是针对性的模拟与目标广告相关的兴趣进化路径了, 所以后面是会话兴趣局部激活层(session interset activating layer), 这个就是注意力机制, 每次关注与当前商品更相关的兴趣。
有LSTM与Transformer相关基础的,看一下结构图就大致了解具体做了什么了。这里的attention局部注意力机制,DIN和DIEN里都见识过了。最后,上面的用户行为特征, 物品行为特征以及求出的会话兴趣特征进行拼接,然后过一个DNN网络,就可以得到输出了。
- 特征处理
由于输入的特征有三大类(离散,连续和变长离散),所以分别建立Input层,然后离散特征还得建立embedding层。下面三大类特征就有了不同的走向:
1)连续特征: 这类特征拼先拼接到一块,然后等待最后往DNN里面输入
2)普通离散特征: 这块从输入 -> embedding -> 拼接到一块,等待DNN里面输入
3)用户的会话特征: 这块从输入 -> embedding -> 会话兴趣分割层(sess_interest_division) -> 会话兴趣提取层(sess_interest_extractor) -> 会话兴趣交互层(BiLSTM) -> 会话兴趣激活层( AttentionPoolingLayer) -> 得到两个兴趣性特征
把上面的连续特征,离散特征和兴趣特征拼接起来,然后过DNN得到输出即可。
6. 多任务学习
6.1 ESMM
- 相关名词解释
以下是推荐系统名词的解释:以电子商务平台为例,用户的购买行为一般遵循以下的顺序决策模式:impression-click-conversion,即用户先观察到系统推荐的产品,然后会对自己感兴趣的商品进行点击,进而产生购买行为。
CVR——点击到购买的比例;
CTR——曝光到点击的比例;
CTCVP——曝光到购买的比例;
- ESMM模型结构
ESMM本质上是以CVR为主任务,引入CTR和CTCVR作为辅助任务,解决CVR预估的挑战。三个任务之间的关系为:
其中x表示曝光,y表示点击,z表示转化。
可以发现这里的特征编码是共享一个embedding查找表的,但是DNN部分是不一样的,也就是说,主任务和辅助任务共享特征,不同任务输出层使用不同的网络。模型训练完成后,可以同时预测cvr、ctr、ctcvr三个指标,线上根据实际需求进行融合或者只采用此模型得到的cvr预估值。
6.2 MMOE
MMO全称是Multi-gate Mixture-of-Experts, 对于多个优化任务,引入了多个专家进行不同的决策和组合,最终完成多目标的预测。解决的是硬共享里面如果多个任务相似性不是很强,底层的embedding学习反而相互影响,最终都学不好的痛点。
- 常见的多任务模型的设计范式大致可以分为三大类:
1)hard parameter sharing 方法: 这是非常经典的一种方式,底层是共享的隐藏层,学习各个任务的共同模式,上层用一些特定的全连接层学习特定任务模式。但是劣势也非常明显,就是底层强制的shared layers难以学习到适用于所有任务的有效表达。 尤其是任务之间存在冲突的时候。MMOE中给出了实验结论,当两个任务相关性没那么好(比如排序中的点击率与互动,点击与停留时长),此时这种结果会遭受训练困境。
2)soft parameter sharing: 底层不是使用共享的一个shared bottom,而是有多个tower, 称为多个专家,然后往往再有一个gating networks在多任务学习时,给不同的tower分配不同的权重,那么这样对于不同的任务,可以允许使用底层不同的专家组合去进行预测,相较于上面所有任务共享底层,这个方式显得更加灵活。
3)任务序列依赖关系建模:这种适合于不同任务之间有一定的序列依赖关系。比如电商场景里面的ctr和cvr,其中cvr这个行为只有在点击之后才会发生。
- MMOE模型结构
所以其实可以发现,门控的机制其实是一个通用的方法,就想是SENet中的注意力机制一样。MMOE的最后公式表示为:
- MMOE的优点:
1)MMOE是针对每个任务都单独有个门控选择专家组合,那么即使任务冲突了,也能根据不同的门控进行调整,选择出对当前任务有帮助的专家组合。所以,我觉得单门控做到了针对所有任务在专家选择上的解耦,而多门控做到了针对各个任务在专家组合选择上的解耦。
2)多门控机制能够建模任务之间的关系了。如果各个任务都冲突, 那么此时有多门控的帮助, 此时让每个任务独享一个专家,如果任务之间能聚成几个相似的类,那么这几类之间应该对应的不同的专家组合,那么门控机制也可以选择出来。如果所有任务都相似,那这几个门控网络学习到的权重也会相似,所以这种机制把任务的无关,部分相关和全相关进行了一种统一。
3)MMOE既兼顾了如果有相似任务,那就参数共享,模式共享,互为补充,如果没有相似任务,那就独立学习,互不影响。 又把这两种极端给进行了统一。
4)训练时能快速收敛,这是因为相似的任务对于特定的专家组合训练都会产生贡献,这样进行一轮epoch,相当于单独任务训练时的多轮epoch。
- MMOE的缺点:
1)MMOE中所有的Expert是被所有任务所共享,这可能无法捕捉到任务之间更复杂的关系,从而给部分任务带来一定的噪声。
2)在复杂任务机制下,MMOE不同专家在不同任务的权重学的差不多
3)不同的Expert之间没有交互,联合优化的效果有所折扣
- 多任务学习有效的原因?
多任务学习有效的原因是引入了归纳偏置,达到了两个效果:
1)互相促进: 可以把多任务模型之间的关系看作是互相先验知识,也称为归纳迁移,有了对模型的先验假设,可以更好提升模型的效果。解决数据稀疏性其实本身也是迁移学习的一个特性,多任务学习中也同样会体现
2)泛化作用:不同模型学到的表征不同,可能A模型学到的是B模型所没有学好的,B模型也有其自身的特点,而这一点很可能A学不好,这样一来模型健壮性更强
6.3 PLE
- 多任务学习中不可避免的两个缺点
1)负迁移(Negative Transfer):针对相关性较差的任务,使用shared-bottom这种硬参数共享的机制会出现负迁移现象,不同任务之间存在冲突时,会导致模型无法有效进行参数的学习,不如对多个任务单独训练。
2)跷跷板现象(Seesaw Phenomenon):针对相关性较为复杂的场景,通常不可避免出现跷跷板现象。多任务学习模式下,往往能够提升一部分任务的效果,但同时需要牺牲其他任务的效果。即使通过MMOE这种方式减轻负迁移现象,跷跷板问题仍然广泛存在。
PLE(Progressive Layered Extraction)模型要为了解决跷跷板问题。
- CGC(Customized Gate Control) 定制门控
同样的特征输入分别送往三类不同的专家模型(任务A专家、任务B专家、任务共享专家),再通过门控机制加权聚合之后输入各自的Tower网络。门控网络,把原始数据和expert网络输出共同作为输入,通过单层全连接网络+softmax激活函数,得到分配给expert的加权权重,与attention机制类型。任务A有m A 个expert,任务B有m B 个expert,另外还有m S 个任务A、B共享的Expert。这样对Expert做一个显式的分割,可以让task-specific expert只受自己任务梯度的影响,不会受到其他任务的干扰(每个任务保底有一个独立的网络模型),而只有task-shared expert才受多个任务的混合梯度影响。
- PLE (progressive layered extraction) 分层萃取
PLE就是上述CGC网络的多层纵向叠加,以获得更加丰富的表征能力。在分层的机制下,Gate设计成两种类型,使得不同类型Expert信息融合交互。task-share gate融合所有Expert信息,task-specific gate只融合specific expert和share expert。处理的方法都是类似的,就是为每个expert赋予不一样的权重。
将任务A、任务B和shared expert的输出输入到下一层,下一层的gate是以这三个上一层输出的结果作为门控的输入,而不是用原始input特征作为输入。这使得gate同时融合task-shares expert和task-specific expert的信息,论文实验中证明这种不同类型expert信息的交叉,可以带来更好的效果。
7. 总结
最后说一下心得,看推荐系统的相关模型其实也看了好几天了,但我本身主要是弄图像相关的。不过,经过这一番大概粗劣的推荐系统学习,觉得有很多方法其实是共用的。比如MOE,比如Transformer的self-attention,比如SE注意力机制,比如Gate门控机制,比如序列的思想embedding的思想等等。如何交互,怎么交互,怎么融合,怎么更有效的融合。
其实很多方法在计算机视觉领域都有出现。也就是说,这些好的思想其实在不同领域,处理不同的任务上是可以复用的,重点是你怎么去使用,怎么较好的移植在这些任务上,以带来一个不错的效果。
而归根到底,其实都是用深度学习,用神经网络来处理不同领域的任务。那么既然用了深度学习就尽量的进行端对端,尽量的去除人工特征的处理,这才是深度学习的亮点。那么怎么在各个领域实现深度学习端对端的处理就是需要研究的内容了,总觉得,这些模型有种熟悉的感觉。希望之后可以在计算机视觉领域带来启发。
之后,可能再学习一下跨域的推荐系统,有机会再更新文章。最后,用沐神的一句话来结束——“广度和深度同样重要”
希望自己脚踏实地,切不可眼高手低。
参考资料:
1. DeepCTR-torch开源算法库
2. Datawhale的推荐系统教程FunRec
3. 博主<翻滚的小@强>的推荐系统专栏