1. 程式人生 > >DeepFM算法解析及Python實現

DeepFM算法解析及Python實現

flush 選擇 step args def ict sigmoid 處理方式 相對

1. DeepFM算法的提出

由於DeepFM算法有效的結合了因子分解機與神經網絡在特征學習中的優點:同時提取到低階組合特征與高階組合特征,所以越來越被廣泛使用。

在DeepFM中,FM算法負責對一階特征以及由一階特征兩兩組合而成的二階特征進行特征的提取;DNN算法負責對由輸入的一階特征進行全連接等操作形成的高階特征進行特征的提取。

具有以下特點:

  1. 結合了廣度和深度模型的優點,聯合訓練FM模型和DNN模型,同時學習低階特征組合和高階特征組合。
  2. 端到端模型,無需特征工程。
  3. DeepFM 共享相同的輸入和 embedding vector,訓練更高效。
  4. 評估模型時,用到了一個新的指標“Gini Normalization”

DeepFM裏關於“Field”和“Feature”的理解: 可參考我的文章FFM算法解析及Python實現中對Field和Feature的描述。

2. DeepFM算法結構圖

算法整體結構圖如下所示:

技術分享圖片

其中,DeepFM的輸入可由連續型變量和類別型變量共同組成,且類別型變量需要進行One-Hot編碼。而正由於One-Hot編碼,導致了輸入特征變得高維且稀疏。

應對的措施是:針對高維稀疏的輸入特征,采用Word2Vec的詞嵌入(WordEmbedding)思想,把高維稀疏的向量映射到相對低維且向量元素都不為零的空間向量中。

實際上,這個過程就是FM算法中交叉項計算的過程,具體可參考我的另一篇文章:

FM算法解析及Python實現 中5.4小節的內容。

由上面網絡結構圖可以看到,DeepFM 包括 FM和 DNN兩部分,所以模型最終的輸出也由這兩部分組成:

技術分享圖片

下面,把結構圖進行拆分。首先是FM部分的結構:

技術分享圖片

FM 部分的輸出如下:

技術分享圖片

這裏需要註意三點:

  1. 這裏的wij,也就是<vi,vj>,可以理解為DeepFM結構中計算embedding vector的權矩陣(看到網上很多文章是把vi認為是embedding vector,但仔細分析代碼,就會發現這種觀點是不正確的)。
  2. 由於輸入特征one-hot編碼,所以embedding vector也就是輸入層到Dense Embeddings層的權重,具體可閱讀我在詞嵌入的那些事兒(一)一文中的3.2小節。
  3. Dense Embeddings層的神經元個數是由embedding vector和field_size共同確定,再直白一點就是:神經元的個數為embedding vector*field_size。

然後是DNN部分的結構:

技術分享圖片

這裏DNN的作用是構造高維特征,且有一個特點:DNN的輸入也是embedding vector。所謂的權值共享指的就是這裏。

關於DNN網絡中的輸入a處理方式采用前向傳播,如下所示:

技術分享圖片

這裏假設a(0)=(e1,e2,...em) 表示 embedding層的輸出,那麽a(0)作為下一層 DNN隱藏層的輸入,其前饋過程如下。

技術分享圖片

3. DeepFM算法的Python實現

同樣的,網上關於DeepFM算法實現有很多很多。需要註意的是兩部分:一是訓練集的構造,二是模型的設計。

3.1 訓練集構造

主要是對連續型變量做正態分布等數據預處理操作、類別型變量的One-hot編碼操作、統計One-hot編碼後的特征數量、field_size的數量(註:原始特征數量)。

feature_value。對應的特征值,如果是離散特征的話,就是1,如果不是離散特征的話,就保留原來的特征值。

技術分享圖片

feature_index。用來記錄One-hot編碼後特征的序號,主要用於通過embedding_lookup選擇我們的embedding。

技術分享圖片

相關代碼如下:

import pandas as pd


