推荐系统Field-aware Factorization Machines(FFM)

Posted 天泽28

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了推荐系统Field-aware Factorization Machines(FFM)相关的知识,希望对你有一定的参考价值。

推荐系统(四)Field-aware Factorization Machines(FFM)

推荐系统系列博客:

  1. 推荐系统(一)推荐系统整体概览
  2. 推荐系统(二)GBDT+LR模型
  3. 推荐系统(三)Factorization Machines(FM)

上一篇博客介绍了FM模型,这篇博客来介绍下FM模型的改进版FFM模型,看名字也能窥探一二,FFM模型相比较FM模型改进点在于“F”,这个F指的是Field-aware,作者也在论文中提到这个idea受rendle大佬的PITF文章[文献2]启发。这篇文章的核心思想点理解起来可能会比较绕,因为这篇博客重点的笔墨都会放在这个idea的理解上。在看这篇博客之前,请务必已经对FM模型非常熟悉,关于FM模型可以参考我的上一篇博客:推荐系统(三)Factorization Machines(FM)

为了方便下面对FFM核心思想的讲解,我们先来造个数据集,假设我们有个数据集如下:

性别年龄教育水平label
45初中0
25研究生1

基于上面这个数据集,我们来对比下FM和FFM,这样大家看起来也一目了然。

FM思想: 为每个特征取值训练一个embedding向量(比如性别=男),这样在做特征二价交叉的时候(比如性别和年龄的交叉,男#45)只需要分别拿出“男”的embedding向量和“45”的embedding向量,然后做个向量内积就可以了。这样做的好处就是即使交叉的两个特征从来没在数据集中出现过,也能得到embedding向量,从而学到对应的参数。

FFM思想: 我们考虑两个二阶交叉特征:[男#45、男#初中]。在FM模型中,男#45=embedding(男) ∗ * embedding(45)男#初中=embedding(男) ∗ * embedding(初中),能够发现这两个交叉特征中,因为都有“性别=男”这个特征,在计算这两个交叉特征时,对于“性别=男”这个特征的embedding向量也是用的一样的,都是embedding(男)。 而FFM认为 [男#45、男#初中] 虽然都是“男”,但因为一个是和年龄交叉,一个是和教育水平交叉,因此如果如果用相同的embedding(男)向量是会导致信息有损。 因此FFM引入了field的概念,即对于“性别=男”这个特征取值,不再只有一个embedding向量(FM中只有一个),而是有 f f f个(这个 f f f是field的个数,比如上面这个数据集有[性别,年龄,教育水平]三个field的,因此 f = 3 f=3 f=3),扩展来说,对于每个特征取值都不再只有一个embedding向量,都是有 f f f个。举个例子,还是交叉特征[男#45],因为男的filed是性别,45的field是年龄,因此,男#45=embedding(男,年龄) ∗ * embedding(45,性别) 。上面这个公式一定要理解,这是整个FFM的核心,并且对于field是双向的,也就是不能只有embedding(男,年龄),还要有embedding(45,性别)。
注:对于[性别,年龄,教育水平] 我们通常称呼为特征,为了和论文中称呼统一,在这篇博客里,我们称为field。

讲完上面这个核心思想,我们再来看看FM和FFM形式化公式的区别:

  • FM
    y ^ ( x ) = w 0 + ∑ i = 1 n w i x i + ∑ i = 1 n ∑ j = i + 1 n < v i , v j > x i , x j (1) \\haty(x) = w_0 + \\sum_i=1^nw_ix_i + \\sum_i=1^n\\sum_j=i+1^n<v_i,v_j>x_i,x_j \\tag1 y^(x)=w0+i=1nwixi+i=1nj=i+1n<vi,vj>xi,xj(1)
  • FFM
    y ^ ( x ) = w 0 + ∑ i = 1 n w i x i + ∑ i = 1 n ∑ j = i + 1 n < v i , f j , v j , f i > x i , x j (2) \\haty(x) = w_0 + \\sum_i=1^nw_ix_i + \\sum_i=1^n\\sum_j=i+1^n<v_i,f_j,v_j,f_i>x_i,x_j \\tag2 y^(x)=w0+i=1nwixi+i=1nj=i+1n<vi,fj,vj,fi>xi,xj(2)

从公式2和公式1的对比中,也能发现,FFM相比较FM,仅在二阶交叉部分引入了field信息(实际上就是个side information)。从时间复杂度上来看,FM的时间复杂度可以简化至 O ( k n ) O(kn) O(kn),而FFM时间复杂度 O ( k n 2 ) O(kn^2) O(kn2),这也是FFM在工业界用的比较少的原因。从二阶交叉项系数个数来看,FM为 n k nk nk,而FFM为 n f k nfk nfk k k k为embedding维度。因此,虽然FFM添加了field information后,相比较FM刻画的更加精细,由此也带来时间复杂度上升和过拟合问题,至于过拟合问题,论文中给出了两种解决办法:1. 添加正则项,2. 早停。

对于FFM的使用,作者在论文中也给出了建议:

  1. FFMs should be effective for data sets that contain categorical features and are transformed to binary features.
  2. If the transformed set is not sparse enough, FFMs seem to bring less benefit.
  3. It is more difficult to apply FFMs on numerical data sets.

翻译下也就是:

  1. 对于含有类别特征的数据集,需要对特征进行二值化处理,这样FFM才会比较有效
  2. 数据集越稀疏,FFM越有优势,也就是FFM在高维稀疏的数据集上表现比较好。
  3. 如果一个数据集只有连续值,则不适用于FFM。

最后来看看实现,作者给出了一个C++版本:LIBFFM,这里就不多介绍,有兴趣的可以自己看看。paddlepaddle官方也给出了paddle版本的实现 ffm,基于的数据集是Display Advertising Challenge所用的Criteo数据集,这个数据集共有13个连续值特征,26个类别型特征。我们一起来看下paddle的实现,官方的代码并没有一些维度的注释,这让人看起来着实难受,我这里给加了一些维度的注释:

class FFM(nn.Layer):
    def __init__(self, sparse_feature_number, sparse_feature_dim,
                 dense_feature_dim, sparse_num_field):
        super(FFM, self).__init__()
        self.sparse_feature_number = sparse_feature_number
        self.sparse_feature_dim = sparse_feature_dim
        self.dense_feature_dim = dense_feature_dim
        self.dense_emb_dim = self.sparse_feature_dim  # 9
        self.sparse_num_field = sparse_num_field
        self.init_value_ = 0.1

        # sparse part coding
        # [1000001, 1]
        self.embedding_one = paddle.nn.Embedding(
            sparse_feature_number,  # 1000001
            1,
            sparse=True,
            weight_attr=paddle.ParamAttr(
                initializer=paddle.nn.initializer.TruncatedNormal(
                    mean=0.0,
                    std=self.init_value_ /
                        math.sqrt(float(self.sparse_feature_dim)))))
        # [1000001, 9*39]
        self.embedding = paddle.nn.Embedding(
            self.sparse_feature_number,
            self.sparse_feature_dim * self.sparse_num_field,
            sparse=True,
            weight_attr=paddle.ParamAttr(
                initializer=paddle.nn.initializer.TruncatedNormal(
                    mean=0.0,
                    std=self.init_value_ /
                        math.sqrt(float(self.sparse_feature_dim)))))

        # dense part coding w
        # shape(13,)
        # Tensor(shape=[13], dtype=float32, place=CPUPlace, stop_gradient=False,
        #        [1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.])
        self.dense_w_one = paddle.create_parameter(
            shape=[self.dense_feature_dim],
            dtype='float32',
            default_initializer=paddle.nn.initializer.Constant(value=1.0))
        # shape(1, 13, 9*39)
        self.dense_w = paddle.create_parameter(
            shape=[
                1, self.dense_feature_dim,
                self.dense_emb_dim * self.sparse_num_field  # 13, 9*39
            ],
            dtype='float32',
            default_initializer=paddle.nn.initializer.Constant(value=1.0))
    

	def forward(self, sparse_inputs, dense_inputs):
        """
        one sample example:
            [array([0]), array([737395]), array([210498]), array([903564]), array([286224]), array([286835]),
            array([906818]), array([906116]), array([67180]), array([27346]), array([51086]), array([142177]),
            array([95024]), array([157883]), array([873363]), array([600281]), array([812592]), array([228085]),
             array([35900]), array([880474]), array([984402]), array([100885]), array([26235]), array([410878]),
             array([798162]), array([499868]), array([306163]),
             array([0. , 0.00497512, 0.05 , 0.08 , 0.20742187, 0.028, 0.35 , 0.08 , 0.082 , 0.,
             0.4  , 0.  , 0.08  ], dtype=float32)]


        :param sparse_inputs:  list[array], 26 len
            [array([0]), array([737395]), array([210498]), array([903564]), array([286224]), array([286835]),
            array([906818]), array([906116]), array([67180]), array([27346]), array([51086]), array([142177]),
            array([95024]), array([157883]), array([873363]), array([600281]), array([812592]), array([228085]),
            array([35900]), array([880474]), array([984402]), array([100885]), array([26235]), array([410878]),
            array([798162]), array([499868]), array([306163]),
        :param dense_inputs:  list 13 len
                    array([0.        , 0.00497512, 0.05      , 0.08      , 0.20742187,
                    0.028     , 0.35      , 0.08      , 0.082     , 0.        ,
                    0.4       , 0.        , 0.08      ]
        :return:
        """
        # -------------------- first order term  --------------------
        # sparse_inputs, list, length 26, [Tensor(shape=[2, 1]),...,]
        #  [[[737395],[715353]],...] feature_name* batch_size*1
        # sparse_inputs_concat, Tensor(shape=[2, 26]) ---> batch_size=2, shape[batch_size, 26]
        # [[737395, 210498, 903564, 286224, 286835, 906818, 906116, 67180 , 27346 , 51086 ,
        # 142177, 95024 , 157883, 873363, 600281, 812592, 228085, 35900 , 880474, 984402,
        # 100885, 26235 , 410878, 798162, 499868, 306163],[]]
        sparse_inputs_concat = paddle.concat(sparse_inputs, axis=1)
        # shape=[batch_size, 26, 1]
        # [[[-0.00620287],
        #          [-0.01724204],
        #          [-0.02544647],
        #          [ 0.01982319],
        #          [-0.03302126],
        #          [ 0.00377966],...,], [[],..[]]]
        sparse_emb_one = self.embedding_one(sparse_inputs_concat)
        # dense_inputs: shape=[batch_size, 13]
        # dense_w_one: shape=[13]
        # 点乘
        # Tensor(shape=[2, 13], dtype=float32, place=CPUPlace, stop_gradient=False,
        # [[0., 0.00497512, 0.05000000, 0.08000000, 0.20742187, 0.02800000, 0.34999999,
        # 0.08000000, 0.08200000, 0., 0.40000001, 0., 0.08000000],
        # [0., 0.93200666, 0.02000000, 0.14000000, 0.03956250, 0.32800001, 0.98000002,
        # 0.12000000, 1.88600004, 0. , 1.79999995, 0., 0.14000000]]))
        dense_emb_one = paddle.multiply(dense_inputs, self.dense_w_one)  # shape=[batch_size, 13]
        # shape=[batch_size, 13, 1]
        # [[       [0.        ],
        #          [0.00497512],
        #          [0.05000000],
        #          [0.08000000],
        #          [0.20742187],
        #          [0.02800000],
        #          [0.34999999],
        #          [0.08000000],
        #          [0.08200000],
        #          [0.        ],
        #          [0.40000001],
        #          [0.        ],
        #          [0.08000000]],
        #
        #         [[0.        ],
        #          [0.93200666],
        #          [0.02000000],
        #          [0.14000000],
        #          [0.03956250],
        #          [0.32800001],
        #          [0.98000002],
        #          [0.12000000],
        #          [1.88600004],
        #          [0.        ],
        #          [1.79999995],
        #          [0.        ],
        #          [0.14000000]]]
        dense_emb_one = paddle.unsqueeze(dense_emb_one, axis=2)  # shape=[batch_size, 13, 1]
        # paddle.sum(sparse_emb_one, 1) --->shape=[2, 1], [[-0.13885814],[-0.21163476]]
        # paddle.sum(dense_emb_one, 1)  --->shape=[2, 1], [[-0.13885814], [-0.21163476]]
        y_first_order = paddle.sum(sparse_emb_one, 1) + paddle.sum(
            dense_emb_one, 1)  # [batch_size, 1]

        # -------------------Field-aware second order term  --------------------
        # shape=[batch_size, 26, 351]
        sparse_embeddings = self.embedding(sparse_inputs_concat)
        # shape=[batch_size, 13, 1],  batch_size=2
        dense_inputs_re = paddle.unsqueeze(dense_inputs, axis=2)
        # shape=[batch_size, 13, 351]
        print("==========dense_inputs_re========", dense_inputs_re)
        print("=============dense_w============", self.dense_w)
        dense_embeddings = paddle.multiply(dense_inputs_re, self.dense_w)  # [2,13,1]*[1,13,351]=[2,13,351]
        print("=============dense_embeddings============", dense_embeddings)
        # shape=[batch_size, 39, 351]
        feat_embeddings = paddle.concat([sparse_embeddings, dense_embeddings], 1)
        # shape=[batch_size, 39, 39, 9]
        field_aware_feat_embedding = paddle.reshape(
            feat_embeddings,
            shape=[-1, self.sparse_num_field, self.sparse_num_field, self.sparse_feature_dim])
        field_aware_interaction_list = []
        for i in range(self.sparse_num_field):  # 39个特征,26个离散值特征+13个连续值特征
            for j in range(i + 1, self.sparse_num_field):  # 39
                field_aware_interaction_list.append(
                    # sum后维度shape=[2, 1],
                    # [
                    # [0.00212428],
                    # [0.00286741]
                    # ]
                    # 对应着FFM二阶部分,embedding(x_i, f_j) * embedding(x_j, f_i)
                    paddle.sum(field_aware_feat_embedding[:, i, j, :] *  # shape=[2, 9], 对应元素相乘
                               field_aware_feat_embedding[:, j, i, :], 1, keepdim=True))
        # shape=[2, 1]
        y_field_aware_second_order = paddle.add_n(field_aware_interaction_list)
        return y_first_order, y_field_aware_second_order




参考文献
[1]: Juan Y, Zhuang Y, Chin W S, et al. Field-aware factorization machines for CTR prediction[C]//Proceedings of the 10th ACM conference on recommender systems. 2016: 43-50.
[2]: Rendle S, Schmidt-Thieme L. Pairwise interaction tensor factorization for personalized tag recommendation[C]//Proceedings of the third ACM international conference on Web search and data mining. 2010: 81-90.
[3]: 推荐系统Field-aware Factorization Machines(FFM)

推荐系统(十六)多任务学习:腾讯PLE模型(Progressive Layered Extraction model)

推荐系统(十六)多任务学习:腾讯PLE模型(Progressive Layered Extraction model)

推荐系统(十六)多任务学习:腾讯PLE模型(Progressive Layered Extraction model)

推荐系统

推荐系统手把手带你学推荐系统 3 实现第一个推荐系统