一文看懂推荐系统:排序09:Field-aware Factorization Machines(FFM),从FM改进来的,效果不咋地

一文看懂推荐系统:排序09:Field-aware Factorization Machines(FFM),从FM改进来的,效果不咋地

提示:最近系统性地学习推荐系统的课程。我们以小红书的场景为例,讲工业界的推荐系统。
我只讲工业界实际有用的技术。说实话,工业界的技术远远领先学术界,在公开渠道看到的书、论文跟工业界的实践有很大的gap,
看书学不到推荐系统的关键技术。
看书学不到推荐系统的关键技术。
看书学不到推荐系统的关键技术。

王树森娓娓道来**《小红书的推荐系统》**
GitHub资料连接:http://wangshusen.github.io/
B站视频合集:https://space.bilibili.com/1369507485/channel/seriesdetail?sid=2249610

基础知识:
【1】一文看懂推荐系统:概要01:推荐系统的基本概念
【2】一文看懂推荐系统:概要02:推荐系统的链路,从召回粗排,到精排,到重排,最终推荐展示给用户
【3】一文看懂推荐系统:召回01:基于物品的协同过滤(ItemCF),item-based Collaboration Filter的核心思想与推荐过程
【4】一文看懂推荐系统:召回02:Swing 模型,和itemCF很相似,区别在于计算相似度的方法不一样
【5】一文看懂推荐系统:召回03:基于用户的协同过滤(UserCF),要计算用户之间的相似度
【6】一文看懂推荐系统:召回04:离散特征处理,one-hot编码和embedding特征嵌入
【7】一文看懂推荐系统:召回05:矩阵补充、最近邻查找,工业界基本不用了,但是有助于理解双塔模型
【8】一文看懂推荐系统:召回06:双塔模型——模型结构、训练方法,召回模型是后期融合特征,排序模型是前期融合特征
【9】一文看懂推荐系统:召回07:双塔模型——正负样本的选择,召回的目的是区分感兴趣和不感兴趣的,精排是区分感兴趣和非常感兴趣的
【10】一文看懂推荐系统:召回08:双塔模型——线上服务需要离线存物品向量、模型更新分为全量更新和增量更新
【11】一文看懂推荐系统:召回09:地理位置召回、作者召回、缓存召回
【12】一文看懂推荐系统:排序01:多目标模型
【13】一文看懂推荐系统:排序02:Multi-gate Mixture-of-Experts (MMoE)
【14】一文看懂推荐系统:排序03:预估分数融合
【15】一文看懂推荐系统:排序04:视频播放建模
【16】一文看懂推荐系统:排序05:排序模型的特征
【17】一文看懂推荐系统:排序06:粗排三塔模型,性能介于双塔模型和精排模型之间
【18】一文看懂推荐系统:特征交叉01:Factorized Machine (FM) 因式分解机
【19】一文看懂推荐系统:物品冷启01:优化目标 & 评价指标
【20】一文看懂推荐系统:物品冷启02:简单的召回通道
【21】一文看懂推荐系统:物品冷启03:聚类召回
【22】一文看懂推荐系统:物品冷启04:Look-Alike 召回,Look-Alike人群扩散
【23】一文看懂推荐系统:物品冷启05:流量调控
【24】一文看懂推荐系统:物品冷启06:冷启的AB测试
【25】推荐系统最经典的 排序模型 有哪些?你了解多少?
【26】一文看懂推荐系统:排序07:GBDT+LR模型
【27】一文看懂推荐系统:排序08:Factorization Machines(FM)因子分解机,一个特殊的案例就是MF,矩阵分解为uv的乘积


提示:文章目录


前言

上一篇博客介绍了FM模型,这篇博客来介绍下FM模型的改进版FFM模型,
看名字也能窥探一二,FFM模型相比较FM模型改进点在于“F”,

这个F指的是Field-aware

作者也在论文中提到这个idea受rendle大佬的PITF文章[文献2]启发。
这篇文章的核心思想点理解起来可能会比较绕,
因为这篇博客重点的笔墨都会放在这个idea的理解上。

在看这篇博客之前,请务必已经对FM模型非常熟悉【我说过了之前】

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

在这里插入图片描述
基于上面这个数据集,我们来对比下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 是field的个数,比如上面这个数据集有[性别,年龄,教育水平]三个field的,因此f = 3),

扩展来说,对于每个特征取值都不再只有一个embedding向量,都是有f个。

举个例子,还是交叉特征[男#45],因为男的filed是性别,45的field是年龄,因此,
*男#45=embedding(男,年龄)embedding(45,性别)

上面这个公式一定要理解,这是整个FFM的核心,并且对于field是双向的,
也就是不能只有embedding(男,年龄),还要有embedding(45,性别)。

注:对于[性别,年龄,教育水平] 我们通常称呼为特征,为了和论文中称呼统一,在这篇博客里,我们称为field

讲完上面这个核心思想,我们再来看看FM和FFM形式化公式的区别:
在这里插入图片描述
从公式2和公式1的对比中,也能发现,FFM相比较FM,
仅在二阶交叉部分引入了field信息(实际上就是个side information)。

从时间复杂度上来看,FM的时间复杂度可以简化至O ( k n )
而FFM时间复杂度O ( k n 2 )

这也是FFM在工业界用的比较少的原因。

从二阶交叉项系数个数来看,FM为n k ,而FFM为n f k ,k 为embedding维度。

因此,虽然FFM添加了field information后,相比较FM刻画的更加精细,

由此也带来时间复杂度上升和过拟合问题,至于过拟合问题,

论文中给出了两种解决办法:1. 添加正则项,2. 早停。

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

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

翻译下也就是:

对于含有类别特征的数据集,需要对特征进行二值化处理,这样FFM才会比较有效
数据集越稀疏,FFM越有优势,也就是FFM在高维稀疏的数据集上表现比较好。

如果一个数据集只有连续值,则不适用于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)找工作投简历的话,你要将招聘单位的岗位需求和你的研究方向和工作内容对应起来,这样才能契合公司招聘需求,否则它直接把简历给你挂了
(2)你到底是要进公司做推荐系统方向?还是纯cv方向?还是NLP方向?还是语音方向?还是深度学习机器学习技术中台?还是硬件?还是前端开发?后端开发?测试开发?产品?人力?行政?这些你不可能啥都会,你需要找准一个方向,自己有积累,才能去投递,否则面试官跟你聊什么呢?
(3)今日推荐系统学习经验:从FM改进来的,效果不咋地

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

冰露可乐

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值