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


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.int32, shape=[None])

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

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

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

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

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

#常用的隨機函數有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_expand = tf.expand_dims(y, axis=1)  #方法1,axis表示在原若干個維度的間隔中第幾個位置插入新維度
y_expand = tf.reshape(y, shape=[tf.shape(x)[0],1])  #方法2,shape為輸出的維度值
y_expand = 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()(求平均)等函數。這些函數除了第一個參數為要操作的矩陣外,還有幾個很有用的參數:axis是要壓縮的維度。對於一個二維矩陣來說,壓縮維度為0意味着壓縮行,生成列向量,壓縮維度為1意味着壓縮列,生成行向量,axis=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逐項相乘,再求壓縮平均(此處之所以不是求和而是求平均,是為了避免樣本數量對損失函數結果產生影響),可以得到假設函數、(負)似然函數、損失函數的運算式:

y_float = tf.to_float(y_expand)
hypothesis = tf.sigmoid(tf.matmul(x, weight) + bais) likelyhood = -(y_float*tf.log(hypothesis) + (1.0-y_float)*(tf.log(1.0-hypothesis))) loss = tf.reduce_mean(likelyhood, axis=0)

還可以使用TensorFlow的損失函數計算loss

TensorFlow的損失函數主要有以下幾個(下述所有公式假設樣本數只有一個,多個的話需要取平均):

  • log_loss:即交叉熵計算,設損失函數l,假設函數h,樣本標簽y,二分類損失公式如下:

\[l =  - y*\log (h) - (1 - y)\log (1 - h)\]

        多分類損失公式如下,注意可以有多個分類(分類數為c)為正確分類:

\[l =  - \sum\limits_i^c {({y_i}\log ({h_i}) - (1 - {y_i})\log (1 - {h_i}))} \]

        注意不是下式,這里跟一些其他庫有區別:

\[l =  - \sum\limits_i^c {{y_i}\log ({h_i})} \]

  • sigmoid_cross_entropy:sigmoid+交叉熵,與log_loss的差別在於先對輸入預測值求sigmoid函數,然后再計算交叉熵。sigmoid函數如下:

\[g(z) = \frac{1}{{1 + {e^{ - z}}}}\]

  • softmax_cross_entropy:softmax+交叉熵,設分類總數為c,softmax函數如下:

\[g({z_i}) = \frac{{{e^{{z_i}}}}}{{\sum\limits_j^c {{e^{{z_j}}}} }}\]

  • sparse_softmax_cross_entropy:計算方式與softmax_cross_entropy,但輸入樣本標簽為整數標簽,而不是one-hot形式,這是唯一一個輸入兩個參數各維度大小不同的損失函數

 

  • mean_squared_error:均方誤差損失函數,損失公式如下:

\[l = {(y - h)^2}\]

  • huber_loss:由於均方誤差對誤差比較大的點相當敏感,為了控制異常點對模型訓練的影響,huber loss對誤差比較小的樣本點使用二次項計算損失,對誤差比較大的樣本點使用線性方程計算損失,

\[l = \left\{ \begin{array}{l}
\frac{1}{2}{(y - h)^2},|y - h| < = d\\
\frac{1}{2}{d^2} + d(|y - h| - d),|y - h| > d
\end{array} \right.\]

  • hinge_loss:hinge loss不太在意預測的有多准確,而主要比較預測正確類別與錯誤類別的間隔,設損失函數l,假設函數h,樣本標簽y,二分類損失公式如下:

\[l = \left\{ \begin{array}{l}
\max (0,1 - h),y = 1\\
\max (0,1 + h),y = 0
\end{array} \right.\]

        設正確分類為i,分類總數為c,多分類損失公式如下:

\[l = \sum\limits_{j \ne i}^c {\max (0,1 + {h_j} - {h_i})} \]

在此處,使用tf.losses.log_loss()或者tf.losses.sigmoid_cross_entropy()可以得到與上面運算式完全相同的結果,即:

hypothesis = tf.sigmoid(tf.matmul(x, weight) + bais)
loss = tf.losses.log_loss(y_expand, hypothesis)

或者

hypothesis = tf.matmul(x, weight) + bais
loss = tf.losses.sigmoid_cross_entropy(y_expand, hypothesis)

然后利用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}\]