def load_data():
    train_data = {}
    file_path = F:/Projects/deep_learning/DeepFM/data/tiny_train_input.csv
    data = pd.read_csv(file_path, header=None)
    data.columns = [c + str(i) for i in range(data.shape[1])]
    label = data.c0.values
    label = label.reshape(len(label), 1)
    train_data[y_train] = label
    co_feature = pd.DataFrame()
    ca_feature = pd.DataFrame()
    ca_col = []
    co_col = []
    feat_dict = {}
    cnt = 1
    for i in range(1, data.shape[1]):
        target = data.iloc[:, i]
        col = target.name
        l = len(set(target))  # 列裏面不同元素的數量
        if l > 10:
            # 正態分布
            target = (target - target.mean()) / target.std()
            co_feature = pd.concat([co_feature, target], axis=1)  # 所有連續變量正態分布轉換後的df
            feat_dict[col] = cnt  # 列名映射為索引
            cnt += 1
            co_col.append(col)
        else:
            us = target.unique()
            print(us)
            feat_dict[col] = dict(zip(us, range(cnt, len(us) + cnt)))  # 類別型變量裏的類別映射為索引
            ca_feature = pd.concat([ca_feature, target], axis=1)
            cnt += len(us)
            ca_col.append(col)

    feat_dim = cnt
    feature_value = pd.concat([co_feature, ca_feature], axis=1)
    feature_index = feature_value.copy()

    for i in feature_index.columns:
        if i in co_col:
            # 連續型變量
            feature_index[i] = feat_dict[i]  # 連續型變量元素轉化為對應列的索引值
        else:
            # 類別型變量
            # print(feat_dict[i])
            feature_index[i] = feature_index[i].map(feat_dict[i])  # 類別型變量元素轉化為對應元素的索引值
            feature_value[i] = 1.

    # feature_index是特征的一個序號,主要用於通過embedding_lookup選擇我們的embedding
    train_data[xi] = feature_index.values.tolist()
    # feature_value是對應的特征值,如果是離散特征的話,就是1,如果不是離散特征的話,就保留原來的特征值。
    train_data[xv] = feature_value.values.tolist()
    train_data[feat_dim] = feat_dim

    return train_data


if __name__ == __main__:
    load_data()

3.2 模型設計

模型設計主要是完成了FM部分和DNN部分的結構設計,具體功能代碼中都進行了註釋。

import os
import sys
import numpy as np
import tensorflow as tf

from build_data import load_data


BASE_PATH = os.path.dirname(os.path.dirname(__file__))


class Args():
    feature_sizes = 100
    field_size = 15
    embedding_size = 256
    deep_layers = [512, 256, 128]
    epoch = 3
    batch_size = 64

    # 1e-2 1e-3 1e-4
    learning_rate = 1.0

    # 防止過擬合
    l2_reg_rate = 0.01
    checkpoint_dir = os.path.join(BASE_PATH, data/saver/ckpt)
    is_training = True


