基於矩陣分解(MF,Matrix Factorization)的推薦算法


LFM

LFM即隱因子模型,我們可以把隱因子理解為主題模型中的主題、HMM中的隱藏變量。比如一個用戶喜歡《推薦系統實踐》這本書,背后的原因可能是該用戶喜歡推薦系統、或者是喜歡數據挖掘、亦或者是喜歡作者項亮本人等等,假如真的是由於這3個原因導致的,那如果項亮出了另外一本數據挖掘方面的書,我們可以推測該用戶也會喜歡,這“背后的原因”我們稱之為隱因子。所以LFM的其中思路就是先計算用戶對各個隱因子的喜好程度$(p_1,p_2,...,p_f)$,再計算物品在各個隱因子上的概率分布$(q_1,q_2,...,q_f)$,兩個向量做內積即得到用戶對物品的喜好程度,下面就講這兩個向量怎么求。

假設我們已經有了一個評分矩陣$R_{m,n}$,$m$個用戶對$n$個物品的評分全在這個矩陣里,當然這是一個高度稀疏的矩陣,我們用$r_{u,i}$表示用戶$u$對物品$i$的評分。LFM認為$R_{m,n}=P_{m,F}\cdot{Q_{F,n}}$即R是兩個矩陣的乘積(所以LFM又被稱為矩陣分解法,MF,matrix factorization model),F是隱因子的個數,P的每一行代表一個用戶對各隱因子的喜歡程序,Q的每一列代表一個物品在各個隱因子上的概率分布。

\begin{equation}\hat{r}_{ui}=\sum_{f=1}^{F}{P_{uf}Q_{fi}}\label{lfm}\end{equation}

這種基於矩陣分解的推薦算法又叫SVD(Singular Value Decomposition,奇異值分解),但實際上它只是從SVD借鑒過來的,跟SVD其實根本不是一回事。

SVD:

$$A_{m\times n} \approx U_{m\times k}\Sigma_{k\times k}V^T_{k\times n}$$

把$\Sigma$去掉是不是跟LFM就很像了?

機器學習訓練的目標是使得對所有的$\color{red}{r_{ui}\ne0}$,$r_{u,i}$和$\hat{r}_{ui}$盡可能接近,即

\begin{equation}min:\ \ Loss=\sum_{\color{red}{r_{ui}\ne0}}{(r_{u,i}-\hat{r}_{ui})^2}\end{equation}

為防止過擬合,加個正則項,以防止$P_{uf},Q_{fi}$過大或過小。

\begin{equation}min:\ \ Loss=\sum_{\color{red}{r_{ui}\ne0}}{(r_{u,i}-\hat{r}_{ui})^2}+\lambda(\sum{P_{uf}^2}+\sum{Q_{fi}^2})=f(P,Q)\label{target_lfm}\end{equation}

采用梯度下降法求解上面的無約束最優化問題,在第$t+1$輪迭代中$P$和$Q$的值分別應該是

\begin{equation}P^{(t+1)}=P^{(t)}-\alpha\frac{\partial{Loss}}{\partial{P^{(t)}}},Q^{(t+1)}=Q^{(t)}-\alpha\frac{\partial{Loss}}{\partial{Q^{(t)}}}\end{equation}

\begin{equation}\frac{\partial{Loss}}{\partial{P^{(t)}}}=\left[\begin{array}{cc}\frac{\partial{Loss}}{\partial{P_{11}^{(t)}}}\ ...\ \frac{\partial{Loss}}{\partial{P_{1F}^{(t)}}}\\...\ \frac{\partial{Loss}}{\partial{P_{uf}^{(t)}}}\ ...\\\frac{\partial{Loss}}{\partial{P_{m1}^{(t)}}}\ ...\ \frac{\partial{Loss}}{\partial{P_{mF}^{(t)}}}\end{array}\right]\end{equation}

\begin{equation}\frac{\partial{Loss}}{\partial{P_{uf}^{(t)}}}=\sum_{\color{red}{i,r_{ui}\ne0}}{-2(r_{u,i}-\hat{r}_{ui})\frac{\partial{\hat{r}_{ui}}}{\partial{P_{uf}^{(t)}}}}+2\lambda{P_{uf}^{(t)}}=\sum_{\color{red}{i,r_{ui}\ne0}}{-2(r_{u,i}-\hat{r}_{ui})Q_{fi}^{(t)}}+2\lambda{P_{uf}^{(t)}}\end{equation}

\begin{equation}\frac{\partial{Loss}}{\partial{Q_{fi}^{(t)}}}=\sum_{\color{red}{u,r_{ui}\ne0}}{-2(r_{u,i}-\hat{r}_{ui})\frac{\partial{\hat{r}_{ui}}}{\partial{Q_{fi}^{(t)}}}}+2\lambda{Q_{fi}^{(t)}}=\sum_{\color{red}{u,r_{ui}\ne0}}{-2(r_{u,i}-\hat{r}_{ui})P_{uf}^\color{red}{(t)}}+2\lambda{Q_{fi}^{(t)}}\end{equation}

以上就是梯度下降法的所有公式,我們注意到:

  1. 求$\frac{\partial{Loss}}{\partial{P_{uf}^{(t)}}}$時用到了用戶$u$對物品的所有評分
  2. 求$\frac{\partial{Loss}}{\partial{P^{(t)}}}$時用到了整個評分矩陣$R$,時間復雜度為$m\times{F}\times{n'}$,$n'$是平均一個用戶對多少個物品有過評分

隨機梯度下降法(SGD,Stochastic Gradient Descent)沒有嚴密的理論證明,但是在實踐中它通常比傳統的梯度下降法需要更少的迭代次數就可以收斂,它有兩個特點:

  1. 單獨更新參數$P_{uf}^{(t+1)}=P_{uf}^{(t)}-\alpha\frac{\partial{Loss}}{\partial{P_{uf}^{(t)}}}$,而原始的梯度下降法要整體更新參數$P^{(t+1)}=P^{(t)}-\alpha\frac{\partial{Loss}}{\partial{P^{(t)}}}$。在$t+1$輪次中計算其他參數的梯度時直接使用$P_{uf}$的最新值$P_{uf}^{(t+1)}$
  2. 計算$\frac{\partial{Loss}}{\partial{P_{uf}^{(t)}}}$時只利用用戶$u$對一個物品的評分,而不是利用用戶$u$的所有評分,即
    \begin{equation}\frac{\partial{Loss}}{\partial{P_{uf}^{(t)}}}=-2(r_{u,i}-\hat{r}_{ui})Q_{fi}^{(t)}+2\lambda{P_{uf}^{(t)}}\end{equation}
    從而\begin{equation}P_{uf}^{(t+1)}=P_{uf}^{(t)}+\alpha[(r_{u,i}-\hat{r}_{ui})Q_{fi}^{(t)}-\lambda{P_{uf}^{(t)}}]\label{pp}\end{equation}
    同理可得\begin{equation}Q_{fi}^{(t+1)}=Q_{fi}^{(t)}+\alpha[(r_{u,i}-\hat{r}_{ui})P_{uf}^\color{red}{(t+1)}-\lambda{Q_{fi}^{(t)}}]\label{pq}\end{equation}

SGD單輪迭代的時間復雜度也是$m\times{F}\times{n'}$,但由於它是單個參數地更新,且更新單個參數時只利用到一個樣本(一個評分),更新后的參數立即可用於更新剩下的參數,所以SGD比批量的梯度下降需要更少的迭代次數。

在訓練模型的時候我們只要求模型盡量擬合$r_{ui}\ne{0}$的情況,對於$r_{ui}=0$的情況我們也不希望$\hat{r}_{ui}=0$,因為$r_{ui}=0$只表示用戶$u$沒有對物品$i$評分,並不代表用$u$戶對物品$i$的喜好程度為0。而恰恰$\hat{r}_{ui}$能反映用$u$戶對物品$i$的喜好程度,對所有$\hat{r}_{ui}(i\in{\{1,2,...,n\}})$降序排列,取出topK就是用戶$u$的推薦列表。

LFM.py

# coding:utf-8
__author__ = "orisun"

import random
import math


class LFM(object):

    def __init__(self, rating_data, F, alpha=0.1, lmbd=0.1, max_iter=500):
        '''rating_data是list<(user,list<(position,rate)>)>類型
        '''
        self.F = F
        self.P = dict()  # R=PQ^T,代碼中的Q相當於博客中Q的轉置
        self.Q = dict()
        self.alpha = alpha
        self.lmbd = lmbd
        self.max_iter = max_iter
        self.rating_data = rating_data

        '''隨機初始化矩陣P和Q'''
        for user, rates in self.rating_data:
            self.P[user] = [random.random() / math.sqrt(self.F)
                            for x in xrange(self.F)]
            for item, _ in rates:
                if item not in self.Q:
                    self.Q[item] = [random.random() / math.sqrt(self.F)
                                    for x in xrange(self.F)]

    def train(self):
        '''隨機梯度下降法訓練參數P和Q
        '''
        for step in xrange(self.max_iter):
            for user, rates in self.rating_data:
                for item, rui in rates:
                    hat_rui = self.predict(user, item)
                    err_ui = rui - hat_rui
                    for f in xrange(self.F):
                        self.P[user][f] += self.alpha * (err_ui * self.Q[item][f] - self.lmbd * self.P[user][f])
                        self.Q[item][f] += self.alpha * (err_ui * self.P[user][f] - self.lmbd * self.Q[item][f])
            self.alpha *= 0.9  # 每次迭代步長要逐步縮小

    def predict(self, user, item):
        '''預測用戶user對物品item的評分
        '''
        return sum(self.P[user][f] * self.Q[item][f] for f in xrange(self.F))

if __name__ == '__main__':
    '''用戶有A B C,物品有a b c d'''
    rating_data = list()
    rate_A = [('a', 1.0), ('b', 1.0)]
    rating_data.append(('A', rate_A))
    rate_B = [('b', 1.0), ('c', 1.0)]
    rating_data.append(('B', rate_B))
    rate_C = [('c', 1.0), ('d', 1.0)]
    rating_data.append(('C', rate_C))

    lfm = LFM(rating_data, 2)
    lfm.train()
    for item in ['a', 'b', 'c', 'd']:
        print item, lfm.predict('A', item)		#計算用戶A對各個物品的喜好程度

輸出:

a 0.860198578815
b 0.901207650363
c 0.853149604409
d 0.814338291689

SVD

在公式$\ref{lfm}$中加入偏置項:

\begin{equation}\hat{r}_{ui}=\sum_{f=1}^{F}{P_{uf}Q_{fi}}+\mu+b_u+b_i\label{bias_lfm}\end{equation}

$\mu$表示訓練集中的所有評分的平均值。$b_u$是用戶偏置,代表一個用戶評分的平均值。$b_i$是物品偏置,代表一個物品被評分的平均值。所以“偏置”這東西反應的是事物固有的、不受外界影響的屬性,用公式$\ref{lfm}$去預估用戶對物品的評分時沒有考慮這個用戶是寬容的還是苛刻的,他傾向於給物品打高分還是打低分,所以在公式$\ref{bias_lfm}$加入了偏置$b_u$。

 $\mu$直接由訓練集統計得到,$b_u$和$b_i$需要通過機器學習訓練得來。對比公式$\ref{target_lfm}$此時我們目標函數變為

\begin{equation}min:\ \ Loss=\sum_{\color{red}{r_{ui}\ne0}}{(r_{u,i}-\hat{r}_{ui})^2}+\lambda(\sum{P_{uf}^2}+\sum{Q_{fi}^2}+\sum{b_u^2}+\sum{b_i^2})\end{equation}

由隨機梯度下降法得到$b_u$和$b_i$的更新方法為

\begin{equation}b_u^{(t+1)}=b_u^{(t)}+\alpha*(r_{u,i}-\hat{r}_{ui}-\lambda*b_u^{(t)})\end{equation}

\begin{equation}b_i^{(t+1)}=b_i^{(t)}+\alpha*(r_{u,i}-\hat{r}_{ui}-\lambda*b_i^{(t)})\end{equation}

$P_{uf}$和$Q_{fi}$的更新方法不變,參見公式$\ref{pp}$和公式$\ref{pq}$。

初始化時把$b_u$和$b_i$全初始化為0即可。

biasLFM.py

# coding:utf-8
__author__ = "orisun"

import random
import math


class BiasLFM(object):

    def __init__(self, rating_data, F, alpha=0.1, lmbd=0.1, max_iter=500):
        '''rating_data是list<(user,list<(position,rate)>)>類型
        '''
        self.F = F
        self.P = dict()
        self.Q = dict()     #相當於博客中Q的轉置
        self.bu = dict()
        self.bi = dict()
        self.alpha = alpha
        self.lmbd = lmbd
        self.max_iter = max_iter
        self.rating_data = rating_data
        self.mu = 0.0

        '''隨機初始化矩陣P和Q'''
        cnt = 0
        for user, rates in self.rating_data:
            self.P[user] = [random.random() / math.sqrt(self.F)
                            for x in xrange(self.F)]
            self.bu[user] = 0
            cnt += len(rates)
            for item, rate in rates:
                self.mu += rate
                if item not in self.Q:
                    self.Q[item] = [random.random() / math.sqrt(self.F)
                                    for x in xrange(self.F)]
                self.bi[item] = 0
        self.mu /= cnt

    def train(self):
        '''隨機梯度下降法訓練參數P和Q
        '''
        for step in xrange(self.max_iter):
            for user, rates in self.rating_data:
                for item, rui in rates:
                    hat_rui = self.predict(user, item)
                    err_ui = rui - hat_rui
                    self.bu[user] += self.alpha * (err_ui - self.lmbd * self.bu[user])
                    self.bi[item] += self.alpha * (err_ui - self.lmbd * self.bi[item])
                    for f in xrange(self.F):
                        self.P[user][f] += self.alpha * (err_ui * self.Q[item][f] - self.lmbd * self.P[user][f])
                        self.Q[item][f] += self.alpha *  (err_ui * self.P[user][f] - self.lmbd * self.Q[item][f])
            self.alpha *= 0.9  # 每次迭代步長要逐步縮小

    def predict(self, user, item):
        '''預測用戶user對物品item的評分
        '''
        return sum(self.P[user][f] * self.Q[item][f] for f in xrange(self.F)) + self.bu[user] + self.bi[item] + self.mu

if __name__ == '__main__':
    '''用戶有A B C,物品有a b c d'''
    rating_data = list()
    rate_A = [('a', 1.0), ('b', 1.0)]
    rating_data.append(('A', rate_A))
    rate_B = [('b', 1.0), ('c', 1.0)]
    rating_data.append(('B', rate_B))
    rate_C = [('c', 1.0), ('d', 1.0)]
    rating_data.append(('C', rate_C))

    lfm = BiasLFM(rating_data, 2)
    lfm.train()
    for item in ['a', 'b', 'c', 'd']:
        print item, lfm.predict('A', item)  # 計算用戶A對各個物品的喜好程度

SVD++

由BiasLFM(即SVD)繼續演化就可以得到SVD++。

SVD++認為任何用戶只要對物品$i$有過評分,不論評分是多少,就已經在一定程度上反應了他對各個隱因子的喜好程度$y_i=(y_{i1},y_{i2},...,y_{iF},)$,$y$是物品所攜帶的屬性,就如同$Q$一樣。在公式$\ref{bias_lfm}$的基礎上,SVD++得出了:

\begin{equation}\hat{r}_{ui}=\sum_{f=1}^{F}{(P_{uf}+\frac{\sum_{j\in{N(u)}}{Y_{jf}}}{\sqrt{|N(u)|}})Q_{fi}}+\mu+b_u+b_i\label{svdpp}\end{equation}

$N(u)$是用戶$u$評價過的物品集合。

跟上文講的一樣,還是基於評分的誤差平方和建立目標函數,正則項里加一個$\lambda\sum{Y_{jf}^2}$,采用隨機梯度下降法解這個優化問題。$\hat{r_{ui}}$對$b_u$、$b_i$、$P_{uf}$的偏導都跟BiasLFM中的一樣,而$\frac{\partial{\hat{r_{ui}}}}{\partial{Q_{fi}}}$會有變化

\begin{equation}\frac{\partial{\hat{r_{ui}}}}{\partial{Q_{fi}}}=P_{uf}+\frac{\sum_{j\in{N(u)}}{Y_{jf}}}{\sqrt{|N(u)|}}\end{equation}

另外引入了$Y$矩陣,所以也需要對$Y_{jf}$求偏導

\begin{equation}\frac{\partial{\hat{r_{ui}}}}{\partial{Y_{jf}}}=\frac{Q_{fi}}{\sqrt{|N(u)|}}\end{equation}

svdpp.py

# coding:utf-8
__author__ = "orisun"

import random
import math


class SVDPP(object):

    def __init__(self, rating_data, F, alpha=0.1, lmbd=0.1, max_iter=500):
        '''rating_data是list<(user,list<(position,rate)>)>類型
        '''
        self.F = F
        self.P = dict()
        self.Q = dict()  # 相當於博客中Q的轉置
        self.Y = dict()
        self.bu = dict()
        self.bi = dict()
        self.alpha = alpha
        self.lmbd = lmbd
        self.max_iter = max_iter
        self.rating_data = rating_data
        self.mu = 0.0

        '''隨機初始化矩陣P、Q、Y'''
        cnt = 0
        for user, rates in self.rating_data:
            self.P[user] = [random.random() / math.sqrt(self.F)
                            for x in xrange(self.F)]
            self.bu[user] = 0
            cnt += len(rates)
            for item, rate in rates:
                self.mu += rate
                if item not in self.Q:
                    self.Q[item] = [random.random() / math.sqrt(self.F)
                                    for x in xrange(self.F)]
                if item not in self.Y:
                    self.Y[item] = [random.random() / math.sqrt(self.F)
                                    for x in xrange(self.F)]
                self.bi[item] = 0
        self.mu /= cnt

    def train(self):
        '''隨機梯度下降法訓練參數P和Q
        '''
        for step in xrange(self.max_iter):
            for user, rates in self.rating_data:
                z = [0.0 for f in xrange(self.F)]
                for item, _ in rates:
                    for f in xrange(self.F):
                        z[f] += self.Y[item][f]
                ru = 1.0 / math.sqrt(1.0 * len(rates))
                s = [0.0 for f in xrange(self.F)]
                for item, rui in rates:
                    hat_rui = self.predict(user, item, rates)
                    err_ui = rui - hat_rui
                    self.bu[user] += self.alpha *  (err_ui - self.lmbd * self.bu[user])
                    self.bi[item] += self.alpha *  (err_ui - self.lmbd * self.bi[item])
                    for f in xrange(self.F):
                        s[f] += self.Q[item][f] * err_ui
                        self.P[user][f] += self.alpha *   (err_ui * self.Q[item][f] - self.lmbd * self.P[user][f])
                        self.Q[item][f] += self.alpha * (err_ui * (self.P[user][f] + z[f] * ru) - self.lmbd * self.Q[item][f])
                for item, _ in rates:
                    for f in xrange(self.F):
                        self.Y[item][f] += self.alpha *  (s[f] * ru - self.lmbd * self.Y[item][f])
            self.alpha *= 0.9  # 每次迭代步長要逐步縮小

    def predict(self, user, item, ratedItems):
        '''預測用戶user對物品item的評分
        '''
        z = [0.0 for f in xrange(self.F)]
        for ri, _ in ratedItems:
            for f in xrange(self.F):
                z[f] += self.Y[ri][f]
        return sum((self.P[user][f] + z[f] / math.sqrt(1.0 * len(ratedItems))) * self.Q[item][f] for f in xrange(self.F)) + self.bu[user] + self.bi[item] + self.mu

if __name__ == '__main__':
    '''用戶有A B C,物品有a b c d'''
    rating_data = list()
    rate_A = [('a', 1.0), ('b', 1.0)]
    rating_data.append(('A', rate_A))
    rate_B = [('b', 1.0), ('c', 1.0)]
    rating_data.append(('B', rate_B))
    rate_C = [('c', 1.0), ('d', 1.0)]
    rating_data.append(('C', rate_C))

    lfm = SVDPP(rating_data, 2)
    lfm.train()
    for item in ['a', 'b', 'c', 'd']:
        print item, lfm.predict('A', item, rate_A)  # 計算用戶A對各個物品的喜好程度

  

go版本的代碼參見:https://github.com/Orisun/lfm


免責聲明!

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



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