傳統推薦算法(四) 手把手教你用tensorflow實現FM算法

1.FM背景與簡介

FM主要是爲了解決稀疏數據下的特徵組合問題。2010年,日本大阪大學(Osaka University)的 Steffen Rendle 在矩陣分解(MF)、SVD++[2]、PITF[3]、FPMC[4] 等基礎之上,歸納出針對高維稀疏數據的因子機(Factorization Machine, FM)模型[13]。因子機模型可以將上述模型全部納入一個統一的框架進行分析[1]。目前,FM被廣泛的應用於廣告預估模型中,相比LR而言,效果強了不少。

1.1 稀疏數據

高維的稀疏矩陣是工程中常見的問題,比如下圖:
在這裏插入圖片描述

圖中每一個樣本對應多個屬性,分別是User,Movie,Other Movies rated,Time,Last Movie rated,由於這些屬性都是categorical類型,所以一般進行One-hot編碼轉換爲數值類型,但是由於每個屬性都有多個離散的取值,所以One-hot編碼之後樣本空間相比原來變大了許多,而特徵矩陣也會變得非常稀疏。假設有10000部電影,有10000個用戶,單看前兩條,每個樣本的二階交叉特徵維度就是一億維,但是每個特徵中只有兩維取值不爲0,非常稀疏。在CTR/CVR預測時,One-Hot編碼常會導致樣本的稀疏性,樣本的稀疏性是實際問題中不可避免的挑戰。

同時通過觀察大量的樣本數據可以發現,某些特徵經過關聯之後,與label之間的相關性就會提高。如:“USA”與“Thanksgiving”、“China”與“Chinese New Year”這樣的關聯特徵,對用戶的點擊有着正向的影響。換句話說,來自“China”的用戶很可能會在“Chinese New Year”有大量的瀏覽、購買行爲,而在“Thanksgiving”卻不會有特別的消費行爲。這種關聯特徵與label的正向相關性在實際問題中是普遍存在的,如“化妝品”類商品與“女”性,“球類運動配件”的商品與“男”性,“電影票”的商品與“電影”品類偏好等。[5]。

顯然,不同特徵之間是有關聯的,表示特徵之間的關聯,一種直接的方法是構建組合特徵[5]。那麼怎麼組合特徵呢?

1.2 FM模型

SVM就曾通過多項式核函數來實現特徵的交叉。實際上,多項式是構建組合特徵的一種非常直觀的模型。我們先看一下二階多項式的建模:

20190618161343.png

外層求和i實際上只取到了n-1,顯然組合特徵有n(n-1)/2個,如果特徵有萬級別,那麼參數就有億級別,這種計算量是很大的。

而且在稀疏性普遍存在的應用場景種,二次項參數的訓練非常困難。以爲每一個wij在訓練時都要要求xi,xj不爲零,否則梯度下降時梯度恆爲零,而實際上xi,xj同時非零的情況非常少見,這會導致大部分參數無法學習,模型的性能也可想而知。那麼如何解決二次項參數的訓練問題呢?

矩陣分解提供了思路,矩陣分解吧rating矩陣分爲user矩陣和item矩陣的乘積來進行低秩近似,這裏FM引入輔助向量V來表示W:

這樣有什麼好處呢?[6]中的解釋我覺得非常好:

2.FM模型求解

目前FM模型的求解方法主要有以下三種[12]:
隨機梯度下降法(Stochastic Gradient Descent, SGD)
交替最小二乘法(Alternating Least Square Method,ALS)
馬爾科夫鏈蒙特卡羅法(Markov Chain Monte Carlo,MCMC)

FM論文裏給出了SGD求解方法,這了直接把論文裏的求解粘貼過來,然後步步解讀:


第一步到第二步根據這個公式理解:

這樣理解起來不太直觀,可以把第一步的式子看成是對矩陣上三角元素的求和,結果就是二分之一矩陣所有元素減去對角線元素的和。

第二步到第三部是直接展開。第三步到第四步我寫了個詳細的過程:

其實就是利用了多重求和的一個基本公式:

20190618172028.png

引入輔助向量V之前,計算複雜度爲O(n2),引入V之後,爲O(kn2),而通過2.1中的交叉項求解,我們知道,計算複雜度爲O(kn)。一般來說k遠小於n,我們可以FM可以在線性時間對新樣本進行預測。

