FM(Factorization Machines)的理論與實踐

FM的paper地址如下:https://www.csie.ntu.edu.tw/~b97053/paper/Rendle2010FM.pdf

1. FM背景

在計算廣告和推薦系統中,CTR預估(click-through rate)是非常重要的一個環節,判斷一個物品是否進行推薦需要根據CTR預估的點擊率排序決定。業界常用的方法有人工特徵工程 + LR(Logistic Regression)、GBDT(Gradient Boosting Decision Tree) + LR、FM(Factorization Machine)和FFM(Field-aware Factorization Machine)模型。

最近幾年出現了很多基於FM改進的方法,如deepFM,FNN,PNN,DCN,xDeepFM等,今後的內容將會分別介紹。

2. FM原理

FM主要是解決稀疏數據下的特徵組合問題,並且其預測的複雜度是線性的,對於連續和離散特徵有較好的通用性。

假設一個電影評分系統,根據用戶和電影的特徵,預測用戶對電影的評分。

系統記錄了用戶(U)在特定時間(t)對電影(i)的評分(r),評分爲1,2,3,4,5。

給定一個例子:

U=\left\{Alice(A),Bob(B),Charlie(C),... \right\}

I=\left\{Titanic (TI), Notting Hill (NH), Star Wars (SW), Star Trek (ST), . . . \right\}

S=\left\{(A, TI, 2010-1, 5),(A, NH, 2010-2, 3),(A, SW, 2010-4, 1),\\ (B, SW, 2009-5, 4),(B, ST, 2009-8, 5),\\ (C, TI, 2009-9, 1),(C, SW, 2009-12, 5)\right\}

 

 

評分是label,用戶id、電影id、評分時間是特徵。由於用戶id和電影id是categorical類型的,需要經過獨熱編碼(One-Hot Encoding)轉換成數值型特徵。因爲是categorical特徵,所以經過one-hot編碼以後,導致樣本數據變得很稀疏。

下圖顯示了從S構建的例子:

每行表示目標 y^{(i)} 與其對應的特徵向量x^{(i)} ,藍色區域表示了用戶變量,紅色區域表示了電影變量,黃色區域表示了其他隱含的變量,進行了歸一化,綠色區域表示一個月內的投票時間,棕色區域表示了用戶上一個評分的電影,最右邊的區域是評分。

2.1 特徵交叉

普通的線性模型,我們都是將各個特徵獨立考慮的,並沒有考慮到特徵與特徵之間的相互關係。但實際上,特徵之間可能具有一定的關聯。以新聞推薦爲例,一般男性用戶看軍事新聞多,而女性用戶喜歡情感類新聞,那麼可以看出性別與新聞的頻道有一定的關聯性,如果能找出這類的特徵,是非常有意義的。

爲了簡單起見,我們只考慮二階交叉的情況,具體的模型如下:

\tilde{y}(x)=w_{0}+\sum_{i=1}^{n}{w_{i}x_{i}}+\sum_{i=1}^{n}{\sum_{j=i+1}^{n}{w_{ij}x_{i}x_{j}}} \tag{1} \\

其中, n 代表樣本的特徵數量, x_{i} 是第i個特徵的值, w_{0 } 、 w_{i} 、 w_{ij } 是模型參數,只有當 x_{i} 與 x_{j} 都不爲0時,交叉纔有意義。

在數據稀疏的情況下,滿足交叉項不爲0的樣本將非常少,當訓練樣本不足時,很容易導致參數 w_{ij} 訓練不充分而不準確,最終影響模型的效果。

那麼,交叉項參數的訓練問題可以用矩陣分解來近似解決,有下面的公式。

\tilde{y}(x)=w_{0}+\sum_{i=1}^{n}{w_{i}x_{i}}+\sum_{i=1}^{n}{\sum_{j=i+1}^{n}{<v_{i},v_{j}>x_{i}x_{j}}} \tag{2}\\

其中模型需要估計的參數是:

w_{0}\ in \mathbb{R},\boldsymbol{w}\in\mathbb{R}^n,\boldsymbol{V}\in\mathbb{R}^{n\times k}\\

<.,.> 是兩個 k維向量的內積:

<v_{i},v_{j}>=\sum_{f=1}^{k}{v_{i,f}v_{j,f}} \tag{3}\\

