一文看懂推荐系统:排序15:DeepFM模型(Factorization-Machine),xDeepFM可不是对DeepFM的改编哦,而是对DCN的改编

本文深入解析DeepFM模型,一种结合因子分解机(FM)与深度神经网络(DNN)的推荐系统模型。该模型旨在自动捕捉特征间的二阶交互,无需人工特征工程,适用于从传统模型向深度学习迁移的场景。

一文看懂推荐系统:排序15:DeepFM模型(Factorization-Machine),xDeepFM可不是对DeepFM的改编哦,而是对DCN的改编

提示:最近系统性地学习推荐系统的课程。我们以小红书的场景为例,讲工业界的推荐系统。
我只讲工业界实际有用的技术。说实话,工业界的技术远远领先学术界,在公开渠道看到的书、论文跟工业界的实践有很大的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的乘积
【28】一文看懂推荐系统:排序09:Field-aware Factorization Machines(FFM),从FM改进来的,效果不咋地
【29】一文看懂推荐系统:排序10:wide&deep模型,wide就是LR负责记忆,deep负责高阶特征交叉而泛化
【30】一文看懂推荐系统:排序11:Deep & Cross Network(DCN)
【31】一文看懂推荐系统:排序12:xDeepFM模型,并不是对DeepFM的改进,而是对DCN的改进哦
【32】一文看懂推荐系统:排序13:FNN模型(FM+MLP=FNN),与PNN同属上海交大张楠的作品

【33】一文看懂推荐系统:排序14:PNN模型(Product-based Neural Networks),和FNN一个作者,干掉FM,加上LR+Product


提示:文章目录


前言

DeepFM是哈工大和华为合作发表在IJCAI2017上的文章,
这篇文章也是受到谷歌wide&deep模型的启发,

是一个左右组合(混合)模型结构,【wide&deep如下】
在这里插入图片描述

不同的是,deepFM在wide部分用了FM模型来代替LR模型。
所以你已经非常明白了吧

因此,强烈建议在看这篇文章之前,先移步看完我之前写的关于wide&deep的博客

我们来看看DeepFM相比较wide&deep模型的改进点及优势(前提是你已经很了解wide&deep模型了):

在wide部分使用FM代替了wide&deep中的LR,

有了FM自动构造学习二阶(考虑到时间复杂度原因,通常都是二阶)交叉特征的能力,因此不再需要特征工程。

Wide&Deep模型中LR部分依然需要人工的特征交叉,
比如【用户已安装的app】与【给用户曝光的app】两个特征做交叉。

另外,仅仅通过人工的手动交叉,又回到了之前在讲FM模型中提到的,
比如要两个特征共现,否则无法训练。

DeepFM模型中,FM模型与DNN模型共享底层embedding向量,然后联合训练。

这种方式也更符合现在推荐/广告领域里多任务模型多塔共享底座embedding的方式
然后end-to-end训练得到的embedding向量也更加准确。

其实如果你很熟悉wide&deep模型,再经过上面的介绍,
你基本已经知道DeepFM的大体网络结构了。

接下来,本文将从两个方面介绍deepFM:

DeepFM的模型结构细节
DeepFM的代码实现
总结


DeepFM的模型结构细节

来看下DeepFM的模型结构(图片来自王喆《深度学习推荐系统》,
ps:原论文的图不清晰,所以没有直接从原论文取图)
你看原图
在这里插入图片描述
中文新图
在这里插入图片描述
整体模型结构也比较简单,自底向上看分别为:

原始输入层:onehot编码的稀疏输入

embedding层:FM和DNN共享的底座

FM与DNN

输出层

1.1 FM

重点说下FM层,先来回顾下FM的公式:我们说了多次了,关键在特征交互部分那一项
在这里插入图片描述
在这里插入图片描述
DNN部分:没什么好讲的,多层全连接网络。

最终的输出:
在这里插入图片描述

二、DeepFM的代码实现

这部分是本博客的重点,我这里直接用paddle官方的代码讲解下,
具体代码参见:搞清楚代码细节,有助于我们对DeepFM模型更深入的了解。

2.1 数据集

这里用的是Criteo数据集,用于广告CTR预估的数据集,
关于数据集的介绍参见:Criteo。
特征方面,这个数据集共26个离散特征,13个连续值特征。

2.2 FM部分实现

我给代码增加了详细的注释(主要是矩阵维度的注释),大家看代码即可。

