《原始论文:Deep & Cross Network for Ad Click Predictions》
Deep&Cross模型(DCN)其实就是对Wide&Deep模型中Wide部分进行改进的模型。
Wide&Deep模型原理很简单,但是最主要的是要掌握Wide&Deep这种线性和非线性,处理高维稀疏向量和embedding稠密向量的方式,能够使得模型同时具有泛化能力和记忆能力。能够根据最新数据在最短时间内以最小的代价做出回应(更新参数)。
一、Deep&Cross结构
Deep&Cross模型是2017年由斯坦福大学和谷歌在ADKDD会议上联合提出的,该模型是对Wide&Deep模型的一种改进。
- 由于Wide&Deep模型的Wide部分的特征交互需要特征工程,而手工设计特征工程非常的繁琐。
- 2阶的FM模型在线性的时间复杂度中自动进行特征交互,但是这些特征交互的表现能力并不够,并且随着阶数的上升,模型复杂度会大幅度提高。
所以作者对Wide部分进行更改,提出了一个Cross Network来自动进行特征之间的交叉,并且网络的时间和空间复杂度都是线性的。
通过与Deep部分相结合,构成了深度交叉网络(Deep&Cross Network),简称DCN。
模型的结构也非常简洁,从下往上依次为:Embedding和Stacking层、Cross网络层与Deep网络层并列、输出合并层,得到最终的预测结果。
可以看到,和W&D模型不同的是在embedding部分和Cross部分,在本文中也是着重讲一下这两部分。其中我打算先讲一下Cross层再讲embedding。
1、Cross 部分
这是本文最大的创新点—Cross网络(Cross Network),设计该网络的目的是增加特征之间的交互力度。交叉网络由多个交叉层组成,假设第l层的输出向量是
x
l
x_l
xl,那么对于第
l
+
1
l+1
l+1 层的输出向量
x
l
+
1
x_{l+1}
xl+1 表示为:
可以看到, 交叉层的操作的二阶部分非常类似PNN提到的外积操作, 在此基础上增加了外积操作的权重向量
w
l
w_l
wl, 以及原输入向量
x
l
x_l
xl 和偏置向量
b
l
b_l
bl。 交叉层的可视化如下:
其实看到这里,Cross模型的基本思想就被讲完了,但是并没有讲清楚。
接下里我会盘点一下这样做的好处,并且从我的理解角度一一进行解析,希望能给小伙伴们带来一个全新的理解。首先上一个大佬总结的例子来进行分析。
2、Embedding层
为什么会后面讲这个呢,主要是不知道大家在前面发现没有Cross层只有一个特征的输入即 x 0 x_0 x0。
我当时看的时候一直在想,输出的特征不应该是二维的么,为什么这里是一维的,那么这个一维的 x x x 是代表每一个用户的全部特征呢,还是同一特征(如ID)下所有用户的数据呢?后面才看明白,原来是同一特征(如ID)下所有用户的数据,只不过这里的 x 0 x_0 x0 是将所有的特征都转化成了一维向量进行输入。
用一位大佬解释的截图来说明,这里我就不多打文字解释了。
后面的代码复现也会体现出这些来的。
3、其他层
二、代码实现
class CrossNetwork(nn.Module):
"""
Cross Network
"""
def __init__(self, layer_num, input_dim):
super(CrossNetwork, self).__init__()
self.layer_num = layer_num
# 定义网络层的参数
self.cross_weights = nn.ParameterList([nn.Parameter(torch.rand(input_dim, 1)) for i in range(self.layer_num)])
self.cross_bias = nn.ParameterList([nn.Parameter(torch.rand(input_dim, 1)) for i in range(self.layer_num)])
def forward(self, x):
# x是(None, dim)的形状, 先扩展一个维度到(None, dim, 1)
x_0 = torch.unsqueeze(x, dim=2)
x = x_0.clone()
xT = x_0.clone().permute((0, 2, 1)) # (None, 1, dim)
for i in range(self.layer_num):
x = torch.matmul(torch.bmm(x_0, xT), self.cross_weights[i]) + self.cross_bias[i] + x # (None, dim, 1)
xT = x.clone().permute((0, 2, 1)) # (None, 1, dim)
x = torch.squeeze(x) # (None, dim)
return x
class DCN(nn.Module):
def __init__(self, feature_columns, hidden_units, layer_num, dnn_dropout=0.):
super(DCN, self).__init__()
self.dense_feature_cols, self.sparse_feature_cols = feature_columns
# embedding
self.embed_layers = nn.ModuleDict({
'embed_' + str(i): nn.Embedding(num_embeddings=feat['feat_num'], embedding_dim=feat['embed_dim'])
for i, feat in enumerate(self.sparse_feature_cols)
})
hidden_units.insert(0, len(self.dense_feature_cols) + len(self.sparse_feature_cols)*self.sparse_feature_cols[0]['embed_dim'])
self.dnn_network = Dnn(hidden_units)
self.cross_network = CrossNetwork(layer_num, hidden_units[0]) # layer_num是交叉网络的层数, hidden_units[0]表示输入的整体维度大小
self.final_linear = nn.Linear(hidden_units[-1]+hidden_units[0], 1)
def forward(self, x):
dense_input, sparse_inputs = x[:, :len(self.dense_feature_cols)], x[:, len(self.dense_feature_cols):]
sparse_inputs = sparse_inputs.long()
sparse_embeds = [self.embed_layers['embed_'+str(i)](sparse_inputs[:, i]) for i in range(sparse_inputs.shape[1])]
sparse_embeds = torch.cat(sparse_embeds, axis=-1)
x = torch.cat([sparse_embeds, dense_input], axis=-1)
# cross Network
cross_out = self.cross_network(x)
# Deep Network
deep_out = self.dnn_network(x)
# Concatenate
total_x = torch.cat([cross_out, deep_out], axis=-1)
# out
outputs = F.sigmoid(self.final_linear(total_x))
return outputs
参考资料:
推荐算法之Deep&Cross模型
Deep&Cross相比Wide&Deep作了哪些改进?Deep&Cross模型的Cross网络是怎么操作的?
推荐系统百问百答(13):Deep&Cross模型