DeepFM结合代码的理解

 

闲谈

众所周知,自从人工智能火了以后,大家现在全民AI,连小学生中学生都在搞所谓的AI。AI的实现应该靠算法与硬件的结合,但是国内貌似搞算法的远超搞硬件的。现阶段来看,算法层面上,主要靠深度网络。我理解所谓的深度网络,就是用一系列的线性函数模拟复杂的非线性函数。举个简单例子,一个正弦函数,我们可以将他的作用域划分成一系列的小区间,将每个区间端点的函数值用直线连接起来。如果这些区间足够小,就可以用一系列的一次线性函数拟合这个正弦函数。神经网络中,每个神经元就可以模拟一个区间。所以理论上,一个两层的神经网络,就可以拟合任意一个复杂的函数。

最近工作中遇到了类似ctr点击的问题,所以看了一些这方面深度学习的方法。ctr最开始用逻辑回归算法,后来发展使用了FM算法。FM算法中,解决了特征之间的interaction的问题。随后研究人员提出使用深度学习的方法来做ctr问题,神经网络可以解决的是高层特征的interaction问题,不仅仅解决了两阶interaction的问题。言归正传,下面我们开始介绍DeepFM这篇文章。

DeepFM

google最先提出利用wide&deep用于ctr点击的预估,这篇文章wide的部分是需要预先训练的FM算法,也就是说它不是一个end-to-end的模型。DeepFM算法将FM算法作为一个训练的参数放到网络中,直接使用原始的特征输入,只需要告诉网络你的特征哪个是numerical,哪个是categories的特征。DeepFM在Wide&Deep的基础上进行改进,成功解决了这两个问题,并做了一些改进,其优势/优点如下:

  1. 不需要预训练FM得到隐向量
  2. 不需要人工特征工程
  3. 能同时学习低阶和高阶的组合特征
  4. FM模块和Deep模块共享Feature Embedding部分,可以更快的训练,以及更精确的训练学习

下面我们结合kaggle比赛的一个数据和代码进行分析。

1.输入处理

首先,利用pandas读取数据,然后获得对应的特征和target,保存到对应的变量中。并且将categories的变量保存下来。

#加载数据
def _load_data():

    #读取csv文件
    dfTrain = pd.read_csv(config.TRAIN_FILE)
    dfTest = pd.read_csv(config.TEST_FILE)

    def preprocess(df):
        cols = [c for c in df.columns if c not in ["id", "target"]]
        df["missing_feat"] = np.sum((df[cols] == -1).values, axis=1)
        df["ps_car_13_x_ps_reg_03"] = df["ps_car_13"] * df["ps_reg_03"]
        return df

    dfTrain = preprocess(dfTrain)
    dfTest = preprocess(dfTest)

    cols = [c for c in dfTrain.columns if c not in ["id", "target"]]
    cols = [c for c in cols if (not c in config.IGNORE_COLS)]#只保留我们需要的列

    X_train = dfTrain[cols].values
    y_train = dfTrain["target"].values
    X_test = dfTest[cols].values
    ids_test = dfTest["id"].values
    cat_features_indices = [i for i,c in enumerate(cols) if c in config.CATEGORICAL_COLS]

    return dfTrain, dfTest, X_train, y_train, X_test, ids_test, cat_features_indices

2.创建模型

fd = FeatureDictionary(dfTrain=dfTrain, dfTest=dfTest,
                           numeric_cols=config.NUMERIC_COLS,
                           ignore_cols=config.IGNORE_COLS)

2.1创建一个特征处理的字典

首先,创建一个特征处理的字典。在初始化方法中,传入第一步读取得到的训练集和测试集。然后生成字典,在生成字典中,循环遍历特征的每一列,如果当前的特征是数值型的,直接将特征作为键值,和目前对应的索引作为value存到字典中。如果当前的特征是categories,统计当前的特征总共有多少个不同的取值,这时候当前特征在字典的value就不是一个简单的索引了,value也是一个字典,特征的每个取值作为key,对应的索引作为value,组成新的字典。总而言之,这里面主要是计算了特征的的维度,numerical的特征只占一位,categories的特征有多少个取值,就占多少位。

