DeepFM: A Factorization-Machine based Neural Network for CTR Prediction简介与代码实现

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_35564813/article/details/86022277

论文简介

Abstract

对于一个基于CTR预估的推荐系统,最重要的是学习到用户点击行为背后隐含的特征组合。在不同的推荐场景中,低阶组合特征或者高阶组合特征可能都会对最终的CTR产生影响。但是现存的方法总是忽视了高阶或低阶组合特征的联系,或者要求专门的特征工程,因此作者建立了DeepFM模型,将FM与DNN结合起来。

Introduction

在这里插入图片描述

DeepFM的预测结果可以写为:

y ^ = s i g m o i d ( y F M + y D N N ) \hat{y}=sigmoid(y_{FM}+y_{DNN})

FM部分

FM部分的详细结构如下:

在这里插入图片描述

已知我们FM公式如下:

在这里插入图片描述

在目前很多的DNN模型中,都是借助了FM这种形式来做的embedding,具体推导如下:参考自该博客

在这里插入图片描述

扫描二维码关注公众号,回复: 4911703 查看本文章

借助原文的图,这里k表示隐向量的维数, V i j V_{ij} 表示第i个特征embeding之后在隐向量的第j维。假设已经给出了V矩阵,

在这里插入图片描述

其中第5-15个特征是同一个field经过one-hot编码后的表示,这是隐向量按列排成矩阵,同时,它也可看作embedding层的参数矩阵,按照神经网络前向传播的方式,embedding后的该slot下的向量值应该表示为:

在这里插入图片描述

可以看到这个结果就是一个5维的向量,而这个普通的神经网络传递时怎么和FM联系到一起的,仔细观察这个式子可以发现,由于是离散化或者one-hot之后的X,所以对于每一个field的特征而言,截断向量X都只有一个值为1,其他都为0。那么假设上述slot的V中,j为7的特征值为1,那么矩阵相乘之后的结果为:

在这里插入图片描述

从结果中可以看到,实质上,每个slot在embedding后,其结果都是one-hot后有值的那一维特征所对应的隐向量。看到这里,在来解释模型中FM部分是如何借助这种方式得到的。回到模型的示意图,可以看到在FM层,是对每两个embedding向量做内积,那么我们来看,假设两个slot分别是第7和第20个特征值为1:

在这里插入图片描述

是不是感觉特别熟悉,没错,这个乘积的结果就是FM中二阶特征组合的其中一例,而对于所有非0组合(embedding向量组合)求和之后,就是FM中所有二阶特征的部分,这就是模型中FM部分的由来。

当然,FM中的一阶特征,则直接在embedding之前对于特征进行组合即可。

DNN部分

在这里插入图片描述

两个主要的特点:

  • 每个field的embedding保持相同的size
  • FM中的隐向量v被用作embedding 权重压缩数据

FM部分和深度部分分享一样的特征嵌入层能够带来两个好处:

  • 可以学到低阶和高阶特征联系
  • 不需要对输入进行专门的特征工程

代码实现

config.py

    import tensorflow as tf
    
    class Config(object):
        """
        用来存储一些配置信息
        """
        def __init__(self):
            self.feature_dict = None
            self.feature_size = None
            self.field_size = None
            self.embedding_size = 8
    
            self.epochs = 100
            self.deep_layers_activation = tf.nn.relu
    
            self.loss = "logloss"
            self.l2_reg = 0.1
            self.learning_rate = 0.1
            self.deep_layers=[32,32]
    
    
    train_file = "./data/train.csv"
    test_file = "./data/test.csv"
    
    IGNORE_FEATURES = [
        'id', 'target'
    ]
    CATEGORITAL_FEATURES = [
        'feat_cat_1', 'feat_cat_2'
    ]
    NUMERIC_FEATURES = [
        'feat_num_1', 'feat_num_2'
    ]

