1. 程式人生 > >FM(Factorization Machine)因式分解機 與 TensorFlow實現 詳解

FM(Factorization Machine)因式分解機 與 TensorFlow實現 詳解

超參數 optimizer 梯度下降 很多 動態 print cor 數量 add

1,線性回歸(Linear Regression)

線性回歸,即使用多維空間中的一條直線擬合樣本數據,如果樣本特征為:

\[x = ({x_1},{x_2},...,{x_n})\]

模型假設函數如下:

\[\hat y = h(w,b) = {w^T}x + b,w = ({w_1},{w_2},...,{w_n})\]

以均方誤差為模型損失,模型輸入樣本為(x(1),y(1)),(x(2),y(2)),...,(x(m),y(m)),損失函數如下:

\[l(w,b) = \sum\limits_{j = 1}^m {{{({{\hat y}^{(j)}} - {y^{(j)}})}^2}} = \sum\limits_{j = 1}^m {(\sum\limits_{i = 1}^n {{w_i}x_i^{(j)} + b} - {y^{(j)}})} \]

2,邏輯回歸(Logistic Regression)

線性回歸用於預測標記為連續的,尤其是線性或準線性的樣本數據,但是有時樣本的標記為離散的,最典型的情況莫過於標記為0或者1,此時的模型被稱為分類模型。

為了擴展線性回歸到分類任務,需要一個函數將(-∞,+∞)映射到(0,1)(或者(-1,1)等任意兩個離散值),函數最好連續可導,並且在自變量趨向於-∞時無限趨近於因變量下限,在自變量趨向於+∞時無限趨近於因變量上限。符合條件的函數有不少,比如tanh函數,logistic函數。

如果映射函數采用logistic函數,設模型假設函數為樣本預測分類概率,模型假設函數為:

\[\begin{array}{l}
P(y = 1) = h(w,b) = \frac{1}{{1 + {e^{ - {h_{linear}}(w,b)}}}} = \frac{1}{{1 + {e^{ - ({w^T}x + b)}}}}\\
P(y = 0) = 1 - P(y = 1)
\end{array}\]

另外,可以從另一個角度考慮從回歸模型擴展為分類模型,令:

\[\ln (\frac{{P(y = 1)}}{{P(y = 0)}}) = \ln (\frac{{h(w,b)}}{{1 - h(w,b)}}) = {h_{linear}}(w,b) = {w^T}x + b\]

同樣可以得到映射函數為logistic函數的假設函數:

\[h(w,b) = \frac{1}{{1 + {e^{ - ({w^T}x + b)}}}} = \frac{1}{{1 + {e^{ - (\sum\limits_{i = 1}^n {{w_i}{x_i}} + b)}}}}\]

使用(負)對數似然函數作為損失函數:

