Funrec读书笔记4️⃣:特征交叉及Wide&Deep系列排序模型

167 阅读7分钟

0. 背景

  • 动机:手工构造的交叉特征有一些缺陷:特征工程需要耗费太多精力,模型是强行记住这些组合特征的,对于未曾出现过的特征组合,权重系数为0,无法进行泛化。
  • 所以为了加强模型的泛化能力,研究者引入了DNN结构,将高维稀疏特征编码为低维稠密的Embedding vector,这种基于Embedding的方式能够有效提高模型的泛化能力。

1. Wide&Deep模型

  • Wide&Deep模型就是围绕记忆性和泛化性进行讨论的,模型能够从历史数据中学习到高频共现的特征组合的能力,称为是模型的Memorization;能够利用特征之间的传递性去探索历史数据中从未出现过的特征组合,称作模型的Generalization。Wide&Deep兼顾这两种能力并在Google Play Store中成功落地。

1.1 模型原理

image.png

  1. 左侧是模型的Wide部分,一般输入为原始的部分特征和原始特征的交叉特征;训练的时候使用的优化器是带L1正则的FTRL算法,而L1 FTLR是非常注重模型稀疏性质的,也就是想让Wide部分变得更加的稀疏,即Wide部分的大部分参数都为0,这就大大压缩了模型权重和特征向量的维度。Wide部分模型训练后留下来的特征都是非常重要的,那么模型的“记忆能力”就可以理解为发现“直接的”,“暴力的”,“显然的”关联规则的能力。
  2. 右侧是模型的Deep部分,一般输入为数值特征(可以直接进入DNN)和类别特征(Embedding后输入DNN);DNN模型随着层数的增加,中间的特征就越抽象,也就提高了模型的泛化能力。Deep部分的DNN模型作者使用了深度学习常用的优化器AdaGrad,为了使得模型可以得到更精确的解。
  3. Wide部分和Deep部分的结合:模型是将两部分输出的结果结合起来联合训练,将deep和wide部分的输出重新使用一个逻辑回归模型做最终的预测,输出概率值。

1.2 Wide&Deep代码

Wide侧记住的是历史数据中常见、高频的模式。实际上wide没有发现新的东西,只是记住这些模型,并且做一些筛选,正因如此,我们应该根据人工经验、业务背景,将我们认为有价值的、显而易见的特征以及特征组合,喂入Wide侧。 Deep侧就是DNN,通过embedding的方式将categorical/id特征映射成稠密向量,让DNN学习到这些特征之间的深层交叉,以增强扩展能力。

# Wide&Deep 模型的wide部分及Deep部分的特征选择,应该根据实际的业务场景去确定哪些特征应该放在Wide部分,哪些特征应该放在Deep部分
def WideNDeep(linear_feature_columns, dnn_feature_columns):
    # 构建输入层,即所有特征对应的Input()层,这里使用字典的形式返回,方便后续构建模型
    dense_input_dict, sparse_input_dict = build_input_layers(linear_feature_columns + dnn_feature_columns)

    # 将linear部分的特征中sparse特征筛选出来,后面用来做1维的embedding
    linear_sparse_feature_columns = list(filter(lambda x: isinstance(x, SparseFeat), linear_feature_columns))

    # 构建模型的输入层,模型的输入层不能是字典的形式,应该将字典的形式转换成列表的形式
    # 注意:这里实际的输入与Input()层的对应,是通过模型输入时候的字典数据的key与对应name的Input层
    input_layers = list(dense_input_dict.values()) + list(sparse_input_dict.values())

    # Wide&Deep模型论文中Wide部分使用的特征比较简单,并且得到的特征非常的稀疏,所以使用了FTRL优化Wide部分(这里没有实现FTRL)
    # 但是是根据他们业务进行选择的,我们这里将所有可能用到的特征都输入到Wide部分,具体的细节可以根据需求进行修改
    linear_logits = get_linear_logits(dense_input_dict, sparse_input_dict, linear_sparse_feature_columns)
    
    # 构建维度为k的embedding层,这里使用字典的形式返回,方便后面搭建模型
    embedding_layers = build_embedding_layers(dnn_feature_columns, sparse_input_dict, is_linear=False)

    dnn_sparse_feature_columns = list(filter(lambda x: isinstance(x, SparseFeat), dnn_feature_columns))

    # 在Wide&Deep模型中,deep部分的输入是将dense特征和embedding特征拼在一起输入到dnn中
    dnn_logits = get_dnn_logits(dense_input_dict, sparse_input_dict, dnn_sparse_feature_columns, embedding_layers)
    
    # 将linear,dnn的logits相加作为最终的logits
    output_logits = Add()([linear_logits, dnn_logits])

    # 这里的激活函数使用sigmoid
    output_layer = Activation("sigmoid")(output_logits)

    model = Model(input_layers, output_layer)
    return model