之後就可以用梯度下降法求解了,FM模型的梯度爲:

3. FM優缺點分析

跑完了代碼,有興趣再來看看優缺點吧。

優點


1)FM模型可以在非常稀疏的數據中進行參數估計;
2)FM模型擁有線性的時間複雜度,可以在億級別的大數據;
3)FM模型是一個通用模型,可以用在任何特徵值爲實值的情況下;
4)FM模型可以將上千萬維的特徵壓縮爲幾百維甚至幾十維,極大地減少了模型的參數,參數大大減少,模型的泛化能力也大大增強。

缺點

引用下知乎用戶“屈偉”的說法[11]:

20190618193423.png

4.期待已久的tensorflow實戰

數據處理

首先來看數據處理函數:

def vectorize_dic(dic, ix=None, p=None):
    if(ix == None):
        d = count(0)
        ix = defaultdict(lambda:next(d))
        
    n = len(list(dic.values())[0])
    g = len(list(dic.keys()))
    nz = n*g
    
    col_ix = np.empty(nz, dtype=int)
    
    i = 0
    for k, lis in dic.items():
        col_ix[i::g] = [ix[str(k)+str(el)] for el in lis]
        i += 1
    
    row_ix = np.repeat(np.arange(n), g)
    data = np.ones(nz);print('data.shape ', data.shape)

    if(p == None):
        p = len(ix)
        
    ixx = np.where(col_ix < p)
    
    return csr.csr_matrix(
        (data[ixx], (row_ix[ixx], col_ix[ixx])), shape=(n, p)), ix

這份代碼裏最難理解的就是這個函數,因爲裏面的小知識點較多。但是我們把握核心就行了,就是這個csr.csr_matrix。由於數據過於稀疏,只需要存儲有用的信息(非零值)即可。csr_matrix提供了兩種不錯的表示方法。

第一種:

csr_matrix((data, (row_ind, col_ind)), [shape=(M, N)])

其中row_ind和col_ind代表data中數據所在的行和列,就這麼簡單。這個也是程序中用到的方法。
第二種:

csr_matrix((data, indices, indptr), [shape=(M, N)])

indicies代表列,indptr的長度爲行數加1,其中的第i個元素代表前i-1行中的data數據個數,那麼第n+1個元素就是是len(data),即data中的所有數據。我們根據indptr可以確定data中每個元素所在的行數,根據indices確定每個元素的列數,那麼矩陣就可以唯一確定了。這種表示方法推薦看[10]中的解讀。

瞭解csr_matrix之後,我們就要關注row_ind和col_ix了。樣本數爲n,屬性數爲g,那麼row_ind表示爲(0,0,…0,1,1,…1,…,n-1,n-1,…,n-1)也就不難理解,每g個元素表示一個樣本的行。那麼col_ix也就很明顯了,就是樣本的各屬性取值再one-hot編碼向量中的位置。

這個函數我推薦大家把每個變量打印出來看看,就知道是什麼意思了。博主在原來代碼的基礎上,加了一些調試代碼(就是打印出關鍵變量)方便大家理解代碼。覺得這個函數不好理解的可以試試。

靜態圖定義

首先看一下各種變量定義:

n, p = x_train.shape
# number of latent factors
k = 10
# design features of users
X = tf.placeholder('float', shape=[None, p])
# target vector
Y = tf.placeholder('float', shape=[None, 1])

# bias and weights
w0 = tf.Variable(tf.zeros([1]))
W = tf.Variable(tf.zeros([p]))

# matrix factorization factors, randomly initialized
V = tf.Variable(tf.random_normal([k, p], stddev=0.01))

# estimation of y, initialized to 0
Y_hat = tf.Variable(tf.zeros([n, 1]))

W這裏的定義可以當成行向量來理解,看看Y_hat就知道列向量怎麼定義了。

然後定義損失函數和優化器:

# calculate output with FM equation
linear_terms = tf.add(w0, tf.reduce_sum(tf.multiply(W, X), 1, keepdims=True))
pair_interactions = tf.multiply(0.5, 
                                tf.reduce_sum(
                                    tf.subtract(
                                        tf.pow(tf.matmul(X,tf.transpose(V)), 2), 
                                        tf.matmul(tf.pow(X, 2), tf.pow(tf.transpose(V), 2))), 
                                    1, keepdims=True))
