zoukankan      html  css  js  c++  java
  • 推荐模型DeepCrossing: 原理介绍与TensorFlow2.0实现

    DeepCrossing是在AutoRec之后,微软完整的将深度学习应用在推荐系统的模型。其应用场景是搜索推荐广告中,解决了特征工程,稀疏向量稠密化,多层神经网路的优化拟合等问题。所使用的特征在论文中描述为两个大类数值型(文中couting feature)和类别型。如下图
    image

    对于数值型特征可以直接拼接在Embedding向量之后,类别多的特征需要经过Embedding过程。要多说一句,数值的统计特征包括了过去广告点击率,这个在以后实际应用中设计特征可以考虑。

    其优化目标就是广告的点击率,即CTR,click through rate。其效果可以看论文的实现对比部分。这里简单介绍,

    1. 与传统模型DSSM进行对比;
    2. 与线上生产环境的模型进行对比;
    3. counting feature的重要性对比。

    2. 算法架构

    网络架构解决的问题是:

    • 离散特征过于稀疏的高维灾难问题;
    • 特征交叉自动组合问题;
    • 输出层中如何优化目标的设计问题。

    网络架构图

    image
    总共包含Embedding,Stacking,Multiple ResidualUnits和Scoring 层
    下面根据网络结构图分别说明各个模块的作用。

    Embedding层

    本层主要作用是降维。使用的是一个单层神经网路,具有如下形式,
    image
    针对每个类别的特征都有一个Embedding操作,但是如果由于高维基数特征太大了,对于目标相关部分排序较低的进行衍生构造。也能降低Embedding部分的参数数量提高训练速度例如,CampaignID十分巨大,但对于点击率排序后10000以外的使用衍生特征来处理,最后一个编号为10000,且添加衍生为将所有ID对应的历史点击率组合成10001维的稠密矩阵,各个元素分别为对应ID的历史CTR,最后一个元素为剩余ID的平均CTR。通过降维引入衍生特征的方式,可以有效的减少高基数特征带来的参数量剧增问题。

    其中,每个特征的维度压缩到256维,如果小于256维则直接连接到Stacking层。

    Stacking层

    主要是将Embedding部分的各个特征的向量进行拼接,小于256维度或者数值型特征不需要Embedding的直接拼接(如Feature #2)。
    得到(X^O=[X^O_0, X^O_1,...,X^O_k])的拼接向量。

    Residual Layers

    首先是残差单元结构为:
    image

    这个残差模块与ResNet的不同是没有使用卷积操作,而是ReLu与线性部分的前向传播加(element-wise add)上输入再经过ReLu得到输出。
    image
    作者通过各种类型各种大小的实验发现,DeepCrossing具有很好的鲁棒性,推测可能是因为残差结构能起到类似于正则的效果,残差结构能更敏感的捕获输入输出之间的信息差 ,引入特征的交叉和非线性。

    残差网络解决的问题:

    • 网络深度增加后,过拟合,通过残差网络的短路操作,起到正则化的作用,减少过拟合;
    • 网络深度增加后,梯度消失,所以使用ReLu激活函数,且短路操作相当于将上上层的梯度传递到下层,收敛更快。

    原结构使用了五个残差块,每个残差块的维度是512,512,256,128,64。

    Scoring Layer

    计算得分,即目标函数(objective function)的应用层。
    image

    二分类使用Sigmoid函数,多分类使用softmax函数。

    3. 代码实现

    基于TensorFlow2.0 和Keras API来实现模型结构。

    根据上节每个模块,需要分别实现各个模型的结构,然后组合在一个即可。(原始论文的部分使用的CNTK实现且GPU加速,获得了效率的显著提高)

    导包

    import numpy as np
    import pandas as pd
    import tensorflow as tf
    from tensorflow import keras
    from sklearn.model_selection import train_test_split
    import gc
    

    Embedding模块

    这里自己实现,不使用tf自带的embedding。

    class EmbeddingBlock(keras.layers.Layer):
        def __init__(self, emb_dim, input_shapes):
            super(EmbeddingBlock, self).__init__()
            self.input_shapes = input_shapes
            self.listlayer = []
            for shape in self.input_shapes:
                self.listlayer.append(keras.layers.Dense(emb_dim, input_shape=(shape, ), activation='relu'))
            
        def call(self, X):
            stacking = []
            last_col = 0
            for idx, shape in enumerate(self.input_shapes): # 离散值的onehot维度部分
                stacking.append(self.listlayer[idx](X[:, last_col:last_col+shape]))
                last_col += shape
            stacking.append(X[:, last_col:]) # 连续值
            X = tf.concat(stacking, axis=1)
            return X
    

    这里主要是将输入X的前一部分作为需要embedding的部分,后部分作为不需要embedding的部分,然后并行运算,并最后连接在一起。

    定义残差层

    这里分为两个模块分别定义,没有使用函数,而是直接继承Keras的API。

    class Residual(keras.models.Model):
        def __init__(self,hidden_units=None, feature_dim=None) -> None:
            super(Residual, self).__init__()
            self.relu_layer = keras.layers.Dense(units=hidden_units, input_shape=(feature_dim,), activation='relu')
            self.linear_layer = keras.layers.Dense(units=feature_dim, input_shape=(hidden_units,)) # 为了后续相加,要回归原来的维度
    
        def call(self, X):
            X1 = self.relu_layer(X)
            X2 = self.linear_layer(X1)
            y = keras.activations.relu(tf.add(X, X2)) # or tf.nn.relu, X+X2
            return y
    
    class ResidualLayer(keras.layers.Layer):
        def __init__(self, units_list=None, feature_dim=None) -> None:
            super(ResidualLayer, self).__init__()
            self.listlayer = []
            for unit in units_list:
                self.listlayer.append(Residual(unit, feature_dim))
            
        def call(self, X):
            for layer in self.listlayer:
                X = layer(X)
            return X
    

    串联整个模型DeepCrossing

    class DeepCrossing(keras.models.Model):
        def __init__(self, emb_dim, emb_shapes, residual_units, feature_dims) -> None:
            super().__init__()
            self.emb = EmbeddingBlock(emb_dim=emb_dim, input_shapes=emb_shapes)
            self.stacking_dim = emb_dim*len(emb_shapes) + feature_dims - np.array(emb_shapes).sum()
            self.residual_layer = ResidualLayer(residual_units, self.stacking_dim)
            self.score_layer = keras.layers.Dense(units=1, input_shape=(self.stacking_dim,), activation='sigmoid')
    
        def call(self, X):
            X = self.emb(X)
            X = self.residual_layer(X)
            X = self.score_layer(X)
            return X
    

    4. 数据验证

    说个小插曲,使用的数据是MovieLens,在train_test_split的时候会有一个报错 大概是MemoryError的问题,因为使用的列比较多。后来就抽取了一千条数据来验证模型。估计使用迭代器和tf.data的生成器会比较好操作。

    合并数据

    rating = pd.read_csv('./ratings.dat', sep='::', names=['UserID', 'MovieID', 'Rating', 'Timestamp'])
    user = pd.read_csv('./users.dat', sep='::', names=['UserID', 'Gender', 'Age', 'Occupation', 'ZipCode'])
    movie = pd.read_csv('./movies.dat', sep='::', names=['MovieID', 'Title', 'Genres'])
    
    data = pd.merge(left=rating, right=user, how='inner', on='UserID')
    data = data.merge(movie, on='MovieID')
    

    构造标签

    为了保证正负样本相对平衡,契合评分层的二分类模型,这里直接将3分以上的认为是正样本(也可以定义为多分类 使用softmax层作为评分层)。

    data['label'] = (data['Rating'] > 3).astype(np.int)

    处理数据

    把电影名字的时间抽取出来

    data['Year'] = data['Title'].apply(lambda x: x[-5:-1]).astype(int)
    data['Title'] = data['Title'].apply(lambda x: x[:-7])
    

    为了方便,不使用Title作为特征(否则使用Token然后Embedding处理也是很好的)。

    统计各个特征数量,以便确定谁要Embedding层:

    tmp = data.copy()
    for col in ['Gender', 'Occupation', 'ZipCode', 'Title', 'Genres']:
        print(col, tmp[col].unique().shape[0])
    =============================================
    Gender 2
    Occupation 21
    ZipCode 3439
    Title 3664
    Genres 301
    

    oenhot处理并合并:

    dummy_col = ['ZipCode', 'Genres', 'Gender', 'Occupation']
    tmp1 = pd.get_dummies(tmp[dummy_col], 
                          prefix=dummy_col, 
                          columns=dummy_col)
    resDF = pd.concat([tmp1, tmp[[ 'Age', 'Year','Timestamp','UserID', 'MovieID', 'label']] ], axis=1)
    

    构造Dataset

    X = resDF.iloc[:1000,:-3]
    y = resDF.iloc[:1000, -1]
    num_or_size_splits = [int(y.shape[0]*0.9), int(y.shape[0]*0.1 + 0.5)]
    num_or_size_splits # [900, 100]
    
    X = tf.constant(X.values, dtype=tf.float32)
    y = tf.constant(y.to_list(), dtype=tf.float32)
    X_train, X_test = tf.split(X, num_or_size_splits, axis=0)
    y_train, y_test = tf.split(y, num_or_size_splits, axis=0)
    
    BATCH = 128
    train_ds = tf.data.Dataset.from_tensor_slices((X_train, y_train)).batch(BATCH).shuffle(2).repeat()
    test_ds = tf.data.Dataset.from_tensor_slices((X_test, y_test)).batch(32)
    

    训练模型

    net = DeepCrossing(emb_dim=128, 
                           emb_shapes=[3439, 301],
                           residual_units=[256, 128, 64],
                           feature_dims=len(resDF.columns)-3)
    net.compile(loss='binary_crossentropy',
                    optimizer=keras.optimizers.Adam(lr=0.01),
                    metrics=['accuracy'])
    
    net.fit(train_ds, epochs=5, steps_per_epoch=X.shape[0]//BATCH)
    
    Train for 7 steps
    Epoch 1/5
    7/7 [==============================] - 5s 690ms/step - loss: 160474554.2098 - accuracy: 0.6192
    Epoch 2/5
    7/7 [==============================] - 0s 7ms/step - loss: 30519627.1429 - accuracy: 0.7679
    Epoch 3/5
    7/7 [==============================] - 0s 8ms/step - loss: 6237030.3564 - accuracy: 0.8692
    Epoch 4/5
    7/7 [==============================] - 0s 7ms/step - loss: 6711226.7366 - accuracy: 0.7461
    Epoch 5/5
    7/7 [==============================] - 0s 7ms/step - loss: 3462408.0007 - accuracy: 0.7345
    

    测试集验证:

    loss, acc = net.evaluate(test_ds)
    print('loss: ', loss, ' acc: ', acc)
    
    =================================
    loss:  1159263.28125  acc:  0.93
    

    4. 小结

    Deep Crossing模型没有引入现代流行的注意力机制,序列模型的特殊结构,但是相比FM,FFM模型只具备二阶特征交叉能力来说,这模型可以更深层次的交叉,且独立特征之外,没有人工设计的组合特征。

  • 相关阅读:
    LeetCode(81): 搜索旋转排序数组 II
    2018年6月8日论文阅读
    LeetCode(80):删除排序数组中的重复项 II
    LeetCode(79): 单词搜索
    LeetCode(78):子集
    LeetCode(77):组合
    LeetCode(76): 最小覆盖子串
    LeetCode(75):分类颜色
    LeetCode(74):搜索二维矩阵
    linux 两个查找工具 locate,find
  • 原文地址:https://www.cnblogs.com/sxzhou/p/14532111.html
Copyright © 2011-2022 走看看