DataReader.py

    import pandas as pd
    import DeepFM1.config as config
    import gc
    def FeatureDictionary(dfTrain=None, dfTest=None, numeric_cols=None, ignore_cols=None):
        """
        目的是给每一个特征维度都进行编号。
        1. 对于离散特征,one-hot之后每一列都是一个新的特征维度。所以,原来的一维度对应的是很多维度,编号也是不同的。
        2. 对于连续特征,原来的一维特征依旧是一维特征。
        返回一个feat_dict,用于根据 原特征名称和特征取值 快速查询出 对应的特征编号。
        :param dfTrain: 原始训练集
        :param dfTest:  原始测试集
        :param numeric_cols: 所有数值型特征
        :param ignore_cols:  所有忽略的特征. 除了数值型和忽略的,剩下的全部认为是离散型
        :return: feat_dict, feat_size
                 1. feat_size: one-hot之后总的特征维度。
                 2. feat_dict是一个{}, key是特征string的col_name, value可能是编号(int),可能也是一个字典。
                 如果原特征是连续特征: value就是int,表示对应的特征编号;
                 如果原特征是离散特征:value就是dict,里面是根据离散特征的 实际取值 查询 该维度的特征编号。 因为离散特征one-hot之后,一个取值就是一个维度,
                 而一个维度就对应一个编号。
        """
        assert not (dfTrain is None), "train dataset is not set"
        assert not (dfTest is None), "test dataset is not set"
    
        # 编号肯定是要train test一起编号的
        df = pd.concat([dfTrain, dfTest], axis=0)
    
        # 返回值
        feat_dict = {}
    
        # 目前为止的下一个编号
        total_cnt = 0
    
        for col in df.columns:
            if col in ignore_cols: # 忽略的特征不参与编号
                continue
    
            # 连续特征只有一个编号
            if col in numeric_cols:
                feat_dict[col] = total_cnt
                total_cnt += 1
    
            else:
                # 离散特征,有多少个取值就有多少个编号
                unique_vals = df[col].unique()
                unique_cnt = df[col].nunique()
                feat_dict[col] = dict(zip(unique_vals, range(total_cnt, total_cnt + unique_cnt)))
                total_cnt += unique_cnt
    
        feat_size = total_cnt
        return feat_dict, feat_size
    
    def parse(feat_dict=None, df=None, has_label=False):
        """
        构造FeatureDict,用于后面Embedding
        :param feat_dict: FeatureDictionary生成的。用于根据col和value查询出特征编号的字典
        :param df: 数据输入。可以是train也可以是test,不用拼接
        :param has_label:  数据中是否包含label
        :return:  Xi, Xv, y
        """
        assert not (df is None), "df is not set"
        assert not (feat_dict is None), "feat_dict is not set"
    
        dfi = df.copy()
    
        if has_label:
            y = df['target'].values.tolist()
            dfi.drop(['id','target'],axis=1, inplace=True)
        else:
            ids = dfi['id'].values.tolist() # 预测样本的ids
            dfi.drop(['id'],axis=1, inplace=True)
    
        # dfi是Feature index,大小和dfTrain相同,但是里面的值都是特征对应的编号。
        # dfv是Feature value, 可以是binary(0或1), 也可以是实值float,比如3.14
        dfv = dfi.copy()
    
        for col in dfi.columns:
            if col in config.IGNORE_FEATURES: # 用到的全局变量: IGNORE_FEATURES, NUMERIC_FEATURES
                dfi.drop([col], axis=1, inplace=True)
                dfv.drop([col], axis=1, inplace=True)
                continue
    
            if col in config.NUMERIC_FEATURES: # 连续特征1个维度,对应1个编号,这个编号是一个定值
                dfi[col] = feat_dict[col]
            else:
                # 离散特征。不同取值对应不同的特征维度,编号也是不同的。
                dfi[col] = dfi[col].map(feat_dict[col])
                dfv[col] = 1.0
    
        # 取出里面的值
        Xi = dfi.values.tolist()
        Xv = dfv.values.tolist()
    
        del dfi, dfv
        gc.collect()
    
        if has_label:
            return Xi, Xv, y
        else:
            return Xi, Xv, ids