最后還需要一個預測操作和准確率判斷操作:

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

也可以使用:

THRESHOLD = 0.5
predictions = tf.to_int32(hypothesis-THRESHOLD)
corrections = tf.equal(predictions, y_expand)
accuracy = tf.reduce_mean(tf.cast(corrections, tf.float32))

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

tf.reset_default_graph()  #清空Graph

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

with tf.name_scope("lr"):
    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_expand = tf.expand_dims(y, axis=1)
    
    hypothesis = tf.sigmoid(tf.matmul(x, weight) + bais)

with tf.name_scope("loss"):
    y_float = tf.to_float(y_expand)
    likelyhood = -(y_float*tf.log(hypothesis) + (1.0-y_float)*(tf.log(1.0-hypothesis)))
    loss = tf.reduce_mean(likelyhood, axis=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_float-THRESHOLD)
    corrections = tf.equal(predictions, labels)
    accuracy = tf.reduce_mean(tf.cast(corrections, tf.float32))

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

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.randint(2,size=10)})
        _accuracy = sess.run([accuracy], feed_dict={x: np.random.rand(5,8), y: np.random.randint(2,size=5)})
        print "epoch:", i, _loss, _accuracy

7,FM的TensorFlow實現

同樣,先定義變量占位符:

FEATURE_NUM = 8  #特征數量
x = tf.placeholder(tf.float32, shape=[None, FEATURE_NUM])
y = tf.placeholder(tf.int32, shape=[None])
y_exband = tf.expand_dims(y, axis=1)

然后實現假設函數:

\[\begin{array}{l}
l(b,w,v) = - \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}} )} )
\end{array}\]

先定義模型權重:

HIDDEN_N = 5
bais = tf.Variable([0.0])
weight = tf.Variable(tf.random_normal([FEATURE_N, 1], 0.0, 1.0))
weight_mix = tf.Variable(tf.random_normal([FEATURE_N, HIDDEN_N], 0.0, 1.0))

一次項和零次項比較好計算,跟LR一樣,但二次項就比較麻煩了。這里的計算過程,一些廣為流傳的博客上代碼都是錯的。

首先可知:

\[x \in {R^{m*n}},v \in {R^{n*k}},y \in {R^{m*1}}\]

先嘗試矩陣乘法降維,可得:

\[xv \in {R^{m*k}},{(xv)_{ab}} = \sum\limits_{i = 1}^n {x_i^{(a)}{v_{ib}}} \]

可見正好與二次項的第一項形式一致,可以通過以下方式計算第一項,一個m*k的矩陣,得到結果后可以通過先壓縮第1維再壓縮第0維,得到損失函數結果(0維標量)

x_weight_mix = tf.matmul(x, weight_mix)
x_weight_mix_square = tf.square(x_weight_mix)

但觀察二次項的第二項,就沒這么簡單了,去除維度無關的加法、減法、log等操作的影響,第二項等同於計算如下式子:

\[\sum\limits_{j = 1}^m {\sum\limits_{r = 1}^k {\sum\limits_{i = 1}^n {{{(x_i^{(j)}{v_{ir}})}^2}} } } \]

即需要得到每個j、r、i的組合,再平方,再依次求和,可知這必須是一個三維矩陣才能完成運算。因此,如果能設計一個三維矩陣d滿足如下就條件,就好計算了:

\[{d_{jri}} = x_i^{(j)}{v_{ir}},d \in {R^{m*k*n}}\]

由於三維矩陣d的每項不涉及求和,只有兩個數相乘,因此最直接的方式是擴展x、v的維度與d相同,然后直接逐項相乘,為方便計算,調整三維矩陣d的定義:

\[{d_{jir}} = x_i^{(j)}{v_{ir}},d \in {R^{m*n*k}}\]

這樣,x在第2維擴展,v在第0為擴展,逐項相乘結果即為d:

