FM算法原理、细节问答、keras实现


一、FM概述

FM = Factorization Machines = 因式分解机

FM 是线性回归+交叉项。通过把所有向量与其后的一个或多个向量做交叉,组合出了二阶或多阶的特征。同时通过将特征交叉对应的联合权重,拆分成独立的特征权重,解决联立数据稀疏问题,具有良好的泛化性能。

二、FM的意义

  1. 解决了稀疏数据下的特征组合的问题
    • 特征组合方面:不需要手工组合,可以自动组合N阶特征
    • 稀疏数据方面:分开训练,避免了无样本可训的问题。见下面具体解释。
  2. 预测的复杂度是线性的
    • 下面有公式推导证明
  3. 训练的复杂度是线性的
    • 见梯度求解部分

三、算法公式

  1. 只考虑二阶情况下的简单公式

\[\tilde{y}(x)=w_0+\sum_{i=1}^{n}w_i x_i+\sum_{i=1}^{n-1}\sum_{j=i+1}^{n}<v_i,v_j>x_i x_j \]

  1. 多阶FM

\[\tilde{y}(x)=w_0+\sum_{i=1}^{n}w_i x_i+\sum_{l=2}^{d}\sum_{i_1=1}^{n}...\sum_{i_l=i_l+1}^{n}(\prod_{j=1}^{l}x_{i_j})(\sum_{f=1}^{k_l}\sum_{j=1}^{l}v_{i_j,f}^{(l)}) \]

其中对第 \(l\) 个交互参数是由PARAFAC模型的参数因子分解得到:

\[V^{l}\in \mathbb{R}^{n*k_l}, k_l \in N_{0}^{+} \]

直接计算多阶公式的时间复杂度是 \(O(k_dn^d)\) ,通过调整后也可以在线性时间内运行。

四、模型图示

输入数据 Demo:

可以认为 FM 把上述每列(或者多列)都相乘,组合了二阶(或多阶)特征。

五、损失函数和学习过程

1.损失函数:

  • Regression任务: \(\hat{y}(x)\) 可以直接用作预测,并且最小平方误差来优化。
  • Binary classification任务: \(\hat{y}(x)\) 作为目标函数并且使用hinge loss或者logit loss来优化。
  • Ranking任务:向量 \(x\) 通过 \(\hat{y}(x)\) 的分数排序,并且通过pairwise的分类损失来优化成对的样本 \((x_a,x_b)\)
  • 对以上的任务中,目标函数中一般加入正则化项参数以防止过拟合。
    • (论文中提到 L2正则,不知道L2是不是在这个问题相比L1更适合?)

2.学习过程:

  • 模型的参数可以通过梯度下降的方法(例如随机梯度下降)来学习,对于各种的损失函数,FM模型中三个可训练参数梯度分别是:

