基于Field的DeepFM稀疏化实现

一、 DeepFM介绍

DeepFM是一个集成了FM和DNN的神经网络框架,思路和Google的Wide&Deep相似,都包括wide和deep两部分。W&D模型的wide部分是广义线性模型,DeepFM的wide部分则是FM模型,两者的deep部分都是深度神经网络。DeepFM神经网络部分,隐含层的激活函数用ReLu和Tanh做信号非线性映射,Sigmoid函数做CTR预估的输出函数。

DeepFM: A Factorization-Machine based Neural Network for CTR Prediction

    上图是原论文中的网络结构图,在大多数人的实现中,都或多或少的忽略了两个问题:

    1. DeepFM的原始特征是非常稀疏的,所以代码实现需要考虑特征的稀疏化运算;

    2. 生产环境中,每一个Field的输入可能是多值,有的实现中,将每一个one-hot特征都看作一个独立的field,这样虽然简单实现DeepFM模型,但是会造成模型的参数爆炸,训练效率和inference效率低下,而更多的实现中则是忽略了这种情况。

    本文的实现方案解决了以上两个问题,使DeepFM可以真正应用于生产环境中。

二、 基于Field的DeepFM稀疏化实现

2.2 网络结构图

网络结构

    如图所示,每一种颜色代表不同Field的特征,我们假设输入是稀疏的维度为N,并且同一个Field的特征可以不相邻,Field的大小为F,FM的embedding维度为D。最终进入神经网络的是F*D维的向量,并且Field多值的情况下,我们进行field-avg-pooling。

    代码地址:https://github.com/ck8275411/deep_rec

2.2 Field-Avg-Pooling原理

    Field-Avg-Pooling最麻烦的地方在于:如何在稀疏化的样本tensor中,找出属于同一个Field的特征,并将这些特征进行avg-pooling。

    我这里设计了一组名为Field-Selector的0-1矩阵,每一个矩阵中仅有属于同一个Field的特征所属的向量值为1,其它特征的向量值为0。具体方法如下:

    1. 将一个Field-Selector与FM embedding矩阵进行element-wise运算,可以得仅与当前Field相关的所有特征的embedding:fm_field_embeddings;

    2. 将Field-Selector与样本的SparseTensor进行点积,可以得到每条样本中该Field的特征个数;

    3. 将fm_field_embeddings与样本的SparseTensor进行点积,可以得到每条样本中该Field的sum-pooling;

    4. sum-pooling值除以特征个数,即得到了avg-pooling。

    示意图如下:

Field-Avg-Pooling示意图

2.2 Field-Avg-Pooling具体实现

1. 生成Field-Selector矩阵

    Field-Selector矩阵主要是从一个Field-特征id的映射字典里得到,字典格式为:第一列为Field_id,第二列为特征id。具体实现如下:

def get_feature_field(self, feature_field_file):
        feature_fields = []
        field_names = {}
        _fields = []

        for line in open(feature_field_file, "r"):
            tokens = line.strip('\r').strip('\n').split(' ')
            #特征id从0开始编码,所以feature_fields数组中的下表就可以表示特征id,值表示特征所属的field
            feature_fields.append(tokens[0])
            #保存去重后的field id
            field_names[tokens[0]] = 1
        for field_name in field_names.keys():
            #遍历所有field
            field = []
            for j in range(self.feature_size):
                '''
                遍历所有特征id
                如果j大于len(feature_fields),则直接置0
                如果第j个特征的field等于当前field,则置1.0
                如果第j个特征的field不等于当前field,则置0.0
                '''
                if j >= len(feature_fields):
                    field.append([0.0])
                elif feature_fields[j] == field_name:
                    field.append([1.0])
                else:
                    field.append([0.0])
            _fields.append(field)
        return _fields, len(field_names)

2. Avg-Pooling实现

    输入是样本SparseTensor,假设样本数为K,则输出为[K,F*D]的样本DenseTensor以及维度K*D。