1.3 思考

  • 一般应用场景中,哪些特征适合放在Wide侧,哪些特征适合放在Deep侧,为什么呢? Wide侧适合高度稀疏特征【用户ID,物品ID】,交叉组合特征; Deep侧适合低维稠密特征【用户年龄、商品价格】,序列特征;

    为什么这么分配?

    1. Wide擅长记忆(简单线性),Deep擅长泛化(复杂非线性)
    2. 效率:wide部分很简单
    3. 解释性:wide部分易于解释

image.png 在原论文中,谷歌商店团队论文中竟然在Wide部分只使用了一个特征 - 已安装应用【历史行为】和曝光应用【待推荐应用】的交叉特征; Deep部分的输入是全量的特征向量【类别特征要Embedding一下,数值直接输入】。

2. DeepFM

2.1 背景

  • 动机:特征组合可以有效提升CTR(点击率)问题,但是随着特征变成二阶、三阶甚至是更高阶,复杂度就几何倍数的升高,无法满足实时性的要求。Wide&Deep模型提出用一部分Wide模型代替,解决低阶特征交叉在神经网络中表现不好的问题,在实际的使用中Wide Module中的部分需要较为精巧的特征工程,换句话说人工处理对于模型的效果具有比较大的影响。并且,在最后的结合阶段,output融合时候直接将低阶和高阶特征进行组合,很容易让模型最终偏向学习到低阶或者高阶的特征,而不能做到很好的结合。

2.2 模型原理

image.png

  • FM部分:由一阶特征和二阶特征Concatenate到一起再经过一个Sigmoid得到logits
  • Deep部分:先进行embedding,再进行神经网络的运算
  • 在特征部分,我们要进行分Field处理,最后再concat起来,比如说用户field的向量就是- 用户Field的特征维度: 16(用户ID嵌入) + 2(性别嵌入) + 5(年龄段嵌入) = 23维;商品Field的特征维度: 16(商品ID嵌入) + 8(类别嵌入) + 8(品牌嵌入) = 32维

2.3 代码

def DeepFM(linear_feature_columns, dnn_feature_columns):
    # 构建输入层,即所有特征对应的Input()层,这里使用字典的形式返回,方便后续构建模型
    dense_input_dict, sparse_input_dict = build_input_layers(linear_feature_columns + dnn_feature_columns)

    # 将linear部分的特征中sparse特征筛选出来,后面用来做1维的embedding
    linear_sparse_feature_columns = list(filter(lambda x: isinstance(x, SparseFeat), linear_feature_columns))

    # 构建模型的输入层,模型的输入层不能是字典的形式,应该将字典的形式转换成列表的形式
    # 注意:这里实际的输入与Input()层的对应,是通过模型输入时候的字典数据的key与对应name的Input层
    input_layers = list(dense_input_dict.values()) + list(sparse_input_dict.values())

    # linear_logits由两部分组成,分别是dense特征的logits和sparse特征的logits
    linear_logits = get_linear_logits(dense_input_dict, sparse_input_dict, linear_sparse_feature_columns)

    # 构建维度为k的embedding层,这里使用字典的形式返回,方便后面搭建模型
    # embedding层用户构建FM交叉部分和DNN的输入部分
    embedding_layers = build_embedding_layers(dnn_feature_columns, sparse_input_dict, is_linear=False)

    # 将输入到dnn中的所有sparse特征筛选出来
    dnn_sparse_feature_columns = list(filter(lambda x: isinstance(x, SparseFeat), dnn_feature_columns))

    fm_logits = get_fm_logits(sparse_input_dict, dnn_sparse_feature_columns, embedding_layers) # 只考虑二阶项

    # 将所有的Embedding都拼起来,一起输入到dnn中
    dnn_logits = get_dnn_logits(sparse_input_dict, dnn_sparse_feature_columns, embedding_layers)
    
    # 将linear,FM,dnn的logits相加作为最终的logits
    output_logits = Add()([linear_logits, fm_logits, dnn_logits])

    # 这里的激活函数使用sigmoid
    output_layers = Activation("sigmoid")(output_logits)

    model = Model(input_layers, output_layers)
    return model

引用:Funrec, 王喆 见微知著,你真的搞懂Google的Wide&Deep模型了吗?