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