美文网首页推荐算法
特征交叉系列:XDeepFM理论和实践,显隐融合的高阶交叉

特征交叉系列:XDeepFM理论和实践,显隐融合的高阶交叉

作者: xiaogp | 来源:发表于2023-10-13 16:42 被阅读0次

    关键词:特征交叉推荐算法XDeepFMDeepFMCNNRNN

    内容摘要

    • XDeepFM知识准备
    • XDeepFM结构简析
    • CIN压缩交叉网络计算流程解析
    • XDeepFM在PyTorch下的实践
    • XDeepFM和DCN-V2模型效果对比

    XDeepFM知识准备

    XDeepFM和DCN

    XDeepFM算法作为本专题特征交叉系列的最后一篇,从算法诞生的时间线来说XDeepFM应该放在特征交叉系列:Deep&Cross(DCN-V2)理论和实践之前,XDeepFM受到DCN-V1的显示交叉启发而构建,而在XDeepFM之后DCN-V1的团队又在DCN-V1基础上优化发表了DCN-V2,而前文在介绍DCN的时候直接跳过了DCN-V1,因此本篇回过头来介绍XDeepFM作为高阶交叉的一个补充。

    显式交叉和隐式交叉

    XDeepFM论文的标题为Combining Explicit and Implicit Feature Interactions,即融合了显式和隐式的特征交叉算法,一般的基于FM构建的CTR算法只能做到显式二阶交叉,对于高阶交叉只能将FM结构和DNN做串联或者并联,期望通过DNN赋予FM高阶交叉能力,而DNN是一种隐式的高阶特征交叉。
    对于高阶显式交叉,在DCN和XDeepFM中都设计了特定的交叉网络来完成显示交叉,在DCN中使用Cross Net,在XDeepFM使用CIN压缩交叉网络,这类交叉网络的目的是让特征之间直接进行追位运算并且可以拓展到任意阶,而不是像DNN通过自身的非线性能力叠加FM的二阶交叉来隐式学习到,在XDeepFM中不仅包含交叉网络CIN,还依旧保留了类似DeepFM的并联DNN结构,因此XDeepFM是一种融合了显式和隐式的高阶交叉算法。

    bit-wise交叉和vector-wise交叉

    DCN的高阶交叉采用的是bit-wise,而XDeepFM继承了传统的FM系算法采用vector-wise的方式进行交叉。所谓bit-wise就是输入中不再区分不同特征field的概念,所有embeding的每个元素当成一个特征单元和其他任意元素单元进行交叉,即同一个field下会内部元素进行交叉,而vector-wise是一个field的向量完整地和另一个field的向量进行交叉,field内部元素之间不进行交叉。即XDeepFM中CIN交叉网络还是采用特征向量之间做两两交叉,而不是像DCN中完全抛弃了特征的概念直接将特征的embedding打碎进行交叉。bit-wise和vector-wise这两种方式本身并没有绝对优劣之分。


    XDeepFM结构简析

    XDeepFM网路结构图框架上和DeepFM基本类似,如下图

    XDeepFM网络结构图

    主要包含三块结构:

    • Linear:一阶线性层,直接对原始输入onehot做逻辑回归输出一个标量
    • CIN:显式压缩交叉网络层,将原始输入做embedidng映射,再输入给CIN,最终也输出一个标量
    • DNN:隐式交叉层,将原始输入做embedidng映射,再输入给DNN,最终也输出一个标量

    最终的Output是三个标量相加Sigmoid的结果,和DeepFM相比仅有CIN部分不同与FM,其他结构是一样的。从整体结构来看XDeepFM不复杂,重点在于CIN的结构是怎样的。


    CIN压缩交叉网络计算流程解析

    XDeepFM作者使用卷积神经网络CNN和递归神经网络来类比描述CIN的过程,并不是说CIN中使用了CNN和RNN,而是CIN的结构中有类似CNN和RNN的结构,具体为每一阶的交叉计算按照前后顺序排列,下一阶的交叉输入依赖于上一阶的输出和一个固定额外的输入(一阶特征),这种特性和RNN类似,同时在每一阶内部,通过权重矩阵W类比卷积核对特征两两交叉的结果做压缩提取,这种特性和CNN类似。
    先从RNN开始看,CIN中每一阶交叉结果是一个三维矩阵(batch_size,D,Hn)在论文的配图上一张张图,这些图从左到右依次生成,如下图

    CIN中类似RNN的操作

    图中包含上下两个流程,上方Sum pooling操作得到CIN的输出,下方是从X0到Xk的交叉计算操作

    • Sum pooling:对每一阶的交叉结果做D维度(原始特征的embedding)上求和池化为一个个标量,再求和得到CIN的输出
    • X0到Xk:每一阶的交叉输出Xk都是由前一个交叉输出Xk-1和一个额外的输入X0计算得来,其中Xk-1表示上一阶的交叉结果,X0是固定的一阶特征表达,因此每一阶都是在前一阶基础上和一阶特征再做一次交叉,自然就得到了更高一阶的交叉

    具体如何从Xk-1和X0得到Xk呢?作者采用了vector-wise的哈达玛积对Xk-1和X0做两两特征做笛卡尔积组合,然后再逐位相乘,而第一个X1是由两个X0哈达玛积而来,具体哈达玛积的计算示意图如下


    CIN中的特征交叉哈达玛积计算

    简单而言是矩阵A=(Hk,D)和矩阵B=(m,D)做哈达玛积变成一个矩阵C=(m,Hk,D),和原来相比升了一维,如果带上batch_size,则最终计算结果是一个四维的矩阵(batch_size,m,Hk,D)。
    哈达玛积的结果并不是每一阶的最终结果,作者采用一个W矩阵最为卷积核在哈达玛积结果上沿着D维度做扫描,引入类似CNN的特性,如下图所示


    CIN中类似CNN的操作

    矩阵W会和(m,Hk)做卷积操作,即对应位置全连接求和为一个标量,再沿着D方向依次从上往下扫描,一共生成一个D长度的一维向量,引入Hk+1个W,则在该阶最终输出一个(D,Hk+1)的图,带上batch_size最终在每一阶生成一个三维矩阵(batch_size,D,Hn),该操作和NLP领域的一维卷积Conv1d类似,其中D(特征embedding)类比seq_length。


    XDeepFM在PyTorch下的实践

    本次实践的数据集和上一篇特征交叉系列:完全理解FM因子分解机原理和代码实战一致,采用用户的购买记录流水作为训练数据,用户侧特征是年龄,性别,会员年限等离散特征,商品侧特征采用商品的二级类目,产地,品牌三个离散特征,随机构造负样本,一共有10个特征域,全部是离散特征,对于枚举值过多的特征采用hash分箱,得到一共72个特征。
    XDeepFM的PyTorch代码实现如下

    class Linear(nn.Module):
        def __init__(self, feat_dim):
            super(Linear, self).__init__()
            self.embedding = nn.Embedding(feat_dim, 1)
            self.bias = nn.Parameter(torch.zeros(1))
            nn.init.xavier_normal_(self.embedding.weight.data)
    
        def forward(self, x):
            # [None, field_dim] => [None, field_dim, 1] => [None, 1]
            return torch.sum(self.embedding(x), dim=1) + self.bias
    
    
    class Embedding(nn.Module):
        def __init__(self, feat_dim, emb_dim):
            super(Embedding, self).__init__()
            self.embedding = nn.Embedding(feat_dim, emb_dim)
            nn.init.xavier_normal_(self.embedding.weight.data)
    
        def forward(self, x):
            # [None, field_dim] => [None, field_dim, emb_dim]
            return self.embedding(x)
    
    
    class DNN(nn.Module):
        def __init__(self, input_dim, fc_dim, dropout=0.1):
            super(DNN, self).__init__()
            layers = []
            pre_dim = input_dim
            for dim in fc_dim:
                layers.append(nn.Linear(pre_dim, dim))
                layers.append(nn.BatchNorm1d(dim))
                layers.append(nn.ReLU())
                layers.append(nn.Dropout(dropout))
                pre_dim = dim
            layers.append(nn.Linear(pre_dim, 1))
            self.fc = nn.Sequential(*layers)
    
        def forward(self, x):
            # [None, field_dim * emb_dim] => [None, 1]
            return self.fc(x)
    
    
    class CIN(nn.Module):
        def __init__(self, field_dim, cin_dim):
            super(CIN, self).__init__()
            layers = []
            pre_dim = field_dim * field_dim
            fc_dim = 0
            for dim in cin_dim:
                layers.append(nn.Conv1d(pre_dim, dim, kernel_size=1, stride=1, dilation=1, bias=True))
                pre_dim = dim * field_dim
                fc_dim += dim
            self.cnn = nn.Sequential(*layers)
            self.cin_dim = cin_dim
            self.fc = nn.Linear(fc_dim, 1)
            nn.init.xavier_normal_(self.fc.weight.data)
            for layer in layers:
                nn.init.xavier_normal_(layer.weight.data)
    
        def forward(self, x):
            x0 = x.unsqueeze(2)  # [None, field_dim, emb_dim] => [None, field_dim, 1, emb_dim]
            pre_x = x.unsqueeze(1)  # [None, field_dim, emb_dim] => [None, 1, field_dim, emb_dim]
            pool = []
            for i in range(len(self.cin_dim)):
                # [None, field_dim0, field_dim_pre, emb_dim]
                cross = x0 * pre_x
                batch_size, field_dim_x0, field_dim_pre_x, emb_dim = cross.shape
                # [None, field_dim0 * field_dim_pre, emb_dim]
                cross = cross.reshape(batch_size, field_dim_x0 * field_dim_pre_x, emb_dim)
                # [None, cin_dim, emb_dim]
                cross = nn.functional.relu(self.cnn[i](cross))
                pre_x = cross.unsqueeze(1)
                pool.append(cross)
            return self.fc(torch.sum(torch.concat(pool, dim=1), 2))
    
    
    class XDeepFM(nn.Module):
        def __init__(self, feat_dim, emb_dim, field_dim, cin_dim, fc_dim, dropout=0.1):
            super(XDeepFM, self).__init__()
            self.embedding = Embedding(feat_dim, emb_dim)
            self.linear = Linear(feat_dim=feat_dim)
            self.dnn = DNN(input_dim=field_dim * emb_dim, fc_dim=fc_dim, dropout=dropout)
            self.cin = CIN(field_dim=field_dim, cin_dim=cin_dim)
            self.dnn_dim = field_dim * emb_dim
    
        def forward(self, x):
            # [None, field_dim] => [None, field_dim, emd_dim]
            emb = self.embedding(x)
            linear_out = self.linear(x)  # [None, 1]
            dnn_out = self.dnn(emb.reshape(-1, self.dnn_dim))  # [None, 1]
            cin_out = self.cin(emb)
            out = linear_out + dnn_out + cin_out
            return torch.sigmoid(out).squeeze(dim=1)
    

    其中CIN模块实现了压缩交叉网络,在哈达玛积后直接将四维矩阵的第二第三维(笛卡尔积的两方)进行合并拉直为三维矩阵,使用nn.Conv1d进行卷积特征提取,CIN代码的计算示意图如下

    CIN计算示意图

    另外不同于DCN,XDeepFM将每一阶的CIN结果进行拼接,拼接的结果作为最终的高阶交叉表达,而在DCN中是直接取了最后一阶。
    本例全部是离散分箱变量,所有有值的特征都是1,因此只要输入有值位置的索引即可,一条输入例如

    >>> train_data[0]
    Out[120]: (tensor([ 2, 10, 14, 18, 34, 39, 47, 51, 58, 64]), tensor(0))
    

    其中x的长度10代表10个特征域,每个域的值是特征的全局位置索引,从0到71,一共72个特征。


    XDeepFM和DCN-V2模型效果对比

    由于本例只有10个特征field,因此对XDeepFM设置CIN层的每层的W输出维度固定为10,则每层是10×10的交叉,分别尝试CIN从1层到4层,对于DNN部分,固定为三层,隐藏层维度分别为128,64,32。以验证集10次AUC不上升作为早停条件,CIN层数不同下验证集平均AUC如下:

    CIN层数 参数量 AUC
    1层(2阶) 33671 0.6307
    2层(3阶) 34691 0.6337
    3层(4阶) 35711 0.6322
    4层(5阶) 36731 0.6316

    其中在2层CIN达到最佳AUC为0.6337,随着CIN层数的上升模型参数小幅度线性增长。
    进一步将XDeepFM和本系列前文中和FM隐式高阶交叉算法,以及DCN-V2显式高阶交叉算法做对比,AUC和参数数量如下

    算法 AUC 参数量
    FM 0.6274 361
    FFM 0.6317 2953
    PNN* 0.6342 29953
    DeepFM 0.6322 12746
    NFM 0.6329 10186
    DCN-parallel-3 0.6348 110017
    XDeepFM-CIN-2 0.6337 34691

    从AUC来看XDeepFM在本数据集上超过大部分FM系算法,但是落后于DCN-V2和内外积融合的PNN,从学习参数量来看XDeepFM略高于PNN,远没有另一种显式高阶交叉DCN-V2参数量庞大,整体表现中规中举。

    相关文章

      网友评论

        本文标题:特征交叉系列:XDeepFM理论和实践,显隐融合的高阶交叉

        本文链接:https://www.haomeiwen.com/subject/wqwbbdtx.html