本文要介绍的是由斯坦福大学联合Google的研究人员发表的论文《Deep & Cross Network for Ad Click Predictions》中提出的Deep&Cross模型,简称DCN。
DCN模型是Wide&Deep的改进版本,其中Deep部分的设计思路与Wide&Deep没有发生本质的变化,DCN主要是重新设计了Cross部分以增加特征之间的交互力度,使用了多层特征交叉层对输入向量进行特征交叉,以获得更加强壮的特征表达能力。
摘要
点击率(CTR)预估任务是一个大规模的问题,尤其是对于价值数百亿美元的在线广告业务。在广告界,广告商向发布商付款,以便在发布商的网站上展示其广告。
较为主流的付款方式是根据每次点击付款(cost-per-click),即当用户点击了一次广告之后,发布商就可以向广告商索取费用。因此,对于发布商而言,其预测广告点击率的能力直接决定了其营收。
做出较为准确的点击率预测的关键是要识别出经常预测的特征,并且同时挖掘出不常见的交叉特征信息。然而,用于Web级推荐系统的数据通常是离散的,且大多是类别数据,这就导致了一个非常庞大且稀疏的特征空间,对特征探索提出了巨大的挑战。这也使得大多数大型系统受限于使用简单的线性模型,比如逻辑回归。线性模型很简单,解释性很强,并且计算快速。
然而,这也限制了它的表达能力。交叉特征已经被证明对提高模型的表达能力很有效。然而它一般需要手工特征工程或者纷繁复杂的搜索才能找到;此外,它也很难推广到那些看不见的特征交互上。
作者在这篇论文中引入了一个新型的神经网络结构,即DCN。它避免了传统的针对特定任务的特征工程,依靠神经网络强大的学习能力,在一定程度上实现自动学习交叉特征组合。
Deep&Cross 网络模型
Deep&Cross模型,简称DCN。DCN模型的输入是稠密和稀疏向量,可以自动进行特征学习,有效捕捉有限度的特征交互,学习高度非线性的特征之间的交互,无需复杂繁琐的特征工程和详细的特征搜索,并且具备较低的计算成本。模型的整体结构图如下:
DCN模型整体结构比较简单,下面分别进行描述。
Embedding和Stack层
输入数据包含稠密和稀疏向量。在Web级的推荐系统任务中,比如点击率预估,其输入大都是类别向量,比如“国家=中国”。这样的特征通常被编码成one-hot向量,比如这样“[0,1,0]”;但是如果稀疏向量很多,且类别较大,那么这将导致一个超高维的特征空间,同时造成空间浪费以及计算复杂度高。
为了减少维度,通常采用embedding处理(即上图红色矩形框部分),将这些二值化特征转化为包含实数的稠密向量,也叫做嵌入向量(embedding vector)。转换公式如下:
Cross网络
Cross网络是DCN模型中最关键的部分,它以一种高效的方式来进行特征交叉。Cross网络包含若干个cross层,每一层都通过以下公式计算:
cross层的工作示意图如下:
现在大家应该可以理解cross layer计算公式的设计意图了。这个例子可以帮助我们更加深入理解cross层的设计:
- 有限高阶:叉乘阶数由网络深度决定,深度对应最高的阶的叉乘
- 自动叉乘: Cross输出包含了原始特征从一阶到n阶的所有叉乘组合,而模型的参数量仅仅随着输入维度成线性增加。
- 参数共享:不同叉乘项对应的权重不同,单并非每个叉乘组合对应独立的权重,通过参数共享,cross有效降低了参数量。此外,参数共享还使得模型具有更强的泛化性和鲁棒性。
复杂度计算
论文表示,cross网络之所以能够高效地学习组合特征,就是因为秩为1,使得我们不用计算并存储整个矩阵就可以得到所有的cross items。
我们再来观察一下cross层的计算迭代公式:
实际代码调试的时候,两种方法都实验了,在我的电脑上测试发现,使用优化过的计算方式大概可以将速度提升6倍左右。
Deep网络
Deep层比较简单,就是一个全连接前向神经网络,每一层的计算方式如下:
复杂度计算
出于简化目的,我们假设所有的Deep层都是通用的大小。
聚合层
聚合的作用是将Deep和Cross网络的输出聚合到一个向量中,并且通过一个标准的逻辑层。计算公式如下:
损失函数是一个标准的交叉熵函数加上一个正则项:
主要贡献
DCN的主要贡献主要包含以下几点:
- 提出一种新型的交叉网络结构,可以用来提取交叉组合特征,并不需要人为设计的特征工程
- 这种网络结构足够简单同时也很有效,可以获得随网络层数增加而增加的多项式阶(polynomial degree)交叉特征
- 十分节约内存(依赖于正确地实现),并且易于使用
- 实验结果表明,DCN相比于其他模型有更出色的效果,与DNN模型相比,较少的参数却取得了较好的效果
完整源码
文章中的完整源码,添加微信号:mlc2060,备注:获取推荐资料
部分代码
模型部分代码,主要包含了Deep和Cross,以及DeepCross模型实现,特别要注意的就是Cross模型中的矩阵计算的时候,有两种方式,默认用的是优化过后的方法。
import torch
import torch.nn as nn
from BaseModel.basemodel import BaseModel
class Deep(nn.Module):
def __init__(self, input_dim, deep_layers):
super(Deep, self).__init__()
deep_layers.insert(0, input_dim)
deep_ayer_list = []
for layer in list(zip(deep_layers[:-1], deep_layers[1:])):
deep_ayer_list.append(nn.Linear(layer[0], layer[1]))
deep_ayer_list.append(nn.BatchNorm1d(layer[1], affine=False))
deep_ayer_list.append(nn.ReLU(inplace=True))
self._deep = nn.Sequential(*deep_ayer_list)
def forward(self, x):
out = self._deep(x)
return out
class Cross(nn.Module):
"""
the operation is this module is x_0 * x_l^T * w_l + x_l + b_l for each layer, and x_0 is the init input
"""
def __init__(self, input_dim, num_cross_layers):
super(Cross, self).__init__()
self.num_cross_layers = num_cross_layers
weight_w = []
weight_b = []
batchnorm = []
for i in range(num_cross_layers):
weight_w.append(nn.Parameter(torch.nn.init.normal_(torch.empty(input_dim))))
weight_b.append(nn.Parameter(torch.nn.init.normal_(torch.empty(input_dim))))
batchnorm.append(nn.BatchNorm1d(input_dim, affine=False))
self.weight_w = nn.ParameterList(weight_w)
self.weight_b = nn.ParameterList(weight_b)
self.bn = nn.ModuleList(batchnorm)
测试数据是criteo数据集的一个很小的子集,测试代码如下:
import torch
from DeepCross.trainer import Trainer
from DeepCross.network import DeepCross
from Utils.criteo_loader import getTestData, getTrainData
import torch.utils.data as Data
deepcross_config = \
{
'deep_layers': [256,128,64,32], # 设置Deep模块的隐层大小
'num_cross_layers': 4, # cross模块的层数
'num_epoch': 2,
'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/DeepCross.model'
}
if __name__ == "__main__":
####################################################################################
# DeepCross 模型
####################################################################################
training_data, training_label, dense_features_col, sparse_features_col = getTrainData(deepcross_config['train_file'], deepcross_config['fea_file'])
train_dataset = Data.TensorDataset(torch.tensor(training_data).float(), torch.tensor(training_label).float())
test_data = getTestData(deepcross_config['test_file'])
test_dataset = Data.TensorDataset(torch.tensor(test_data).float())
deepCross = DeepCross(deepcross_config, dense_features_cols=dense_features_col, sparse_features_cols=sparse_features_col)
####################################################################################
# 模型训练阶段
####################################################################################
# # 实例化模型训练器
trainer = Trainer(model=deepCross, config=deepcross_config)
# 训练
trainer.train(train_dataset)
# 保存模型
trainer.save()
####################################################################################
# 模型测试阶段
####################################################################################
deepCross.eval()
if deepcross_config['use_cuda']:
deepCross.loadModel(map_location=lambda storage, loc: storage.cuda(deepcross_config['device_id']))
deepCross = deepCross.cuda()
else:
deepCross.loadModel(map_location=torch.device('cpu'))
y_pred_probs = deepCross(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))
测试代码就是对criteo的测试集中的每一个样本数据输出对应的点击率预测,0或者1。以下是部分测试结果: