当前位置:   article > 正文

推荐算法实战项目:Deep Crossing 模型原理以及案例实战(附完整 Python 代码)

deep crossing

本文要介绍的Deep Crossing模型是由微软研究院在论文《Deep Crossing: Web-Scale Modeling without Manually Crafted Combinatorial Features》中提出的,它主要是用来解决大规模特征自动组合问题,从而减轻或者避免手工进行特征组合的开销。

Deep Crossing可以说是深度学习CTR模型的最典型和基础性的模型。

完整源码

技术要学会分享、交流,不建议闭门造车。一个人走的很快、一堆人可以走的更远。

文章中的完整源码、资料、数据、技术交流提升, 均可加知识星球交流群获取,群友已超过2000人,添加时切记的备注方式为:来源+兴趣方向,方便找到志同道合的朋友。

方式①、添加微信号:mlc2060,备注:来自 获取推荐资料
方式②、微信搜索公众号:机器学习社区,后台回复:推荐资料

背景知识

传统机器学习算法充分利用所有的输入特征来对新实例进行预测和分类。但是,仅仅使用原始特征很难获得最佳结果。因此无论是在工业界还是学术界,都进行着大量的工作来对原始特征进行转换。一种有效的特征转换方式是进行多种特征的组合,然后将融合后的特征输入到学习器中去。

组合特征在很多领域已经被证实能发挥强大的功能。在Kaggle社区里,顶级的数据科学家往往都十分擅长特征融合,甚至能横跨3~5个维度进行特征组合,直觉和创造有效组合特征的能力是他们赢得比赛的制胜法宝。

同理,在图像识别等领域,类似SIFT的特征提取也是某些算法能够在ImageNet等数据集上取得最佳结果的关键因素。然而,进行高效的特征融合却需要高昂的成本代价。

随着特征数量的增加,管理,维护变得充满挑战,尤其是在大规模的网络应用程序中。庞大的搜索空间和样本数量,导致训练和评估变得异常缓慢,因此寻找额外的组合特征来改进现有模型是一项艰巨的任务。

深度学习模型天然就可以从独立特征中进行学习,并且无需人工干预。在计算机视觉以及自然语言处理等领域已经发挥出了它强大的功能,比如基于CNN的模型在图像识别比赛中取得的成绩就已经超过了基于传统手工特征SIFT的相关方法取得的最好成绩。

Deep Crossing模型将深度学习从图像和自然语言处理等领域扩展到了更加广泛的环境中,比如每个输入特征都具有不同的性质。更具体地说,它可以输入诸如文本、类别、ID以及数值信息等特征,并且根据特定任务要求,自动搜索最佳的特征组合。

模型介绍

首先给出Deep Crossing的整体模型架构图,如下:

Deep Crossing模型架构图

模型的输入是一系列的独立特征,模型总共包含4层,分别是Embedding层、Stacking层、Residual Unit层、Scoring层,模型的输出是用户点击率预测值。
注意上图中红色方框部分,输入特征没有经过Embedding层就直接连接到了Stacking层了。这是因为输入特征可能是稠密的也可能是稀疏的,论文中指出,对于维度小于256的特征直接连接到Stacking层。

损失函数

论文中使用的是交叉熵损失函数,但是也可以使用Softmax或者其他损失函数。定义如下:

Embedding层

Embedding层的主要作用是对输入特征进行特征转换,Embedding层包含了一个单层神经网络,网络定义如下:

Stacking层

当得到了所有输入特征的Embedding表示之后(特征维度小于256的除外)。Stacking层所做的事情只是简单把这些特征聚合起来,形成一个向量。表示如下:


Residual层

残差层的是由下图所示的残差单元构建成的。残差单元如下所示:

残差单元

Deep Crossing模型中使用的残差单元与ResNet中使用的不太一样,它不包含卷积操作。残差单元的特有属性就是将输入加到了隐层的输出上,上图所示的残差单元的计算可由下式来表示:

Scoring层

Residual层的输出首先连接到全连接层,其次再经过Sigmoid激活函数,最后输出的是一个广告的预测点击率。

代码实践

论文中给出了基于CNTK的伪代码实现,如下:

模型训练和测试使用的是内部的一个数据集。

我使用pytorch对代码进行了改写,并且基于criteo数据集进行训练和测试。模型部分代码如下:

import torch
import torch.nn as nn

class ResidualBlock(nn.Module):
    def __init__(self, input_dim, hidden_dim):
        super(ResidualBlock, self).__init__()
        self.linear1 = nn.Linear(in_features=input_dim, out_features=hidden_dim, bias=True)
        self.linear2 = nn.Linear(in_features=hidden_dim, out_features=input_dim, bias=True)

    def forward(self, x):
        out = self.linear2(torch.relu(self.linear1(x)))
        out += x
        out = torch.relu(out)
        return out

