基於協同過濾,NMF和Baseline的推薦算法

雜談

        老早就想整理一篇推薦算法的入門博文,今天抽空寫一下。本文以電影推薦系統爲例,簡單地介紹基於協同過濾,PMF機率矩陣分解,NMF非負矩陣分解和Baseline的推薦系統算法。NMF的實現具體能夠參考Reference中的「基於矩陣分解的推薦算法,簡單入門」一文,對我啓發很大。html

基於協同過濾的推薦算法

        什麼是協同過濾?協同過濾是利用集體智慧的一個典型方法。要理解什麼是協同過濾 (Collaborative Filtering, 簡稱 CF),首先想一個簡單的問題,若是你如今想看個電影,但你不知道具體看哪部,你會怎麼作?大部分的人會問問周圍的朋友,看看最近有什麼好看的電影推薦,而咱們通常更傾向於從口味比較相似的朋友那裏獲得推薦;或者,搜索與你喜歡的電影同類型的電影推薦。
java

User-based的推薦算法

         如上圖,咱們收集到用戶-電影評價矩陣,假設用戶A對於物品D的評價爲null,這時咱們對比用戶A、用戶B、用戶C的特徵向量(以物品評價爲特徵),能夠發現用戶A與用戶C的類似度較大,這時咱們能夠認爲,對於用戶C喜歡的物品D,用戶A也應該喜歡它,這是就把物品D推薦給用戶A。web

Item-based的推薦算法

        同理,咱們對比物品A、物品B、物品C的特徵向量(以用戶對該物品的喜歡程度爲特徵),發現物品A與物品C很像,就把用品C推薦給喜歡物品A的用戶C。算法

SVD和PMF機率矩陣分解 

        在實際業務場景中,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的基礎上加入一點約束,具體約束公式以下:函數

基於NMF非負矩陣分解的推薦算法

        咱們知道,要作推薦系統,最基本的一個數據就是,用戶-物品的評分矩陣,以下圖所示:工具

        矩陣中,描述了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();
    }
}

        實驗結果:

基於Baseline的推薦算法

        要評估一個策略的好壞,就須要創建一個對比基線,以便後續觀察算法效果的提高。此處咱們能夠簡單地對推薦算法進行建模做爲基線。假設咱們的訓練數據爲:  <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,取梯度的反方向進行逐步更新至整體損失減少… …在我看來,其實,數據挖掘 = ①線性代數+②應用機率統計+③高數(積分、梯度等數理意義)+④李航《統計機器學習》神書+⑤算法與數據結構… …只要好好努力打好基礎,人就能不斷向前走下去^_^


Reference

從item-base到svd再到rbm,多種Collaborative Filtering(協同過濾算法)從原理到實現

百度電影推薦系統比賽 小結 ——記個人初步推薦算法實踐

白話NMF(Non-negative Matrix Factorization)

基於矩陣分解的推薦算法,簡單入門(證實算法,很是有用!)

SVD因式分解實現協同過濾-及源碼實現

探索推薦引擎內部的祕密,第 2 部分: 深刻推薦引擎相關算法 - 協同過濾

推薦系統中近鄰算法與矩陣分解算法效果的比較——基於movielens數據集

相關文章
相關標籤/搜索