FFM模型在推荐系统中的应用与优化

随着互联网技术的发展,数据量呈指数级增长,推荐系统已成为连接用户与海量信息的重要桥梁。推荐算法的目标是通过分析用户的兴趣偏好来预测用户对未接触过的信息的兴趣度,从而实现个性化推荐。其中,因子化机器(Factorization Machines, FM)因其强大的特征组合能力而备受青睐,而Field-aware Factorization Machine (FFM)作为FM的一种改进版本,在处理稀疏数据和捕捉交叉特征方面表现尤为出色。

什么是FFM

FFM是由Steinbach等人提出的,它是在传统的FM基础上发展而来的一种模型。与传统的FM相比,FFM为不同字段间的交互赋予了不同的因子向量,这意味着它能够更精细地捕捉到不同字段之间的复杂关系。这种“场感知”机制使得FFM能够在处理分类变量的交叉特征时更加灵活有效。

基本原理

  • 线性部分:类似于线性回归或逻辑回归,用于捕获特征的基本贡献。
  • 交叉部分:这是FFM的核心所在,它通过为每个特征在每个场中定义一组独立的因子向量来建模特征间的相互作用。场的概念指的是特征所属的类别或领域,例如“性别”、“年龄”等都是不同的场。

通过这种方式,FFM不仅能够学习单个特征的重要性,还能学习到不同特征之间如何协同作用以影响目标变量。

数学公式

好的,下面分别介绍Factorization Machines (FM) 和 Field-aware Factorization Machines (FFM) 的数学公式。

Factorization Machines (FM)

Factorization Machines 是一种通用的预测模型,它可以处理任何实值特征,并且能够有效地学习特征之间的二阶相互作用。FM模型的一般形式如下:

其中:

  • 表示预测的目标值。
  • 是全局偏置项。
  • 是特征 的权重。
  • 分别是特征 的因子向量。
  • 表示内积操作。
  • 是输入特征向量中的元素。

Field-aware Factorization Machines (FFM)

FFM 对FM进行了扩展,为每个特征在每个场(field)中定义了一组独立的因子向量,这有助于更好地捕捉不同特征之间的交互作用。FFM模型的预测函数可以表示为:

与FM的区别在于:

  • 表示第 个特征在第 个特征所在的场中的因子向量,反之亦然。这意味着对于每个特征对,存在两个独立的因子向量。
  • 这样做的目的是为了捕捉不同场中特征之间的交互效应,从而使得模型更加灵活和强大。

通过上述公式可以看出,FM 和 FFM 都是通过引入因子向量来捕捉特征之间的二阶相互作用,但是FFM通过为每个特征在不同场中定义独立的因子向量,从而可以更细致地建模特征间的交互。

这两种模型在实际应用中都需要通过优化算法(如梯度下降法)来估计参数,包括权重向量 和因子向量 ,以最小化损失函数。在实际应用中,还需要考虑到正则化项来防止过拟合。

FFM代码

参考:推荐系统(四)Field-aware Factorization Machines(FFM)-CSDN博客

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. 高效率:由于FFM在计算特征交叉项时只考虑了不同场之间的交互,因此相比于全矩阵因式分解的方法,其计算开销大大降低。

  2. 稀疏性支持:FFM非常适合处理稀疏数据集,即使某些特征组合很少出现或从未出现过,也能够给出合理的预测值。

  3. 强泛化能力:通过为每个场分配独立的因子向量,FFM可以在训练过程中学到更加丰富的特征交互模式,从而提高模型的泛化能力。

在推荐系统中的应用

在推荐系统中,FFM可以通过以下方式提升推荐效果:

  • 用户画像构建:利用FFM对用户行为数据进行深度挖掘,构建精细化的用户画像,从而实现精准推荐。
  • 商品属性理解:通过对商品信息的理解,将商品的不同属性映射到场域中,更好地理解和预测用户对不同类型商品的兴趣。
  • 上下文感知推荐:结合用户的历史行为、地理位置等多种上下文信息,通过FFM建模,提供更加个性化的推荐结果。

结论

FFM作为一种先进的推荐算法,凭借其优秀的特征交叉能力和高效的计算特性,在推荐系统领域有着广泛的应用前景。未来的研究方向可能会集中在进一步提高模型的可解释性和减少训练时间上,以满足实际应用场景的需求。