對任意正定矩陣 W ,只要 k 足夠大,就存在矩陣 W ,使得 W=VV^T 。然而在數據稀疏的情況下,應該選擇較小的k,因爲沒有足夠的數據來估計 w_{ij} 。限制k的大小提高了模型更好的泛化能力。

爲什麼說可以提高模型的泛化能力呢?

以上述電影評分系統爲例,假設我們要計算用戶 A 與電影 ST 的交叉項,很顯然,訓練數據裏沒有這種情況,這樣會導致 w_{A,ST}=0 ,但是我們可以近似計算出 <V_{A},V_{ST}> 。首先,用戶 B和 C 有相似的向量 V_{B} 和 V_{C} ,因爲他們對 SW 的預測評分比較相似, 所以<V_{B},V_{SW}> 和 <V_{C},V_{SW}> 是相似的。用戶 A 和 C 有不同的向量,因爲對 TI和 SW的預測評分完全不同。接下來, ST 和 SW 的向量可能相似,因爲用戶 B 對這兩部電影的評分也相似。最後可以看出, <V_{A},V_{ST}> 與 <V_{A},V_{SW}> 是相似的。

直接計算公式(2)的時間複雜度是 O(kn^2) ,因爲所有的交叉特徵都需要計算。但是通過公式變換,可以減少到線性複雜度,方法如下:

\begin{align} & \sum_{i=1}^{n} {\sum_{j=i+1}^{n}{<v_{i},v_{j}>x_{i}x_{j}}} \\ &=\frac{1}{2}\sum_{i=1}^{n}{\sum_{j=1}^{n}{<v_{i},v_{j}>x_{i}x_{j}}}-\frac{1}{2}\sum_{i=1}^{n}{<v_{i},v_{i}>x_{i}x_{i}} \\ &=\frac{1}{2}(\sum_{i=1}^{n}{\sum_{j=1}^{n}{\sum_{f=1}^{k}{v_{i,f}v_{j,f}x_{i}x_{j}}}}-\sum_{i=1}^{n}{\sum_{f=1}^{k}{v_{i,f}v_{i,f}x_{i}x_{i}}}) \\ &=\frac{1}{2}\sum_{f=1}^{k}{((\sum_{i=1}^{n}{v_{i,f}x_{i}})(\sum_{j=1}^{n}{v_{j,f}x_{j}})-\sum_{i=1}^{n}{v_{i,f}^2x_{i}^2})} \\ &=\frac{1}{2}\sum_{f=1}^{k}{((\sum_{i=1}^{n}{v_{i,f}x_{i}})^2-\sum_{i=1}^{n}{v_{i,f}^2x_{i}^2})} \end{align}

可以看到這時的時間複雜度爲 O(kn) 。

2.2 預測

FM算法可以應用在多種的預測任務中,包括:

  • Regression: \hat{y}(x) 可以直接用作預測,並且最小平方誤差來優化。
  • Binary classification: \hat{y}(x) 作爲目標函數並且使用hinge loss或者logit loss來優化。
  • Ranking:向量 x 通過 \hat{y}(x) 的分數排序,並且通過pairwise的分類損失來優化成對的樣本(x^{(a)},x^{(b)})

對以上的任務中,正則化項參數一般加入目標函數中以防止過擬合。

2.3 參數學習

從上面的描述可以知道FM可以在線性的時間內進行預測。因此模型的參數可以通過梯度下降的方法(例如隨機梯度下降)來學習,對於各種的損失函數。FM模型的梯度是:

\frac{\partial}{\partial{\theta}}\hat{y}(x)=\left\{ \begin{aligned} &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{aligned} \right.

由於 \sum_{j=1}^{n}{v_{j,f}x_{j}} 只與 f 有關,與 i 是獨立的,可以提前計算出來,並且每次梯度更新可以在常數時間複雜度內完成,因此FM參數訓練的複雜度也是 O(kn) 。綜上可知,FM可以在線性時間訓練和預測,是一種非常高效的模型。

2.4 多階FM

2階FM可以很容易泛化到高階:

\hat{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}+1}^{n}({\prod_{j=1}^{l}x_{i_{j}})(\sum_{f=1}^{k_{l}}{\sum_{j=1}^{l}{v_{i_{j},f}^{(l)}}})}}} \tag{6}\\

其中對第 l 個交互參數是由PARAFAC模型的參數因子分解得到: 

V^{(l)}\in\mathbb{R}^{n\times k_{l}}, k_{l}\in\mathbb{N_{0}^+} \\

