TensorFlow-Keras 17. FM原理与自定义实现

 

一.引言

之前讲到过常用的 CTR 模型类似 WideAndDeep,其中包含了 LR 与 DNN,LR主要着重于挖掘一阶特征,DNN主要挖掘特征高阶交叉,FM模型则主要针对与模型的二阶交叉,下面看下 FM 的原理以及如何通过 Keras 自定义层实现 FM Layer,有了 FM,后续 DeepFM 也更好的引入。

 

二.FM原理

1.FM 流程图

下面是经典的 FM 实现流程图,Filed i,..,m 代表m个特征域,FM 会对每个特征域内的特征生成一个 K 维的隐向量,随后通过 Addition 相加操作实现 LR 的一阶交叉,通过 Inner Product 内积操作实现特征之间的二阶交叉,这里 bias 偏置可以根据自己的情况添加,最后将 Addtion 与 InnerProduct 得到结果进行合并输出到最后的 Dense 层,这里激活函数采用 Sigmoid,因为 FM 模型的强项是处理 CTR 相关问题,所以我们需要一个 0-1 之间的概率。

TensorFlow-Keras 17. FM原理与自定义实现_第1张图片

 

2.FM公式

                                    y=w_0 + \sum_{i=1}^{n}w_i x_i + \sum_{i=1}^{n-1}\sum_{j=i+1}^{n}(v_i\cdot v_j)x_i x_j

有了上面的简介,这个公式也就很好理解了,前面两个部分 w0 + ∑ wi·xi 代表线性部分即 LR,后面 ∑∑(vi · vj)xi·xj 代表二阶交叉部分,与传统的 POLY2 模型相比,FM 通过引入隐向量 vi,vj 代替了 w_ij ,从而把模型参数由 n^2 优化至 nk (k为隐向量维度,n >> k),在使用梯度下降训练 FM 的复杂度也降低到 nk 级别。

(1) 为何引入隐向量?

除了上面提到的可以优化参数个数与算法复杂度,还有一个原因是隐向量的引入解决了稀疏数据特征的表示问题,vi vj 并不需要 i-j 特征才统一样本内出现才能训练,甚至对于之前没有见过或者样本中没有交叉过的特征,也可以通过隐向量内积算出其交叉得分。

(2) 为何算法复杂度由 n^2 降低为 nk?

传统 POLY2 模型 n个特征二阶交叉,需要对每个特征学习其他特征的交叉权重,所以 W_ij 共需要n^2 个参数,但是对于 特征i 与 特征j 而言,w_ij 与 wi_ji 是相同的,所以特征矩阵 W 是对称矩阵,通过对对称阵的优化,参数的复杂度可以优化到 nk。网上对复杂度简化的内容比较多,这里不再展开证明,这里只给出最终优化的结果。

         y=w_0 + \sum_{i=1}^{n}w_i x_i + \sum_{i=1}^{n-1}\sum_{j=i+1}^{n}(v_i\cdot v_j)x_i x_j =\frac{1}{2}\sum_{1}^{k}((\sum_{i=1}^{n}v_{i,f}x_i)^2-\sum_{1}^{n}v_{i,f}^2x_i^2)

其中 K 代表隐向量的维度,i,j代表特征,f代表隐向量中具体index的值,复杂的双求和乘积操作转换为 v·x 矩阵的 1/2 * (∑vx)^2 - ∑(vx)^2 即先求和再平方 减去 先平方再求和。具体的操作步骤可以看下述图示:

TensorFlow-Keras 17. FM原理与自定义实现_第2张图片

 

3.到底什么是FM 

如果看了流程图,看了公式,看了矩阵图示,看了公式证明还是没搞懂什么是FM的话,你就只需要知道 FM 为每个特征训练得到一个 K 维隐向量,两个特征的二阶交叉系数可以通过其对应隐向量内积结果得到,这样就解决了二阶交叉的问题。

 

三.keras自定义FMLayer

1.训练样本准备

假设有4个特征A、B、C、D,其特征域范围都为100,则原始特征空间共4x100个特征,预测问题为 CTR,所以 label 为 0/1。

    # 原始特征输入
    num_samples = 60000
    categoryA = np.random.randint(0, 100, (num_samples, 1))
    categoryB = np.random.randint(100, 200, (num_samples, 1))
    categoryC = np.random.randint(200, 300, (num_samples, 1))
    categoryD = np.random.randint(300, 400, (num_samples, 1))

    train = np.concatenate([categoryA, categoryB, categoryC, categoryD], axis=-1).astype('int32')
    print("训练数据样例与Size:")
    print(train[0:5])
    print(train.shape)

    labels = np.random.randint(0, 2, size=num_samples)
    labels = np.asarray(labels)
    print("样本labels:")
    print(labels[0:10])