DeepFM.py

    import numpy as np
    import pandas as pd
    import tensorflow as tf
    from DeepFM1.DataReader import FeatureDictionary
    from DeepFM1.DataReader import parse
    import DeepFM1.config as con
    
    ##################################
    # 1. 配置信息
    ##################################
    
    config = con.Config()
    
    ##################################
    # 2. 读取文件
    ##################################
    dfTrain = pd.read_csv(con.train_file)
    dfTest = pd.read_csv(con.test_file)
    
    
    ##################################
    # 3. 准备数据
    ##################################
    
    # FeatureDict
    config.feature_dict, config.feature_size = FeatureDictionary(dfTrain=dfTrain, dfTest=dfTest, numeric_cols=con.NUMERIC_FEATURES, ignore_cols=con.IGNORE_FEATURES)
    print(config.feature_dict)
    print(config.feature_size)
    # Xi, Xv
    Xi_train, Xv_train, y = parse(feat_dict=config.feature_dict, df=dfTrain, has_label=True)
    Xi_test, Xv_test, ids = parse(feat_dict=config.feature_dict, df=dfTest, has_label=False)
    config.field_size = len(Xi_train[0])
    print(Xi_train)
    print(Xv_train)
    print(config.field_size)
    
    ##################################
    # 4. 建立模型
    ##################################
    
    # 模型参数
    
    # BUILD THE WHOLE MODEL
    tf.set_random_seed(2018)
    
    
    # init_weight
    weights = dict()
    # Sparse Features 到 Dense Embedding的全连接权重。[其实是Embedding]
    weights['feature_embedding'] = tf.Variable(initial_value=tf.random_normal(shape=[config.feature_size, config.embedding_size],mean=0,stddev=0.1),
                                               name='feature_embedding',
                                               dtype=tf.float32)
    # Sparse Featues 到 FM Layer中Addition Unit的全连接。 [其实是Embedding,嵌入后维度为1]
    weights['feature_bias'] = tf.Variable(initial_value=tf.random_uniform(shape=[config.feature_size, 1],minval=0.0,maxval=1.0),
                                          name='feature_bias',
                                          dtype=tf.float32)
    # Hidden Layer
    num_layer = len(config.deep_layers)
    input_size = config.field_size * config.embedding_size
    glorot = np.sqrt(2.0 / (input_size + config.deep_layers[0])) # glorot_normal: stddev = sqrt(2/(fan_in + fan_out))
    weights['layer_0'] = tf.Variable(initial_value=tf.random_normal(shape=[input_size, config.deep_layers[0]],mean=0,stddev=glorot),
                                     dtype=tf.float32)
    weights['bias_0'] = tf.Variable(initial_value=tf.random_normal(shape=[1, config.deep_layers[0]],mean=0,stddev=glorot),
                                    dtype=tf.float32)
    for i in range(1, num_layer):
        glorot = np.sqrt(2.0 / (config.deep_layers[i - 1] + config.deep_layers[i]))
        # deep_layer[i-1] * deep_layer[i]
        weights['layer_%d' % i] = tf.Variable(initial_value=tf.random_normal(shape=[config.deep_layers[i - 1], config.deep_layers[i]],mean=0,stddev=glorot),
                                              dtype=tf.float32)
        # 1 * deep_layer[i]
        weights['bias_%d' % i] = tf.Variable(initial_value=tf.random_normal(shape=[1, config.deep_layers[i]],mean=0,stddev=glorot),
                                             dtype=tf.float32)
    # Output Layer
    deep_size = config.deep_layers[-1]
    fm_size = config.field_size + config.embedding_size
    input_size = fm_size + deep_size
    glorot = np.sqrt(2.0 / (input_size + 1))
    weights['concat_projection'] = tf.Variable(initial_value=tf.random_normal(shape=[input_size,1],mean=0,stddev=glorot),
                                               dtype=tf.float32)
    weights['concat_bias'] = tf.Variable(tf.constant(value=0.01), dtype=tf.float32)
    
    
    # build_network
    feat_index = tf.placeholder(dtype=tf.int32, shape=[None, config.field_size], name='feat_index') # [None, field_size]
    feat_value = tf.placeholder(dtype=tf.float32, shape=[None, config.field_size], name='feat_value') # [None, field_size]
    label = tf.placeholder(dtype=tf.float16, shape=[None,1], name='label')
    
    # Sparse Features -> Dense Embedding
    embeddings_origin = tf.nn.embedding_lookup(weights['feature_embedding'], ids=feat_index) # [None, field_size, embedding_size]
    
    feat_value_reshape = tf.reshape(tensor=feat_value, shape=[-1, config.field_size, 1]) # -1 * field_size * 1
    
    # --------- 一维特征 -----------
    y_first_order = tf.nn.embedding_lookup(weights['feature_bias'], ids=feat_index) # [None, field_size, 1]
    w_mul_x = tf.multiply(y_first_order, feat_value_reshape) # [None, field_size, 1]  Wi * Xi
    y_first_order = tf.reduce_sum(input_tensor=w_mul_x, axis=2) # [None, field_size]
    
    # --------- 二维组合特征 ----------
    embeddings = tf.multiply(embeddings_origin, feat_value_reshape) # [None, field_size, embedding_size] multiply不是矩阵相乘,而是矩阵对应位置相乘。这里应用了broadcast机制。
    
    # sum_square part 先sum,再square
    summed_features_emb = tf.reduce_sum(input_tensor=embeddings, axis=1) # [None, embedding_size]
    summed_features_emb_square = tf.square(summed_features_emb)
    
    # square_sum part
    squared_features_emb = tf.square(embeddings)
    squared_features_emb_summed = tf.reduce_sum(input_tensor=squared_features_emb, axis=1) # [None, embedding_size]
    
    # second order
    y_second_order = 0.5 * tf.subtract(summed_features_emb_square, squared_features_emb_summed)
    
    
    # ----------- Deep Component ------------
    y_deep = tf.reshape(embeddings, shape=[-1, config.field_size * config.embedding_size]) # [None, field_size * embedding_size]
    for i in range(0, len(config.deep_layers)):
        y_deep = tf.add(tf.matmul(y_deep, weights['layer_%d' % i]), weights['bias_%d' % i])
        y_deep = config.deep_layers_activation(y_deep)
    
    # ----------- output -----------
    concat_input = tf.concat([y_first_order, y_second_order, y_deep], axis=1)
    out = tf.add(tf.matmul(concat_input, weights['concat_projection']), weights['concat_bias'])
    out = tf.nn.sigmoid(out)
    
    config.loss = "logloss"
    config.l2_reg = 0.1
    config.learning_rate = 0.1
    
    # loss
    if config.loss == "logloss":
        loss = tf.losses.log_loss(label, out)
    elif config.loss == "mse":
        loss = tf.losses.mean_squared_error(label, out)
    
    # l2
    if config.l2_reg > 0:
        loss += tf.contrib.layers.l2_regularizer(config.l2_reg)(weights['concat_projection'])
        for i in range(len(config.deep_layers)):
            loss += tf.contrib.layers.l2_regularizer(config.l2_reg)(weights['layer_%d' % i])
    
    # optimizer
    optimizer = tf.train.AdamOptimizer(learning_rate=config.learning_rate, beta1=0.9, beta2=0.999, epsilon=1e-8).minimize(loss)
    
    ##################################
    # 5. 训练
    ##################################
    
    # init session
    sess = tf.Session(graph=tf.get_default_graph())
    sess.run(tf.global_variables_initializer())
    
    # train
    feed_dict = {
        feat_index: Xi_train,
        feat_value: Xv_train,
        label:      np.array(y).reshape((-1,1))
    }
    
    
    for epoch in range(config.epochs):
        train_loss,opt = sess.run((loss, optimizer), feed_dict=feed_dict)
        print("epoch: {0}, train loss: {1:.6f}".format(epoch, train_loss))
    
    
    
    
    ##################################
    # 6. 预测
    ##################################
    dummy_y = [1] * len(Xi_test)
    feed_dict_test = {
        feat_index: Xi_test,
        feat_value: Xv_test,
        label: np.array(dummy_y).reshape((-1,1))
    }
    
    prediction = sess.run(out, feed_dict=feed_dict_test)
    
    sub = pd.DataFrame({"id":ids, "pred":np.squeeze(prediction)})
    print("prediction:")
    print(sub)

数据集

train.csv

id,target,feat_cat_1,feat_cat_2,feat_num_1,feat_num_2
1,0,1,2,3.1,2.2
2,0,2,3,2.1,3.1
3,1,0,2,1.0,3.4
4,1,1,1,2.1,1.6
5,0,0,0,0.5,1.8

test.csv

id,target,feat_cat_1,feat_cat_2,feat_num_1,feat_num_2
6,0,1,2,3.1,2.2
7,0,2,3,2.1,3.1
8,1,0,2,1.0,3.4
9,1,1,1,2.1,1.6
10,0,0,0,0.5,1.8

以上数据纯属手工捏造,感兴趣的朋友可以使用
kaggle上的数据集,挺多大佬的deepfm代码实现使用该数据集。

猜你喜欢

转载自blog.csdn.net/qq_35564813/article/details/86022277