class FeatureDictionary(object):
    def __init__(self, trainfile=None, testfile=None,
                 dfTrain=None, dfTest=None, numeric_cols=[], ignore_cols=[]):
        assert not ((trainfile is None) and (dfTrain is None)), "trainfile or dfTrain at least one is set"
        assert not ((trainfile is not None) and (dfTrain is not None)), "only one can be set"
        assert not ((testfile is None) and (dfTest is None)), "testfile or dfTest at least one is set"
        assert not ((testfile is not None) and (dfTest is not None)), "only one can be set"
        self.trainfile = trainfile
        self.testfile = testfile
        self.dfTrain = dfTrain
        self.dfTest = dfTest
        self.numeric_cols = numeric_cols
        self.ignore_cols = ignore_cols
        #根据特征的种类是numerical还是categories的类别 计算输入到网络里面的特征的长度
        self.gen_feat_dict()

    def gen_feat_dict(self):
        if self.dfTrain is None:
            dfTrain = pd.read_csv(self.trainfile)
        else:
            dfTrain = self.dfTrain
        if self.dfTest is None:
            dfTest = pd.read_csv(self.testfile)
        else:
            dfTest = self.dfTest
        df = pd.concat([dfTrain, dfTest])
        self.feat_dict = {}
        tc = 0
        #通过下面的循环 计算输入到模型中特征的总的长度
        for col in df.columns:
            if col in self.ignore_cols:
                continue
            if col in self.numeric_cols:
                # map to a single index
                self.feat_dict[col] = tc
                tc += 1
            else:
                us = df[col].unique()#查看当前categories种类的特征有多少个唯一的值
                self.feat_dict[col] = dict(zip(us, range(tc, len(us)+tc)))
                tc += len(us)
        self.feat_dim = tc

2.2数据解析

data_parser = DataParser(feat_dict=fd)
#解析数据 Xi_train存放的是特征对应的索引 Xv_train存放的是特征的具体的值
Xi_train, Xv_train, y_train = data_parser.parse(df=dfTrain, has_label=True)
Xi_test, Xv_test, ids_test = data_parser.parse(df=dfTest)
在解析数据中,逐行处理每一条数据,dfi记录了当前的特征在总的输入的特征中的索引。dfv中记录的是具体的值,如果是numerical特征,存的是原始的值,如果是categories类型的,就存放1。这个相当于进行了one-hot编码,在dfi存储了特征所在的索引。输入到网络中的特征的长度是numerical特征的个数+categories特征one-hot编码的长度。最终,Xi和Xv是一个二维的list,里面的每一个list是一行数据,Xi存放的是特征所在的索引,Xv存放的是具体的特征值。
#解析数据
class DataParser(object):
    def __init__(self, feat_dict):
        self.feat_dict = feat_dict

    def parse(self, infile=None, df=None, has_label=False):
        assert not ((infile is None) and (df is None)), "infile or df at least one is set"
        assert not ((infile is not None) and (df is not None)), "only one can be set"
        if infile is None:
            dfi = df.copy()
        else:
            dfi = pd.read_csv(infile)
        if has_label:
            y = dfi["target"].values.tolist()
            dfi.drop(["id", "target"], axis=1, inplace=True)
        else:
            ids = dfi["id"].values.tolist()
            dfi.drop(["id"], axis=1, inplace=True)
        # dfi for feature index
        # dfv for feature value which can be either binary (1/0) or float (e.g., 10.24)
        # dfi 记录的是特征所对应的索引 也就是输入样本在输入维度的第几个地方不等于0 dfv记录的是特征的具体的值
        dfv = dfi.copy()
        for col in dfi.columns:
            if col in self.feat_dict.ignore_cols:
                dfi.drop(col, axis=1, inplace=True)
                dfv.drop(col, axis=1, inplace=True)
                continue
            if col in self.feat_dict.numeric_cols:
                dfi[col] = self.feat_dict.feat_dict[col]
            else:
                dfi[col] = dfi[col].map(self.feat_dict.feat_dict[col])
                dfv[col] = 1.

        # list of list of feature indices of each sample in the dataset
        Xi = dfi.values.tolist()
        # list of list of feature values of each sample in the dataset
        Xv = dfv.values.tolist()
        if has_label:
            return Xi, Xv, y
        else:
            return Xi, Xv, ids