class model():
    def __init__(self, args):
        self.feature_sizes = args.feature_sizes
        self.field_size = args.field_size
        self.embedding_size = args.embedding_size
        self.deep_layers = args.deep_layers
        self.l2_reg_rate = args.l2_reg_rate

        self.epoch = args.epoch
        self.batch_size = args.batch_size
        self.learning_rate = args.learning_rate
        self.deep_activation = tf.nn.relu
        self.weight = dict()
        self.checkpoint_dir = args.checkpoint_dir
        self.build_model()

    def build_model(self):
        self.feat_index = tf.placeholder(tf.int32, shape=[None, None], name=feature_index)
        self.feat_value = tf.placeholder(tf.float32, shape=[None, None], name=feature_value)
        self.label = tf.placeholder(tf.float32, shape=[None, None], name=label)

        # One-hot編碼後的輸入層與Dense embeddings層的權值定義,即DNN的輸入embedding。註:Dense embeddings層的神經元個數由field_size和決定
        self.weight[feature_weight] = tf.Variable(
            tf.random_normal([self.feature_sizes, self.embedding_size], 0.0, 0.01),
            name=feature_weight)

        # FM部分中一次項的權值定義
        # shape (61,1)
        self.weight[feature_first] = tf.Variable(
            tf.random_normal([self.feature_sizes, 1], 0.0, 1.0),
            name=feature_first)

        # deep網絡部分的weight
        num_layer = len(self.deep_layers)
        # deep網絡初始輸入維度:input_size = 39x256 = 9984 (field_size(原始特征個數)*embedding個神經元)
        input_size = self.field_size * self.embedding_size
        init_method = np.sqrt(2.0 / (input_size + self.deep_layers[0]))

        # shape (9984,512)
        self.weight[layer_0] = tf.Variable(
            np.random.normal(loc=0, scale=init_method, size=(input_size, self.deep_layers[0])), dtype=np.float32
        )
        # shape(1, 512)
        self.weight[bias_0] = tf.Variable(
            np.random.normal(loc=0, scale=init_method, size=(1, self.deep_layers[0])), dtype=np.float32
        )

        # 生成deep network裏面每層的weight 和 bias
        if num_layer != 1:
            for i in range(1, num_layer):
                init_method = np.sqrt(2.0 / (self.deep_layers[i - 1] + self.deep_layers[i]))

                # shape  (512,256)  (256,128)
                self.weight[layer_ + str(i)] = tf.Variable(
                    np.random.normal(loc=0, scale=init_method, size=(self.deep_layers[i - 1], self.deep_layers[i])),
                    dtype=np.float32)

                # shape (1,256)  (1,128)
                self.weight[bias_ + str(i)] = tf.Variable(
                    np.random.normal(loc=0, scale=init_method, size=(1, self.deep_layers[i])),
                    dtype=np.float32)

        # deep部分output_size + 一次項output_size + 二次項output_size 423
        last_layer_size = self.deep_layers[-1] + self.field_size + self.embedding_size
        init_method = np.sqrt(np.sqrt(2.0 / (last_layer_size + 1)))
        # 生成最後一層的結果
        self.weight[last_layer] = tf.Variable(
            np.random.normal(loc=0, scale=init_method, size=(last_layer_size, 1)), dtype=np.float32)
        self.weight[last_bias] = tf.Variable(tf.constant(0.01), dtype=np.float32)

        # embedding_part
        # shape (?,?,256)
        self.embedding_index = tf.nn.embedding_lookup(self.weight[feature_weight],
                                                      self.feat_index)  # Batch*F*K

        # shape (?,39,256)
        self.embedding_part = tf.multiply(self.embedding_index,
                                          tf.reshape(self.feat_value, [-1, self.field_size, 1]))
        # [Batch*F*1] * [Batch*F*K] = [Batch*F*K],用到了broadcast的屬性
        print(embedding_part:, self.embedding_part)

        """
        網絡傳遞結構
        """
        # FM部分
        # 一階特征
        # shape (?,39,1)
        self.embedding_first = tf.nn.embedding_lookup(self.weight[feature_first],
                                                      self.feat_index)  # bacth*F*1
        self.embedding_first = tf.multiply(self.embedding_first, tf.reshape(self.feat_value, [-1, self.field_size, 1]))
        # shape (?,39)
        self.first_order = tf.reduce_sum(self.embedding_first, 2)
        print(first_order:, self.first_order)

        # 二階特征
        self.sum_second_order = tf.reduce_sum(self.embedding_part, 1)
        self.sum_second_order_square = tf.square(self.sum_second_order)
        print(sum_square_second_order:, self.sum_second_order_square)

        self.square_second_order = tf.square(self.embedding_part)
        self.square_second_order_sum = tf.reduce_sum(self.square_second_order, 1)
        print(square_sum_second_order:, self.square_second_order_sum)

        # 1/2*((a+b)^2 - a^2 - b^2)=ab
        self.second_order = 0.5 * tf.subtract(self.sum_second_order_square, self.square_second_order_sum)

        # FM部分的輸出(39+256)
        self.fm_part = tf.concat([self.first_order, self.second_order], axis=1)
        print(fm_part:, self.fm_part)

        # DNN部分
        # shape (?,9984)
        self.deep_embedding = tf.reshape(self.embedding_part, [-1, self.field_size * self.embedding_size])
        print(deep_embedding:, self.deep_embedding)

        # 全連接部分
        for i in range(0, len(self.deep_layers)):
            self.deep_embedding = tf.add(tf.matmul(self.deep_embedding, self.weight["layer_%d" % i]),
                                         self.weight["bias_%d" % i])
            self.deep_embedding = self.deep_activation(self.deep_embedding)

        # FM輸出與DNN輸出拼接
        din_all = tf.concat([self.fm_part, self.deep_embedding], axis=1)
        self.out = tf.add(tf.matmul(din_all, self.weight[last_layer]), self.weight[last_bias])
        print(output:, self.out)

        # loss部分
        self.out = tf.nn.sigmoid(self.out)

        self.loss = -tf.reduce_mean(
            self.label * tf.log(self.out + 1e-24) + (1 - self.label) * tf.log(1 - self.out + 1e-24))

        # 正則:sum(w^2)/2*l2_reg_rate
        # 這邊只加了weight,有需要的可以加上bias部分
        self.loss += tf.contrib.layers.l2_regularizer(self.l2_reg_rate)(self.weight["last_layer"])
        for i in range(len(self.deep_layers)):
            self.loss += tf.contrib.layers.l2_regularizer(self.l2_reg_rate)(self.weight["layer_%d" % i])

        self.global_step = tf.Variable(0, trainable=False)
        opt = tf.train.GradientDescentOptimizer(self.learning_rate)
        trainable_params = tf.trainable_variables()
        print(trainable_params)
        gradients = tf.gradients(self.loss, trainable_params)
        clip_gradients, _ = tf.clip_by_global_norm(gradients, 5)
        self.train_op = opt.apply_gradients(
            zip(clip_gradients, trainable_params), global_step=self.global_step)

    def train(self, sess, feat_index, feat_value, label):
        loss, _, step = sess.run([self.loss, self.train_op, self.global_step], feed_dict={
            self.feat_index: feat_index,
            self.feat_value: feat_value,
            self.label: label
        })
        return loss, step

    def predict(self, sess, feat_index, feat_value):
        result = sess.run([self.out], feed_dict={
            self.feat_index: feat_index,
            self.feat_value: feat_value
        })
        return result

    def save(self, sess, path):
        saver = tf.train.Saver()
        saver.save(sess, save_path=path)

    def restore(self, sess, path):
        saver = tf.train.Saver()
        saver.restore(sess, save_path=path)