\[\begin{array}{l}
{d_{jir}} = {x_{(new),}}_{ir}^{(j)}{v_{(new)}}_{,jir},d \in {R^{m*n*k}}\\
{x_{(new),}}_{ir}^{(j)} = x_i^{(j)},{x_{(new)}} \in {R^{m*n*k}}\\
{v_{(new)}}_{,jir} = {v_{ir}},{v_{(new)}} \in {R^{m*n*k}}
\end{array}\]

這里需要用到tf.tile()和tf.reshape(),tf.tile()的功能是將矩陣的某一維(或者n維)的數據重復數次,tf.reshape()可以將兩維向量變為三維矩陣。這里需要注意,如果先tile再reshape,其實是很容易出錯的,例如將x的第2維擴展k次,可得:

\[{x_{(tile)}} \in {R^{m*(n*k)}}\]

但是,到底x(tile)經過“合適的”reshape升維后是m*n*k維矩陣還是m*k*n維矩陣,是一個難以直接判斷的問題,因此,最好先reshape(或者expand_dims),再tile。二次項的第二項計算方式如下:

SAMPLE_N = tf.shape(x)[0]
x_ = tf.reshape(x, [SAMPLE_N,FEATURE_N,1]) #SAMPLE_N*FEATURE_N*1
x__ = tf.tile(x_, [1,1,HIDDEN_N]) #SAMPLE_N*FEATURE_N*HIDDEN_N
w_ = tf.reshape(weight_mix, [1,FEATURE_N,HIDDEN_N]) #1*FEATURE_N*HIDDEN_N
w__ = tf.tile(x_, [SAMPLE_N,1,1]) #SAMPLE_N*FEATURE_N*HIDDEN_N
embeddings = tf.multiply(x__, w__) #SAMPLE_N*FEATURE_N*HIDDEN_N
embeddings_square = tf.square(embeddings) #SAMPLE_N*FEATURE_N*HIDDEN_N
embeddings_square_sum = tf.reduce_sum(embeddings_square, 1) #SAMPLE_N*HIDDEN_N

此時,可以發現,構造出embeddings矩陣后,二次項的第一項也可以通過embeddings來計算:

embeddings_sum = tf.reduce_sum(embeddings, 1) #SAMPLE_N*HIDDENT_N
embeddings_sum_square = tf.square(embeddings_sum) #SAMPLE_N*HIDDENT_N

然后將損失函數的零次項、一次項、二次項組合起來,可得損失函數:

z = bais + tf.matmul(X, weight) + 1.0/2.0*tf.reduce_sum(tf.subtract(embeddings_sum_square,embeddings_square_sum), 1, keepdims=True)
hypothesis  = tf.sigmoid(z)

這里可以發現,z的計算公式中包含大量二次項的求和,絕對值很可能會比較大,此時再經過sigmoid函數,由於浮點數表達精度有限,結果可能會近似等於1或等於0,造成模型無法更新,因此此處需要使用tf.clip_by_value()對z進行截斷,防止超出精度:

z = bais + tf.matmul(x, weight) + 1.0/2.0*tf.reduce_sum(tf.subtract(embeddings_sum_square,embeddings_square_sum), 1, keepdims=True)
z_ = tf.clip_by_value(z,-4.0,4.0)
hypothesis  = tf.sigmoid(z_)

接下來優化過程與判斷過程與LR基本一樣,可得完整代碼(包括測試運行代碼)如下:

tf.reset_default_graph()  #清空Graph

FEATURE_NUM = 8  #特征數量
with tf.name_scope("input"):
    x = tf.placeholder(tf.float32, shape=[None, FEATURE_NUM])
    y = tf.placeholder(tf.int32, shape=[None])
    y_expand = tf.expand_dims(y, axis=1)

