從SVD到推薦系統


最近在學習推薦系統(Recommender System),跟大部分人一樣,我也是從《推薦系統實踐》學起,同時也想跟學機器學習模型時一樣使用幾個開源的python庫玩玩。於是找到了surprise,挺新的,代碼沒有sklearn那么臃腫,我能看的下去,於是就開始了自己不斷的挖坑。

這篇文章介紹基於SVD的矩陣分解推薦預測模型。一開始我還挺納悶,SVD不是降維的方法嘛?為什么可以用到推薦系統呢?研究后,實則異曲同工。

有關SVD推導可以看這篇文章:降維方法PCA與SVD的聯系與區別

了解推薦系統的人一定會知道協同過濾算法!

協同過濾算法主要分為兩類,一類是基於領域的方法(neighborhood methods),另一類是隱語義模型(latent factor models),后者一個最成功的實現就是矩陣分解(matrix factorization),矩陣分解我們這篇文章使用的方法就是SVD(奇異值分解)

提問❓:SVD在推薦系統中到底在什么位置呢?

舉手🙋‍♂️:推薦系統 -> 協同過濾算法 -> 隱語義模型 -> 矩陣分解 -> SVD

一、Singular Value Decomposition(SVD)

Guide

SVD的思想在推薦系統中用得很巧妙,我們借助下面這個表來理解:

上圖有三幅小圖,我們來按順序看(圖中的數字請不要糾結於具體數值)。左中圖每行代表一個人,每列代表一部電影。每一個數字對應一個人給一部電影的評分,如李四的給《我不是葯神》評分為5,給《變形金剛4》的評分為1。

假設我們的目的是給圖中的李四推薦電影,你只知道他對這三部電影的評分。這似乎很難,但是設想如果我們知道李四喜歡電影的類型,這樣是不是就更好推薦了! 我們的信息量有限,但是並不是少到離譜,所以能不能通過僅有的信息推測出李四喜歡電影的類型呢?

SVD就是做這個事的,繼續看右邊的兩個圖。我們現在定義兩個電影類型(features:劇情、動畫),右上圖的表(矩陣)給出了每個電影對應類型的程度(如:《我不是葯神》更屬於劇情片,不屬於動畫),右下圖的表中的數字給出了每個人喜歡每種電影類型的程度(李四更喜歡劇情片,不喜歡動畫)。

那么光給出這兩個表(矩陣)有什么用呢?我們把右上表中的第二列(col=《我不是葯神》)和第三列(col=《變形金剛4》)分別與右下表的第二列(col=李四)對應元素相乘后相加(即點乘),得到的結果分別是(55 + -40=25)和(-25 + 50=-10),這兩個分數的物理意義分別是李四喜歡《我不是葯神》和李四喜歡《變形金鋼4》的程度,可以看出李四更喜歡《我不是葯神》。那么我們把右上表的每列和右下表的每列各元素相乘后相加,是不是可以得到每個人喜歡每部片子的程度呢?這不就是我們左邊Rating matirx圖表示的意思嘛!

現在我們給一部新電影《環太平洋》,給定他的類型指標(劇情:-1,動畫:4),我們想知道李四喜歡這部片的程度。仍然依據上面的方法(即:李四喜歡劇情片的程度5✖️《環太平洋》是劇情片的程度-1 ➕李四喜歡動畫的程度0✖️《環太平洋》是動畫的程度4 = -5)。好吧,結果看來李四不喜歡這部片。

但是我們現在只有用戶評分的數據呀,相當於只有左邊的那個稀疏矩陣怎么辦?一個字,學!🎓

Math

我們把評分矩陣(Rating Matrix)計作V, \(V\in R^{n\times m}\),那么V的每一行\(V_{i}\)代表一個人的所有評分,每一列\(V_{j}\)代表某一部電影所有人的評分,\(V_{ij}\)代表某個人i對某部電影j的評分。對應電影推薦來說,V必定是稀疏的,因為電影數量(列的數目)是巨大的,V中必定有很多很多項為null。

我們接下來看這兩個矩陣U(Users Features Matrix )和M(Movie Features Matrix)。U為用戶對特征的偏好程度矩陣,M為電影對特征的擁有程度矩陣。\(U\in R^{f\times n}\)的每一行表示用戶,每一列表示一個特征,它們的值表示用戶與某一特征的相關性,值越大,表明特征越明顯。矩陣\(M\in R^{f\times m}\),的每一行表示電影,每一列表示電影與特征的關聯。

那么U和M怎么得到呢?🤔️

還記得SVD的公式嘛?\(V=UM^{T}\),其實公式右邊的中間還有個對角矩陣S,我們可以把他看成跟U相乘合並(The S matrix is left blended into the feature vectors, so only U and M remain)。其實,U和M這兩個矩陣是通過學習的方式得到的,而不是直接做矩陣分解。我們定義如下的損失函數:

