[推荐系统]粗排之COLD
1 引言
COLD全程:Computing power cost-aware Online and Lightweight Deep pre-ranking system。是阿里巴巴提出的,主要用于推荐中的粗排阶段。该文将粗排的演化分成4个阶段,
第一阶段:简单的规则
第二阶段:LR
第三阶段:基于DNN的向量内积,其实就是著名的双塔模型
第四阶段:COLD
通过上图可得知,双塔模型是user一个向量,item一个向量,然后最后做cos的相似性,但是无法使用到user和item的一些交叉特征,COLD直接将其作为另外的输入,
从上图看出,双塔模型的这种,就是先联合离线训练,然后再把所有的item和所有的user都离线的经过模型算出各自的DNN输出,然后再线上进行提供服务时,可以采用诸如faiss这种向量引擎系统去计算相似性即可。
2 COLD结构
主要从网络结构上,以及工程上做了几个优化,结构上,就是相比embedding&mlp网络结构增加了SE模块,以及输入的时候,增加了交叉特征
其中SE模块用于获取特征重要性,SE本身并不在后续的训练和部署中
精简网络的方法有很多,例如网络剪枝 ( network pruning),特征筛选 ( feature selection),网络结构搜索 ( neural architecture search)等。我们选择了特征筛选以实现效果和算力的平衡。当然其他技术也可以进行尝试。具体来说,我们把SE (Squeeze-and-Excitation) block引入到了特征筛选过程中,它最初被用于计算机视觉领域以便对不同通道间的内部关系进行建模。这里我们用SE block来得到特征重要性分数。假设一共有M个特征,ei表示第i个特征的embedding向量,SE block把ei压缩成一个实数si。具体来说先将M个特征的embedding拼接在一起,经过全连接层并用sigmoid函数激活以后,得到M维的向量s:
。
这里向量s的第i维对应第i个特征的重要得分,然后再将si乘回到ei,得到新的加权后的特征向量用于后续计算。
在得到特征的重要性得分之后,我们把所有特征按重要性选择最高的top K个特征(每个特征在测试集上每条样本上都会有一个特征重要性得分。这里最后会把每个特征在测试集上所有样本上的重要性求个平均,来代表这个特征的重要性,用于后面的特征选择)作为候选特征,并基于GAUC,QPS和RT指标等离线指标,对效果和算力进行平衡,最终在满足QPS和RT要求情况下,选择GAUC最高的一组特征组合,作为COLD最终使用的特征。后续的训练和线上打分都基于选择出来的特征组合。通过这种方式,可以灵活的进行效果和算力的平衡。
下图是离线训练后的GAUC结果
2.1 SE模块的pytorch实现
如SE模块理解+SE-Resnet模块pytorch实现所示,
上图右边就是增加的SE模块,
SE可以实现注意力机制最重要的两个地方一个是全连接层,另一个是相乘特征融合; 假设输入图像H×W×C,通过global pooling+FC层,拉伸成1×1×C,然后再与原图像相乘,将每个通道赋予权重(即一个通道生成一个权重,该通道的所有神经元共享该权重值)。在去噪任务中,将每个噪声点赋予权重,自动去除低权重的噪声点,保留高权重噪声点,提高网络运行时间,减少参数计算。这也就是SE模块具有attention机制的原因。
import torch
import torch.nn as nn
# 定义residual,
class RB(nn.Module):
def __init__(self, nin, nout, ksize=3, stride=1, pad=1):
super(RB, self).__init__()
self.rb = nn.Sequential(nn.Conv2d(nin, nout, ksize, stride, pad),
nn.BatchNorm2d(nout),
nn.ReLU(inplace=True),
nn.Conv2d(nout, nout, ksize, stride, pad),
nn.BatchNorm2d(nout))
def forward(self, input):
x = input
x = self.rb(x)
return nn.ReLU(input + x)
# 定义SE模块
class SE(nn.Module):
def __init__(self, nin, nout, reduce=16):
super(SE, self).__init__()
self.gp = nn.AvgPool2d(1)
self.rb1 = RB(nin, nout) # 就这里,获取上一个模块的输入和输出
self.se = nn.Sequential(nn.Linear(nout, nout // reduce),
nn.ReLU(inplace=True),
nn.Linear(nout // reduce, nout),
nn.Sigmoid())
def forward(self, input):
x = input
x = self.rb1(x) # 假定输入经过了残差模块,得到输出
# 下面就是SE的核心代码,主要获取每个通道下的权重,即下面的y
# -------------------------------
b, c, _, _ = x.size()
y = self.gp(x).view(b, c)
y = self.se(y).view(b, c, 1, 1)
y = x * y.expand_as(x) # 将y 乘以x的每个通道,即对每个通道进行加权
# ------------------------------
# 下面这是残差网络的部分
out = y + input
return out
net=SE(64,64)
print(net)
3 COLD的工程优化
1)采用gpu,并且基于混合精度,这样可以减少计算量的同时增加QPS,同时使用cuda的MPS特性
2)将向量的乘积或者布局以列向量存储,这主要是考虑到数据在内存条上的存放规则,因为内存条的存放时,cpu去取数据往往是会将相邻的一起取出来,如果数据存储布局合理,将会用到这个特性,比如一个cpu取数据指令取出一条向量,而如果存放的不合理,那么就需要N个取指令才能取出一个完整的向量
3)从服务架构上,将一个user的query分解成内部的多个请求,每个之间并行计算
不过,论文结论是,如下图,
还是双塔模型快
参考文献:
阿里粗排模型-cold
COLD作者的知乎