def get_field_embeddings(self, sparse_features):
        input_layers = []
        k = 0
        with tf.variable_scope("fm_layer", reuse=tf.AUTO_REUSE):
            fm_layer_embeddings = tf.get_variable("weights",
                                                  self.fm_weights_shape)
            for i in range(self.fields_num):
                fm_field_embeddings = tf.multiply(fm_layer_embeddings, self.field_embedding_masks[i])
                # 计算每个field的feature_cnt
                sparse_x_feature_cnt = tf.maximum(tf.sparse_tensor_dense_matmul(sparse_features, self.field_embedding_masks[i]), 1.0)
                sparse_x_field_embedded = tf.sparse_tensor_dense_matmul(sparse_features, fm_field_embeddings)
                # 计算embedding计算的均值
                sparse_x_field_embedded = tf.divide(sparse_x_field_embedded, sparse_x_feature_cnt)
                input_layers.append(sparse_x_field_embedded)
                k += self.embedding_dim
            nn_inputs = tf.concat(input_layers, 1)
        return nn_inputs, k

2.3 效果对比

    跑了一个实际数据集,验证两种deepfm的实现在训练效率以及效果的差异:

1. 将one-hot特征每一维看做一个独立的field的deepfm;

2. 增加field-avg-pooling层的field-deepfm;

batch_size = 1024

sample_num = 1700w

feature_size = 10560

field_size = 29

nn_layer_shape = [128, 64, 8]

learning_rate = 0.01

optimizer = adam

    效果如下:

实验结果

    可以看到,deepfm的auc比field-deepfm好了0.006,但是训练耗时是field-deepfm的9.2倍,考虑到生产环境的训练效率和Inference效率,显然field-deepfm的实现是好于普通的deepfm的。

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

发表于

Deep Learning in Ads

1 篇文章1 人订阅

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏java闲聊

JDK1.8 ArrayList 源码解析

当运行 ArrayList<Integer> list = new ArrayList<>() ; ,因为它没有指定初始容量,所以它调用的是它的无参构造

1192
来自专栏学海无涯

Android开发之奇怪的Fragment

说起Android中的Fragment,在使用的时候稍加注意,就会发现存在以下两种: v4包中的兼容Fragment,android.support.v4.ap...

3155
来自专栏计算机视觉与深度学习基础

Leetcode 114 Flatten Binary Tree to Linked List

Given a binary tree, flatten it to a linked list in-place. For example, Given...

1938
来自专栏刘君君

JDK8的HashMap源码学习笔记

3008
来自专栏赵俊的Java专栏

从源码上分析 ArrayList

1161
来自专栏ml

朴素贝叶斯分类器(离散型)算法实现(一)

1. 贝叶斯定理:        (1)   P(A^B) = P(A|B)P(B) = P(B|A)P(A)   由(1)得    P(A|B) = P(B|...

3427
来自专栏开发与安全

算法:AOV网(Activity on Vextex Network)与拓扑排序

在一个表示工程的有向图中,用顶点表示活动,用弧表示活动之间的优先关系,这样的有向图为顶点表示活动的网,我们称之为AOV网(Activity on Vextex ...

2517
来自专栏xingoo, 一个梦想做发明家的程序员

AOE关键路径

这个算法来求关键路径,其实就是利用拓扑排序,首先求出,每个节点最晚开始时间,再倒退求每个最早开始的时间。 从而算出活动最早开始的时间和最晚开始的时间,如果这两个...

2507
来自专栏alexqdjay

HashMap 多线程下死循环分析及JDK8修复

1K4
来自专栏拭心的安卓进阶之路

Java 集合深入理解(6):AbstractList

今天心情比天蓝,来学学 AbstractList 吧! ? 什么是 AbstractList ? AbstractList 继承自 AbstractCollec...

19110

扫码关注云+社区