HIDDEN_NUM = 5  #隱藏特征維度
with tf.name_scope("fm"):
    bais = tf.Variable([0.0])
    weight = tf.Variable(tf.random_normal([FEATURE_NUM, 1], 0.0, 1.0))
    weight_mix = tf.Variable(tf.random_normal([FEATURE_NUM, HIDDEN_NUM], 0.0, 1.0))
    
    SAMPLE_NUM = tf.shape(x)[0]  #獲取樣本數
    
    x_ = tf.reshape(x, [SAMPLE_NUM,FEATURE_NUM,1]) #SAMPLE_NUM*FEATURE_NUM*1
    x__ = tf.tile(x_, [1,1,HIDDEN_NUM]) #SAMPLE_NUM*FEATURE_NUM*HIDDEN_NUM
    w_ = tf.reshape(weight_mix, [1,FEATURE_NUM,HIDDEN_NUM]) #1*FEATURE_NUM*HIDDEN_NUM
    w__ = tf.tile(w_, [SAMPLE_NUM,1,1]) #SAMPLE_NUM*FEATURE_NUM*HIDDEN_NUM
    
    embeddings = tf.multiply(x__, w__) #SAMPLE_NUM*FEATURE_NUM*HIDDEN_NUM
    embeddings_sum = tf.reduce_sum(embeddings, 1) #SAMPLE_NUM*HIDDEN_NUM
    embeddings_sum_square = tf.square(embeddings_sum) #SAMPLE_NUM*HIDDEN_NUM
    embeddings_square = tf.square(embeddings) #SAMPLE_NUM*FEATURE_NUM*HIDDEN_NUM
    embeddings_square_sum = tf.reduce_sum(embeddings_square, 1) #SAMPLE_NUM*HIDDEN_NUM
    
    z = bais + tf.matmul(x, weight) + 1.0/2.0*tf.reduce_sum(tf.subtract(embeddings_sum_square,embeddings_square_sum), 1, keepdims=True)
    z_ = tf.clip_by_value(z,-4.0,4.0)
    hypothesis  = tf.sigmoid(z_)

with tf.name_scope("loss"):
    loss = tf.losses.log_loss(y_expand, hypothesis)

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.to_int32(hypothesis-THRESHOLD)
    corrections = tf.equal(predictions, y_expand)
    accuracy = tf.reduce_mean(tf.cast(corrections, tf.float32))

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

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.randint(2,size=10)})
        _accuracy = sess.run([accuracy], feed_dict={x: np.random.rand(5,8), y: np.random.randint(2,size=5)})
        print "epoch:", i, _loss, _accuracy

但是這時可以發現,模型本身已經比較復雜了,其中包含了若干變量,在嵌入工程,或者作為模型中的一個模塊嵌入網絡中時,很有可能會造成變量名稱的混亂,並且也不利於生成明晰的網絡結構,因此,這里考慮將FM構建網絡部分包裹成函數或者類:

#FM模型
class FmModel(object):
    def __init__(self, x, y, feature_num, hidden_num):
        self.x = x
        self.y = y
        self.feature_num = feature_num  #獲取特征數,這個值要建Variable,所以不能動態獲取
        self.sample_num = tf.shape(x)[0]  #獲取樣本數
        self.hidden_num = hidden_num  #獲取隱藏特征維度
        
        self.bais = tf.Variable([0.0])
        self.weight = tf.Variable(tf.random_normal([self.feature_num, 1], 0.0, 1.0))
        self.weight_mix = tf.Variable(tf.random_normal([self.feature_num, self.hidden_num], 0.0, 1.0))
        
        x_ = tf.reshape(self.x, [self.sample_num,self.feature_num,1]) #SAMPLE_NUM*FEATURE_NUM*1
        x__ = tf.tile(x_, [1,1,self.hidden_num]) #SAMPLE_NUM*FEATURE_NUM*HIDDEN_NUM
        w_ = tf.reshape(self.weight_mix, [1,self.feature_num,self.hidden_num]) #1*FEATURE_NUM*HIDDEN_NUM
        w__ = tf.tile(w_, [self.sample_num,1,1]) #SAMPLE_NUM*FEATURE_NUM*HIDDEN_NUM
        
        embeddings = tf.multiply(x__, w__) #SAMPLE_NUM*FEATURE_NUM*HIDDEN_NUM
        embeddings_sum = tf.reduce_sum(embeddings, 1) #SAMPLE_NUM*HIDDEN_NUM
        embeddings_sum_square = tf.square(embeddings_sum) #SAMPLE_NUM*HIDDEN_NUM
        embeddings_square = tf.square(embeddings) #SAMPLE_NUM*FEATURE_NUM*HIDDEN_NUM
        embeddings_square_sum = tf.reduce_sum(embeddings_square, 1) #SAMPLE_NUM*HIDDEN_NUM
        
        z = self.bais + tf.matmul(self.x, self.weight) + 1.0/2.0*tf.reduce_sum(tf.subtract(embeddings_sum_square,embeddings_square_sum), 1, keepdims=True)
        z_ = tf.clip_by_value(z,-4.0,4.0)
        
        self.hypothesis  = tf.sigmoid(z_)
        y_expand = tf.expand_dims(self.y, axis=1)
        self.loss = tf.losses.log_loss(y_expand, self.hypothesis)