class DeepCrossing(nn.Module):
    def __init__(self, config, dense_features_cols, sparse_features_cols):
        super(DeepCrossing, self).__init__()
        self._config = config
        # 稠密特征的数量
        self._num_of_dense_feature = dense_features_cols.__len__()
        # 稠密特征
        self.sparse_features_cols = sparse_features_cols
        self.sparse_indexes = [idx for idx, num_feat in enumerate(self.sparse_features_cols) if num_feat > config['min_dim']]
        self.dense_indexes = [idx for idx in range(len(self.sparse_features_cols)) if idx not in self.sparse_indexes]

        # 对特征类别大于config['min_dim']的创建Embedding层,其余的直接加入Stack层
        self.embedding_layers = nn.ModuleList([
            # 根据稀疏特征的个数创建对应个数的Embedding层,Embedding输入大小是稀疏特征的类别总数,输出稠密向量的维度由config文件配置
            nn.Embedding(num_embeddings = self.sparse_features_cols[idx], embedding_dim=config['embed_dim'])
                for idx  in self.sparse_indexes
        ])

        self.dim_stack = self.sparse_indexes.__len__()*config['embed_dim'] + self.dense_indexes.__len__() + self._num_of_dense_feature

        self.residual_layers = nn.ModuleList([
            # 根据稀疏特征的个数创建对应个数的Embedding层,Embedding输入大小是稀疏特征的类别总数,输出稠密向量的维度由config文件配置
            ResidualBlock(self.dim_stack, layer)
            for layer in config['hidden_layers']
        ])

        self._final_linear = nn.Linear(self.dim_stack, 1)

    def forward(self, x):
        # 先区分出稀疏特征和稠密特征,这里是按照列来划分的,即所有的行都要进行筛选
        dense_input, sparse_inputs = x[:, :self._num_of_dense_feature], x[:, self._num_of_dense_feature:]
        sparse_inputs = sparse_inputs.long()

        sparse_embeds = [self.embedding_layers[idx](sparse_inputs[:, i]) for idx, i in enumerate(self.sparse_indexes)]
        sparse_embeds = torch.cat(sparse_embeds, axis=-1)

        # 取出sparse中维度小于config['min_dim']的Tensor
        indices = torch.LongTensor(self.dense_indexes)
        sparse_dense = torch.index_select(sparse_inputs, 1, indices)

        output = torch.cat([sparse_embeds, dense_input, sparse_dense], axis=-1)

        for residual in self.residual_layers:
            output = residual(output)

        output = self._final_linear(output)
        output = torch.sigmoid(output)
        return output

    def saveModel(self):
        torch.save(self.state_dict(), self._config['model_name'])

    def loadModel(self, map_location):
        state_dict = torch.load(self._config['model_name'], map_location=map_location)
        self.load_state_dict(state_dict, strict=False)

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71

测试部分代码:

import torch
from DeepCrossing.trainer import Trainer
from DeepCrossing.network import DeepCrossing
from Utils.criteo_loader import getTestData, getTrainData
import torch.utils.data as Data

deepcrossing_config = \
{
    'embed_dim': 8, # 用于控制稀疏特征经过Embedding层后的稠密特征大小
    'min_dim': 256, # 稀疏特征维度小于min_dim的直接进入stack layer,不用经过embedding层
    'hidden_layers': [512,256,128,64,32],
    'num_epoch': 30,
    'batch_size': 32,
    'lr': 1e-3,
    'l2_regularization': 1e-4,
    'device_id': 0,
    'use_cuda': False,
    'train_file': '../Data/criteo/processed_data/train_set.csv',
    'fea_file': '../Data/criteo/processed_data/fea_col.npy',
    'validate_file': '../Data/criteo/processed_data/val_set.csv',
    'test_file': '../Data/criteo/processed_data/test_set.csv',
    'model_name': '../TrainedModels/DeepCrossing.model'
}

if __name__ == "__main__":
    ####################################################################################
    # DeepCrossing 模型
    ####################################################################################
    training_data, training_label, dense_features_col, sparse_features_col = getTrainData(deepcrossing_config['train_file'], deepcrossing_config['fea_file'])
    train_dataset = Data.TensorDataset(torch.tensor(training_data).float(), torch.tensor(training_label).float())
    test_data = getTestData(deepcrossing_config['test_file'])
    test_dataset = Data.TensorDataset(torch.tensor(test_data).float())

    deepCrossing = DeepCrossing(deepcrossing_config, dense_features_cols=dense_features_col, sparse_features_cols=sparse_features_col)

    ####################################################################################
    # 模型训练阶段
    ####################################################################################
    # # 实例化模型训练器
    trainer = Trainer(model=deepCrossing, config=deepcrossing_config)
    # 训练
    trainer.train(train_dataset)
    # 保存模型
    trainer.save()

    ####################################################################################
    # 模型测试阶段
    ####################################################################################
    deepCrossing.eval()
    if deepcrossing_config['use_cuda']:
        deepCrossing.loadModel(map_location=lambda storage, loc: storage.cuda(deepcrossing_config['device_id']))
        deepCrossing = deepCrossing.cuda()
    else:
        deepCrossing.loadModel(map_location=torch.device('cpu'))

    y_pred_probs = deepCrossing(torch.tensor(test_data).float())
    y_pred = torch.where(y_pred_probs>0.5, torch.ones_like(y_pred_probs), torch.zeros_like(y_pred_probs))
    print("Test Data CTR Predict...\n ", y_pred.view(-1))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58

点击率预估部分测试结果:

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/花生_TL007/article/detail/494613
推荐阅读
相关标签
  

闽ICP备14008679号