預(yù)測電影偏好?如何利用自編碼器實現(xiàn)協(xié)同過濾方法
推薦系統(tǒng)使用協(xié)同過濾的方法,通過收集用戶的偏好信息來預(yù)測特定用戶的興趣。協(xié)同過濾技術(shù)的基本假設(shè)是,如果用戶 A 對某個問題與人 B 有相同的口味或意見,那么 A 就更有可能在其他問題上擁有與 B 的相同的意見。
本文將介紹如何根據(jù)用戶的偏好、觀看歷史、相同評級和其他電影的其他用戶的評價預(yù)測用戶對電影的評分。
一、介紹
自動編碼器是一種深度學習神經(jīng)網(wǎng)絡(luò)架構(gòu),可實現(xiàn)協(xié)同過濾領(lǐng)域最佳的性能。文章的第一部是理論概述,將會介紹簡單的自動編碼器及深度自編碼器的基礎(chǔ)數(shù)學概念。在第二部分中,我們將深入實際展示如何在 TensorFlow 中逐步應(yīng)用這一技術(shù)。本文僅覆蓋和評價模型中最重要的部分。
整個模型的輸入渠道和預(yù)處理可以在相應(yīng)的 GitHub 中查看:
https://github.com/artem-oppermann/Deep-Autoencoders-For-Collaborative-Filtering
二、深度自編碼器
1. 自編碼器
討論深度自編碼器之前,我們先來介紹它稍微簡單些的版本。自編碼器(Autoencoder)是一種人工神經(jīng)網(wǎng)絡(luò),用于學習一組輸入數(shù)據(jù)的表示(編碼),通常用于實現(xiàn)降維。
在結(jié)構(gòu)上,自編碼器的形式是一個前饋神經(jīng)網(wǎng)絡(luò),由輸入層、一個隱藏層和一個輸出層(圖 1)構(gòu)成。輸出層與輸入層的神經(jīng)元數(shù)量相同,因此自編碼器屬于無監(jiān)督學習,這意味著它不需要標記數(shù)據(jù)——只需要一組輸入數(shù)據(jù)即可,而不是輸入—輸出對。
圖 1. 典型的 AutoEncoder 架構(gòu)
自編碼器的隱藏層比輸入層小,這使得模型可以通過學習數(shù)據(jù)中的相關(guān)性在隱藏層中創(chuàng)建數(shù)據(jù)的壓縮表示。
輸入層到隱藏層的轉(zhuǎn)換被稱為編碼步驟,從隱藏層到輸出層的轉(zhuǎn)換稱為解碼步驟。我們也可以在數(shù)學上將這些轉(zhuǎn)換定義為映射:
該映射是通過將輸入數(shù)據(jù)向量乘以權(quán)重矩陣,添加一個偏差項并將所得到的向量應(yīng)用于非線性運算,如 sigmoid,tanh 或整流線性單元來實現(xiàn)的。
2. 自編碼器的訓練
在訓練期間,編碼器接收輸入數(shù)據(jù)樣本 x 并將其映射到所謂的隱藏層或隱層表示 z 上。然后解碼器將 z 映射到輸出向量 x' 上,后者是(在最好的情況下)輸入數(shù)據(jù) x 的準確表示。需要注意的是,通常情況下準確地重建 x 是不可能的。
具有輸出 x' 的訓練包括應(yīng)用隨機梯度下降以最小化預(yù)定損失,例如均方誤差:
3. 深度自編碼器
簡單自動編碼器的擴展版是 Deep Autoencoder(圖 2)。從圖 2 中可以看出,它與簡單的計數(shù)器部分唯一的區(qū)別在于隱藏層的數(shù)量。
圖 2. 深度自編碼器的架構(gòu)
額外的隱藏層使自編碼器可以從數(shù)學上學習數(shù)據(jù)中更復雜的底層模式。深度自編碼器的第一層可以學習原始輸入中的一階特征(例如圖像中的邊緣)。第二層可以學習對應(yīng)于一階特征的外觀中的圖案的二階特征(例如,根據(jù)哪些邊緣傾向于一起發(fā)生——例如以形成輪廓或角檢測器)。深度自編碼器更深層的特性往往可以學習到更高階的特性。
把所有東西放在一起:我們需要更多的層來處理更為復雜的數(shù)據(jù)——比如我們在協(xié)作過濾中使用的數(shù)據(jù)。
三、實現(xiàn)
如前文所述,你將學會預(yù)測用戶對電影的評級。就此而言,我們將使用著名的 MovieLens 數(shù)據(jù)集
(https://grouplens.org/datasets/movielens/)。MovieLensis 是一個基于網(wǎng)絡(luò)的推薦系統(tǒng)和推薦用戶觀看電影的在線社區(qū)。
更具體地說,我們將使用 ml_1m.zip 數(shù)據(jù)集,該數(shù)據(jù)集包含 6,040 個 MovieLens 用戶制作的,約 3,900 部電影的 1,000,209 個匿名評級。我們需要的導入文件是 ratings.dat。該文件包含 1,000,209 行,全部格式如下:user_id :: movie_id :: rating:time_stamp。
例如 ratings.dat 中的第一行:
- 1::595::5::978824268
這意味著用戶 1 給了 595 號電影打了五星評分。評分時間可以被忽略,因為在這里我們不會使用它。
我們的深度學習模型需要一個特定的數(shù)據(jù)結(jié)構(gòu)來進行訓練和測試。這種數(shù)據(jù)結(jié)構(gòu)是一個 UxM 矩陣,其中 U 是用戶數(shù)量,M 是電影數(shù)量。每行 i∈U 是唯一的用戶 ID,每列 j∈M 是唯一的電影 ID。這種矩陣的可視化效果如圖 3 所示。
此矩陣中的每個條目都是用戶給出特定電影的評分。輸入 0 意味著用戶沒有給這部電影任何評價。例如。上圖中,1 號用戶給電影 3 的評級為四星,而電影第 1 則根本沒有評級。
由于本教程將重點介紹深度學習模型的實現(xiàn),因此不會在這里介紹使用 User-Movie-Matrix 超出 ratings.dat 文件的步驟。對于關(guān)于這個主題的進一步問題,你可以去我的 GitHub 頁面
(https://github.com/artem-oppermann/Deep-Autoencoders-For-Collaborative-Filtering/blob/master/data/preprocess_data.py), 查看相應(yīng)的 python 腳本。
1. 訓練和測試數(shù)據(jù)集
在模型實現(xiàn)和訓練之前,我們需要對數(shù)據(jù)進行其他重新處理步驟,將數(shù)據(jù)劃分為訓練和測試數(shù)據(jù)集。這一步驟簡單明了。到目前為止,我們有一個 User-Movie Matrix,其中每一行都是評級列表。要從列表中獲得訓練和測試集,我們需要從每一行中取一部分評級,并將它們用于訓練,其余子集則用于測試。
作為描述過程的一個例子,我們考慮一個僅包含 15 部電影的小得多的數(shù)據(jù)集。一個特定的用戶可能出給這樣的電影評級:
- Movie Nr. : 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
- Rating: 5 0 2 4 0 0 2 1 5 1 0 4 5 1 3
請記住,0 表示該電影未被評級。現(xiàn)在我們將前 10 部電影中的一部分作為訓練集并假設(shè)其余的還沒有被評分:
- Movie Nr. : 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
- Rating: 5 0 2 4 0 0 2 1 5 0 0 0 0 0 0
因此,原始數(shù)據(jù)的最后 5 個電影等級被用作測試數(shù)據(jù),而電影 1-10 被掩蓋為未被評級:
- Movie Nr. : 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
- Rating: 0 0 0 0 0 0 0 0 0 1 0 4 5 1 3
此處僅僅簡單演示了如何獲得不同的組合。在原始的 MovieLens 數(shù)據(jù)集中,我僅使用每個用戶的 10 個電影評級進行測試,其余(絕大多數(shù))用于模型的訓練。
2. TensorFlow 實現(xiàn)
(1) 模型架構(gòu)
深度自編碼器在這里作為一個類來實現(xiàn),其中包含所有必需的操作,如類內(nèi)的推理、優(yōu)化、損失、準確性等。
在構(gòu)造器中,內(nèi)核初始化器設(shè)置了權(quán)重和偏差。下一步,網(wǎng)絡(luò)中的所有權(quán)重和偏差都會被初始化。權(quán)重是遵從正態(tài)分布的,平均值為 0.0,方差為 0.02,而偏差在開始時都設(shè)置為 0.0。
在這個特定的例子中,網(wǎng)絡(luò)有三個隱藏層,每層包含 128 個神經(jīng)元。輸入層(和輸出層)的大小對應(yīng)于數(shù)據(jù)集中所有當前影片的數(shù)量。
- class DAE:
- ''' Implementation of Deep Autoencoder class'''
- def __init__(self, FLAGS):
- self.FLAGS=FLAGS
- self.weight_initializer=model_helper._get_weight_initializer()
- self.bias_initializer=model_helper._get_bias_initializer()
- self.init_parameters()
- def init_parameters(self):
- ''' Initializing the weights and biasis of the neural network.'''
- with tf.name_scope('weights'):
- self.W_1=tf.get_variable(name='weight_1', shape=(self.FLAGS.num_v,self.FLAGS.num_h),
- initializer=self.weight_initializer)
- self.W_2=tf.get_variable(name='weight_2', shape=(self.FLAGS.num_h,self.FLAGS.num_h),
- initializer=self.weight_initializer)
- self.W_3=tf.get_variable(name='weight_3', shape=(self.FLAGS.num_h,self.FLAGS.num_h),
- initializer=self.weight_initializer)
- self.W_4=tf.get_variable(name='weight_5', shape=(self.FLAGS.num_h,self.FLAGS.num_v),
- initializer=self.weight_initializer)
- with tf.name_scope('biases'):
- self.b1=tf.get_variable(name='bias_1', shape=(self.FLAGS.num_h),
- initializer=self.bias_initializer)
- self.b2=tf.get_variable(name='bias_2', shape=(self.FLAGS.num_h),
- initializer=self.bias_initializer)
- self.b3=tf.get_variable(name='bias_3', shape=(self.FLAGS.num_h),
- initializer=self.bias_initializer)
(2) 訓練
給定一個輸入數(shù)據(jù)樣本 x(用戶—電影矩陣的一行),正向傳遞并計算網(wǎng)絡(luò)輸出。隱藏層使用 sigmoid 作為激活函數(shù)。請注意,最后一層沒有非線性或偏置項。
- def _inference(self, x):
- '''Making one forward pass. Predicting the outputs, given the inputs.'''
- with tf.name_scope('inference'):
- a1=tf.nn.sigmoid(tf.nn.bias_add(tf.matmul(x, self.W_1),self.b1))
- a2=tf.nn.sigmoid(tf.nn.bias_add(tf.matmul(a1, self.W_2),self.b2))
- a3=tf.nn.sigmoid(tf.nn.bias_add(tf.matmul(a2, self.W_3),self.b3))
- a4=tf.matmul(a3, self.W_4)
- return a4
通過網(wǎng)絡(luò)預(yù)測,我們可以計算這些預(yù)測與相應(yīng)標簽(網(wǎng)絡(luò)輸入 x)之間的損失。為了計算損失的平均值,我們還需要知道非零標簽的數(shù)量——也就是訓練集中用戶的總評分數(shù)。
- def _compute_loss(self, predictions, labels,num_labels):
- ''' Computing the Mean Squared Error loss between the input and output of the network.
- @param predictions: predictions of the stacked autoencoder
- @param labels: input values of the stacked autoencoder which serve as labels at the same time
- @param num_labels: number of labels !=0 in the data set to compute the mean
- @return mean squared error loss tf-operation
- '''
- with tf.name_scope('loss'):
- loss_op=tf.div(tf.reduce_sum(tf.square(tf.subtract(predictions,labels))),num_labels)
- return loss_op
網(wǎng)絡(luò)的優(yōu)化/訓練步驟似乎有點棘手,讓我們一步一步討論。給定輸入 x,計算相應(yīng)的輸出。你可能已經(jīng)注意到,輸入 x 中的大部分值都是零值,因為用戶肯定沒有觀看和評估數(shù)據(jù)集中的所有 5953 部電影。因此,建議不要直接使用網(wǎng)絡(luò)的原始預(yù)測。相反,我們必須確定數(shù)據(jù)輸入 x 中零值的索引,并將與這些索引相對應(yīng)的預(yù)測向量中的值也設(shè)置為零。這種預(yù)測操縱極大地減少了網(wǎng)絡(luò)的訓練時間,使網(wǎng)絡(luò)有機會將訓練努力集中在用戶實際給出的評分上。
在此步驟之后,可以計算損失以及正則化損失(可選)。AdamOptimizer 會將損失函數(shù)最小化。請注意,該方法會返回一個均方根誤差(RMSE)而不是均方誤差(MSE),以測得更好的精度。
- def _optimizer(self, x):
- '''Optimization of the network parameter through stochastic gradient descent.
- @param x: input values for the stacked autoencoder.
- @return: tensorflow training operation
- @return: ROOT!! mean squared error
- '''
- outputs=self._inference(x)
- mask=tf.where(tf.equal(x,0.0), tf.zeros_like(x), x) # indices of zero values in the training set (no ratings)
- num_train_labels=tf.cast(tf.count_nonzero(mask),dtype=tf.float32) # number of non zero values in the training set
- bool_mask=tf.cast(mask,dtype=tf.bool) # boolean mask
- outputs=tf.where(bool_mask, outputs, tf.zeros_like(outputs)) # set the output values to zero if corresponding input values are zero
- MSE_loss=self._compute_loss(outputs,x,num_train_labels)
- if self.FLAGS.l2_reg==True:
- l2_loss = tf.add_n([tf.nn.l2_loss(v) for v in tf.trainable_variables()])
- MSE_lossMSE_loss = MSE_loss + self.FLAGS.lambda_ * l2_loss
- train_op=tf.train.AdamOptimizer(self.FLAGS.learning_rate).minimize(MSE_loss)
- RMSE_loss=tf.sqrt(MSE_loss)
- return train_op, RMSE_loss
(3) 測試
訓練幾個 epoch 之后,神經(jīng)網(wǎng)絡(luò)已經(jīng)在訓練集中看到每個用戶的所有評分以及時間了。此時該模型應(yīng)該已經(jīng)了解數(shù)據(jù)中潛在的隱藏模式以及用戶對應(yīng)的電影評級規(guī)律。給定用戶評分訓練樣本 x,該模型預(yù)測輸出 x'。該向量由輸入值 x 的重構(gòu)(如預(yù)期)組成,但現(xiàn)在還包含輸入 x 中先前為零的值。這意味著該模型在給未評分的電影打分。這個評級對應(yīng)于用戶的偏好——模型從數(shù)據(jù)中已識別和學習到的偏好。
為了能夠測量模型的準確性,我們需要訓練和測試數(shù)據(jù)集。根據(jù)訓練集進行預(yù)測。類似于訓練階段,我們只考慮對應(yīng)于測試集中非零值的索引的輸出值。
現(xiàn)在我們可以計算預(yù)測值與實際評分之間的均方根誤差損失(RMSE)。RMSE 表示預(yù)測值與觀測值之間差異的樣本標準偏差。例如,RMSE 為 0.5 意味著平均預(yù)測評分與實際評分相差 0.5 星。
- def _validation_loss(self, x_train, x_test):
- ''' Computing the loss during the validation time.
- @param x_train: training data samples
- @param x_test: test data samples
- @return networks predictions
- @return root mean squared error loss between the predicted and actual ratings
- '''
- outputs=self._inference(x_train) # use training sample to make prediction
- mask=tf.where(tf.equal(x_test,0.0), tf.zeros_like(x_test), x_test) # identify the zero values in the test ste
- num_test_labels=tf.cast(tf.count_nonzero(mask),dtype=tf.float32) # count the number of non zero values
- bool_mask=tf.cast(mask,dtype=tf.bool)
- outputs=tf.where(bool_mask, outputs, tf.zeros_like(outputs))
- MSE_loss=self._compute_loss(outputs, x_test, num_test_labels)
- RMSE_loss=tf.sqrt(MSE_loss)
- return outputs, RMSE_loss
(4) 訓練結(jié)果
最后一步包括執(zhí)行訓練過程并檢查模型性能。在這一點上,我不會詳細討論構(gòu)建數(shù)據(jù)輸入管道、圖表、會話等細節(jié)。因為這些步驟通常是已知的。對此主題感興趣的讀者可以在我的 GitHub 中查看這些步驟:https://github.com/artem-oppermann/Deep-Autoencoders-For-Collaborative-Filtering/blob/master/train.py
在這里,你可以看到前 50 個迭代次數(shù)的訓練和測試表現(xiàn)。50 次后,測試集的預(yù)測和實際評分間的偏差是 0.929 星。
- epoch_nr: 0, train_loss: 1.169, test_loss: 1.020
- epoch_nr: 10, train_loss: 0.936, test_loss: 0.959
- epoch_nr: 20, train_loss: 0.889, test_loss: 0.931
- epoch_nr: 30, train_loss: 0.873, test_loss: 0.923
- epoch_nr: 40, train_loss: 0.859, test_loss: 0.925
- epoch_nr: 50, train_loss: 0.844, test_loss: 0.929
原文地址:
https://towardsdatascience.com/deep-autoencoders-for-collaborative-filtering-6cf8d25bbf1d
【本文是51CTO專欄機構(gòu)“機器之心”的原創(chuàng)譯文,微信公眾號“機器之心( id: almosthuman2014)”】