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。
給定一個例子:
評分是label,用戶id、電影id、評分時間是特徵。由於用戶id和電影id是categorical類型的,需要經過獨熱編碼(One-Hot Encoding)轉換成數值型特徵。因爲是categorical特徵,所以經過one-hot編碼以後,導致樣本數據變得很稀疏。
下圖顯示了從S構建的例子:
每行表示目標 與其對應的特徵向量 ,藍色區域表示了用戶變量,紅色區域表示了電影變量,黃色區域表示了其他隱含的變量,進行了歸一化,綠色區域表示一個月內的投票時間,棕色區域表示了用戶上一個評分的電影,最右邊的區域是評分。
2.1 特徵交叉
普通的線性模型,我們都是將各個特徵獨立考慮的,並沒有考慮到特徵與特徵之間的相互關係。但實際上,特徵之間可能具有一定的關聯。以新聞推薦爲例,一般男性用戶看軍事新聞多,而女性用戶喜歡情感類新聞,那麼可以看出性別與新聞的頻道有一定的關聯性,如果能找出這類的特徵,是非常有意義的。
爲了簡單起見,我們只考慮二階交叉的情況,具體的模型如下:
其中, 代表樣本的特徵數量, 是第i個特徵的值, 、 、 是模型參數,只有當 與 都不爲0時,交叉纔有意義。
在數據稀疏的情況下,滿足交叉項不爲0的樣本將非常少,當訓練樣本不足時,很容易導致參數 訓練不充分而不準確,最終影響模型的效果。
那麼,交叉項參數的訓練問題可以用矩陣分解來近似解決,有下面的公式。
其中模型需要估計的參數是:
是兩個 維向量的內積:
對任意正定矩陣 ,只要 足夠大,就存在矩陣 ,使得 。然而在數據稀疏的情況下,應該選擇較小的k,因爲沒有足夠的數據來估計 。限制k的大小提高了模型更好的泛化能力。
爲什麼說可以提高模型的泛化能力呢?
以上述電影評分系統爲例,假設我們要計算用戶 與電影 的交叉項,很顯然,訓練數據裏沒有這種情況,這樣會導致 ,但是我們可以近似計算出 。首先,用戶 和 有相似的向量 和 ,因爲他們對 的預測評分比較相似, 所以 和 是相似的。用戶 和 有不同的向量,因爲對 和 的預測評分完全不同。接下來, 和 的向量可能相似,因爲用戶 對這兩部電影的評分也相似。最後可以看出, 與 是相似的。
直接計算公式(2)的時間複雜度是 ,因爲所有的交叉特徵都需要計算。但是通過公式變換,可以減少到線性複雜度,方法如下:
可以看到這時的時間複雜度爲 。
2.2 預測
FM算法可以應用在多種的預測任務中,包括:
- Regression: 可以直接用作預測,並且最小平方誤差來優化。
- Binary classification: 作爲目標函數並且使用hinge loss或者logit loss來優化。
- Ranking:向量 通過 的分數排序,並且通過pairwise的分類損失來優化成對的樣本
對以上的任務中,正則化項參數一般加入目標函數中以防止過擬合。
2.3 參數學習
從上面的描述可以知道FM可以在線性的時間內進行預測。因此模型的參數可以通過梯度下降的方法(例如隨機梯度下降)來學習,對於各種的損失函數。FM模型的梯度是:
由於 只與 有關,與 是獨立的,可以提前計算出來,並且每次梯度更新可以在常數時間複雜度內完成,因此FM參數訓練的複雜度也是 。綜上可知,FM可以在線性時間訓練和預測,是一種非常高效的模型。
2.4 多階FM
2階FM可以很容易泛化到高階:
其中對第 個交互參數是由PARAFAC模型的參數因子分解得到:
直接計算公式 的時間複雜度是 。通過調整也可以在線性時間內運行。
2.5 Factorization Machines With FTRL
FTRL是Google在2013年放出這個優化方法,該方法有較好的稀疏性和收斂性。FTRL是一個在線學習的框架,論文中用於求解LR,具體求解方法如下圖:
我們只需要把論文中的僞代碼進行修改,即可用於FM的參數求解。僞代碼如下:
2.6 總結
FM模型有兩個優勢:
- 在高度稀疏的情況下特徵之間的交叉仍然能夠估計,而且可以泛化到未被觀察的交叉
- 參數的學習和模型的預測的時間複雜度是線性的
FM模型的優化點:
1.特徵爲全交叉,耗費資源,通常user與user,item與item內部的交叉的作用要小於user與item的交叉
2.使用矩陣計算,而不是for循環計算
3.高階交叉特徵的構造
3. FM實踐
代碼是用python3.5寫的,tensorflow的版本爲1.10.1,其他低版本可能不兼容。完整代碼可參考我的github地址:
本文使用的數據是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模型理論和實踐 - 雲+社區 - 騰訊雲