\(E = \frac{1}{2}\sum_{i=1}^{n}\sum_{j=1}^{m}I_{ij}(V_{ij} - p(U_i,M_j))^2+\frac{k_u}{2}\sum_{i=1}^{n}\lVert U_i \rVert^2 + \frac{k_m}{2}\sum_{j=1}^{m}\lVert M_j \rVert^2 \tag{1}\)

這里的評價指標metric采用的是RMSE。其中\(p(U_i,M_j)\)代表用戶i對電影j的預測,最常用的預測函數p就是點乘,即 \(p(U_i,M_j)=U_{i}^{T}M_{j}\)。式(1)中的 \(I\in \{0,1\}^{n\times m}\),為一個指示器,指示相應位置是否有評分,有評分為1,沒有為0。等式右邊最后兩項是正則化項,防止過擬合,這里不過多展開。

到這里,已經變成了一個機器學習的常見問題了,即最小化損失函數,用得最多的優化方法就是梯度下降,當然還有很多梯度下降的變體。下圖是簡單的梯度下降算法📖。

其中,\(\mu\)為學習率learning rate。且:

\(-\nabla _{U} =-\frac{\partial E}{\partial U_i} = \sum_{j=1}^{M}I_{ij}((V_{ij}-p(U_i,M_j))M_j) - k_uU_i \tag{2}\)
\(-\nabla _{M} =-\frac{\partial E}{\partial M_j} = \sum_{i=1}^{n}I_{ij}((V_{ij}-p(U_i,M_j))U_i) - k_mM_j \tag{3}\)

Improvements(adding bias)

可是,無奈的是🤷‍♂️,我們考慮這樣一個事實:

有的人評價電影的時候喜歡打高分,比如我自己,在豆瓣上,就算很一般的電影我也會打3分左右,因為我貌似不是能做出打一分這種殘忍的事的人😂。而有的人很嚴格,跟我正好相反。對於不同電影來說,比如姜文的《邪不壓正》,我可能智商不高,實在覺得不好看,但是由於豆瓣評分頗高,為了掩飾我的看不懂,我還是打了4分。所以,分數這個事情有很強的主觀性。即:

typical collaborative filtering data exhibits large systematic tendencies for some users to give higher ratings than others, and for some items to receive higher ratings than others.

所以,如果僅僅用 \(p(U_i,M_j)=U_{i}^{T}M_{j}\) 進行評分的預測就不那么明智了。在這里,我們考慮根據個人的口味和電影的級別來給予評分過程加一個偏置,即:(這里換一種形式:\(\mathbf{q}_i^T\mathbf{p}_u\)\(U_{i}^{T}M_{j}\)等價)

\(\hat{r}_{ui} = \mu + b_i + b_u + \mathbf{q}_i^T\mathbf{p}_u \tag{4}\)

\(\mu\): 訓練集中所有記錄的評分的全局平均數。在不同網站中,因為網站定位和銷售的物品不同,網站的整體評分分布也會顯示出一些差異。比如有些網站中的用戶就是喜歡打高分,而另一些網站的用戶就是喜歡打低分。而全局平均數可以表示網站本身對用戶評分的影響。

\(b_u\)用戶偏置(user bias)項。這一項表示了用戶的評分習慣中和物品沒有關系的那種因素。比如有些用戶就是比較苛刻,對什么東西要求都很高,那么他的評分就會偏低,而有些用戶比較寬容,對什么東西都覺得不錯,那么他的評分就會偏高。

\(b_i\)物品偏置(item bias)項。這一項表示了物品接受的評分中和用戶沒有什么關系的因素。比如有些物品本身質量就很高,因此獲得的評分相對都比較高,而有些物品本身質量很差,因此獲得的評分相對都會比較低。

有人就說了,這些偏置參數怎么定呢?難道我要預先把所有的數據集計算一遍?❌

才不需要呢,這些偏置參數也是通過學習而來,所以現在我們需要學習的矩陣參數變成了5個。😺

這個時候我們的損失函數變為:

\(E = \sum_{(u,i)\in \mathcal{k}}(r_{ui}-\mu - b_i - b_u - \mathbf{q}_i^T\mathbf{p}_u)^2 + \lambda (\lVert p_u \rVert^2 + \lVert q_i \rVert^2 + b_u^2 + b_i^2) \tag{5}\)

此時,我們的梯度下降也變成了這樣:(\(\gamma\)為學習率)

\(b_u\leftarrow b_u + \gamma (e_{ui} - \lambda b_u) \tag{6}\)

\(b_i \leftarrow b_i + \gamma (e_{ui} - \lambda b_i \tag{7})\)

\(p_u \leftarrow p_u + \gamma (e_{ui} \cdot q_i - \lambda p_u) \tag{8}\)

\(q_i \leftarrow q_i + \gamma (e_{ui} \cdot p_u - \lambda q_i) \tag{9}\)

說到這里,SVD算法的本質是幫我們找到(學習出)item中隱含的維度(features),這些隱含的維度可以是(類型,派別,國別,或者兩個組合,n種組合等等),SVD還可以找到(學習出)用戶對各個維度(features)的類別喜愛程度。我們要做的只是指定維度的數量(n_factors),SVD會自動幫我們干好接下來的工作。

二、surprise

其實,當我知道要使用一個新庫👖的時候,我是拒絕的。但是沒辦法,從頭寫代碼實在是力不從心,把庫中的代碼先變成自己的再說是吧。下面先介紹surprise庫的基本使用,然后重點分析里面的SVD算法。首先,import:

from surprise import SVD
from surprise import Dataset, Reader
from surprise.model_selection import cross_validate, train_test_split

1、初始化reader

制定評分范圍為1-5,數據格式為四個

reader = Reader(rating_scale=(1, 5), line_format='user item rating timestamp')

2、初始化Dataset

傳入的數據有四列。其實Reader和Dataset這兩個類可以這樣理解,前者為框架,定好各種參數,后者填入相應數據。

df_data = pd.read_csv('./data/ml-latest-small/ratings.csv', usecols=['userId','movieId','rating'])

data = Dataset.load_from_df(df_data, reader)
data
<surprise.dataset.DatasetAutoFolds at 0x1192bff60>

這里我們創建了一種新的類型 surprise.dataset,這已經不是pandas的dataframe,這樣做的目的只是方便我們更好使用surprise的各種API。里面的數據格式是這樣的:

elif df is not None:
            self.df = df
            self.raw_ratings = [(uid, iid, float(r) + self.reader.offset, None)
                                for (uid, iid, r) in self.df.itertuples(index=False)]

PS: 在查看源碼的過程后,我發現surprise的load_from_df無法把dataframe里面的timestamp列包含進去。可能這是個bug?可以issue一下。

3、拆分data

trainset, testset = train_test_split(data, test_size=0.2)   ###  實際調用 .split.ShuffleSplit()
trainset
<surprise.trainset.Trainset at 0x1131e6160>

4、訓練模型

我們先來剖開surprise的SVD函數內部看看:

根據(4)式,定義p和q:

pu = rng.normal(self.init_mean, self.init_std_dev, (trainset.n_users, self.n_factors))
qi = rng.normal(self.init_mean, self.init_std_dev, (trainset.n_items, self.n_factors))

其中rng為np.random.RandomState(random_state),參數為隨機數種子。

定義后,pu為一個n✖️f矩陣,qi為一個m✖️f矩陣

for current_epoch in range(self.n_epochs):

    for u, i, r in trainset.all_ratings():

        # 計算(5)的第一項
        dot = 0  # <q_i, p_u>
        for f in range(self.n_factors):
            dot += qi[i, f] * pu[u, f]
        err = r - (global_mean + bu[u] + bi[i] + dot)

        # update biases
        if self.biased:
            # 根據式(6)
            bu[u] += lr_bu * (err - reg_bu * bu[u])
            # 根據式(7)
            bi[i] += lr_bi * (err - reg_bi * bi[i])

        # update factors
        for f in range(self.n_factors):
            puf = pu[u, f]
            qif = qi[i, f]
            # 根據式(8)
            pu[u, f] += lr_pu * (err * qif - reg_pu * puf)
            # 根據式(9)
            qi[i, f] += lr_qi * (err * puf - reg_qi * qif)

好,現在開始訓練模型,這里給定隱含的特征為30個。

model = SVD(n_factors=30)
model.fit(trainset)
<surprise.prediction_algorithms.matrix_factorization.SVD at 0x1138c1da0>

我們來看看模型生成的pu和qi矩陣,似乎很符合我們的要求。

print(model.pu.shape)
print(model.qi.shape)
(671, 30)
(8382, 30)

5、推薦

終於到推薦環節了,前面做的所有工作都是為了這一刻。 推薦也分為幾種類型。一種是預測某個人對某部電影的評分,另一種是推薦給某個人新的幾部電影。

這里我們預測userId為2對於movieId為14的評分:

model.predict(2,14)
Prediction(uid=2, iid=14, r_ui=None, est=3.3735354350374438, details={'was_impossible': False})

推薦電影可以用.get_neighbors()函數

Reference:

  1. https://surprise.readthedocs.io/en/stable/prediction_algorithms_package.html
  2. http://charleshm.github.io/2016/03/SVD-Recommendation-System/#fn:3
  3. https://juejin.im/post/5b1108bae51d4506c7666cac
  4. https://www.cnblogs.com/FengYan/archive/2012/05/06/2480664.html

文獻:

  1. Ma C C. A Guide to Singular Value Decomposition for Collaborative Filtering[J]. 2008.
  2. Koren Y, Bell R, Volinsky C. Matrix Factorization Techniques for Recommender Systems[J]. Computer, 2009, 42(8):30-37.


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM