老早就想整理一篇推薦算法的入門博文,今天抽空寫一下。本文以電影推薦系統爲例,簡單地介紹基於協同過濾,PMF機率矩陣分解,NMF非負矩陣分解和Baseline的推薦系統算法。NMF的實現具體能夠參考Reference中的「基於矩陣分解的推薦算法,簡單入門」一文,對我啓發很大。html
什麼是協同過濾?協同過濾是利用集體智慧的一個典型方法。要理解什麼是協同過濾 (Collaborative Filtering, 簡稱 CF),首先想一個簡單的問題,若是你如今想看個電影,但你不知道具體看哪部,你會怎麼作?大部分的人會問問周圍的朋友,看看最近有什麼好看的電影推薦,而咱們通常更傾向於從口味比較相似的朋友那裏獲得推薦;或者,搜索與你喜歡的電影同類型的電影推薦。
java
如上圖,咱們收集到用戶-電影評價矩陣,假設用戶A對於物品D的評價爲null,這時咱們對比用戶A、用戶B、用戶C的特徵向量(以物品評價爲特徵),能夠發現用戶A與用戶C的類似度較大,這時咱們能夠認爲,對於用戶C喜歡的物品D,用戶A也應該喜歡它,這是就把物品D推薦給用戶A。web
同理,咱們對比物品A、物品B、物品C的特徵向量(以用戶對該物品的喜歡程度爲特徵),發現物品A與物品C很像,就把用品C推薦給喜歡物品A的用戶C。算法
在實際業務場景中,user-Item矩陣有可能很是稀疏,存儲率有可能連1%都達不到。怎麼辦呢?一般使用矩陣分解算法來提取出更有用的信息。SVD在矩陣分解方面能夠參考基於SVD實現PCA的圖像識別 一文,把它分解成用戶矩陣(左奇異特徵向量矩陣)和物品矩陣(右奇異特徵向量矩陣)分別表明各自的特性。數據結構
可是SVD算法的時間複雜度很大,不適合用來解決這種比數據量較大的問題,這時就有PMF機率矩陣分解。把用戶-電影 評分當作一個矩陣,rui表示u對電影i的評分,因而電影評分矩陣能夠這樣來估計:dom
其中P和Q就至關於SVD中的前k 個特徵向量構成的矩陣,分別描述user-based和item-based。在PMF中使用SGD(隨機梯度降低)進行優化時,使用以下的迭代公式:機器學習
咱們把證實放到下一章節 NMF非負矩陣分解,NMF其實就是在PMF的基礎上加入一點約束,具體約束公式以下:函數
咱們知道,要作推薦系統,最基本的一個數據就是,用戶-物品的評分矩陣,以下圖所示:工具
矩陣中,描述了5個用戶(U1,U2,U3,U4 ,U5)對4個物品(D1,D2,D3,D4)的評分(1-5分),「-」 表示沒有評分,如今目的是把沒有評分的 給預測出來,而後按預測的分數高低,給用戶進行推薦。學習
如何預測缺失的評分呢?對於缺失的評分,能夠轉化爲基於機器學習的迴歸問題,也就是連續值的預測,對於矩陣分解有以下式子,R是相似圖的評分矩陣,假設N*M維(N表示行數,M表示列數),能夠分解爲P跟Q矩陣,其中P矩陣維度N*K,P矩陣維度K*M。
對於P,Q矩陣的解釋,直觀上,P矩陣是N個用戶對K個主題的關係,Q矩陣是K個主題跟M個物品的關係,至於K個主題具體是什麼,在算法裏面K是一個參數,須要調節的,一般10~100之間。
對於上式的左邊項,表示的是R^ 第i 行,第j 列的元素值,對於如何衡量矩陣分解的好壞,咱們給出以下風險函數:
有了風險函數,咱們就能夠採用梯度降低法不斷地減少損失值,直到不能再減少爲止,最後的目標,就是每個元素(非缺失值)的e(i,j)的總和 最小。咱們能夠獲得以下梯度以及p、q的更新方式(其中α是學習步長,詳見李航《統計機器學習》):
在訓練p、q參數過程當中,爲了防止過擬合,咱們給出一個正則項,風險函數修改以下:
相應的p、q參數學習更新方式以下:
至此,咱們就能夠學習出p、q矩陣,將p x q就能夠獲得新的估計矩陣,因爲加入了非負處理(缺失值部分的處理),咱們能夠發現原先缺失的地方有了一個估計值,這個估計值就做爲了推薦的分值(其實就是拿非缺失值部分做參數學習訓練,學習出來的結果固然不會有負數)。NMF實現代碼以下:
package nmf; public class Nmf { public double[][] RM, PM, QM; public int Kc, Uc, Oc; public int steps; public double Alpha, Beta; public void run() { for (int s = 0; s < steps; s++) { // 梯度降低更新 for (int i = 0; i < Uc; i++) { for (int j = 0; j < Oc; j++) { if (RM[i][j] > 0) { // 計算eij double e = 0, pq = 0; for (int k = 0; k < Kc; k++) { pq += PM[i][k] * QM[k][j]; } e = RM[i][j] - pq; // 更新Pik和Qkj,同時保證非負 for (int k = 0; k < Kc; k++) { PM[i][k] += Alpha * (2 * e * QM[k][j] - Beta * PM[i][k]); // PM[i][k] = PM[i][k] > 0 ? PM[i][k] : 0; QM[k][j] += Alpha * (2 * e * PM[i][k] - Beta * QM[k][j]); // QM[k][j] = QM[k][j] > 0 ? QM[k][j] : 0; } } } } // 計算風險損失 double loss = 0; for (int i = 0; i < Uc; i++) { for (int j = 0; j < Oc; j++) { if (RM[i][j] > 0) { // 計算eij^2 double e2 = 0, pq = 0; for (int k = 0; k < Kc; k++) { pq += PM[i][k] * QM[k][j]; } e2 = Math.pow(RM[i][j] - pq, 2); for (int k = 0; k < Kc; k++) { e2 += Beta / 2 * (Math.pow(PM[i][k], 2) + Math.pow( QM[k][j], 2)); } loss += e2; } } } if (loss < 0.01) { System.out.println("OK"); break; } // if (s % 100 == 0) { // System.out.println(loss); // } } } public Nmf(double[][] RM, double[][] PM, double[][] QM, int Kc, int Uc, int Oc, int steps, double Alpha, double Beta) { this.RM = RM; this.PM = PM; this.QM = QM; this.Kc = Kc; this.Uc = Uc; this.Oc = Oc; this.steps = steps; this.Alpha = Alpha; this.Beta = Beta; } }
package nmf; import java.util.Scanner; public class Keyven { public static void main(String[] args) { int Uc = 5, Oc = 4, Kc = 2; double[][] RM = new double[Uc][Oc]; double[][] PM = new double[Uc][Kc]; double[][] QM = new double[Kc][Oc]; /* * 5 3 0 1 4 0 0 1 1 1 0 5 1 0 0 4 0 1 5 4 */ Scanner input = new Scanner(System.in); for (int i = 0; i < Uc; i++) { for (int j = 0; j < Oc; j++) { RM[i][j] = input.nextDouble(); } } for (int i = 0; i < Uc; i++) { for (int j = 0; j < Oc; j++) { System.out.printf("%.2f\t", RM[i][j]); } System.out.println(); } System.out.println(); for (int i = 0; i < Uc; i++) { for (int j = 0; j < Kc; j++) { PM[i][j] = Math.random() % 9; } } for (int i = 0; i < Kc; i++) { for (int j = 0; j < Oc; j++) { QM[i][j] = Math.random() % 9; } } // 最多迭代5000次,學習步長控制爲0.002,正則項參數設置爲0.02 Nmf nmf = new Nmf(RM, PM, QM, Kc, Uc, Oc, 5000, 0.002, 0.02); nmf.run(); for (int i = 0; i < Uc; i++) { for (int j = 0; j < Oc; j++) { double temp = 0; for (int k = 0; k < Kc; k++) { temp += PM[i][k] * QM[k][j]; } System.out.printf("%.2f\t", temp); } System.out.println(); } input.close(); } }
實驗結果:
要評估一個策略的好壞,就須要創建一個對比基線,以便後續觀察算法效果的提高。此處咱們能夠簡單地對推薦算法進行建模做爲基線。假設咱們的訓練數據爲: <user, item, rating>三元組, 其中user爲用戶id, item爲物品id(item能夠是MovieLens上的電影,Amazon上的書, 或是百度關鍵詞工具上的關鍵詞), rating爲user對item的投票分數, 其中用戶u對物品i的真實投票分數咱們記爲rui,基線(baseline)模型預估分數爲bui,則可建模以下:
其中mu(希臘字母mu)爲全部已知投票數據中投票的均值,bu爲用戶的打分相對於平均值的誤差(若是某用戶比較苛刻,打分都相對偏低, 則bu會爲負值;相反,若是某用戶常常對不少片都打正分, 則bu爲正值); bi爲該item被打分時,相對於平均值得誤差,可反映電影受歡迎程度。 bui則爲基線模型對用戶u給物品i打分的預估值。該模型雖然簡單, 但其中其實已經包含了用戶個性化和item的個性化信息, 並且特別簡單(不少時候, 簡單就是一個很是大的特色, 特別是面對大規模數據時)。
基線模型中, mu能夠直接統計獲得,咱們的優化函數能夠寫爲(其實就是最小二乘法):
也能夠直接寫成以下式子,由於它自己就是經驗似然:
上述式子中u∈R(i) 表示評價過電影 i 的全部用戶,|R(i)| 爲其集合的個數;同理,i∈R(u) 表示用戶 u 評價過的全部電影,|R(u)| 爲其集合的個數。實現代碼以下:
package baseline; public class Baseline { public double[] bi, bu; public double[][] RM; public int Uc, Ic; public double lamada2, lamada3; public Baseline(double[][] RM, int Uc, int Ic, double lamada2, double lamada3) { this.RM = RM; this.lamada2 = lamada2; this.lamada3 = lamada3; this.Uc = Uc; this.Ic = Ic; this.bu = new double[Uc]; this.bi = new double[Ic]; } public void run() { // 計算μ double avg = 0; for (int i = 0; i < Uc; i++) { for (int j = 0; j < Ic; j++) { avg += RM[i][j]; } } avg = avg / Uc / Ic; // 更新bi for (int i = 0; i < Ic; i++) { double bis = 0; int Icnt = 0; // 點評過電影i的全部User個數 for (int tu = 0; tu < Uc; tu++) { if (RM[tu][i] != 0) { bis += RM[tu][i] - avg; Icnt++; } } bi[i] = bis / ((double)Icnt + lamada2); } // 更新bu for (int u = 0; u < Uc; u++) { double bus = 0; int Ucnt = 0; // 用戶u點評過得電影Item個數 for (int ti = 0; ti < Ic; ti++) { if (RM[u][ti] != 0) { bus += RM[u][ti] - avg - bi[ti]; Ucnt++; } } bu[u] = bus / ((double)Ucnt + lamada3); } for (int u = 0; u < Uc; u++) { for (int i = 0; i < Ic; i++) { if (RM[u][i] == 0) { RM[u][i] = avg + bi[i] + bu[u]; } } } } }
package baseline; import java.util.Scanner; public class Keyven { public static void main(String[] args) { int Uc = 5, Ic = 4; double[][] RM = new double[Uc][Ic]; /* * 5 3 0 1 4 0 0 1 1 1 0 5 1 0 0 4 0 1 5 4 */ Scanner input = new Scanner(System.in); for (int i = 0; i < Uc; i++) { for (int j = 0; j < Ic; j++) { RM[i][j] = input.nextDouble(); } } Baseline bl = new Baseline(RM, Uc, Ic, 0, 0); bl.run(); for (int i = 0; i < Uc; i++) { for (int j = 0; j < Ic; j++) { System.out.printf("%.2f\t", RM[i][j]); } System.out.println(); } input.close(); } }
Baseline基線模型與NMF矩陣分解模型試驗效果對好比下:
什麼是梯度降低?考慮下圖一種簡單的情形,風險函數爲loss = kx,則整體損失就是積分∫kx dx,取梯度的反方向進行逐步更新至整體損失減少… …在我看來,其實,數據挖掘 = ①線性代數+②應用機率統計+③高數(積分、梯度等數理意義)+④李航《統計機器學習》神書+⑤算法與數據結構… …只要好好努力打好基礎,人就能不斷向前走下去^_^
從item-base到svd再到rbm,多種Collaborative Filtering(協同過濾算法)從原理到實現
白話NMF(Non-negative Matrix Factorization)
基於矩陣分解的推薦算法,簡單入門(證實算法,很是有用!)
探索推薦引擎內部的祕密,第 2 部分: 深刻推薦引擎相關算法 - 協同過濾
推薦系統中近鄰算法與矩陣分解算法效果的比較——基於movielens數據集