這里需要注意,無論使用類也好,函數也好,其中很多變量從全局變量變為了局部變量,函數執行完變量就被銷毀了。但只要執行完一次,網絡就已經建立起來了,變量的生存期並不影響網絡節點的生存期。這時又涉及到另一個問題:如何在Session中想要得到這些類或函數中定義的網絡節點的值。

TensorFlow中會話運行網絡節點的方式有兩種:

  • 使用網絡建立過程中,仍處於生存期內的python變量。這樣做有兩個局限性,一種情況是python變量生存期外就無法使用,另一情況是如果模型加載自已經訓練好的網絡,沒有對應節點的python變量。
  • 使用網絡中的節點名,賦給python變量。TensorFlow中的Tensor都可以在定義時添加一個附加參數name,可以通過tf.get_default_graph().get_tensor_by_name()來獲取網絡中任何的節點。

比如上例中如果要獲取embeddings變量的值,可以如此操作:

#FM模型
class FmModel(object):
    def __init__(self, x, y, feature_num, hidden_num):
        ...
        embeddings = tf.multiply(x__, w__, name="embeddings") #SAMPLE_NUM*FEATURE_NUM*HIDDEN_NUM
        ...
        
HIDDEN_NUM = 5  #隱藏特征維度
with tf.name_scope("fm"):
    fm = FmModel(x, y, FEATURE_NUM, HIDDEN_NUM)
    
embeddings = tf.get_default_graph().get_tensor_by_name("fm/embeddings:0")  #注意name_scope與后面的冒號和序號

使用FmModel類的完整代碼(包括測試運行代碼)如下:

tf.reset_default_graph()  #清空Graph

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

HIDDEN_NUM = 5  #隱藏特征維度
with tf.name_scope("fm"):
    fm = FmModel(x, y, FEATURE_NUM, HIDDEN_NUM)

LEARNING_RATE = 0.02  #學習速率
with tf.name_scope("train"):
    optimizer = tf.train.GradientDescentOptimizer(LEARNING_RATE)
    training_op = optimizer.minimize(fm.loss)
    
THRESHOLD = 0.5  #判斷門限
with tf.name_scope("eval"):
y_expand = tf.expand_dims(y, axis=1) predictions
= tf.to_int32(fm.hypothesis-THRESHOLD) corrections = tf.equal(predictions, fm.y_expand) accuracy = tf.reduce_mean(tf.cast(corrections, tf.float32)) init = tf.global_variables_initializer() #初始化所有變量 EPOCH = 10 #迭代次數 with tf.Session() as sess: sess.run(init) for i in range(EPOCH): _training_op, _loss = sess.run([training_op, fm.loss], feed_dict={x: np.random.rand(10,8), y: np.random.randint(2,size=10)}) _accuracy = sess.run([accuracy], feed_dict={x: np.random.rand(5,8), y: np.random.randint(2,size=5)}) print "epoch:", i, _loss, _accuracy

7,稀疏FM的TensorFlow實現

上述TensorFlow實現中使用了不少二維甚至三維的矩陣運算,在生產環境中,使用FM模型的樣本數據往往是維數巨大並且稀疏的,這時直接使用矩陣乘法會占用大量的系統資源、浪費大量的運算時間。

針對稀疏矩陣乘法,TensorFlow有一種專門的方式簡化運算過程。此時要用到tf.nn.embedding_lookup()函數,這個函數的輸入一個參數矩陣w和一個稀疏索引矩陣i,作用是在在參數矩陣w的第一維中查找索引矩陣i中各值對應的“行”,抽取出索引值對應的“一行”參數,放到索引矩陣中對應位置,組合為有效特征對應的參數矩陣。可知索引矩陣的值必須小於w第一維的大小,結果維數等於w的維數與i的維數之和減一。

由於有效特征矩陣會參與矩陣運算,矩陣運算中不能出現每行長度不同的情況,所以這里有一點局限性,就是每個樣本的有效特征數必須是一樣的,如果多了需要截取,如果少了需要補零。

針對稀疏特征的FM完整代碼(包括測試運行代碼)如下:

#FM模型
class FmModel(object):
    def __init__(self, i, x, y, feature_num, valid_num, hidden_num):
        self.i = i
        self.x = x
        self.y = y
        self.feature_num = feature_num  #獲取特征數,這個值要建Variable,所以不能動態獲取
        self.valid_num = valid_num  #獲取有效特征數,這個值要建Variable,所以不能動態獲取
        self.sample_num = tf.shape(x)[0]  #獲取樣本數
        self.hidden_num = hidden_num  #獲取隱藏特征維度
        
        self.bais = tf.Variable([0.0])
        self.weight = tf.Variable(tf.random_normal([self.feature_num, 1], 0.0, 1.0))
        self.weight_mix = tf.Variable(tf.random_normal([self.feature_num, self.hidden_num], 0.0, 1.0))
        
        x_ = tf.reshape(self.x, [self.sample_num,self.valid_num,1]) #SAMPLE_NUM*VALID_NUM*1
        w_ = tf.nn.embedding_lookup(self.weight, self.i) #SAMPLE_NUM*VALID_NUM*1
        
        expressings = tf.multiply(x_, w_) #SAMPLE_NUM*VALID_NUM*1
        expressings_reduce = tf.reshape(self.x, [self.sample_num,self.valid_num]) #SAMPLE_NUM*VALID_NUM
        
        x__ = tf.tile(x_, [1,1,self.hidden_num]) #SAMPLE_NUM*VALID_NUM*HIDDEN_NUM
        w__ = tf.nn.embedding_lookup(self.weight_mix, self.i) #SAMPLE_NUM*VALID_NUM*HIDDEN_NUM
        
        embeddings = tf.multiply(x__, w__) #SAMPLE_NUM*VALID_NUM*HIDDEN_NUM
        embeddings_sum = tf.reduce_sum(embeddings, 1) #SAMPLE_NUM*HIDDEN_NUM
        embeddings_sum_square = tf.square(embeddings_sum) #SAMPLE_NUM*HIDDEN_NUM
        embeddings_square = tf.square(embeddings) #SAMPLE_NUM*VALID_NUM*HIDDEN_NUM
        embeddings_square_sum = tf.reduce_sum(embeddings_square, 1) #SAMPLE_NUM*HIDDEN_NUM
        
        z = self.bais + \
            tf.reduce_sum(expressings_reduce, 1, keepdims=True) + \
            1.0/2.0*tf.reduce_sum(tf.subtract(embeddings_sum_square,embeddings_square_sum), 1, keepdims=True)
        z_ = tf.clip_by_value(z,-4.0,4.0)
        
        self.hypothesis  = tf.sigmoid(z_)
        
        self.y_expand = tf.expand_dims(self.y, axis=1)
       
        self.loss = tf.losses.log_loss(self.y_expand, self.hypothesis)

tf.reset_default_graph()  #清空Graph

VALID_NUM = 8  #有效特征數量
with tf.name_scope("input"):
    i = tf.placeholder(tf.int32, shape=[None, VALID_NUM])
    x = tf.placeholder(tf.float32, shape=[None, VALID_NUM])
    y = tf.placeholder(tf.int32, shape=[None])
    y_expand = tf.expand_dims(y, axis=1)

FEATURE_NUM = 20  #特征數量
HIDDEN_NUM = 5  #隱藏特征維度
with tf.name_scope("fm"):
    fm = FmModel(i, x, y, FEATURE_NUM, VALID_NUM, HIDDEN_NUM)

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

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

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

 

 

 

 

參考文獻:

https://www.cnblogs.com/pinard/p/6370127.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM