论文介绍
DCN-v2优化了DCN的cross layer,权重参数w由原来的vector变为方阵matrix,增加了网络层的表达能力;同时,为了保证线上应用的耗时不会因为cross layer参数量的增加而增加。观察到cross layer的matrix具有低秩性,使用矩阵分解,将方阵matrix转换为两个低维的矩阵、最后在低秩空间内,利用MoE多专家系统,对特征交叉做非线性变化,进一步增加对交叉特征的建模。vector -> matrix; moe
设想:DCNv2是不是可以结合多任务学习、MMoE架构,设计一个新的点击率、转换率模型。效果会不会很好呢?不过,这种方法对训练样本量可能要求比较高。可以尝试一下。
构建推荐系统的关键点在于学习有效的交叉特征。特征的交叉一般通过哈达玛积来表示,比如x1表示性别,男、女;x2表示是否喜欢体育;x1&x2联合特征,会有4种取值。通过特征交叉,可以给模型带来一定的非线性表示。DCN在实际应用中,当处理十亿级别的训练数据样本时,其Cross网络部分在建模特征交叉时表达能力受限。尽管,交叉特征建模在学术上已经提出了很多新的技术方法, 但在实际工程中,许多深度学习模型仍然是通过传统的前馈神经网络来实现特征交叉建模。
通盘考虑DCN网络的优缺点以及交叉特征的建模方式,这里提出了一种新模型架构DCNv2:优化DCN的Cross部分,丰富其对交叉特征的建模能力。
我们直接对比DCN、DCNv2的网络架构。、
两者的差别在于Cross网络的建模方式。
DCN Cross layer:
DCNv2 Cross layer:
网络层权重参数由原来的vector变为matrix。DCN网络的cross layer的建模是element-wise;DCNv2 cross layer可以实现element-wise和feature-wise的特征交叉。
我们先回归一下DCN网络模型。DCN借鉴Google的Wide&Deep模型结构,Deep部分是一个N的MLP,用于学习隐性高阶交叉特征;Cross网络通过显性的交叉公式建模高阶交叉特征。cross网络的参数为两个d维度的向量:w和b。通过公式优化,可以简化运算,不会影响模型的线上耗时。这是DCN网络的优点。
但是,DCN网络的Cross网络、Deep网络的参数量不平衡,Deep部分的参数占据DCN总参数的绝大部分。
举例来说,对于1层cross和1层deep的DCN网络输入经过embedding和stack处理后维度为d,Cross部分网络参数为2d,Deep为d*d,当MLP的层数增多时,deep部分的参数量也急速增加。DCN网路的绝大部分参数都用于对隐性交叉特征进行建模。Cross部分的表达能力反而受限。
DCN-v2优化了cross网络的建模方式,增加了cross网络部分的表达能力;deep部分保持不变。下面主要就Cross网络进行详细介绍。
从原来的向量变为矩阵。由于线上应用对模型的耗时、资源占用要求很高。原来的DCN的网络正是由于Cross网络既能对交叉特征进行显性建模,而且对模型的耗时影响忽略不计。如果改进后的cross layer直接应用到线上,cross网络虽然会增加模型的表达能力,但是cross部分的权重参数会给模型的耗时带来影响。所以,为了能进一步减少cross网络的模型耗时,论文通过实验发现了cross layer的权重参数具有低秩性。
我们可以对cross layer的权重参数进行矩阵分解,将原来[d,d]的W方阵,分解为两个矩阵U,V,[d,r], r << d. 这样就可以实现cross layer的参数缩减,同时也增加了cross网络的表达能力。
通过上面的公式,我们可以发现:
- cross在低维空间进行交叉特征的建模;
- 我们将输入x从维度d映射到r,然后又映射会维度d。[d >> r].
低维空间的交叉特征建模使得我们可以利用MoE。MoE由两部分组成:experts专家和gating门(一个关于输入x的函数)。我们可以使用多个专家,每个专家学习不同的交叉特征,最后通过gating将各个专家的学习结果整合起来,作为输出。这样就又能进一步增加对交叉特征的建模能力。
同时,我们也可以对低纬映射空间中做更多的非线性变化,进一步增加模型的表达能力。
通过实验发现,对比其他模型,DCN-v2表现更好。
其他,需要注意的是。论文提出,deep和cross的结合方式,选用并行or串行结构,与训练数据密切相关。不同的数据分布,应该选择不同的处理方式,并行结构并不是通用的。
源码阅读
Cross Layer
将DCN的Cross layer和不使用MoE结构的cross layer。两者的区别在于交叉特征的建模方式,权重参数一个是向量,一个是矩阵。
[图片上传失败...(image-801292-1609475511103)]
class CrossNet(nn.Module):
"""The Cross Network part of Deep&Cross Network model,
which leans both low and high degree cross feature.
Input shape
- 2D tensor with shape: ``(batch_size, units)``.
Output shape
- 2D tensor with shape: ``(batch_size, units)``.
Arguments
- **in_features** :输入特征维度
- **input_feature_num**: Positive integer, shape(Input tensor)[-1]
- **layer_num**: cross网络的网络层数
- **parameterization**: "vector" or "matrix" ,表示网络层的建模方式
- **l2_reg**: 正则系数
- **seed**: 随机种子
"""
def __init__(self, in_features, layer_num=2, parameterization='vector', seed=1024, device='cpu'):
super(CrossNet, self).__init__()
self.layer_num = layer_num
self.parameterization = parameterization
# 参数声明
if self.parameterization == 'vector': # DCN
# weight in DCN. (in_features, 1)
self.kernels = torch.nn.ParameterList([nn.Parameter(nn.init.xavier_normal_(torch.empty(in_features, 1))) for i in range(self.layer_num)])
elif self.parameterization == 'matrix': # DCNv2
# weight matrix in DCN-M. (in_features, in_features)
self.kernels = torch.nn.ParameterList([nn.Parameter(nn.init.xavier_normal_(torch.empty(in_features, in_features))) for i in range(self.layer_num)])
else: # error
raise ValueError("parameterization should be 'vector' or 'matrix'")
self.bias = torch.nn.ParameterList([nn.Parameter(nn.init.zeros_(torch.empty(in_features, 1))) for i in range(self.layer_num)])
self.to(device)
def forward(self, inputs):
x_0 = inputs.unsqueeze(2)
x_l = x_0
for i in range(self.layer_num):
if self.parameterization == 'vector':
# x0 * (xl.T * w )+ b + xl
xl_w = torch.tensordot(x_l, self.kernels[i], dims=([1], [0]))
dot_ = torch.matmul(x_0, xl_w)
x_l = dot_ + self.bias[i]
elif self.parameterization == 'matrix':
# x0 * (Wxl + bl) + xl
dot_ = torch.matmul(self.kernels[i], x_l) # W * xi (bs, in_features, 1)
dot_ = dot_ + self.bias[i] # W * xi + b
dot_ = x_0 * dot_ # x0 · (W * xi + b) Hadamard-product
else: # error
print("parameterization should be 'vector' or 'matrix'")
pass
x_l = dot_ + x_l
x_l = torch.squeeze(x_l, dim=2)
return x_l
Cross Network + MoE
计算每个专家的输出,然后通过gating将多个专家的输出进行加权平均。
class CrossNetMix(nn.Module):
"""DCN-Mix:
1 添加MoE,学习不同子空间内的交叉特征
2 在低维空间增加更多的非线性变换
Input shape
- 2D tensor with shape: ``(batch_size, units)``.
Output shape
- 2D tensor with shape: ``(batch_size, units)``.
Arguments
- **in_features** : 输入特征维度
- **low_rank** : 低维度空间的维度
- **num_experts** : 专家数目.
- **layer_num**: cross网路层数
- **device**:"cpu" or "cuda:0"
"""
def __init__(self, in_features, low_rank=32, num_experts=4, layer_num=2, device='cpu'):
super(CrossNetMix, self).__init__()
self.layer_num = layer_num
self.num_experts = num_experts
# 参数声明
# U: (in_features, low_rank)
self.U_list = torch.nn.ParameterList([nn.Parameter(nn.init.xavier_normal_(
torch.empty(num_experts, in_features, low_rank))) for i in range(self.layer_num)])
# V: (in_features, low_rank)
self.V_list = torch.nn.ParameterList([nn.Parameter(nn.init.xavier_normal_(
torch.empty(num_experts, in_features, low_rank))) for i in range(self.layer_num)])
# C: (low_rank, low_rank)
self.C_list = torch.nn.ParameterList([nn.Parameter(nn.init.xavier_normal_(
torch.empty(num_experts, low_rank, low_rank))) for i in range(self.layer_num)])
self.gating = nn.ModuleList([nn.Linear(in_features, 1, bias=False) for i in range(self.num_experts)])
self.bias = torch.nn.ParameterList([nn.Parameter(nn.init.zeros_(
torch.empty(in_features, 1))) for i in range(self.layer_num)])
self.to(device)
def forward(self, inputs):
x_0 = inputs.unsqueeze(2) # (bs, in_features, 1)
x_l = x_0
for i in range(self.layer_num):# 逐个网络层
output_of_experts = []
gating_score_of_experts = []
for expert_id in range(self.num_experts): # 每个的若干个专家
# 针对每个专家,计算gating score,output
# (1) G(x_l)
# 根据输入计算gating值,最后softmax计算权重系数
gating_score_of_experts.append(self.gating[expert_id](x_l.squeeze(2)))
# (2) E(x_l)
# 计算每个专家的输出
# 2.1 将输入映射到低维空间
v_x = torch.matmul(self.V_list[i][expert_id].t(), x_l) # (bs, low_rank, 1)
# 2.2 在低维空间进行非线性变换
v_x = torch.tanh(v_x)
v_x = torch.matmul(self.C_list[i][expert_id], v_x)
v_x = torch.tanh(v_x)
# 2.3 从低维空间映射回高维度
uv_x = torch.matmul(self.U_list[i][expert_id], v_x) # (bs, in_features, 1)
dot_ = uv_x + self.bias[i]
# 哈达玛积:逐元素乘法
dot_ = x_0 * dot_ # Hadamard-product
output_of_experts.append(dot_.squeeze(2))# [bs, d]
# (3) 专家输出结果加权平均,得到这一层网络的输出内容
output_of_experts = torch.stack(output_of_experts, 2) # (bs, in_features, num_experts)
gating_score_of_experts = torch.stack(gating_score_of_experts, 1) # (bs, num_experts, 1)
moe_out = torch.matmul(output_of_experts, gating_score_of_experts.softmax(1))
x_l = moe_out + x_l # (bs, in_features, 1)
x_l = x_l.squeeze() # (bs, in_features)
return x_l
进一步思考:将DCN-V2和MTL多任务学习结合起来效果会怎么样呢?
后续打算,参考PyTorch实现,写一版TensorFlow版本的DCN-v2。