直接計算公式 (6) 的時間複雜度是 O(k_{d}n^d) 。通過調整也可以在線性時間內運行。

2.5 Factorization Machines With FTRL

FTRL是Google在2013年放出這個優化方法,該方法有較好的稀疏性和收斂性。FTRL是一個在線學習的框架,論文中用於求解LR,具體求解方法如下圖:

我們只需要把論文中的僞代碼進行修改,即可用於FM的參數求解。僞代碼如下:

 

2.6 總結

FM模型有兩個優勢:

  1. 在高度稀疏的情況下特徵之間的交叉仍然能夠估計,而且可以泛化到未被觀察的交叉
  2. 參數的學習和模型的預測的時間複雜度是線性的

FM模型的優化點:

1.特徵爲全交叉,耗費資源,通常user與user,item與item內部的交叉的作用要小於user與item的交叉

2.使用矩陣計算,而不是for循環計算

3.高階交叉特徵的構造

3. FM實踐

代碼是用python3.5寫的,tensorflow的版本爲1.10.1,其他低版本可能不兼容。完整代碼可參考我的github地址:

LLSean/data-mining

本文使用的數據是movielens-100k,數據包括u.item,u.user,ua.base及ua.test,u.item包括的數據格式爲:

movie id | movie title | release date | video release date |
IMDb URL | unknown | Action | Adventure | Animation |
Children's | Comedy | Crime | Documentary | Drama | Fantasy |
Film-Noir | Horror | Musical | Mystery | Romance | Sci-Fi |
Thriller | War | Western |

u.user包括的數據格式爲:

user id | age | gender | occupation | zip code

ua.base和ua.test的數據格式爲:

user id | item id | rating | timestamp

本文將評分等於5分的評分數據作爲用戶的點擊數據,評分小於5分的數據作爲用戶的未點擊數據,構造爲一個二分類問題。

數據輸入

要使用FM模型,首先要將數據處理成一個矩陣,本文使用了pandas對數據進行處理,生成輸入的矩陣,並且對label做onehot編碼處理。

def onehot_encoder(labels, NUM_CLASSES):
    enc = LabelEncoder()
    labels = enc.fit_transform(labels)
    labels = labels.astype(np.int32)
    batch_size = tf.size(labels)
    labels = tf.expand_dims(labels, 1)
    indices = tf.expand_dims(tf.range(0, batch_size,1), 1)
    concated = tf.concat([indices, labels] , 1)
    onehot_labels = tf.sparse_to_dense(concated, tf.stack([batch_size, NUM_CLASSES]), 1.0, 0.0) 
    with tf.Session() as sess:
        return sess.run(onehot_labels)

def load_dataset():
    header = ['user_id', 'age', 'gender', 'occupation', 'zip_code']
    df_user = pd.read_csv('data/u.user', sep='|', names=header)
    header = ['item_id', 'title', 'release_date', 'video_release_date', 'IMDb_URL', 'unknown', 'Action', 'Adventure', 'Animation', 'Children',
            'Comedy', 'Crime', 'Documentary', 'Drama', 'Fantasy', 'Film-Noir', 'Horror', 'Musical', 'Mystery', 'Romance', 'Sci-Fi', 
            'Thriller', 'War', 'Western']
    df_item = pd.read_csv('data/u.item', sep='|', names=header, encoding = "ISO-8859-1")
    df_item = df_item.drop(columns=['title', 'release_date', 'video_release_date', 'IMDb_URL', 'unknown'])
    
    df_user['age'] = pd.cut(df_user['age'], [0,10,20,30,40,50,60,70,80,90,100], labels=['0-10','10-20','20-30','30-40','40-50','50-60','60-70','70-80','80-90','90-100'])
    df_user = pd.get_dummies(df_user, columns=['gender', 'occupation', 'age'])
    df_user = df_user.drop(columns=['zip_code'])
    
    user_features = df_user.columns.values.tolist()
    movie_features = df_item.columns.values.tolist()
    cols = user_features + movie_features
    
    header = ['user_id', 'item_id', 'rating', 'timestamp']
    df_train = pd.read_csv('data/ua.base', sep='\t', names=header)
    df_train['rating'] = df_train.rating.apply(lambda x: 1 if int(x) == 5 else 0)
    df_train = df_train.merge(df_user, on='user_id', how='left') 
    df_train = df_train.merge(df_item, on='item_id', how='left')
    
    df_test = pd.read_csv('data/ua.test', sep='\t', names=header)
    df_test['rating'] = df_test.rating.apply(lambda x: 1 if int(x) == 5 else 0)
    df_test = df_test.merge(df_user, on='user_id', how='left') 
    df_test = df_test.merge(df_item, on='item_id', how='left')
    train_labels = onehot_encoder(df_train['rating'].astype(np.int32), 2)
    test_labels = onehot_encoder(df_test['rating'].astype(np.int32), 2)
    return df_train[cols].values, train_labels, df_test[cols].values, test_labels