def get_batch(Xi, Xv, y, batch_size, index):
    start = index * batch_size
    end = (index + 1) * batch_size
    end = end if end < len(y) else len(y)
    return Xi[start:end], Xv[start:end], np.array(y[start:end])


if __name__ == __main__:
    args = Args()
    data = load_data()
    args.feature_sizes = data[feat_dim]
    args.field_size = len(data[xi][0])
    args.is_training = True

    with tf.Session() as sess:
        Model = model(args)
        # init variables
        sess.run(tf.global_variables_initializer())
        sess.run(tf.local_variables_initializer())

        cnt = int(len(data[y_train]) / args.batch_size)
        print(time all:%s % cnt)
        sys.stdout.flush()
        if args.is_training:
            for i in range(args.epoch):
                print(epoch %s: % i)
                for j in range(0, cnt):
                    X_index, X_value, y = get_batch(data[xi], data[xv], data[y_train], args.batch_size, j)
                    loss, step = Model.train(sess, X_index, X_value, y)
                    if j % 100 == 0:
                        print(the times of training is %d, and the loss is %s % (j, loss))
                        Model.save(sess, args.checkpoint_dir)
        else:
            Model.restore(sess, args.checkpoint_dir)
            for j in range(0, cnt):
                X_index, X_value, y = get_batch(data[xi], data[xv], data[y_train], args.batch_size, j)
                result = Model.predict(sess, X_index, X_value)
                print(result)

最終計算結果如下:

技術分享圖片

4. 總結

到此,關於CTR問題的三個算法(FM、FFM、DeepFM)已經介紹完畢,當然這僅僅是冰山一角,此外還有FNN、Wide&Deep等算法。感興趣的同學可以自行研究。

此外,個人認為CTR問題的核心在於特征的構造,所以不同算法的差異主要體現在特征構造方面。

最後,附上一個CTR問題各模型的效果對比圖。

技術分享圖片

DeepFM算法解析及Python實現