美文网首页推荐算法
特征交叉系列:FFM场感知因子分解机原理与实践

特征交叉系列:FFM场感知因子分解机原理与实践

作者: xiaogp | 来源:发表于2023-09-04 19:17 被阅读0次

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

    内容摘要

    • 从FM到FFM知识准备
    • FFM原理综述
    • FFM PyTorch实践
    • FFM和FM模型效果对比

    从FM到FFM知识准备

    在上一节中特征交叉系列:完全理解FM因子分解机原理和代码实战介绍了FM算法,FM因子分解机通过在逻辑回归基础上增加所有特征的二阶交互项实现特征的交叉,但是随着特征数的增多二阶交互的数量呈平方级别增长,FM巧妙地给每个特征分配一个低维隐向量,通过隐向量之间的内积点乘来计算二阶交互项的权重,从而使得

    • 减少了二阶权重的参数数量,从 n(n-1)/2 降低到 nk
    • 避免了稀疏数据导致二阶权重无法求解的问题
    • 计算可以优化为训练和预测都是随着特征数量n的线性增长复杂度

    而FFM(Field-aware Factorization Machines)场感知因子分解机,基于FM算法,在FM的公式基础上在二阶隐向量相乘的时候对不同的特征域进行了区分,使得不同业务特征的交叉具有个性化。


    FFM原理综述

    FFM中的field-aware场感知中的场代表特征域,一个特征域就是一个业务特征,而特征域下的特征代表业务特征的取值枚举结果,比如性别是一个特征域,而男,女分别是两个该特征域下的特征。
    在明确何为特征域之后直接看FFM的公式

    FFM公式

    其中只有最右侧二阶特征交互部分和FM略有不一样,内积部分<vi,fj,vj,fi>增加了特征域f,在FM中特征i和特征j的二阶项权重是特征i的隐向量和特征j的隐向量进行内积,而在FFM中,该组二阶权重是特征i的隐向量组中对应于特征j的那个隐向量,和特征j的隐向量组中对应特征i的那个隐向量进行内积,仅此而已,本质上并没有改变FM这种模型的架构模式,FM是FFM的一个特例,及不细分f的作用,全部一视同仁。
    在FFM中每个特征会有一组隐向量,组中隐向量的个数和特征域的个数相等,代表虽然是同一个特征,但是对来自于不同特征域的另一个特征都分配了一个单独不一样的隐向量,因此隐向量的维度是n * f * k,如下图所示


    FFM隐向量示意图

    给定一对特征<A,B>,每个特征分别拿到属于对方特征域的隐向量,而不是像FM使用统一的隐向量,从业务含义上来看,FFM觉得如果一个特征在和其他特征进行交互的时候使用同一个隐向量,隐向量容易做全局妥协,从而不能彻底学习到个性化的特征交互关系,不能指望一个隐向量能适配和所有的特征交叉学习,因此给每种不同特征的交叉单独分配了隐向量,充分发挥出模型的表达能力。


    FFM PyTorch实践

    FM可以进行计算优化,而FFM不能只能按照原始的公式进行计算,因此FFM的计算复杂度是O(kn方),即完成一个关于特征数量n的嵌套循环,每次循环计算一个长度是k的向量的内积,因此代码实现中会有一个嵌套循环计算再聚合的过程,另外FFM需要让模型知道特征域的数量,因为需要给每一个特征域构造隐向量,隐向量是一个三维的矩阵[f,n,k],分别为特征域数,特征数,隐向量维度。
    本次实践的数据集和上一篇特征交叉系列:完全理解FM因子分解机原理和代码实战一致,采用用户的购买记录流水作为训练数据,用户侧特征是年龄,性别,会员年限等离散特征,商品侧特征采用商品的二级类目,产地,品牌三个离散特征,随机构造负样本,一共有10个特征域,全部是离散特征,对于枚举值过多的特征采用hash分箱,得到一共72个特征。
    通过PyTorch构造网络结构如下

    class Linear(nn.Module):
        def __init__(self, feat_num):
            super(Linear, self).__init__()
            self.embedding = nn.Embedding(feat_num, 1)
            self.bias = nn.Parameter(torch.zeros(1))
    
        def forward(self, x):
            # [None, 10] => [None, 10, 1] => [None, 1]
            x_emb = torch.sum(self.embedding(x), dim=1) + self.bias
            return x_emb
    
    
    class Cross(nn.Module):
        def __init__(self, field_num, feat_num, emb_dim=4):
            super(Cross, self).__init__()
            self.field_num = field_num
            self.feat_num = feat_num
            self.embedding = nn.ModuleList([nn.Embedding(feat_num, emb_dim) for x in range(field_num)])
            for embedding in self.embedding:
                torch.nn.init.xavier_uniform_(embedding.weight.data)
    
        def forward(self, x):
            # 每个特征域下,所有有值的特征的隐向量
            # [None, 10] => [None, 10, 4] => [...[None, 10, 4]...]
            x_emb = [self.embedding[i](x) for i in range(self.field_num)]
            cross_part = []
            for i in range(self.field_num - 1):
                for j in range(i + 1, self.field_num):
                    # 对方是j这个特征域时,i对应的隐向量
                    vifj = x_emb[j][:, i]
                    # 对方是i这个特征域时,j对应的隐向量
                    vjfi = x_emb[i][:, j]
                    # [None, 4] * [None, 4] => [None, 1] 内积计算权重,vi*vj=1
                    dot = torch.sum(vifj * vjfi, dim=1, keepdim=True)
                    cross_part.append(dot)
            cross_part = torch.sum(torch.cat(cross_part, dim=1), dim=1, keepdim=True)
            return cross_part
    
    
    class FFM(nn.Module):
        def __init__(self, field_num, feat_dim, emb_dim=4):
            super(FFM, self).__init__()
            self.linear = Linear(feat_dim)
            self.cross = Cross(field_num, feat_dim, emb_dim)
    
        def forward(self, x):
            linear_part = self.linear(x)  # [None, 1]
            inter = self.cross(x)  # [None, 1]
            output = linear_part + inter  # [None, 1]
            output = torch.sigmoid(output)
            return output.squeeze(dim=1)
    

    在FFM主类中定义了两个子网络分别是线性层linear和FFM交叉层cross,注意网络的输入x是所有有值(值为1)位置的特征的索引,从FFM的公式来看,值为0的特征对结果没有影响,因此只需要输入有值的特征,再者本例全部是离散分箱变量,所有有值的特征都是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个特征,索引的目的是在模型中通过nn.Embedding映射到对应的隐向量。由于x输入的特殊结构,在FFM的线性层也无法使用nn.Linear直接构造,而是采用nn.Embedding取映射对应的权重。
    代码中的核心FFM部分为关于特征域数量的嵌套循环

            for i in range(self.field_num - 1):
                for j in range(i + 1, self.field_num):
                    # 对方是j这个特征域时,i对应的隐向量
                    vifj = x_emb[j][:, i]
                    # 对方是i这个特征域时,j对应的隐向量
                    vjfi = x_emb[i][:, j]
                    # [None, 4] * [None, 4] => [None, 1] 内积计算权重,vi*vj=1
                    dot = torch.sum(vifj * vjfi, dim=1, keepdim=True)
    

    在计算过程中任意两个特征域之间必定会有一次交叉,且交叉的特征结果vi*vj等于1,原因是样本在某个特征域上必定有一个分箱会有值,所以任意两个特征域的交叉都有意义,由于有值的特征必为1,因此交叉的结果也等于1,因此在代码中只需要计算内积部分即可。
    训练代码省略可参考FM的训练代码,FFM训练日志如下

    epoch: 6 step: 342 loss: 0.6628243923187256 auc: 0.6277077959960697
    epoch: 6 step: 344 loss: 0.6709915399551392 auc: 0.6240200959018551
    epoch: 6 step: 346 loss: 0.6623412370681763 auc: 0.6348941361849251
    epoch: 6 step: 348 loss: 0.6673620343208313 auc: 0.6245207652466411
    epoch: 6 step: 350 loss: 0.6685708165168762 auc: 0.6267709923664122
    [evaluation] loss: 0.6645713235650744 auc: 0.6287447944840227
    本轮auc比之前最大auc下降:0.0012863341431852415, 当前最大auc: 0.6300311286272079
    
    ---------early stop!----------
    

    在上一节FM的实践中,验证集的早停最大AUC在0.627左右,而本节FFM的验证集早停在多次训练观察之后维持在0.630左右,略微提升了0.3个百分点,说明FFM这种提高特征交叉隐向量的数量和自由度对模型预测精度有一定的提升。

    相关文章

      网友评论

        本文标题:特征交叉系列:FFM场感知因子分解机原理与实践

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