模型設計

得到輸入之後,我們使用tensorflow來設計我們的模型,目標函數包括兩部分,線性以及交叉特徵的部分,交叉特徵直接使用我們最後推導的形式即可。

#輸入
def add_input(self):
    self.X = tf.placeholder('float32', [None, self.p])
    self.y = tf.placeholder('float32', [None, self.num_classes])
    self.keep_prob = tf.placeholder('float32')

#forward過程
def inference(self):
    with tf.variable_scope('linear_layer'):
        w0 = tf.get_variable('w0', shape=[self.num_classes],
                            initializer=tf.zeros_initializer())
        self.w = tf.get_variable('w', shape=[self.p, num_classes],
                             initializer=tf.truncated_normal_initializer(mean=0,stddev=0.01))
        self.linear_terms = tf.add(tf.matmul(self.X, self.w), w0) 

    with tf.variable_scope('interaction_layer'):
        self.v = tf.get_variable('v', shape=[self.p, self.k],
                            initializer=tf.truncated_normal_initializer(mean=0, stddev=0.01))
        self.interaction_terms = tf.multiply(0.5,
                                             tf.reduce_sum(
                                                 tf.subtract(
                                                     tf.pow(tf.matmul(self.X, self.v), 2),
                                                     tf.matmul(self.X, tf.pow(self.v, 2))),
                                                 1, keep_dims=True))
    self.y_out = tf.add(self.linear_terms, self.interaction_terms)
    if self.num_classes == 2:
        self.y_out_prob = tf.nn.sigmoid(self.y_out)
    elif self.num_classes > 2:
        self.y_out_prob = tf.nn.softmax(self.y_out)

#loss
def add_loss(self):
    if self.num_classes == 2:
        cross_entropy = tf.nn.sigmoid_cross_entropy_with_logits(labels=self.y, logits=self.y_out)
    elif self.num_classes > 2:
        cross_entropy = tf.nn.softmax_cross_entropy_with_logits(labels=self.y, logits=self.y_out)
    mean_loss = tf.reduce_mean(cross_entropy)
    self.loss = mean_loss
    tf.summary.scalar('loss', self.loss)

#計算accuracy
def add_accuracy(self):
    # accuracy
    self.correct_prediction = tf.equal(tf.cast(tf.argmax(self.y_out,1), tf.float32), tf.cast(tf.argmax(self.y,1), tf.float32))
    self.accuracy = tf.reduce_mean(tf.cast(self.correct_prediction, tf.float32))
    # add summary to accuracy
    tf.summary.scalar('accuracy', self.accuracy)

#訓練
def train(self):
    self.global_step = tf.Variable(0, trainable=False)
    optimizer = tf.train.FtrlOptimizer(self.lr, l1_regularization_strength=self.reg_l1,
                                       l2_regularization_strength=self.reg_l2)
    extra_update_ops = tf.get_collection(tf.GraphKeys.UPDATE_OPS)
    with tf.control_dependencies(extra_update_ops):
        self.train_op = optimizer.minimize(self.loss, global_step=self.global_step)

#構建圖
def build_graph(self):
    self.add_input()
    self.inference()
    self.add_loss()
    self.add_accuracy()
    self.train()

本文如有錯誤的地方,請私信或者留言指出。

歡迎關注微信公衆號數據挖掘雜貨鋪!

參考資料:

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

https://static.googleusercontent.com/media/research.google.com/zh-CN//pubs/archive/41159.pdf

http://wnzhang.net/share/rtb-papers/fm-ftrl.pdf

推薦系統遇上深度學習(一)--FM模型理論和實踐 - 雲+社區 - 騰訊雲

FM算法論文 Factorization Machines 閱讀筆記

深入FFM原理與實踐

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