DeepCrossing

一、动机
通过在传统神经网络的基础上加入embedding 残差连接等思想,完整的解决了特征工程,稀疏向量稠密化,多层神经网络进行优化目标拟合等一系列深度学习在推荐系统的应用问题。

二、模型结构及原理
为了完成端到端的训练,DeepCrossing模型要在内部网络结构中解决如下问题:
1.离散类特征编码过于稀疏,不利于直接输入神经网络训练,需要解决稀疏特征向量稠密化问题
2.如何解决特征自动交叉组合
3.如何在输出层中达成问题设定的优化目标

1

4.各层的作用
4.1 Embedding层
将稀疏矩阵的类别型特征转化成稠密的Embedding向量,Embedding的维度会远小于原始的稀疏特征向量。这里的Feature #1表示的类别特征(one-hot编码后的稀疏特征向量),Feature #2是数值型特征。不需要Embedding,直接到Stacking Layer。关于Embedding Layer的实现,往往一个全连接层即可,Tensoflow中有实现好的层可以直接使用。

4.2 Stacking Layer
这个层是把不同的Embedding特征和数值型特征拼在一起,形成新的包含全部特征的特征向量,该层通常也称为连接层,具体的实现如下,先将所有的数值特征拼起来,然后将所有的Embedding拼接起来,最后将数值特征和Embedding特征拼接起来作为DNN的输入,这里TF是通过Concatnate层进行拼接

#将所有的dense特征拼接到一起
dense_dnn_list = list(dense_input_dict.values())
dense_dnn_inputs = Concatenate(axis = 1)(dense_dnn_list) #B x n(n表示数值特征的数量)

#因为需要将其与dense特征拼接到一起,所以需要Flatten.不进行Flatten的Embedding层输入的维度为 B x 1 x dim
sparse_dnn_list = concat_embedding_list(dnn_feature_columns , spares_input_dict , embedding_layer_dict , flatten = True)
spares_dnn_inputs = Concatenate(axis=1)(sparse_dnn_list) #B x m*dim(n表示类别特征的数量,dim表示embedding的维度)

#将dense特征和Sparse特征拼接在一起
dnn_inputs = Concatenate(axis = 1)([dense_dnn_inputs , sparse_dnn_inputs]) #B x (n + m*dim)

4.3 Multiple Residual Units Layer
该层主要结果是MLP,但DeepCrossing采用了残差网络进行连接。通过多层残差对特征向量各个维度充分的交叉组合,使得模型能够抓取更多的非线性特征和组合特征信息,增加模型的表达能力。残差网络结构如下图:


2

Deep Crossing模型使用稍微修改过的残差单元,它不使用卷积内核,改为了两层神经网络。我们可以看到,残差单元是通过两层ReLU变换再将原始输入特征相加实现的。具体代码如下:

