3月学习打卡|深度推荐模型|task04

NFM

Abstract

传统FM模型虽然考虑了组合特征,但是其本质仍是一种线性模型,模型的表征能力终究是有限的。后续的Wide&Deep、DeepCrossing 等模型在FM的基础上引入了DNN,试图加强模型的非线性能力,但这类模型对于参数较为敏感,训练难度较大。且在Wide&Deep等模型中,对于二阶交叉特征向量仅进行简单concatenation,然后送入后续DNN部分,将捕获高阶特征交叉信息的任务交给了DNN结构。这种简单拼接的方式,并没有将二阶交叉特征的信息完全表征出来,对于后续DNN来说,基于此结构学习更高阶交叉信息效率太低。而Neural Factorization Machine(NFM)模型同样是在FM的基础上引入DNN,利用非线性结构来学习更多数据信息。不同于Wide&Deep、DeepCrossing等模型,NFM使用Bi-Interaction Layer(Bi-linear interaction)结构来对二阶交叉信息进行处理,使交叉特征的信息能更好的被DNN结构学习,降低DNN学习更高阶交叉特征信息的难度。减轻DNN的负担,意味着不再需要更深的网络结构,从而模型参数量也减少了,模型训练更便捷。

分析

1. NFM 结构定义

NFM公式化定义如下:

[公式]

模型结构图如下所示,注意这个部分并未涵盖一阶项与偏置,完整的NFM是将三者涵盖其中的。

​从结构图可以看出NFM共有四个部分,下面分别对于每个部分进行详细讲解。

1.1 Embedding Layer

embedding vector的计算与之前介绍的模型保持一致,可通过lookup table获取。最终输入特征向量是由输入特征值 [公式] 与 embedding vector [公式] 相乘得到,即 [公式] ​ 。个人认为这个部分与以前的模型没有区别。

1.2 Bi-Interaction Layer

Bi-Interaction Layer是NFM的核心,其本质是一个pooling操作,将embedding vector集合归并为一个向量。

[公式]
其中, [公式] ​ 表示两个向量对应元素相乘,其结果为一个向量。所以,Bi-Interation Layer将embedding vectors 进行两两交叉 ​ [公式] 运算,然后将所有向量进行对应元素求和,最终 [公式] ​ 为pooling之后的一个向量。
公式(2)的计算时间复杂度为 [公式] ​ ,​ [公式] 为embedding vector维度,类似于FM,可以对公式(2)进一步改写为:
[公式]
改写之后的时间复杂度为 ​ [公式] ,其中 ​ [公式] 为输入特征 ​ [公式] 的非零元素个数。Bi-Interaction Layer与FM中的二阶交叉项相比,没有引入额外的参数,同时也能以线性时间复杂度进行训练,这是非常好的性质。

1.3 Hidden Layer

DNN部分的定义如下:

[公式]
其中 [公式] ​ 分别表示参数矩阵与偏置向量, [公式] ​ 表示激活函数,可以取 ​ [公式] 等。关于隐藏层的结构,可以使用类似于FNN中的几种结构: [公式] ​ 等。

1.4 Prediction Layer

最后一层隐藏层加上一个线性变换,作为结果输出,即: [公式] ​ 。

最终,公式(1)可以表示为公式(4)

[公式]

由此也可以看出,如果将中间的隐藏层去掉,仅保留最终的prediction layer,同时将 [公式] ​ 设定为全1的向量,那么NFM完全复现了FM,可以认为NFM就是FM的推广。

a trainable h can not improve the expressiveness of FM, since its impact on prediction can be absorbed into feature embeddings.[1]

[公式] 不会增强FM的表征能力,因为该参数可以吸收到特征的embedding 向量中。换句话说,就算 ​ [公式] 不是全为1的向量,我们也可将其视为FM的等价模型。
仔细观察公式(4),上述Figure2的的结构图仅对应公式中的 ​ [公式] 项。如果将全局偏置 [公式] ​ 与一阶项 [公式] ​ 综合考虑,其实NFM的结构图与Wide&Deep极为相似,但NFM中的二阶项与DNN是 串联结构 。 NFM左侧同样可以看做是一个LR模型,但不同于Wide&Deep,NFM左侧的LR模型仅输入单特征,并没有将组合特征送入LR模型,所以也就无需进行额外的特征工程工作。

2. 过拟合风险

将模型复杂度提高,不可避免的会面临训练过拟合的问题,NFM作者使用了dropout与batch normalization技术来缓解过拟合问题。后续实验表明,结合使用这两种技术能够有效的避免过拟合风险。

2.1 dropout

为了防止过拟合,可以使用dropout技术。需要注意的是,dropout一般是用于Bi-Interaction Layer输出之后,对于后续的每一层隐层都可使用。[5]

2.2 batch normalization

为了加速模型的收敛,同时还可使用batch normalization技术。同样的,作用于Bi-Interaction的输出,以及后续的隐层。[5]

2.3 附加层相对顺序

同时使用dropout,与batch normalization技术,需要注意两者的使用顺序。在 Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shift 一文中,作者提到 :

we would like to ensure that for any parameter values, the network always produces activations with the desired distribution.[2]

也就是说,需要为激活函数提供需要的数据分布。所以,应该在Bi-Interaction Layer之后接入batch normalization,然后直接进行dropout。需要注意的是,Bi-Interaction Layer是没有激活函数的。后续隐层需要进行batch normalization调整数据分布,然后再加上激活函数,最后使用dropout技术。

3.实践

class NFM(object):
    def __init__(self, vec_dim=None, field_lens=None, dnn_layers=None, lr=None, dropout_rate=None):
        self.vec_dim = vec_dim
        self.field_lens = field_lens
        self.field_num = len(field_lens)
        self.dnn_layers = dnn_layers
        self.lr = lr
        self.dropout_rate = dropout_rate
        assert isinstance(dnn_layers, list) and dnn_layers[-1] == 1
        self._build_graph()

    def _build_graph(self):
        self.add_input()
        self.inference()

    def add_input(self):
        self.x = [tf.placeholder(tf.float32, name='input_x_%d'%i) for i in range(self.field_num)]
        self.y = tf.placeholder(tf.float32, shape=[None], name='input_y')
        self.is_train = tf.placeholder(tf.bool)

    def inference(self):
        with tf.variable_scope('linear_part'):
            w0 = tf.get_variable(name='bias', shape=[1], dtype=tf.float32)
            linear_w = [tf.get_variable(name='linear_w_%d'%i, shape=[self.field_lens[i]], dtype=tf.float32) for i in range(self.field_num)]
            linear_part = w0 + tf.reduce_sum(
                tf.concat([tf.reduce_sum(tf.multiply(self.x[i], linear_w[i]), axis=1, keep_dims=True) for i in range(self.field_num)], axis=1),
                axis=1, keep_dims=True) # (batch, 1)
        with tf.variable_scope('emb_part'):
            emb = [tf.get_variable(name='emb_%d'%i, shape=[self.field_lens[i], self.vec_dim], dtype=tf.float32) for i in range(self.field_num)]
            emb_layer = tf.concat([tf.matmul(self.x[i], emb[i]) for i in range(self.field_num)], axis=1) # (batch, F*K)
            emb_layer = tf.reshape(emb_layer, shape=(-1, self.field_num, self.vec_dim)) # (batch, F, K)
        with tf.variable_scope('bi_interaction_part'):
            sum_square_part = tf.square(tf.reduce_sum(emb_layer, axis=1)) # (batch, K)
            square_sum_part = tf.reduce_sum(tf.square(emb_layer), axis=1) # (batch, K)
            nfm = 0.5 * (sum_square_part - square_sum_part)
            nfm = tf.layers.batch_normalization(nfm, training=self.is_train, name='bi_interaction_bn')
            nfm = tf.layers.dropout(nfm, rate=self.dropout_rate, training=self.is_train)
        with tf.variable_scope('dnn_part'):
            in_node = self.vec_dim
            for i in range(len(self.dnn_layers)-1):
                out_node = self.dnn_layers[i]
                w = tf.get_variable(name='w_%d'%i, shape=[in_node, out_node], dtype=tf.float32)
                b = tf.get_variable(name='b_%d'%i, shape=[out_node], dtype=tf.float32)
                in_node = out_node
                nfm = tf.matmul(nfm, w) + b
                nfm = tf.layers.batch_normalization(nfm, training=self.is_train, name='bn_%d'%i)
                nfm = tf.nn.relu(nfm)
                nfm = tf.layers.dropout(nfm, rate=self.dropout_rate, training=self.is_train)
            h = tf.get_variable(name='h', shape=[in_node, 1], dtype=tf.float32)
            nfm = tf.matmul(nfm, h) # (batch, 1)

        self.y_logits = linear_part + nfm
        self.y_hat = tf.nn.sigmoid(self.y_logits)
        self.pred_label = tf.cast(self.y_hat > 0.5, tf.int32)
        self.loss = -tf.reduce_mean(self.y*tf.log(self.y_hat+1e-8) + (1-self.y)*tf.log(1-self.y_hat+1e-8))
        reg_variables = tf.get_collection(tf.GraphKeys.REGULARIZATION_LOSSES)
        if len(reg_variables) > 0:
            self.loss += tf.add_n(reg_variables)
        update_op = tf.get_collection(tf.GraphKeys.UPDATE_OPS)
        with tf.control_dependencies(update_op):
            self.train_op = tf.train.AdamOptimizer(self.lr).minimize(self.loss)```
1赞