class FM(nn.Layer):
    def __init__(self, sparse_feature_number, sparse_feature_dim,
                 dense_feature_dim, sparse_num_field):
        super(FM, self).__init__()
        self.sparse_feature_number = sparse_feature_number  # 1000001
        self.sparse_feature_dim = sparse_feature_dim   # 9
        self.dense_feature_dim = dense_feature_dim  # 13
        self.dense_emb_dim = self.sparse_feature_dim  # 9
        self.sparse_num_field = sparse_num_field   # 26
        self.init_value_ = 0.1
        use_sparse = True
        # sparse coding
        # Embedding(1000001, 1, padding_idx=0, sparse=True)
        self.embedding_one = paddle.nn.Embedding(
            sparse_feature_number,
            1,
            padding_idx=0,
            sparse=use_sparse,
            weight_attr=paddle.ParamAttr(
                initializer=paddle.nn.initializer.TruncatedNormal(
                    mean=0.0,
                    std=self.init_value_ /
                    math.sqrt(float(self.sparse_feature_dim)))))
        # Embedding(1000001, 9, padding_idx=0, sparse=True)
        self.embedding = paddle.nn.Embedding(
            self.sparse_feature_number,
            self.sparse_feature_dim,
            sparse=use_sparse,
            padding_idx=0,
            weight_attr=paddle.ParamAttr(
                initializer=paddle.nn.initializer.TruncatedNormal(
                    mean=0.0,
                    std=self.init_value_ /
                    math.sqrt(float(self.sparse_feature_dim)))))

        # dense coding
        """
        Tensor(shape=[13], dtype=float32, place=CPUPlace, stop_gradient=False,
        [-0.00486396,  0.02755001, -0.01340683,  0.05218775,  0.00938804,  0.01068084,  0.00679830,  
        0.04791596, -0.04357519,  0.06603041, -0.02062148, -0.02801327, -0.04119579]))
        """
        self.dense_w_one = paddle.create_parameter(
            shape=[self.dense_feature_dim],
            dtype='float32',
            default_initializer=paddle.nn.initializer.TruncatedNormal(
                mean=0.0,
                std=self.init_value_ /
                math.sqrt(float(self.sparse_feature_dim))))

        # Tensor(shape=[1, 13, 9])
        self.dense_w = paddle.create_parameter(
            shape=[1, self.dense_feature_dim, self.dense_emb_dim],
            dtype='float32',
            default_initializer=paddle.nn.initializer.TruncatedNormal(
                mean=0.0,
                std=self.init_value_ /
                math.sqrt(float(self.sparse_feature_dim))))
    
    
    def forward(self, sparse_inputs, dense_inputs):
        # -------------------- first order term  --------------------
        """
        sparse_inputs: list, length:26, list[tensor], each tensor shape: [2, 1]
        dense_inputs: Tensor(shape=[2, 13]), 2 --> train_batch_size
        """
        # Tensor(shape=[2, 26])
        sparse_inputs_concat = paddle.concat(sparse_inputs, axis=1)
        # Tensor(shape=[2, 26, 1])
        sparse_emb_one = self.embedding_one(sparse_inputs_concat)
        # dense_w_one: shape=[13], dense_inputs: shape=[2, 13]
        # dense_emb_one: shape=[2, 13]
        dense_emb_one = paddle.multiply(dense_inputs, self.dense_w_one)
        # shape=[2, 13, 1]
        dense_emb_one = paddle.unsqueeze(dense_emb_one, axis=2)
        # paddle.sum(sparse_emb_one, 1): shape=[2, 1]
        # paddle.sum(dense_emb_one, 1): shape=[2, 1]
        # y_first_order: shape=[2, 1]
        y_first_order = paddle.sum(sparse_emb_one, 1) + paddle.sum(
            dense_emb_one, 1)
        # -------------------- second order term  --------------------
        # Tensor(shape=[2, 26, 9])
        sparse_embeddings = self.embedding(sparse_inputs_concat)
        # Tensor(shape=[2, 13, 1])
        dense_inputs_re = paddle.unsqueeze(dense_inputs, axis=2)
        # dense_inputs_re: Tensor(shape=[2, 13, 1])
        # dense_w: Tensor(shape=[1, 13, 9])
        # dense_embeddings: Tensor(shape=[2, 13, 9])
        dense_embeddings = paddle.multiply(dense_inputs_re, self.dense_w)
        # Tensor(shape=[2, 39, 9])
        feat_embeddings = paddle.concat([sparse_embeddings, dense_embeddings],
                                        1)
        # sum_square part
        # Tensor(shape=[2, 9])
        # \sum_{i=1}^n(v_{i,f}x_i) ---> for each embedding element: e_i, sum all feature's e_i
        summed_features_emb = paddle.sum(feat_embeddings,
                                         1)  # None * embedding_size
        # Tensor(shape=[2, 9]) 2-->batch_size
        summed_features_emb_square = paddle.square(
            summed_features_emb)  # None * embedding_size
        # square_sum part
        # Tensor(shape=[2, 39, 9])
        squared_features_emb = paddle.square(
            feat_embeddings)  # None * num_field * embedding_size
        # Tensor(shape=[2, 9]) 2-->batch_size
        squared_sum_features_emb = paddle.sum(squared_features_emb,
                                              1)  # None * embedding_size
        # Tensor(shape=[2, 1])
        y_second_order = 0.5 * paddle.sum(
            summed_features_emb_square - squared_sum_features_emb,
            1,
            keepdim=True)  # None * 1

        return y_first_order, y_second_order, feat_embeddings