num_samples 为训练样本数量,train 为训练数据,维度为 (None x 4),labels 为标签,取值为0/1,数据均为 random 随机构造。 

训练数据样例与Size:
[[ 93 153 238 306]
 [ 46 118 249 366]
 [ 20 135 293 376]
 [ 25 132 243 318]
 [ 67 108 201 343]]
(60000, 4)
样本labels:
[0 0 1 1 1 1 0 0 0 1]

 

2.FM层构建: FM_Layer

根据上面的公式拆解,FM层实现主要实现 LR + 二阶交叉,这里每个特征对应 LR 有1个权重,对于二阶交叉,其隐向量维度为K,所以不考虑截距项的情况下,每个特征对应 1+K 个权重,基于这个前提构造的 FM 层可以通过 num_samples x K+1 维的参数矩阵同时训练 LR 与 隐向量权重,优化了算法设计逻辑与效率。

(1) FM层整体构造

这里自定义 Layer 需要类继承 from tensorflow.keras.layers import Layer 类。

init : 定义了模型的特征维度 feature_num 以及输出的隐向量的维度 output_dim

build : 定义可训练的权重矩阵,本例 feature_num = 400,隐向量维度 k=8,则生成参数矩阵维度为 400 x 9 

call : call函数内层的具体实现逻辑,first_order 代表一阶段LR部分,second_order 代表二阶交叉部分,最终通过 concat 输出到下一层,first_order,second_order 实现随后给出。

compute_output_shape: 计算输出维度,供后续 keras 层推断,这里输出维度为: (输入样本数 * 9)

class Fm_Layer(Layer):
    """
        init: 初始化参数
        build: 定义权重
        call: 层的功能与逻辑
        compute_output_shape: 推断输出模型维度

    """

    def __init__(self, feature_num, output_dim, **kwargs):

        self.feature_num = feature_num
        self.output_dim = output_dim

        super().__init__(**kwargs)

    # 定义模型初始化 根据特征数目
    def build(self, input_shape):
        # create a trainable weight variable for this layer
        self.kernel = self.add_weight(name='kernel',
                                      shape=(self.feature_num, self.output_dim + 1),
                                      initializer='glorot_normal',
                                      trainable=True)
        super(Fm_Layer, self).build(input_shape)  # Be sure to call this at the end

    def call(self, inputs, **kwargs):
        # input 为多个样本的稀疏特征表示
        first_order = get_first_order(inputs, self.kernel)
        seconder_order = get_second_order(inputs, self.kernel)
        concat_order = tf.concat([first_order, seconder_order], axis=-1)
        return concat_order

    def compute_output_shape(self, input_shape):
        return (input_shape(0), self.output_dim+1)

(2) LR部分实现

featIndex为特征索引,args 为参数矩阵,即 layer 的可训练权重,这里 lookup 得到每个权重的最后一位作为其线性部分权重,并求和得到 LR 部分。

def get_first_order(featIndex, args):
    embedding = tf.nn.embedding_lookup(args, featIndex)[:, :, -1]
    print('embedding', embedding)
    linear = tf.reduce_sum(embedding, axis=-1)
    sum_embedding = K.expand_dims(linear, axis=1)
    return sum_embedding

 (3) 二阶交叉部分

featIndex为特征索引,args为参数矩阵,这里权重矩阵去掉了最后一维,剩下 K 维代表隐向量权重,先求和再平方,先平方再求和的图示逻辑可以参考上面的流程图,这里不再赘述代码逻辑,最后套公式相减后乘 1/2 即可得到二阶交叉结果。

def get_second_order(featIndex, args):
    embedding = tf.nn.embedding_lookup(args, featIndex)[:, :, :args.shape[-1]-1]
    # 先求和再平方
    sum_embedding = tf.reduce_sum(embedding, axis=1)
    sum_square = K.square(sum_embedding)
    # 先平方在求和
    squared = K.square(embedding)
    square_sum = tf.reduce_sum(squared, axis=1)
    # 二阶交叉项
    second_order = 0.5 * tf.subtract(sum_square, square_sum)
    return second_order

 

3.FM模型构造: fm_model

输入样本维度为 60000 x 4,所以 Input 层的shape为4,fm层特征数为400,隐向量维度为8,最终concat LR与二阶交叉部分结果并连接 Dense 层通过 sigmoid 归一化到 0-1 的浮点数,整个模型构建完成。

    # 构建模型
    deep_input = layers.Input(shape=4, name='deep', dtype='int32')
    fm_layer = Fm_Layer(400, 8)(deep_input)
    re = layers.Dense(1, activation='sigmoid')(fm_layer)
    fm_model = Model(deep_input, re)

TensorFlow-Keras 17. FM原理与自定义实现_第3张图片

 

4.模型编译与训练

损失函数选择交叉熵,也有同学使用 MSE,其他超参也可以自己调整。

    # 模型编译
    fm_model.compile(optimizer='rmsprop',
                  loss='binary_crossentropy',
                  metrics='accuracy')
    fm_model.summary()

    # 模型训练
    fm_model.fit(train, labels , epochs=10, batch_size=128)
Epoch 1/10
469/469 [==============================] - 0s 459us/step - loss: 0.6932 - accuracy: 0.4997
Epoch 2/10
469/469 [==============================] - 0s 450us/step - loss: 0.6922 - accuracy: 0.5253

......

Epoch 9/10
469/469 [==============================] - 0s 425us/step - loss: 0.6657 - accuracy: 0.5937
Epoch 10/10
469/469 [==============================] - 0s 430us/step - loss: 0.6640 - accuracy: 0.5992

 由于是随机数据,训练相关指标仅供参考。

 

5.模型预测

构造4维的原始特征,调用 model.predict 即可,结果是 0-1 之间的浮点数,代表CTR的高低。

    # 模型预测
    print("模型预测结果:")
    test_sample = [[1, 101, 201, 301], [7, 110, 222, 323], [81, 114, 270, 342], [63, 139, 204, 323], [33, 173, 201, 396]]
    print(fm_model.predict(test_sample))
模型预测结果:
[[0.5398645 ]
 [0.43967327]
 [0.5679844 ]
 [0.45938677]
 [0.69800216]]

 

6.隐向量获取

隐向量维度为8,lR权重维度为1,通过 getWeigths() 方法即可获得对应 index 特征的隐向量与权重。

    # 获取线性权重与隐向量
    index = 100
    weigths = np.array(fm_model.get_layer(index=1).get_weights()).reshape(400, 9)
    vector = weigths[index]
    print("V-隐向量")
    v = vector[:8]
    print(v)
    print("W-线性:")
    w = vector[-1]
    print(w)
V-隐向量
[-0.04662997 -0.11286838  0.23590335 -0.09384488  0.03299862  0.23256397
  0.10323359  0.05801315]
W-线性:
0.16635393

 

7.隐向量维度对模型的影响

    fm_layer = Fm_Layer(400, 64)(deep_input)

隐向量维度刻画了特征的画像,理论上维度越高描述越精确,但随之而来的过拟合和性能问题也会凸显,只需修改 fm_layer 的第二个参数即可修改隐向量维度,修改为 64 维表征后,模型的指标均得到提升。

Epoch 1/10
469/469 [==============================] - 1s 726us/step - loss: 0.6933 - accuracy: 0.4970
Epoch 2/10
469/469 [==============================] - 0s 691us/step - loss: 0.6910 - accuracy: 0.5614

......

Epoch 9/10
469/469 [==============================] - 0s 707us/step - loss: 0.5414 - accuracy: 0.7333
Epoch 10/10
469/469 [==============================] - 0s 701us/step - loss: 0.5300 - accuracy: 0.7408

 

四.总结

1.FM 优化了特征组合交叉部分,交叉维度介于 LR 与 DNN ,LR与DNN可以参考 WideAndDeep

2.自定义 Layer 必须实现的3个函数为: build,call,compute_output_shape

3.自定义回调可以参考: 自定义回调函数,你可以定期保存模型ckpt或者提前结束模型训练

4.调整样本权重可以参考: 训练样本加权,调整样本权重类似于修改采样策略,使模型偏好于某一类样本

5.自定义训练指标可以参考: 自定义Loss,Metrics,这里可以修改自己的梯度更新方式,指标计算方法

6.修改参数初始化方法可以参考:keras常见参数初始化方法,不同的参数初始化对模型的训练也有较大关系

7.模型性能的优化以及大规模特征输入可以参考: sparseTensor 与 lookup_embedding_sparse

8.训练得到的隐向量也可以作为特征表征作为其他模型的输入特征,所以并不限于计算内积

你可能感兴趣的:(tensorflow,keras,机器学习,tensorflow,keras,FM,自定义FM_Layer)