2.3准备batch

    #解析数据 Xi_train存放的是特征对应的索引 Xv_train存放的是特征的具体的值
    Xi_train, Xv_train, y_train = data_parser.parse(df=dfTrain, has_label=True)
    Xi_test, Xv_test, ids_test = data_parser.parse(df=dfTest)
 #feature_size记录特征的维度 field_size记录了特征的个数
    dfm_params["feature_size"] = fd.feat_dim
    dfm_params["field_size"] = len(Xi_train[0])

    y_train_meta = np.zeros((dfTrain.shape[0], 1), dtype=float)
    y_test_meta = np.zeros((dfTest.shape[0], 1), dtype=float)
    _get = lambda x, l: [x[i] for i in l]
    gini_results_cv = np.zeros(len(folds), dtype=float)
    gini_results_epoch_train = np.zeros((len(folds), dfm_params["epoch"]), dtype=float)
    gini_results_epoch_valid = np.zeros((len(folds), dfm_params["epoch"]), dtype=float)
    for i, (train_idx, valid_idx) in enumerate(folds):
        Xi_train_, Xv_train_, y_train_ = _get(Xi_train, train_idx), _get(Xv_train, train_idx), _get(y_train, train_idx)
        Xi_valid_, Xv_valid_, y_valid_ = _get(Xi_train, valid_idx), _get(Xv_train, valid_idx), _get(y_train, valid_idx)

2.4构造DeepFM模型

1.在初始化方法中,先设置一些初始化的参数,比如特征的长度,总共有多少个原始的字段等。然后最重要的就是初始化图self._init_graph()方法。

2.在构造图的方法中,先定义了6个placeholder,每个大小的None代表的是batch_size的大小。

self.feat_index = tf.placeholder(tf.int32, shape=[None, None],
                                     name="feat_index")  # None * F    batch_size * field_size
self.feat_value = tf.placeholder(tf.float32, shape=[None, None],
                                     name="feat_value")  # None * F    batch_size * field_size
self.label = tf.placeholder(tf.float32, shape=[None, 1], name="label")  # None * 1
self.dropout_keep_fm = tf.placeholder(tf.float32, shape=[None], name="dropout_keep_fm")
self.dropout_keep_deep = tf.placeholder(tf.float32, shape=[None], name="dropout_keep_deep")
self.train_phase = tf.placeholder(tf.bool, name="train_phase")

在这之后,调用权重的初始化方法。将所有的权重放到一个字典中。feature_embeddings本质上就是FM中的latent vector。对于每一个特征都建立一个隐特征向量。feature_bias代表了FM中的w的权重。然后就是搭建深度图,输入到深度网络的大小为:特征的个数*每个隐特征向量的长度。根据每层的配置文件,生产相应的权重。对于输出层,根据不同的配置,生成不同的输出的大小。如果只是使用FM算法,那么输入的大小则为:field_size+embedding_size ,添加deep的特征,输入大小为:field_size+embedding_size+deep_size。

# todo 初始化权重
    def _initialize_weights(self):
        weights = dict()

        # embeddings
        weights["feature_embeddings"] = tf.Variable(
            tf.random_normal([self.feature_size, self.embedding_size], 0.0, 0.01),
            name="feature_embeddings")  # feature_size * K
        weights["feature_bias"] = tf.Variable(
            tf.random_uniform([self.feature_size, 1], 0.0, 1.0), name="feature_bias")  # feature_size * 1

        # deep layers
        num_layer = len(self.deep_layers)
        #计算输入的大小
        input_size = self.field_size * self.embedding_size

        #todo ========================第一层的网络结构=============================
        glorot = np.sqrt(2.0 / (input_size + self.deep_layers[0]))
        weights["layer_0"] = tf.Variable(
            np.random.normal(loc=0, scale=glorot, size=(input_size, self.deep_layers[0])), dtype=np.float32)
        weights["bias_0"] = tf.Variable(np.random.normal(loc=0, scale=glorot, size=(1, self.deep_layers[0])),
                                                        dtype=np.float32)  # 1 * layers[0]
        for i in range(1, num_layer):
            glorot = np.sqrt(2.0 / (self.deep_layers[i-1] + self.deep_layers[i]))
            weights["layer_%d" % i] = tf.Variable(
                np.random.normal(loc=0, scale=glorot, size=(self.deep_layers[i-1], self.deep_layers[i])),
                dtype=np.float32)  # layers[i-1] * layers[i]
            weights["bias_%d" % i] = tf.Variable(
                np.random.normal(loc=0, scale=glorot, size=(1, self.deep_layers[i])),
                dtype=np.float32)  # 1 * layer[i]

        # final concat projection layer
        if self.use_fm and self.use_deep:
            input_size = self.field_size + self.embedding_size + self.deep_layers[-1]
        elif self.use_fm:
            input_size = self.field_size + self.embedding_size
        elif self.use_deep:
            input_size = self.deep_layers[-1]
        glorot = np.sqrt(2.0 / (input_size + 1))
        weights["concat_projection"] = tf.Variable(
                        np.random.normal(loc=0, scale=glorot, size=(input_size, 1)),
                        dtype=np.float32)  # layers[i-1]*layers[i]
        weights["concat_bias"] = tf.Variable(tf.constant(0.01), dtype=np.float32)

        return weights

3.根据每次输入的特征的索引,从隐特征向量中取出其对应的隐向量。将每一个特征对应的具体的值,和自己对应的隐向量相乘。如果是numerical的,就直接用对应的value乘以隐向量。如果是categories的特征,其对应的特征值是1.相乘完还是原来的隐向量。最后,self.embeddings存放的就是输入的样本的特征值和隐向量的乘积。大小为batch_size*field_size*embedding_size

self.embeddings = tf.nn.embedding_lookup(self.weights["feature_embeddings"],self.feat_index)  # None * F * K 由于one-hot是01编码 所以取出的大小为batch_size*field_size*embedding_size
feat_value = tf.reshape(self.feat_value, shape=[-1, self.field_size, 1])
self.embeddings = tf.multiply(self.embeddings, feat_value) # 两个矩阵中对应元素各自相乘 这个就是就是每个值和自己的隐向量的乘积

 4.计算一阶项,从self.weights["feature_bias"]取出对应的w,得到一阶项,大小为batch_size*field_size。

二阶项的计算,也就是FM的计算,利用了(a+b+c)^{2}-(a^{2}+b^{2}+c^{2})的技巧。先将embeddings在filed_size的维度上求和,最后得到红框里面的项。

5.计算deep的项。将self.embeddings(大小为batch_size*self.field_size * self.embedding_size)  reshape成batch_size*(self.field_size * self.embedding_size)的大小,然后输入到网络里面进行计算。

6.最后将所有项 concat起来,投影到一个值。如果是只要FM,不要deep的部分,则投影的大小为filed_size+embedding_size的大小。如果需要deep的部分,则大小再加上deep的部分。利用最后的全连接层,将特征映射到一个scalar。

7.最后一项就是定义损失和优化器。如果是回归问题,则使用mse损失,分类则使用log损失。

 # loss 损失函数
            if self.loss_type == "logloss":
                self.out = tf.nn.sigmoid(self.out)
                self.loss = tf.losses.log_loss(self.label, self.out)
            elif self.loss_type == "mse":
                self.loss = tf.nn.l2_loss(tf.subtract(self.label, self.out))
            # l2 regularization on weights 正则化项目
            if self.l2_reg > 0:
                self.loss += tf.contrib.layers.l2_regularizer(
                    self.l2_reg)(self.weights["concat_projection"])
                if self.use_deep:
                    for i in range(len(self.deep_layers)):
                        self.loss += tf.contrib.layers.l2_regularizer(
                            self.l2_reg)(self.weights["layer_%d"%i])

            # optimizer
            if self.optimizer_type == "adam":
                self.optimizer = tf.train.AdamOptimizer(learning_rate=self.learning_rate, beta1=0.9, beta2=0.999,
                                                        epsilon=1e-8).minimize(self.loss)
            elif self.optimizer_type == "adagrad":
                self.optimizer = tf.train.AdagradOptimizer(learning_rate=self.learning_rate,
                                                           initial_accumulator_value=1e-8).minimize(self.loss)
            elif self.optimizer_type == "gd":
                self.optimizer = tf.train.GradientDescentOptimizer(learning_rate=self.learning_rate).minimize(self.loss)
            elif self.optimizer_type == "momentum":
                self.optimizer = tf.train.MomentumOptimizer(learning_rate=self.learning_rate, momentum=0.95).minimize(
                    self.loss)
            elif self.optimizer_type == "yellowfin":
                self.optimizer = YFOptimizer(learning_rate=self.learning_rate, momentum=0.0).minimize(
                    self.loss)
发布了8 篇原创文章 · 获赞 36 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/horizonheart/article/details/89632046