2.3 DNN部分实现

这部分着实没什么好说的,直接略过。

class DNN(paddle.nn.Layer):
    def __init__(self, sparse_feature_number, sparse_feature_dim,
                 dense_feature_dim, num_field, layer_sizes):
        super(DNN, self).__init__()
        self.sparse_feature_number = sparse_feature_number
        self.sparse_feature_dim = sparse_feature_dim
        self.dense_feature_dim = dense_feature_dim
        self.num_field = num_field
        self.layer_sizes = layer_sizes
        # [351, 512, 256, 128, 32, 1]
        sizes = [sparse_feature_dim * num_field] + self.layer_sizes + [1]
        acts = ["relu" for _ in range(len(self.layer_sizes))] + [None]
        self._mlp_layers = []
        for i in range(len(layer_sizes) + 1):
            linear = paddle.nn.Linear(
                in_features=sizes[i],
                out_features=sizes[i + 1],
                weight_attr=paddle.ParamAttr(
                    initializer=paddle.nn.initializer.Normal(
                        std=1.0 / math.sqrt(sizes[i]))))
            self.add_sublayer('linear_%d' % i, linear)
            self._mlp_layers.append(linear)
            if acts[i] == 'relu':
                act = paddle.nn.ReLU()
                self.add_sublayer('act_%d' % i, act)

    def forward(self, feat_embeddings):
        """
        feat_embeddings: Tensor(shape=[2, 39, 9])
        """
        # Tensor(shape=[2, 351]) --> 351=39*9, 
        # 39 is the number of features(category feature+ continous feature), 9 is embedding size
        y_dnn = paddle.reshape(feat_embeddings,
                               [-1, self.num_field * self.sparse_feature_dim])
        for n_layer in self._mlp_layers:
            y_dnn = n_layer(y_dnn)
        return y_dnn

2.4 FM和DNN部分结合

    def forward(self, sparse_inputs, dense_inputs):

        y_first_order, y_second_order, feat_embeddings = self.fm.forward(
            sparse_inputs, dense_inputs)
        # feat_embeddings: Tensor(shape=[2, 39, 9])
        # y_dnn: Tensor(shape=[2, 1])
        y_dnn = self.dnn.forward(feat_embeddings)
        print("y_dnn:", y_dnn)

        predict = F.sigmoid(y_first_order + y_second_order + y_dnn)

        return predict

三、总结

总得来说,DeepFM还是一个挺不错的模型,在工业界应用的也挺多。

还是那句话,如果你的场景下之前是LR,正在往深度学习迁移,
为了最大化节约成本,可以尝试下wide&deep模型

如果原来是xgboost一类的树模型,需要尝试深度学习模型,建议直接deepFM

此外,deepFM与DCN都是在2017年发表的,
因此这两篇paper里均没有直接有过实验数据对比,
但在DCN V2里给出了实验效果对比,在论文给定的数据集下,两个模型效果差不多。

注意我之前讲的xDeepFM不是对deepFM的改进,而是对DCN的改进哦!老复杂了,人家DCN挺好的


总结

提示:如何系统地学习推荐系统,本系列文章可以帮到你

(1)找工作投简历的话,你要将招聘单位的岗位需求和你的研究方向和工作内容对应起来,这样才能契合公司招聘需求,否则它直接把简历给你挂了
(2)你到底是要进公司做推荐系统方向?还是纯cv方向?还是NLP方向?还是语音方向?还是深度学习机器学习技术中台?还是硬件?还是前端开发?后端开发?测试开发?产品?人力?行政?这些你不可能啥都会,你需要找准一个方向,自己有积累,才能去投递,否则面试官跟你聊什么呢?
(3)今日推荐系统学习经验:之前讲的xDeepFM不是对deepFM的改进,而是对DCN的改进哦!老复杂了,人家DCN挺好的

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

冰露可乐

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

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

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

打赏作者

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

抵扣说明:

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

余额充值