#DNN残差定义
class ResidualBlock(Layer):
    def __init__(self,units): #units表示的是DNN隐藏层神经元数量
        super(ResidualBlock , self) . __init__()
        self.units = units

    def build(self , input_shape):
        out_dim = input_shape[-1]
        self.dnn1 = Dense(self.units , activation = 'relu')
        self.dnn2 = Dense(out_dim , activation = 'relu') #保证输入的维度和输出的维度一致才可以进行残差连接

    def call(self,inputs):
        x = inputs
        x = self.dnn1(x)
        x = self.dnn2(x)
        x = Activation('relu)(x +inputs) #残差操作
        return x 

4.4 Scoring Layer
这个作为输出层,为了拟合优化目标存在。对于CTR预估二分类问题,Scoring往往采取逻辑回归,模型通过叠加多个残差块加深网络的深度,最后将结果转换成一个概率值输出。

#block_nums表示DNN残差块的数量
def get_dnn_logits(dnn_inputs , block_nums = 3):
    dnn_out = dnn_inputs
    for i in range(block_nums):
        dnn_out = ResidualBlock(64)(dnn_out)

    # 将dnn的输出转化成logits
    dnn_logits = Dense(1, activation = 'sigmoid')(dnn_out)

    return dnn_logits

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

如果是用于点击率预估模型的损失函数就是对数损失函数:

logloss=-\frac 1N\sum_1^N(y_ilog(p_i)+(1-y_i)log(1-p_i) 其中y_i表示真实的标签(点击或未点击),p_i表示Scoring Layer输出的结果。但是在实际应用中,根据不同的需求可以灵活替换为其他目标函数。

  1. 代码实现
    从模型的代码结构上来看,DeepCrossing的模型输入主要由数值特征和类别特征组成,并将经过Embedding之后的类别特征及类别特征拼接在一起,详细的拼接代码如Staking Layer所示,下面是构建模型的核心代码
def DeepCrossing(dnn_feature_columns):
    # 构建输入层,即所有特征对应的Input()层,这里使用字典的形式返回,方便后续构建模型
    dense_input_dict, sparse_input_dict = build_input_layers(dnn_feature_columns)

    # 构建模型的输入层,模型的输入层不能是字典的形式,应该将字典的形式转换成列表的形式
    # 注意:这里实际的输入与Input()层的对应,是通过模型输入时候的字典数据的key与对应name的Input层
    input_layers = list(dense_input_dict.values()) + list(sparse_input_dict.values())
    
    # 构建维度为k的embedding层,这里使用字典的形式返回,方便后面搭建模型
    embedding_layer_dict = build_embedding_layers(dnn_feature_columns, sparse_input_dict, is_linear=False)

    #将所有的dense特征拼接到一起
    dense_dnn_list = list(dense_input_dict.values())
    dense_dnn_inputs = Concatenate(axis=1)(dense_dnn_list) # B x n (n表示数值特征的数量)

    # 因为需要将其与dense特征拼接到一起所以需要Flatten,不进行Flatten的Embedding层输出的维度为:Bx1xdim
    sparse_dnn_list = concat_embedding_list(dnn_feature_columns, sparse_input_dict, embedding_layer_dict, flatten=True) 

    sparse_dnn_inputs = Concatenate(axis=1)(sparse_dnn_list) # B x m*dim (n表示类别特征的数量,dim表示embedding的维度)

    # 将dense特征和Sparse特征拼接到一起
    dnn_inputs = Concatenate(axis=1)([dense_dnn_inputs, sparse_dnn_inputs]) # B x (n + m*dim)

    # 输入到dnn中,需要提前定义需要几个残差块
    output_layer = get_dnn_logits(dnn_inputs, block_nums=3)

    model = Model(input_layers, output_layer)
    return model
3
4

构建DeepCrossing

import warnings
warinings.filterwarnings("ignore")
import itertools
import numpy as np
import pandas as pd
from tqdm import tqdm
from collections import namedtuple

import tensorflow as tf
from tensorflow import keras
from tensorflow.keras.layers import *
from tensorflow.keras.model import *

from sklearn.model_selections import train_test_split
from sklearn.preprocessing import MinMaxScaler , LabelEncoder

from utils import SparseFeat , DenseFeat , VarLenSpareFeat

def data_process(date_df , dense_features , spares_features):
    """
     简单处理特征,包括缺失值,数值处理,类别编码
     param data_df :DataFrame格式的数据
     param dense_df : 数值特征名称列表
     param sparse_features: 类别特征名称列表
    """
    data_df[dense_features] = data_df[dense_feaures].fillna(0.0)
    for f in dense_features:
        data_df[f] = data_df[f].apply(lambda x: np.log(x+1) if x > -1 else -1)
    data_df[spare_features] = data_df[sparse_features].fillna("-1")
    for f in spare_features:
        lbe = LabelEncoder()
        data_df[f] = lbe.fit_transform(data_df[f])

    return data_df[dense_features + sparse_features]

def build_input_layers(feature_columns):
    """
      构建输入层
      param features_columns :数据集中的所有特征对应的特征标记之
    """
    dense_input_dict , spare_input_dict = {} , {}
    
    for fc in feature_columns:
        if isinstance(fc , SparseFeat):
            sparse_input_dict[fc.name] = Input(shape = (1, ) , name = fc.name)
        elif isinstance(fc , DenseFeat):
            dense_input_dict[fc.name] = Input(shape = (fc.dimension , ) , name = fc.name)

    return dense_input_dict , sparse_input_dict

def build_embedding_layers(features_columns, input_layers_dict , is_linear):
    #定义一个embedding层对应的字典
    embedding_layers_dict = dict()

    #将特征中的spares特征筛选出来
    sparse_feature_columns = list(filter(lambda x: isinstance(x , SparseFeat) , feature_columns)) if feature_columns else []

    #如果是用于线性部分embedding层,其维度为1,否则维度就是自己定义的embedding维度

    if is_linear:
        for fc in spare_feature_columns:
            embedding_layers_dict[fc.name] = Embedding(fc.vocabulary_size +1 , 1, name = '1d_emb_'+fc.name)
    else:
        for fc in sparse_feature_columns:
            embedding_layers_dict[fc.name] = Embedding(fc.vocabulary_size + 1 , fc.embedding_dim , name = 'kd_emb_' + fc.name)

    return embedding_layers_dict

#将所有的sparse特征embedding拼接
def concat_embedding _list(feature_columns , input_layer_dict , embedding_layer_dict , flatten = False):
    #将sparse特征筛选出来
    sparse_feature_columns = list(filter(lambda x: isinstance(x , SparseFeat) , feature_columns))

    embedding_list = []
    for fc in sparse_feature_columns:
        _input = input_layer_dict[fc.name] #获取出入层
        _embed = embedding_layer_dict[fc.name] #B*1*dim 获取对应的embedding层
        embed = _embed(_input) #B * dim 将input层输入到embedding层中

    #是否需要flatten , 如果embedding列表最终是直接输入到Dense层中,需要进行Flatten, 否则不需要
    if flatten:
        embed = Flatten()(embed)
    embedding_list.append(embed)

return embedding_list


#DNN残差块的定义 
class ResidualBlock(Layer):
    def __init__(self , units): #units表示的是DNN隐层的神经元数量
        super(ResidualBlock , self).__init__()
        self.units = units

    def build(self, input_shape):
        out_dim = input_shape[-1]
        self.dnn1 = Dense(self.units , activation = 'relu')
        self.dnn2 = Dense(out_dim , activation = 'relu') #保证输入的维度和输出的维度一致才能进行残差连接

    def call(self , inputs):
        x = inputs
        x = self.dnn1(x)
        x = self.dnn2(x)
        x = Activation('relu')(x + inputs) #残差操作
        return x 
 
# block_nums表示DNN残差块的数量
def get_dnn_logits(dnn_inputs, block_nums=3):
    dnn_out = dnn_inputs
    for i in range(block_nums):
        dnn_out = ResidualBlock(64)(dnn_out)
    
    # 将dnn的输出转化成logits
    dnn_logits = Dense(1, activation='sigmoid')(dnn_out)

    return dnn_logits


def DeepCrossing(dnn_feature_columns):
    # 构建输入层,即所有特征对应的Input()层,这里使用字典的形式返回,方便后续构建模型
    dense_input_dict, sparse_input_dict = build_input_layers(dnn_feature_columns)
    # 构建模型的输入层,模型的输入层不能是字典的形式,应该将字典的形式转换成列表的形式
    # 注意:这里实际的输入与Input()层的对应,是通过模型输入时候的字典数据的key与对应name的Input层
    input_layers = list(dense_input_dict.values()) + list(sparse_input_dict.values())
    
    # 构建维度为k的embedding层,这里使用字典的形式返回,方便后面搭建模型
    embedding_layer_dict = build_embedding_layers(dnn_feature_columns, sparse_input_dict, is_linear=False)

    #将所有的dense特征拼接到一起
    dense_dnn_list = list(dense_input_dict.values())
    dense_dnn_inputs = Concatenate(axis=1)(dense_dnn_list) # B x n (n表示数值特征的数量)

    # 因为需要将其与dense特征拼接到一起所以需要Flatten,不进行Flatten的Embedding层输出的维度为:Bx1xdim
    sparse_dnn_list = concat_embedding_list(dnn_feature_columns, sparse_input_dict, embedding_layer_dict, flatten=True) 

    sparse_dnn_inputs = Concatenate(axis=1)(sparse_dnn_list) # B x m*dim (n表示类别特征的数量,dim表示embedding的维度)

    # 将dense特征和Sparse特征拼接到一起
    dnn_inputs = Concatenate(axis=1)([dense_dnn_inputs, sparse_dnn_inputs]) # B x (n + m*dim)

    # 输入到dnn中,需要提前定义需要几个残差块
    output_layer = get_dnn_logits(dnn_inputs, block_nums=3)

    model = Model(input_layers, output_layer)
    return model


if __name__ == "__main__":
    # 读取数据
    data = pd.read_csv('./data/criteo_sample.txt')

    # 划分dense和sparse特征
    columns = data.columns.values
    dense_features = [feat for feat in columns if 'I' in feat]
    sparse_features = [feat for feat in columns if 'C' in feat]

    # 简单的数据预处理
    train_data = data_process(data, dense_features, sparse_features)
    train_data['label'] = data['label']

    # 将特征做标记
    dnn_feature_columns = [SparseFeat(feat, vocabulary_size=data[feat].nunique(),embedding_dim=4)
                            for feat in sparse_features] + [DenseFeat(feat, 1,)
                            for feat in dense_features]

    # 构建DeepCrossing模型
    history = DeepCrossing(dnn_feature_columns)

    history.summary()
    history.compile(optimizer="adam", 
                loss="binary_crossentropy", 
                metrics=["binary_crossentropy", tf.keras.metrics.AUC(name='auc')])

    # 将输入数据转化成字典的形式输入
    train_model_input = {name: data[name] for name in dense_features + sparse_features}
    # 模型训练
    history.fit(train_model_input, train_data['label'].values,
            batch_size=64, epochs=5, validation_split=0.2, )
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容