Y_hat = tf.add(linear_terms, pair_interactions)
lambda_w = tf.constant(0.001, name='lambda_w')
lambda_v = tf.constant(0.001, name='lambda_v')
l2_norm = tf.reduce_sum(tf.multiply(lambda_w, tf.pow(W,2)))+tf.reduce_sum(tf.multiply(lambda_v, tf.pow(V,2)))
error = tf.reduce_mean(tf.square(tf.subtract(Y, Y_hat)))
loss =  tf.add(error, l2_norm)

optimizer = tf.train.GradientDescentOptimizer(learning_rate=0.01).minimize(loss)

原作者對l2_norm的處理我覺得有些問題,做了些改進。問題不大。

這裏對矩陣的處理推薦大家看着代碼自己嘗試一下,不然下次自己可能寫不出來。關鍵是要時刻注意各種變量的shape。

訓練和測試

# In[77]: train process
epoches = 2
batch_size = 100

init = tf.global_variables_initializer()
sess = tf.Session()

sess.run(init)

for epoch in tqdm(range(epoches), unit='epoch'):
    perm = np.random.permutation(x_train.shape[0])
    for bx, by in batcher(x_train[perm], y_train[perm], batch_size):
        sess.run(optimizer, feed_dict={
            X:bx.reshape(-1, p),
            Y:by.reshape(-1, 1)
        })


# In[81]: test process


errors = []
for bx, by in batcher(x_test, y_test):
    errors.append(sess.run(error, feed_dict={
        X:bx.reshape(-1, p),
        Y:by.reshape(-1, 1)
    }))
RMSE = np.sqrt(np.array(errors).mean())
print(RMSE)

sess.close()

這裏使用tqdm可以再jupyter notebook上實時顯示訓練進度。

本文參考python2代碼:
https://github.com/babakx/fm_tensorflow/blob/master/fm_tensorflow.ipynb

完整代碼網址:
https://github.com/wyl6/Recommender-Systems-Samples/tree/master/RecSys%20Traditional/MF/FM

參考

[1] https://tracholar.github.io/machine-learning/2017/03/10/factorization-machine.html

[2] S. Rendle and L. Schmidt-Thieme, “Pairwise interaction tensor factorization for personalized tag recommendation,” in WSDM ’10: Proceedings of the third ACM international conference on Web search and data mining. New York, NY, USA: ACM, 2010, pp. 81–90.

[3] S. Rendle, C. Freudenthaler, and L. Schmidt-Thieme, “Factorizing personalized markov chains for next-basket recommendation,” in WWW ’10: Proceedings of the 19th international conference on World wide web. New York, NY, USA: ACM, 2010, pp. 811–820.

[4]: A two-stage ensemble of diverse models for advertisement ranking in KDD Cup 2012[C]//KDDCup. 2012.

[5] https://plushunter.github.io/2017/07/13/%E6%9C%BA%E5%99%A8%E5%AD%A6%E4%B9%A0%E7%AE%97%E6%B3%95%E7%B3%BB%E5%88%97%EF%BC%8826%EF%BC%89%EF%BC%9A%E5%9B%A0%E5%AD%90%E5%88%86%E8%A7%A3%E6%9C%BA%EF%BC%88FM%EF%BC%89%E4%B8%8E%E5%9C%BA%E6%84%9F%E7%9F%A5%E5%88%86%E8%A7%A3%E6%9C%BA%EF%BC%88FFM%EF%BC%89/

[6] https://zhuanlan.zhihu.com/p/37963267

[7] https://zhuanlan.zhihu.com/p/35753471

[8] https://docs.scipy.org/doc/scipy/reference/generated/scipy.sparse.csr_matrix.html

[9] https://blog.csdn.net/u012871493/article/details/51593451

[10] https://blog.csdn.net/u012871493/article/details/51593451

[11] https://www.zhihu.com/question/62109451

[12] https://zhuanlan.zhihu.com/p/61096338

[13] Rendle, S. (2010, December). Factorization machines. In 2010 IEEE International Conference on Data Mining (pp. 995-1000). IEEE.

公衆號

更多精彩內容請移步公衆號:推薦算法工程師

感覺公衆號內容不錯點個關注唄

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章