\[\frac{\partial}{\partial\theta}\hat{y}(x)=\left\{\begin{matrix}1,& if\:\theta\:is\:w_0 \\ x_i,& if\:\theta\:is\:w_i \\ x_i\sum_{j=1}^{n}v_{j,f}x_j-v{i,f}x_{i}^{2},& if\:\theta\:is\:v_{i,f} \end{matrix}\right. \]

六、细节要点

  1. FM算法怎样解决了稀疏特征问题(FM的矩阵分解)
    • 在FM算法出现之前,如果要考虑二阶特征交叉,会有以下模型方程:
    • \[\tilde{y}(x)=w_0+\sum_{i=1}^{n}w_i x_i+\sum_{i=1}^{n-1}\sum_{j=i+1}^{n}w_{ij}x_i x_j \]

    • 然而上述方程会因为数据稀疏导致问题,例如我们有 \(x_{张三}\) 代表观众张三, \(x_{星球大战}\) 代表电影《星球大战》,可惜电影实在太多,张三看电影也不多,所以张三没看过《星球大战》。那么方程中的 \(w_{张三,星球大战}\) 就没有样本可以训练。概括来说,对于样本中未出现过交互的特征向量,这种建模方式不能对相应的参数进行估计。
    • FM算法的出现就克服了这个问题。FM算法引入了辅助向量
    • \[V_i=(v_{i,1},v_{i,2},...,v_{i,k}), i=1,2,...,n\\w_{i,j}=V_{i}^{T}V_j:=\sum_{l=1}^{k}v_{il}v_{jl}\\k为超参数 \]

    • 这样一来,\(w_{张三,星球大战}\) 就被拆解成了 \(v_{张三,1..k}\)\(v_{星球大战,1..k}\) 的乘积。就 \(v_{张三,1..k}\) 而言,不必要在与 \(x_{星球大战}\) 一起出现的时候进行训练,假如 \(x_{张三}\)\(x_{黑客帝国}\) 共现,我们就可以训练 \(v_{张三,1..k}\)\(v_{黑客帝国,1..k}\) 。如果《星球大战》和《黑客帝国》具有很高的相似性,那么理论上 \(v_{星球大战,1..k}\)\(v_{黑客帝国,1..k}\) 相似,那么 \(V_{张三}^{T}V_{黑客帝国}≈V_{张三}^{T}V_{星球大战}\) 。这就是FM算法解决系数数据下特征组合交叉的核心逻辑。
  2. 如何将算法的复杂度从 \(O(kn^{2})\) 降低到 \(O(kn)\)
    • FM算法直接进行计算的时间复杂度是 \(O(kn^{2})\) ,因为所有的交叉特征都需要计算。但是通过公式变换,可以减少到线性复杂度(时间复杂度 \(O(kn)\) ),方法如下:
    • 第二行很好理解,从字面上可以推导
    • 第三行出现的 \(k\) 如何理解?
      • \(W=V^{T}V\) 中,有 \(V_{n,k}\) ,理论上需要 \(k\) 足够大(\(k=n\))时求解出合理的 \(V_{n,k}\) ,但是 FM 采用远小于 \(n\)\(k\) 值进行了这一步。由于 \(k\) 代替了 \(n\) 且足够小,所以把计算复杂度从 \(O(kn^{2})\) 降低到了 \(O(kn)\),同时带来了更好的泛化性能。
      • 数学意义上如果k=n这是一种Cholesky分解。这里采用远小于 n 的 k 值,数学意义上不完全相等,但是可以理解为一种近似,一种算法上的正则化。
      • 这也是 Factorization Machines 因式分解的核心!
    • 后面四、五行都是基本推导,在第三行后,复杂度已经变成了 \(O(kn)\)
  3. FM的算法复杂度真的是 \(O(kn)\) 吗?
    • 在理论上是 \(O(kn)\) ,但是考虑到实际 FM 应用的场景是大量稀疏数据,大部分的 \(x\) 都是 0,因此实际上可以跳过这些为0的数据,复杂度远小于 \(O(kn)\)
  4. FM学到的隐向量可以看做是特征的一种embedding表示,把离散特征转化为Dense Feature,这种Dense Feature还可以后续和DNN来结合,作为DNN的输入,事实上用于DNN的CTR也是这个思路来做的。
  5. FM 和 SVD 的关联和区别?
    • 两者同样都用到了矩阵分解,尤其是SVD,它就是个矩阵分解。。。
    • FM 是对两两特征之间的隐向量做矩阵分解,而SVD是对整个样本特征集合做矩阵分解,两者作用的维度不一样,FM更复杂细致。
  6. FM 和 线性回归(或者逻辑回归)的关联和区别?
    • 可以理解 FM 是线性回归的二阶版本。同时通过一定技巧解决特征组合数据系数场景的问题。如果应用在分类问题,加了sigmoid,那就理解为逻辑回归的二阶版本吧~
  7. 泛化特征(连续特征)在FM上该如何使用?直接输入好还是做离散化好?[1]
    • 一般而言应当使用离散特征而非泛化特征(连续特征)。如果的确有连续特征非常重要,可以考虑在FM之外引入使用。
    • FM模型天然为离散(类别)特征设计,离散化后更符合模型的定义和设计初衷
    • 增加模型建模能力
      • 离散特征通常比连续特征能表征更多复杂信息;对比连续特征不离散化当做一个特征只有一个向量,和离散化为n个特征拥有n个向量相比,n个向量的一般效果更好。
    • 减少特征异常造成的模型鲁棒性风险
      • 例如年龄的feature 可能出现10000这样的异常值,在连续系统中对结果可能有较大的影响,但是离散化之后,根据离散化的规则,10000这样的值可能和60就无差异;
    • 减少算法框架实现的复杂度
      • 1001:0.1 这类特征可以离散化表示为100001,而去掉:0.1。
      • 针对稀疏离散的数据,由于每个 \(x\) 值都为0-1,所以计算 \(wx\) 的时候,可以变为非0元素对应的\(w\)求和,不需要求乘积,这种做法可以大大减少样本存储以及网络传输开销,也减少了计算复杂度;
    • 离散化后通常特征处理的逻辑会比较简单
      • 特征拼接、分桶加上交叉,适合标准化,来完成线上的特征组装,达到online learning的需求;

七、模型优缺点

优点

  • 在高度系数的数据条件下,特征之间的交叉仍然可以做估计,而且可以泛化到未被观察到的交叉
  • 模型的训练和预测在时间复杂度上都是线性的

缺点

  • 特征为全交叉,耗费资源。且通常而言,user和user,item和item交叉,效果不如user和item交叉

优化方向

  • 使用矩阵计算,而不是for循环计算
  • 高阶交叉特征的构造

八、模型实现

import keras.backend as K
from keras import activations
from keras.engine.topology import Layer, InputSpec
from keras.utils import plot_model, to_categorical
 
class FMLayer(Layer):
    def __init__(self, output_dim,
                 factor_order,
                 activation=None,
                 **kwargs):
        if 'input_shape' not in kwargs and 'input_dim' in kwargs:
            kwargs['input_shape'] = (kwargs.pop('input_dim'),)
        super(FMLayer, self).__init__(**kwargs)
 
 
        self.output_dim = output_dim
        self.factor_order = factor_order
        self.activation = activations.get(activation)
        self.input_spec = InputSpec(ndim=2)
 
 
    def build(self, input_shape):
        assert len(input_shape) == 2
        input_dim = input_shape[1]
 
        self.input_spec = InputSpec(dtype=K.floatx(), shape=(None, input_dim))
 
        self.b = self.add_weight(name='bias',
                                 shape=(self.output_dim,),
                                 initializer='zeros',
                                 trainable=True)
        self.w = self.add_weight(name='one',
                                 shape=(input_dim, self.output_dim),
                                 initializer='glorot_uniform',
                                 trainable=True)
        self.v = self.add_weight(name='two',
                                 shape=(input_dim, self.factor_order),
                                 initializer='glorot_uniform',
                                 trainable=True)
 
        super(FMLayer, self).build(input_shape)
 
 
    def call(self, inputs, **kwargs):
        X_square = K.square(inputs)
 
        xv = K.square(K.dot(inputs, self.v))
        xw = K.dot(inputs, self.w)
 
        p = 0.5 * K.sum(xv - K.dot(X_square, K.square(self.v)), 1)
        rp = K.repeat_elements(K.reshape(p, (-1, 1)), self.output_dim, axis=-1)
 
        f = xw + rp + self.b
 
        output = K.reshape(f, (-1, self.output_dim))
        if self.activation is not None:
            output = self.activation(output)
        return output
 
 
    def compute_output_shape(self, input_shape):
        assert input_shape and len(input_shape) == 2
        return input_shape[0], self.output_dim
 
# imdb使用fm实验
 
import numpy as np
from keras.datasets import imdb
from keras.preprocessing import sequence
from keras.layers import Dense, Input, Dropout, Embedding, Conv1D, GlobalMaxPooling1D
from keras.models import Model
 
def fm_model(x_train, x_test, y_train, y_test, train=False):
    inp = Input(shape=(100,))
    x = Embedding(20000, 50)(inp)
    x = Dropout(0.2)(x)
    x = Conv1D(250, 3, padding='valid', activation='relu', strides=1)(x)
    x = GlobalMaxPooling1D()(x)
    x = FMLayer(200, 100)(x)
    x = Dropout(0.2)(x)
    x = Dense(1, activation='sigmoid')(x)
    model = Model(inputs=inp, outputs=x)
    if train:
        model.compile(loss='binary_crossentropy',
                      optimizer='adam',
                      metrics=['accuracy'])
        model.fit(x_train, y_train,
                  batch_size=32,
                  epochs=2,
                  validation_data=(x_test, y_test))
        # model.save_weights('model.h5')
    return model
 
 
def test_model(x_train, x_test, y_train, y_test, train=False):
    # 不用fm的对照模型
    inp = Input(shape=(100,))
    x = Embedding(20000, 50)(inp)
    x = Dropout(0.2)(x)
    x = Conv1D(250, 3, padding='valid', activation='relu', strides=1)(x)
    x = GlobalMaxPooling1D()(x)
    x = Dense(250, activation='relu')(x)
    x = Dropout(0.2)(x)
    x = Dense(1, activation='sigmoid')(x)
 
    model = Model(inputs=inp, outputs=x)
    if train:
        model.compile(loss='binary_crossentropy',
                      optimizer='adam',
                      metrics=['accuracy'])
        model.fit(x_train, y_train,
                  batch_size=32,
                  epochs=2,
                  validation_data=(x_test, y_test))
        # model.save_weights('model.h5')
    return model
 
 
(x_train, y_train), (x_test, y_test) = imdb.load_data(num_words=100)
x_train = sequence.pad_sequences(x_train, maxlen=100)
x_test = sequence.pad_sequences(x_test, maxlen=100)
 
test_model(x_train, x_test, y_train, y_test, train=True)
 
model = fm_model(x_train, x_test, y_train, y_test, train=True)

model.summary()
 
plot_model(model, show_shapes=True)

参考资料:

论文pdf:《Factorization Machines 》[2]
《Factorization Machines 学习笔记》[3]
《FM算法解析》[4]


  1. https://www.zhihu.com/question/328925143/answer/716867399 ↩︎

  2. https://www.csie.ntu.edu.tw/~b97053/paper/Rendle2010FM.pdf ↩︎

  3. https://blog.csdn.net/itplus/article/details/40534885 ↩︎

  4. https://zhuanlan.zhihu.com/p/37963267 ↩︎


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM