1. 程式人生 > >DeepFM: A Factorization-Machine based Neural Network for CTR Prediction簡介與程式碼實現

DeepFM: A Factorization-Machine based Neural Network for CTR Prediction簡介與程式碼實現

論文簡介

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,具體推導如下:參考自該部落格

在這裡插入圖片描述

藉助原文的圖,這裡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程式碼實現使用該資料集。