1. 簡介
本篇文章先簡單介紹論文思路,然后使用Tensoflow2.0、Keras API復現算法部分。包括:
-
自定義模型
-
自定義損失函數
-
自定義評價指標RMSE
就題目而言《AutoRec: Autoencoders Meet Collaborative Filtering》,自編碼機遇見協同過濾,可見是使用自編碼機結合協同過濾思想進行的算法。論文經過數據集Movielens和Netfix驗證有不錯的效果,更重要的是它是對特征交叉引入深度學習的開端,論文兩頁,簡單易懂。
2. 算法模型
令m個用戶,n個物品,構成用戶-物品矩陣,每個物品對被用戶進行評分。根據協同過濾思想,有基於用戶的方式,也有基於物品的方式,取決於輸入是物品分表示的用戶向量,還是用戶評分表示物品向量\(r^{i},r^{u}\)。自編碼機部分將評分向量進行低維壓縮,用低維空間表示評分向量,並對向量不同部分進行交叉,然后重構向量。
算法模型圖為:
采用的模型公式為(基本就是邏輯回歸的方式):
其中,中間的神經元數量,即映射低維空間設為k,則k是一個超參數根據效果進行調控。
需要注意的部分是
-
每個評分向量(無論物品還是用戶向量),都存在稀疏性即沒有用戶對其進行評分,則在反向傳播的過程中不能考慮這部分內容;
-
為了防止過擬合,需要在損失函數中添加W和V參數矩陣正則化:
前一部分為真實值與預測值的平方誤差(僅僅計算有評分的部分),第二部分為正則項,所求為F范數。
預測公式:
若是基於物品的AutoRec(I-AutoRec)則,輸入待求的物品向量,到下面預測公式中,得到一個完整的向量,求第幾個用戶就取第幾個維度從而得到此用戶對此物品的評分,即
基於用戶的AutoRec(U-AutoRec)表示類似。
實驗驗證
論文的評價方式,使用的RMSE,與其他算法比較得到比較好的參數是k=500,f映射使用線性函數,g映射使用Sigmoid函數。
3. 代碼復現
復現包括網絡模型,損失函數,以及評價指標三部分,由於部分的改動不能直接使用TF原生的庫函數。
首先導入需要使用的工具包:
import numpy as np
import pandas as pd
import tensorflow as tf
from tensorflow import keras
from sklearn.model_selection import train_test_split
定義模型
基於Keras的API的模型定義需要繼承Model
類,重寫方法call
(前向傳播過程),如果需要加入dropout
的模型需要將訓練和預測分開可以使用參數training=None
的方式來指定,這里不需要這個參數,因此省略。
class AutoRec(keras.Model):
def __init__(self, feature_nums, hidden_units, **kwargs):
super(AutoRec, self).__init__()
self.feature_nums = feature_nums # 基於物品則為物品特征數-即用戶數,基於用戶則為物品數量
self.hidden_units = hidden_units # 論文中的k參數
self.encoder = keras.layers.Dense(self.hidden_units, input_shape=[self.feature_nums], activation='sigmoid') # g映射
self.decoder = keras.layers.Dense(self.feature_nums, input_shape=[self.hidden_units]) # f映射
def call(self, X):
# 前向傳播
h = self.encoder(X)
y_hat = self.decoder(h)
return y_hat
定義損失函數
此損失函數雖然為MSE形式,但是在計算的過程中發現,僅僅計算有評分的部分,無評分部分不進入損失,同時還有正則化,這里一起寫出來。
基於Keras API的方式,需要繼承Loss
類,和方法call
初始化傳入model
參數為了取出W和V參數矩陣。
mask_zero
表示沒有評分的部分不進入損失函數,同時要保證數據類型統一tf.int32,tf.float32
否則會報錯。
class Mse_Reg(keras.losses.Loss):
def __init__(self, model, reg_factor=None):
super(Mse_Reg, self).__init__()
self.model = model
self.reg_factor = reg_factor
def call(self, y_true, y_pred) :
y_sub = y_true - y_pred
mask_zero = y_true != 0
mask_zero = tf.cast(mask_zero, dtype=y_sub.dtype)
y_sub *= mask_zero
mse = tf.math.reduce_sum(tf.math.square(y_sub)) # mse損失部分
reg = 0.0
if self.reg_factor is not None:
weight = self.model.weights
for w in weight:
if 'bias' not in w.name:
reg += tf.reduce_sum(tf.square(w)) # 求矩陣的Frobenius范數的平方
return mse + self.reg_factor * 0.5 * reg
return mse
定義RMSE評價指標
定義評價指標需要繼承類Metric
,方法update_state和result以及reset
,reset方法感覺使用較少,主要是更新狀態和得到結果。
class RMSE(keras.metrics.Metric):
def __init__(self):
super(RMSE, self).__init__()
self.res = self.add_weight(name='res', dtype=tf.float32, initializer=tf.zeros_initializer())
def update_state(self, y_true, y_pred, sample_weight=None):
y_sub = y_true - y_pred
mask_zero = y_true != 0
mask_zero = tf.cast(mask_zero, dtype=y_sub.dtype)
y_sub *= mask_zero
values = tf.math.sqrt(tf.reduce_mean(tf.square(y_sub)))
self.res.assign_add(values)
def result(self):
return self.res
定義數據集
定義好各個部分之后,就可以構造訓練集然后訓練模型了。
get_data
表示從path中加載數據,然后加數據通過pandas的透視表功能構造一個行為物品,列為用戶的矩陣;
data_iter
表示通過tf.data
構造數據集。
# 定義數據
def get_data(path, base_items=True):
data = pd.read_csv(path)
rate_matrix = pd.pivot_table(data, values='rating', index='movieId', columns='userId',fill_value=0.0)
if base_items:
return rate_matrix
else :
return rate_matrix.T
def data_iter(df, shuffle=True, batch_szie=32, training=False) :
df = df.copy()
X = df.values.astype(np.float32)
ds = tf.data.Dataset.from_tensor_slices((X, X)).batch(batch_szie)
if training:
ds = ds.repeat()
return ds
訓練模型
萬事俱備,就准備數據放給模型就好了。
要說明的是,如果fit的時候不設置steps_per_epoch
會在數據量和batch大小不能整除的時候報迭代器的超出范圍的錯誤。設置了此參數當然也要加上validation_steps=2
,不然還是會報錯,不信可以試試看。
path = 'ratings.csv' # 我這里用的是10w數據,不是原始的movielens-1m
# I-AutoRec,num_users為特征維度
rate_matrix = get_data(path)
num_items, num_users = rate_matrix.shape
# 划分訓練測試集
BARCH = 128
train, test = train_test_split(rate_matrix, test_size=0.1)
train, val = train_test_split(train, test_size=0.1)
train_ds = data_iter(train, batch_szie=BARCH, training=True)
val_ds = data_iter(val, shuffle=False)
test_ds = data_iter(test, shuffle=False)
# 定義模型
net = AutoRec(feature_nums=num_users, hidden_units=500) # I-AutoRec, k=500
net.compile(loss=Mse_Reg(net), #keras.losses.MeanSquaredError(),
optimizer=keras.optimizers.Adam(),
metrics=[RMSE()])
net.fit(train_ds, validation_data=val_ds, epochs=10, validation_steps=2, steps_per_epoch=train.shape[0]//BARCH)
loss, rmse = net.evaluate(test_ds)
print('loss: ', loss, ' rmse: ', rmse)
預測
df = test.copy()
X = df.values.astype(np.float32)
ds = tf.data.Dataset.from_tensor_slices(X) # 這里沒有第二個X了
ds = ds.batch(32)
pred = net.predict(ds)
# 隨便提出來一個測試集中有的評分看看預測的分數是否正常,pred包含原始為0.0的分數現在已經預測出來分數的。
print('valid: pred user1 for item1: ', pred[1][X[1].argmax()], 'real: ', X[1][X[1].argmax()])
得到結果(沒有達到論文的精度,可能是數據量不足,而valid部分可以看到預測的精度還是湊合的):
4. 小結
本篇文章主要是針對AutoRec論文的主要部分進行了介紹,然后使用TensorFlow2.0的Keras接口實現了自定義的模型,損失,以及指標,並訓練了I-AutoRec模型。
關於AutoRec要說的是,
編碼器部分如果使用深層網絡比如三層會增加預測的准確性;
自編碼器部分的輸出向量經過了編碼過程的泛化相當於對缺失部分有了預測能力,這是自編碼機用於推薦的原因;
I-AutoRec推薦過程,需要輸入物品的矩陣然后得到每個用戶對物品的預測評分,然后取用戶自己評分的Top可以進行推薦,U-AutoRec只需要輸入一次目標用戶的向量就可以重建用戶對所有物品的評分,然后得到推薦列表,但是用戶向量可能稀疏性比較大影響最終的推薦效果。
AutoRec使用了單層網絡,存在表達能力不足的問題,但對於基於機器學習的矩陣分解,協同過濾來說,由於這層網絡的加入特征的表達能力得到提高。