\[\begin{array}{l}
l(w,b) = - \log (\prod\limits_{j = 1}^m {P{{({y^{(j)}} = 1)}^{{y^{(j)}}}}P{{({y^{(j)}} = 0)}^{(1 - {y^{(j)}})}}} )\\
= - \sum\limits_{j = 1}^m {({y^{(j)}}\log P({y^{(j)}} = 1) + (1 - {y^{(j)}})\log P({y^{(j)}} = 0))} \\
= - \sum\limits_{j = 1}^m {({y^{(j)}}\log (g(b + \sum\limits_{i = 1}^n {{w_i}x_i^{(j)}} )) + (1 - {y^{(j)}})\log (1 - g(b + \sum\limits_{i = 1}^n {{w_i}x_i^{(j)}} ))} ,g(z) = \frac{1}{{1 + {e^{ - z}}}}
\end{array}\]

很多場景下,樣本包含一些離散的label特征,這些特征很難連續量化,最簡單的處理方式就是one-hot,即將離散的每個數映射到多維空間中的一維。離散特征包含多少個可能值,轉換後的特征就包含多少維。這樣處理後樣本特征的維數會變得非常巨大,另外大部分維度的值都是0。

一方面,邏輯回歸核心是一個線性模型,因此計算規模隨著樣本特征數的增長而線性增長,相較其他機器學習模型來說計算量(隨特征數增長)的增長率較小;另一方面,邏輯回歸假設函數與損失函數中,各個特征對於結果是獨立起作用的,因此在樣本數足夠的前提下,也不會受到大量值為0的特征的幹擾。因此特別適合這類場景下的分類問題。

3,因式分解機(Factorization Machine)

邏輯回歸最大的優勢是簡單,最大的劣勢也是太簡單,沒有考慮特征之間的相互關系,需要手動對特征進行預處理。因此就有了包含特征兩兩交叉項的假設函數:

\[h(w,b) = g(b + \sum\limits_{i = 1}^n {{w_i}{x_i}} + \sum\limits_{i = 1}^n {\sum\limits_{j = i + 1}^n {{\omega _{ij}}{x_i}} {x_j}} ),g(z) = \frac{1}{{1 + {e^{ - z}}}}\]

此時有一個嚴重的問題,由於特征維數有可能是非常巨大的,很有可能訓練樣本中有一些特征組合xixj(xi、xj都不為0)是完全不存在的,這樣ωij就無法得到訓練。實際使用模型時,如果出現特征組合xixj,模型就無法正常工作。

為了減小訓練計算量,也為了規避上面說的這種情況,我們引入輔助矩陣v,n為特征總維數,k為超參數

\[v \in {R^{n*k}}\]

然後使用vvT代替參數矩陣ω,可得

\[{\omega _{ij}} = \sum\limits_{r = 1}^k {{v_{ir}}v_{rj}^T} = \sum\limits_{r = 1}^k {{v_{ir}}{v_{jr}}} \]

可得假設函數線性部分最後一項可化簡為:

\[\begin{array}{l}
\sum\limits_{i = 1}^n {\sum\limits_{j = i + 1}^n {{\omega _{ij}}{x_i}} {x_j}} \\
= \sum\limits_{i = 1}^n {\sum\limits_{j = i + 1}^n {\sum\limits_{r = 1}^k {{v_{ir}}{v_{jr}}{x_i}{x_j}} } } \\
= \sum\limits_{r = 1}^k {(\sum\limits_{i = 1}^n {\sum\limits_{j = i + 1}^n {{v_{ir}}{v_{jr}}{x_i}{x_j})} } } \\
= \frac{1}{2}\sum\limits_{r = 1}^k {(\sum\limits_{i = 1}^n {\sum\limits_{j = 1}^n {{v_{ir}}{v_{jr}}{x_i}{x_j}} } - \sum\limits_{i = 1}^n {{v_{ir}}{v_{ir}}{x_i}{x_i}} )} \\
= \frac{1}{2}\sum\limits_{r = 1}^k {(\sum\limits_{i = 1}^n {{v_{ir}}{x_i}} \sum\limits_{j = 1}^n {{v_{jr}}{x_j}} - \sum\limits_{i = 1}^n {{v_{ir}}{v_{ir}}{x_i}{x_i}} )} \\
= \frac{1}{2}\sum\limits_{r = 1}^k {({{(\sum\limits_{i = 1}^n {{v_{ir}}{x_i}} )}^2} - \sum\limits_{i = 1}^n {{{({v_{ir}}{x_i})}^2}} )}
\end{array}\]

同邏輯回歸一樣,令模型假設函數為樣本預測分類概率,還是使用(負)對數似然函數為損失函數:

\[\begin{array}{l}
l(b,w,v) = - \log (\prod\limits_{j = 1}^m {P{{({y^{(j)}} = 1)}^{{y^{(j)}}}}P{{({y^{(j)}} = 0)}^{(1 - {y^{(j)}})}}} ) = - \sum\limits_{j = 1}^m {({y^{(j)}}\log (h(w,b){|_{x = {x^{(j)}}}}) + (1 - {y^{(j)}})\log (1 - h(w,b){|_{x = {x^{(j)}}}}))} \\
h(w,b){|_{x = {x^{(j)}}}} = g(b + \sum\limits_{i = 1}^n {{w_i}x_i^{(j)}} + \frac{1}{2}\sum\limits_{r = 1}^k {({{(\sum\limits_{i = 1}^n {{v_{ir}}x_i^{(j)}} )}^2} - \sum\limits_{i = 1}^n {{{({v_{ir}}x_i^{(j)})}^2}} )} )\\
g(z) = \frac{1}{{1 + {e^{ - z}}}}
\end{array}\]

4,關於FM假設函數的討論

仔細看FM假設函數線性部分最後一項的化簡過程,會發現一個問題,這個“化簡”過程僅僅是化簡了式子的形式,其實並沒有減少計算量,反倒是增加了計算量,如果假設函數不是:

\[h(w,b) = g(b + \sum\limits_{i = 1}^n {{w_i}{x_i}} + \sum\limits_{i = 1}^n {\sum\limits_{j = i + 1}^n {{\omega _{ij}}{x_i}} {x_j}} )\]

而是:

\[h(w,b) = g(b + \sum\limits_{i = 1}^n {{w_i}{x_i}} + \sum\limits_{i = 1}^n {\sum\limits_{j = 1}^n {{\omega _{ij}}{x_i}} {x_j}} )\]

表面上看假設函數增加了若幹項, 但經過化簡後,可得線性部分最後一項為:

\[\sum\limits_{i = 1}^n {\sum\limits_{j = 1}^n {{\omega _{ij}}{x_i}} {x_j}} = \sum\limits_{i = 1}^n {\sum\limits_{j = i + 1}^n {\sum\limits_{r = 1}^k {{v_{ir}}{v_{jr}}{x_i}{x_j}} } } = \sum\limits_{r = 1}^k {({{(\sum\limits_{i = 1}^n {{v_{ir}}{x_i}} )}^2})} \]

反倒是更簡單了,為什麽不用後面式子計算,而要用更復雜的前者呢?

這是由於ω的每一項並不是獨立的,我們使用了vvT代替了ω

\[{\omega _{ij}} = \sum\limits_{r = 1}^k {{v_{ir}}v_{rj}^T} = \sum\limits_{r = 1}^k {{v_{ir}}{v_{jr}}} \]

即可認為vi=(vi1,vi2,...,vik)代表了特征i與其他特征交叉關系的隱藏特征

如果不去掉交叉特征中的xixi項,則等同於vi不僅隱含了交叉關系,還隱含了xi的二次項,不符合我們的初衷

5,TensorFlow概述

TensorFlow即tensor+flow,tensor指數據,以任意維的類型化數組為具體形式,flow則指數據的流向

描述flow有三個層次的概念:

  • operation:是最基本的單元,每個operation獲得0個或多個tensor,產生0個或多個tensor。operation可能為獲取一個常量tf.constant(),獲取一個變量tf.get_variable()或者進行一個操作,比如加法tf.add()、乘法tf.multiply()。
  • graph:定義了數據流,由tf.Graph()初始化(一般情況下省略)。graph由節點和節點間的連線構成,graph中的節點即operation。graph是對數據流向組成的網絡結構的定義,本身並不進行任何計算。
  • session:定義了graph運行的環境,由tf.Session()初始化。session中可以運行graph中一個或多個operation,可以在運行前通過feed給operation輸入數據,也可以通過返回值獲取計算結果。

概念上講,圖中所有節點間的連線都是tensor,但總需要有一些數據作為數據來源,描述這些數據來源的組件有以下幾種類型:

  • 常量:用tf.constant()定義,即運行過程中不會改變的數據,一般用來保存一些固有參數。常量可以被認為是一個operation,輸入為空,輸出為常數(數組)。
  • 變量:理論上廣義的變量包含所有除常量以外的所有tensor,這裏的變量專指tf.Variable()。值得註意的是,在TensorFlow中,小寫開頭的才是operation,大寫開頭的都是類,因此tf.Variable()不是一個operation,而是包含了一系列operation,如初始化、賦值的類成員。每個graph運行前必須初始化所有使用到的變量。
  • 占位符:用tf.placeholder()定義,是一種特殊類型的operation,不需要初始化,但需要在運行中通過feed接收數據。

6,LR的TensorFlow實現

首先需要定義輸入樣本的特征維度和接收輸入樣本特征和樣本標簽的占位符。

特征維度可以使用普通python變量指定,此時等同於使用tf.constant創建了一個常量

FEATURE_NUM = 8
#shape參數的列表長度代表占位符的維度
#如果shape不為None,則輸入數據維度必須等同於shape參數列表長度
#每一維的大小可以不指定(為None),也可以指定,如果指定則輸入數據該維度長度必須與shape指定的一致
x = tf.placeholder(tf.float32, shape=[None, FEATURE_NUM])
#等同於x = tf.placeholder(tf.float32, shape=[None, tf.constant(FEATURE_NUM, dtype=tf.int64)])
y = tf.placeholder(tf.float32, shape=[None])

不可以通過使用placeholder指定特征維度

m = tf.placeholder(tf.int64, shape=None)
x = tf.placeholder(tf.float32, shape=[None, m])  #無法執行
y = tf.placeholder(tf.float32, shape=[None])

雖然placeholder初始shape中不能包含其他tensor,但可以根據輸入樣本的列數動態指定

x = tf.placeholder(tf.float32, shape=[None, None])  #或者可以不指定維數,輸入shape=None,此時可以輸入任意維數,任意大小的數據
y = tf.placeholder(tf.float32, shape=[None])
m = tf.shape(x)[1]  #此處不能使用x.get_shape()[1].value,get_shape()獲取的為靜態大小,返回結果為[None,None]

接下來要定義放置LR假設函數中w與b的變量,在定義參數w前,需要定義一個w的初始值。對於LR來說,參數初始值為隨機數或者全零都沒關系,因為LR的損失函數一定是凸函數(求二階導數可知4),但一般情況下還是習慣采用隨機數初始化。

#常用的隨機函數有random_uniform、random_normal和truncated_normal
#分別是均勻分布、正態分布、被截斷的正態分布
weight_init = tf.truncated_normal(shape=[FEATURE_NUM, 1],mean=0.0,stddev=1.0)  #此處shape初始化只能使用常量,不支持使用tensor
weight = tf.Variable(weight_init)  
bais = tf.Variable([0.0])

為了保持習慣用法,將一維向量y擴展為二維列向量,TensorFlow中擴維可以使用tf.expand_dims()或者tf.reshape():

y_exband = tf.expand_dims(y, axis=1)  #方法1,axis表示在原若幹個維度的間隔中第幾個位置插入新維度
y_exband = tf.reshape(y, shape=[tf.shape(x)[0],1])  #方法2,shape為輸出的維度值
y_exband = tf.reshape(y, shape=[-1,1])  #方法3,shape參數列表中最多只能有一個參數未知,寫為-1

可知:

\[x \in {R^{m*n}},y \in {R^{m*1}},w \in {R^{n*1}},b \in {R^0}\]

回頭看LR損失函數的定義

\[l(w,b) = - \sum\limits_{j = 1}^m {({y^{(j)}}\log (g(b + \sum\limits_{i = 1}^n {{w_i}x_i^{(j)}} )) + (1 - {y^{(j)}})\log (1 - g(b + \sum\limits_{i = 1}^n {{w_i}x_i^{(j)}} ))} ,g(z) = \frac{1}{{1 + {e^{ - z}}}}\]

在TensorFlow中,“+”等同於tf.add(),“-”等同於tf.subtract(),“*”等同於tf.multiply(),“/”等同於tf.div(),所有這些操作是“逐項操作”:

  • 如果兩個操作數都為標量,則結果為標量
  • 如果一個操作數為標量,另一個操作數為向量,則標量會分別與向量每一個元素逐個操作,得到結果
  • 如果兩個操作數為相同維度,每個維度大小相同的向量,則結果為向量每兩個對應位置上的元素的操作得到的結果
  • 如果兩個操作數維度不相同,如果向量維度數為a、b,並且a>b,則b的各維度大小需要與a的高維度相對應,低維向量在高維向量的低維展開,操作得到結果
  • 除上面幾種情況,調用不合法

因此,在邏輯回歸的損失函數中,我們無需關心“b+”、“1-”等操作,這些標量自然會展開到正確的維度。同樣,tf.log()、tf.sigmoid()函數也會對輸入向量每一個元素逐個操作,不會改變向量的維度和對應值的位置。

對於式子中的求和Σ,在TensorFlow中有兩種處理方式:

  • 一種是矩陣乘法tf.matmul(),如矩陣a、b、c關系為c=tf.matmul(a,b),則可得:

\[{c_{ij}} = \sum\limits_k {{a_{ik}}{b_{kj}}} \]

  • 另一種是矩陣壓縮函數tf.reduce_sum()(求和)、tf.reduce_mean()(求平均)等函數。這些函數除了第一個參數為要操作的矩陣外,還有幾個很有用的參數:reduction_indices是要壓縮的維度。對於一個二維矩陣來說,壓縮維度為0意味著壓縮行,生成列向量,壓縮維度為1意味著壓縮列,生成行向量,reduction_indices=None意味著壓縮成標量;keepdim指壓縮後是否保持原有維度,如果keepdims=True,操作矩陣被壓縮的維度長度會保持,但長度一定是1

在損失函數中,先看最外層求和項(以j求和),可以看做y與log函數的結果的逐項相乘求和,因此log函數的輸出向量形狀必須與y或者y的轉置一致。而log函數的輸出向量形狀取決於內部求和項(以i求和)。可以通過求和式得到合適的運算形式為x*w,也可以通過形狀來推理合適的運算形式,即:

\[tf.matmul(x,w) \in {R^{m*1}}\]

然後經過log函數得到的結果與y逐項相乘,再求壓縮平均(此處之所以不是求和而是求平均,是為了避免樣本數量對損失函數結果產生影響),可以得到假設函數、(負)似然函數、損失函數的運算式:

hypothesis = tf.sigmoid(tf.matmul(x, weight) + bais)
likelyhood = -(y_exband*tf.log(hypothesis) + (1.0-y_exband)*(tf.log(1.0-hypothesis)))
loss = tf.reduce_mean(likelyhood, reduction_indices=0)

然後利用TensorFlow的自動微分功能,即優化類之一來定義模型的優化過程:

LEARNING_RATE = 0.02
optimizer = tf.train.GradientDescentOptimizer(LEARNING_RATE)
training_op = optimizer.minimize(loss)

TensorFlow的優化類主要有以下幾個:

  • GradientDescentOptimizer:最普通的批量梯度下降,令學習速率為η,t代表本次叠代,t+1代表下次叠代,則梯度叠代公式如下:

\[{\theta _{t + 1}} = {\theta _t} - \eta \frac{{\partial l(\theta )}}{{\partial \theta }}\]

  • AdagradOptimizer:進行參數叠代的同時記錄了每個參數每次叠代的梯度的平方和,下次叠代時梯度與累積平方和的平方根成反比。這樣會對低頻的參數做較大的更新,對高頻的參數做較小的更新,對於稀疏數據表現的更好;但是由於學習速率越來越小,有可能沒有到達最低點學習速率就變得很慢了,難以收斂。令s為梯度累積平方和,ε為極小量,t代表本次叠代,t-1代表上次叠代,t+1代表下次叠代,梯度叠代公式如下:

\[{s_t} = {s_{t - 1}} + {(\frac{{\partial l(\theta )}}{{\partial \theta }})^2},{\theta _{t + 1}} = {\theta _t} - \frac{\eta }{{\sqrt {{s_t} + \varepsilon } }}\frac{{\partial l(\theta )}}{{\partial \theta }}\]

  • RMSPropOptimizer:為解決AdagradOptimizer後期更新速率過慢的問題,RMSprop使用加權累積平方和替換累積平方和。令m代表梯度加權累積平方和,ε為極小量,β為權重,t代表本次叠代,t-1代表上次叠代,t+1代表下次叠代,梯度叠代公式如下:

\[{m_t} = \beta {m_{t - 1}} + (1 - \beta ){(\frac{{\partial l(\theta )}}{{\partial \theta }})^2},{\theta _{t + 1}} = {\theta _t} - \frac{\eta }{{\sqrt {{m_t} + \varepsilon } }}\frac{{\partial l(\theta )}}{{\partial \theta }}\]

  • MomentumOptimizer:多一個必須參數“動量速率”,每次叠代時參考前一次叠代的“動量”,在叠代中方向不變的維度做較大的更新,叠代中方向反復改變的維度做較小的更新。適用於在不同維度梯度差距很大的情況,更新不會在小梯度方向反復震蕩。令γ代表動量速率,t代表本次叠代,t-1代表上次叠代,t+1代表下次叠代,梯度叠代公式如下:

\[{v_t} = \gamma {v_{t - 1}} + \eta \frac{{\partial l(\theta )}}{{\partial \theta }},{\theta _{t + 1}} = {\theta _t} - {v_t}\]

  • AdamOptimizer:綜合了MomentumOptimizer和RMSPropOptimizer,既包含動量(一次項)部分也包含衰減(兩次項)部分。令f代表動量,g代表衰減,β1、β2為權重,t代表本次叠代,t-1代表上次叠代,t+1代表下次叠代:

\[\begin{array}{l}
{f_t} = {\beta _2}{f_{t - 1}} + (1 - {\beta _2})\frac{{\partial l(\theta )}}{{\partial \theta }}\\
{g_t} = {\beta _1}{g_{t - 1}} + (1 - {\beta _1}){(\frac{{\partial l(\theta )}}{{\partial \theta }})^2}
\end{array}\]

註意此處第一次叠代的梯度值和速率值偏小,為了校正偏差,計算校正後的動量f′和衰減g′,再計算叠代公式:

\[\begin{array}{l}
{{f‘}_t} = \frac{{{f_t}}}{{1 - {\beta _1}}}\\
{{g‘}_t} = \frac{{{g_t}}}{{1 - {\beta _2}}}\\
{\theta _{t + 1}} = {\theta _t} - \frac{\eta }{{\sqrt {{{g‘}_t} + \varepsilon } }}{{f‘}_t}
\end{array}\]

最後還需要一個預測操作和準確率判斷操作,使用TensorFlow搭建機器學習網絡一般采用輸出多維向量,使用tf.nn.softmax_cross_entropy_with_logits、

tf.nn.sparse_softmax_cross_entropy_with_logit或tf.nn.sigmoid_cross_entropy_with_logits預測樣本label,但此處還嚴格按照LR原始定義來進行預測:

THRESHOLD = 0.5  
predictions = tf.sign(hypothesis-THRESHOLD)  #符號函數,判斷向量對應位置的符號,輸出對應位置為-1、0或1
labels = tf.sign(y_exband-THRESHOLD)
corrections = tf.equal(predictions, labels)  #比較向量對應位置的兩個值,相等則輸出對應位置為True
accuracy = tf.reduce_mean(tf.cast(corrections, tf.float32))

完整代碼(包括測試運行代碼)如下:

tf.reset_default_graph()  #清空Graph

FEATURE_NUM = 8  #特征數量
with tf.name_scope("lr"):
    x = tf.placeholder(tf.float32, shape=[None, FEATURE_NUM])
    y = tf.placeholder(tf.float32, shape=[None])

    weight_init = tf.truncated_normal(shape=[FEATURE_NUM, 1],mean=0.0,stddev=1.0)
    weight = tf.Variable(weight_init)  
    bais = tf.Variable([0.0])
    
    y_exband = tf.expand_dims(y, axis=1)
    
    hypothesis = tf.sigmoid(tf.matmul(x, weight) + bais)

with tf.name_scope("loss"):
    likelyhood = -(y_exband*tf.log(hypothesis) + (1.0-y_exband)*(tf.log(1.0-hypothesis)))
    loss = tf.reduce_mean(likelyhood, reduction_indices=0)

LEARNING_RATE = 0.02  #學習速率
with tf.name_scope("train"):
    optimizer = tf.train.GradientDescentOptimizer(LEARNING_RATE)
    training_op = optimizer.minimize(loss)
    
THRESHOLD = 0.5  #判斷門限
with tf.name_scope("eval"):
    predictions = tf.sign(hypothesis-THRESHOLD)
    labels = tf.sign(y_exband-THRESHOLD)
    corrections = tf.equal(predictions, labels)
    accuracy = tf.reduce_mean(tf.cast(corrections, tf.float32))

init = tf.global_variables_initializer()  #初始化所有變量的operation

EPOCH = 10  #叠代次數
with tf.Session() as sess:
    sess.run(init)
    for i in range(EPOCH):
        _training_op, _loss = sess.run([training_op, loss], feed_dict={x: np.random.rand(10,8), y: np.random.rand(10)})
        _accuracy = sess.run([accuracy], feed_dict={x: np.random.rand(5,8), y: np.random.rand(5)})
        print "epoch:", i, _loss, _accuracy

7,FM的TensorFlow實現

待續

參考文獻:

FM(Factorization Machine)因式分解機 與 TensorFlow實現 詳解