LFM(Latent factor model)隱語義模型算法原理以及代碼實踐

簡介

協(xié)同過濾(collaborative filtering)是一種在推薦系統(tǒng)中廣泛使用的技術。該技術通過分析用戶或者事物之間的相似性干厚,來預測用戶可能感興趣的內容并將此內容推薦給用戶。這里的相似性可以是人口特征的相似性种玛,也可以是歷史瀏覽內容的相似性尔当,還可以是個人通過一定機制給與某個事物的回應配喳。比如,A和B是無話不談的好朋友族淮,并且都喜歡看電影辫红,那么協(xié)同過濾會認為A和B的相似度很高,會將A喜歡但是B沒有關注的電影推薦給B祝辣,反之亦然贴妻。

協(xié)同過濾推薦分為3種類型:

  • 基于用戶(user-based)的協(xié)同過濾(UserCF)
  • 基于物品(item-based)的協(xié)同過濾(ItemCF算法)
  • 基于模型(model-based)的協(xié)同過濾 (ModelCF算法)

本文主要講述基于矩陣分解的隱語義(LFM)模型算法的原理以及代碼實現(xiàn)。


算法原理

之前兩篇文章分別講過UserCF算法ItemCF算法蝙斜,首先回想一下這兩種方法是如何進行工作的名惩,還是以電影推薦為例。

  • UserCF
    對于用戶A孕荠,首先找到與其最相似的用戶B娩鹉,然后給A推薦B喜歡的電影。
  • ItemCF
    對于用戶A稚伍,首先找到與A看過的電影最相似的電影弯予,然后給A推薦這些電影。

總的來說槐瑞,UserCF是計算用戶之間的相似性熙涤,ItemCF是計算物品之間的相似性。那還有什么方法可以用于電影推薦么困檩?
我們?yōu)g覽電影網站的時候祠挫,可以看到頂部一般會有很多分類,比如喜劇悼沿、動作等舔、科幻、愛情糟趾、懸疑慌植、恐怖等甚牲。對于我而言,我一般比較喜歡看喜劇蝶柿、科幻丈钙、動作片等,因此如果網站給我推送這幾種類型的電影交汤,我是有較大概率去點擊觀看的雏赦。但是如果給推送言情、古裝芙扎、穿越等題材的電影則根本激不起我的興趣星岗。
因此我們首先可以對電影進行分類,接著得到用戶對每一種電影類型的感興趣度戒洼,然后挑出用戶最感興趣的那個類別俏橘,挑選出屬于那個類別的電影推薦給用戶。
我們首先來量化一下用戶的感興趣程度圈浇,使用0到1范圍內的數字來代表用戶對某種電影類型的感興趣程度寥掐,從0到1依次代表喜歡程度加深。
假如我們現(xiàn)在有8個分類標簽汉额,分別是喜劇曹仗、動作榨汤、科幻蠕搜、愛情、懸疑收壕、恐怖妓灌、劇情、冒險蜜宪,以用戶A為例虫埂,A可能會做出以下評分:

喜劇 動作 科幻 愛情 懸疑 恐怖 劇情 冒險
A 0.8 0.5 0.6 0.6 0.4 0.2 0.3 0.6

現(xiàn)在有兩部電影,分別是《鋼鐵俠3》和《傲慢與偏見》圃验,通過看電影介紹掉伏,我們知道《鋼鐵俠3》包含了動作、科幻澳窑、冒險3個標簽,《傲慢與偏見》包含了劇情斧散、愛情兩個標簽。
我們假設這兩部電影對這8個標簽的符合程度如下:

喜劇 動作 科幻 愛情 懸疑 恐怖 劇情 冒險
鋼鐵俠3 0 0.7 0.9 0 0 0 0 0.6
傲慢與偏見 0 0 0 0.9 0 0 0.9 0

我們可以計算出A對《鋼鐵俠3》的喜歡程度為: 0.5x0.7+0.9x0.6+0.6x0.6 = 1.25摊聋。同理鸡捐,A對《傲慢與偏見》的喜歡程度為: 0.9x0.6+0.3x0.9=0.81。

具體計算方法參見公式(1)麻裁。

可以看到箍镜,用戶A對《鋼鐵俠3》的喜愛程度要高于《傲慢與偏見》源祈,故推薦《鋼鐵俠3》是一個比較明智的選擇。

但是色迂,問題是我們如何能夠得到用戶對每個類別的感興趣程度呢香缺?以及如何給每個物品進行分類呢?你也會說歇僧,我們可以讓用戶對各種類別標簽進行打分赫悄,或者請標注人員對每個商品進行分類。這其中有諸多問題馏慨,比如標注人員的分類不一定代表整體用戶的意見埂淮,細粒度很難控制等問題,因此在實際應用中很少會這么做写隶。
我們實際上收集到的數據一般是用戶對物品的評分倔撞,以上述的電影為例,那么我們得到的原始數據集可能是這樣的:

鋼鐵俠3 傲慢與偏見
A 1.25 0.81

那么我們有沒有一種方法慕趴,能夠通過原始數據集得到用戶對每個類別的感興趣程度以及每個物品的分類呢痪蝇?為了解決這些問題,研究人員想到為什么我們不從數據出發(fā)冕房,自動找到那些類躏啰。然后進行個性化推薦呢?于是耙册,隱含語義分析技術出現(xiàn)了给僵。

我將上面的表格數據用矩陣表示,并寫成下面的形式:
圖1

上圖中详拙,我們令R代表用戶對電影的評分數據帝际,這是我們已有的數據集。令P代表用戶對電影類別的感興趣程度饶辙,令Q為每部電影對每個類別的符合程度蹲诀,因此可得PQ=R。
由于我們已有的數據集是R弃揽,要求P和Q脯爪,首先可能會想到SVD矩陣分解,但是有一個很大的問題矿微,就是SVD分解要求矩陣是稠密的痕慢,也就是說矩陣的所有位置不能有空白。而我們的原始數據集有很多缺失值冷冗,因為不可能每個用戶都會對每一件商品進行評分守屉。
下面我們來看下LFM算法是如何解決這個問題的。令R(u,i)代表的是用戶u對物品i的感興趣程度蒿辙,定義如下:

上式中p_{u}p_{i} 是模型的參數拇泛,也就是分別上圖中的P矩陣和Q矩陣滨巴,其中p_{u,k}代表的含義是用戶u對第k個分類的感興趣程度,q_{k,i}代表的是第i個物品與第k個分類的符合程度俺叭。
假如我們的數據集R是3x4大小的恭取,則上式可以用下圖描述:

圖2

由于用戶一般不會對全部商品評分,故上圖R中有很多項沒有數據熄守。其中K是分類的個數蜈垮,對比圖1,圖1中的K值是8裕照,因為我們將電影分成了8個類別攒发,但是這里并沒有指定具體的K值,因為在LFM算法中K是可以調整的晋南,即我們可能會產生多個沒有具體意義的類別惠猿,我想這也是為什么叫隱語義模型的一個原因吧。
我們發(fā)現(xiàn)使用了LFM之后:

  1. 我們不需要關心分類的角度负间,結果都是基于用戶行為統(tǒng)計自動聚類的偶妖,全憑數據自己說了算。
  2. 不需要關心分類粒度的問題政溃,通過設置LFM的最終分類數就可控制粒度铃剔,分類數越大誉结,粒度越細少办。
  3. 對于一個item获讳,并不是明確的劃分到某一類,而是計算其屬于每一類的概率空扎,是一種標準的軟分類藏鹊。
  4. 對于一個user,我們可以得到他對于每一個類別的興趣度转锈,而不是只關心可見列表中的那幾個類。
  5. 對于每一個class楚殿,我們可以得到類中每個item的權重撮慨,權重越高,說明這個item與這個類別的匹配程度越高脆粥。

接下去的問題就是如何計算矩陣P和矩陣Q中參數值砌溺,一般做法就是通過最優(yōu)化損失函數來求參數。在定義損失函數之前变隔,我們需要準備一下數據集并對興趣度的取值做統(tǒng)一說明规伐。
數據集應該包含所有的user和他們有過行為的(也就是喜歡)的item,所有的這些item構成了一個item全集匣缘。對于每個user來說猖闪,我們把他有過行為的item稱為正樣本鲜棠,規(guī)定興趣度R(u,i)=1,此外我們還需要從item全集中隨機抽樣培慌,選取與正樣本數量相當的樣本作為負樣本豁陆,規(guī)定興趣度為R(u,1)=0。因此吵护,興趣的取值范圍為[0,1]盒音。采樣之后原有的數據集得到擴充,得到一個新的user-item集K=\{(u,i)\}馅而,其中如果(u,i)是正樣本祥诽,則R(u,i)=1,否則R(u,i)=0瓮恭。

推薦系統(tǒng)的用戶行為分為顯性反饋和隱形反饋原押。LFM在顯性反饋數據(也就是評分數據)上解決評分預測問題并且達到了很好的精度。不過本文主要討論的是隱形反饋數據集偎血,這種數據集的特點是只有正樣本和負樣本诸衔,即用戶只有喜歡和不喜歡兩個選擇,而不是對每個物品進行一個評分颇玷。

損失函數如下所示:

使用梯度下降算法:

  1. 對損失函數C求p_{u,k}p_{k,i}的偏導笨农,確定最快的下降方向:
  1. 迭代計算更新參數

其中\alpha是學習率,\lambda是懲罰因子帖渠,均需要通過反復實驗獲得谒亦。


代碼實現(xiàn)

本文將以MovieLens數據集為例,演示下代碼具體實現(xiàn)流程空郊。在這其中會略過一些數據預處理的流程份招,具體可以參考UserCF算法

準備訓練集數據

我們收集的原始數據是用戶對電影的評分數據狞甚,但是這其中并沒有用戶對不喜歡的電影的評分(用戶一般也不會去評分)锁摔,為了訓練我們的模型,我們需要準備一個訓練數據集哼审,這其中包含了每個用戶喜歡的電影和不喜歡的電影谐腰,通過學習這個數據集,就可以獲得上面的模型參數涩盾。
那么在隱形反饋數據集上應用LFM算法的第一個問題就是如何給用戶生成負樣本十气,即用戶不喜歡的電影。負樣本的生成需要遵循以下原則:

  • 對每個用戶春霍,要保證正負樣本數量均衡
  • 對每個用戶采樣負樣本時砸西,要選取那些很熱門,而用戶卻沒有行為的物品

一般認為,物品很熱門而用戶卻沒有行為芹枷,更加代表用戶對這個物品不感興趣衅疙。因為對于冷門物品,用戶可能壓根沒在網站中發(fā)現(xiàn)這個物品杖狼,所以談不上是否感興趣炼蛤。
下面代碼用于隨機選擇負樣本,對于每個用戶喜愛的電影集合蝶涩,選擇數量相等的不感興趣電影:

def _select_negatives(self, movies):
    """
    選擇負樣本
    :param movies: 一個用戶喜愛的電影集合
    :return: 包含正負樣本的電影樣本集合
    """
    ret = dict()
    for i in movies: # 記錄正樣本理朋,興趣度為1
        ret[i] = 1

    number = 0
    while number < len(movies):
        # 從所有商品集合中隨機選取一個當做負樣本,興趣度置為0
        negative_sample = random.choice(self._item_pool)
        if negative_sample in ret:
            continue
        ret[negative_sample] = 0
        number += 1
    return ret

初始化矩陣P和Q

采用均值為0绿聘,方差為1的高斯分布數據來初始化矩陣P和Q嗽上,代碼如下:

def _init_matrix(self):
    '''
    初始化P和Q矩陣,同時選擇高斯分布的隨機值作為初始值
    '''
    print("start build latent matrix.")
    # User-LF user_p 是一個 m x k 的矩陣,其中m是用戶的數量熄攘,k是隱含類別的數量
    self.user_p = dict()
    for user in self._trainData.keys():
        self.user_p[user] = np.random.normal(size=(self._k))
    # Item-LF movie_q 是一個 n x k 的矩陣,其中n是電影的數量兽愤,k是隱含類別的數量
    self.item_q = dict()
    for movie in self._item_pool:
        self.item_q[movie] = np.random.normal(size=(self._k))

隨機梯度優(yōu)化

采用隨機梯度優(yōu)化算法,在每次迭代中對每個用戶的電影列表都重新選擇負樣本挪圾,并且更新參數:

def SGD(self):
    # 隨機梯度下降算法
    alpha = self._alpha
    for epoch in range(self._epochs):
        print("start {0}th epoch training...".format(epoch))
        for user, positive_movies in self._trainData.items():
            # 每次迭代都對用戶重新選擇負樣本
            select_samples = self._select_negatives(positive_movies)
            for movie, rui in select_samples.items():
                # 使用模型去預測user對movie的相似度浅萧,并且得到與真實值之間的誤差
                eui = rui - self.predict(user, movie)
                user_latent = self.user_p[user]
                movie_latent = self.item_q[movie]
                # 更新參數
                self.user_p[user] += alpha * (eui * movie_latent - self._lmbda * user_latent)
                self.item_q[movie] += alpha * (eui * user_latent - self._lmbda * movie_latent)
        alpha *= 0.9 #使學習率線性減小

完整代碼

import random
import numpy as np
from operator import itemgetter
from Utils import modelManager

class LFM(object):
    def __init__(self, trainData, alpha, regularization_rate, number_LatentFactors=10, number_epochs=10):
        self._trainData = trainData # User-Item表
        self._alpha = alpha # 學習率
        self._lmbda = regularization_rate # 正則化懲罰因子
        self._k = number_LatentFactors # 隱語義類別數量
        self._epochs = number_epochs # 訓練次數
        self._item_pool = self._getAllItems() # 所有物品集合
        self._init_matrix()

    def _getAllItems(self):
        # 獲取全體物品列表
        print("start collect all items...")
        items_pool = set()
        for user, items in self._trainData.items():
            for item in items:
                items_pool.add(item)
        return list(items_pool)

    def _init_matrix(self):
        '''
        初始化P和Q矩陣,同時選擇高斯分布的隨機值作為初始值
        '''
        print("start build latent matrix.")
        # User-LF user_p 是一個 m x k 的矩陣,其中m是用戶的數量哲思,k是隱含類別的數量
        self.user_p = dict()
        for user in self._trainData.keys():
            self.user_p[user] = np.random.normal(size=(self._k))
        # Item-LF movie_q 是一個 n x k 的矩陣,其中n是電影的數量洼畅,k是隱含類別的數量
        self.item_q = dict()
        for movie in self._item_pool:
            self.item_q[movie] = np.random.normal(size=(self._k))

    def predict(self, user, item):
        # 通過公式 Rui = ∑P(u,k)Q(k,i)求出user對item的感興趣程度
        return np.dot(self.user_p[user], self.item_q[item])

    def _select_negatives(self, movies):
        """
        選擇負樣本
        :param movies: 一個用戶喜愛的電影集合
        :return: 包含正負樣本的電影樣本集合
        """
        ret = dict()
        for i in movies: # 記錄正樣本,興趣度為1
            ret[i] = 1

        number = 0
        while number < len(movies):
            # 從所有商品集合中隨機選取一個當做負樣本棚赔,興趣度置為0
            negative_sample = random.choice(self._item_pool)
            if negative_sample in ret:
                continue
            ret[negative_sample] = 0
            number += 1
        return ret

    def _loss(self):
        C = 0.
        for user, user_latent in self.user_p.items():
            for movie, movie_latent in self.item_q.items():
                rui = 0
                for u, m in self._trainData.items():
                    if user == u:
                        if movie in m: # 如果movie出現(xiàn)在了user的喜愛列表里面帝簇,則rui=1
                            rui = 1
                        break
                    else:
                        continue

                eui = rui - self.predict(user, movie)
                C += (np.square(eui) +
                      self._lmbda * np.sum(np.square(self.user_p[user])) +
                      self._lmbda * np.sum(np.square(self.item_q[movie])))
        return C

    def SGD(self):
        # 隨機梯度下降算法
        alpha = self._alpha
        for epoch in range(self._epochs):
            print("start {0}th epoch training...".format(epoch))
            for user, positive_movies in self._trainData.items():
                # 每次迭代都對用戶重新選擇負樣本
                select_samples = self._select_negatives(positive_movies)
                for movie, rui in select_samples.items():
                    # 使用模型去預測user對movie的相似度,并且得到與真實值之間的誤差
                    eui = rui - self.predict(user, movie)
                    # print("error : ", eui)
                    user_latent = self.user_p[user]
                    movie_latent = self.item_q[movie]
                    # 更新參數
                    self.user_p[user] += alpha * (eui * movie_latent - self._lmbda * user_latent)
                    self.item_q[movie] += alpha * (eui * user_latent - self._lmbda * movie_latent)
            alpha *= 0.9
            print("{0}td training finished, loss: {1}".format(epoch, self._loss()))

    def recommend(self, user, N):
        """
        給user推薦N個商品
        :param user: 被推薦的用戶user
        :param N: 推薦的商品個數
        :return: 按照user對推薦物品的感興趣程度排序的N個商品
        """
        recommends = dict()

        for movie in self._item_pool:
            recommends[movie] = self.predict(user, movie)

        # 根據被推薦物品的相似度逆序排列靠益,然后推薦前N個物品給到用戶
        return dict(sorted(recommends.items(), key=itemgetter(1), reverse=True)[:N])

    def train(self):
        try:
            print("start load latent factor matrix P and Q")
            model = modelManager.load("../Models/lfm.pkl", 3)
            self.user_p = model[0]
            self.item_q = model[1]
            self._item_pool = model[2]
        except BaseException as e:
            print("Exception occurs: " + str(e))
            print("load latent factor matrix failed, start train...")
            self.SGD()
            modelManager.save("../Models/lfm.pkl", self.user_p, self.item_q, self._item_pool)

測試代碼

以下代碼對測試集中3個用戶進行了Top5推薦丧肴,即每個用戶推薦5部電影,輸出結果按照用戶對電影的感興趣程度從大到小排序胧后。LFM算法學習率采用0.02芋浮,正則化參數0.01,隱類設置為10绩卤,迭代次數為30途样。代碼如下:

from Utils import movielens_loader
from LFM import lfm

if __name__ == "__main__":
    #######
    # LFM
    #######
    train, test = movielens_loader.LoadMovieLensData("../Data/ml-1m/ratings.dat", 0.8)
    print("train data size: %d, test data size: %d" % (len(train), len(test)))
    lfm = lfm.LFM(train, 0.02, 0.01, 10, 30)
    lfm.train()
    print(lfm.user_p)
    print(lfm.item_q)

    # 給測試集中的用戶推薦5部電影
    print(lfm.recommend(list(test.keys())[0], 5))
    print(lfm.recommend(list(test.keys())[1], 5))
    print(lfm.recommend(list(test.keys())[2], 5))

結果如下,輸出了每個用戶感興趣程度最高的5部電影:
推薦結果

完整代碼見https://github.com/HeartbreakSurvivor/RsAlgorithms/blob/main/Test/lfm_test.py濒憋。


總結

LFM算法的優(yōu)缺點如下:

  • 優(yōu)點

    1. LFM具有比較好的理論基礎,它是一種學習方法陶夜,通過優(yōu)化一個設定的指標建立最優(yōu)的模型凛驮。
    2. LFM只是存儲user向量和Item向量,空間占用較小条辟。
    3. 泛化能力強黔夭,在一定程度上解決了數據稀疏問題宏胯。
    4. 更好的擴展性和靈活性。
  • 缺點

    1. 不支持在線實時推薦
    2. 可解釋性差
    3. 不方便加入用戶本姥、物品和上下文相關的特征肩袍。

參考

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市婚惫,隨后出現(xiàn)的幾起案子氛赐,更是在濱河造成了極大的恐慌,老刑警劉巖先舷,帶你破解...
    沈念sama閱讀 212,454評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件艰管,死亡現(xiàn)場離奇詭異,居然都是意外死亡蒋川,警方通過查閱死者的電腦和手機牲芋,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,553評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來捺球,“玉大人缸浦,你說我怎么就攤上這事〉” “怎么了裂逐?”我有些...
    開封第一講書人閱讀 157,921評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長胆剧。 經常有香客問我絮姆,道長,這世上最難降的妖魔是什么秩霍? 我笑而不...
    開封第一講書人閱讀 56,648評論 1 284
  • 正文 為了忘掉前任篙悯,我火速辦了婚禮,結果婚禮上铃绒,老公的妹妹穿的比我還像新娘鸽照。我一直安慰自己,他們只是感情好颠悬,可當我...
    茶點故事閱讀 65,770評論 6 386
  • 文/花漫 我一把揭開白布矮燎。 她就那樣靜靜地躺著,像睡著了一般赔癌。 火紅的嫁衣襯著肌膚如雪诞外。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,950評論 1 291
  • 那天灾票,我揣著相機與錄音峡谊,去河邊找鬼。 笑死,一個胖子當著我的面吹牛既们,可吹牛的內容都是我干的濒析。 我是一名探鬼主播,決...
    沈念sama閱讀 39,090評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼啥纸,長吁一口氣:“原來是場噩夢啊……” “哼号杏!你這毒婦竟也來了?” 一聲冷哼從身側響起斯棒,我...
    開封第一講書人閱讀 37,817評論 0 268
  • 序言:老撾萬榮一對情侶失蹤盾致,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后名船,有當地人在樹林里發(fā)現(xiàn)了一具尸體绰上,經...
    沈念sama閱讀 44,275評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,592評論 2 327
  • 正文 我和宋清朗相戀三年渠驼,在試婚紗的時候發(fā)現(xiàn)自己被綠了蜈块。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,724評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡迷扇,死狀恐怖百揭,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情蜓席,我是刑警寧澤器一,帶...
    沈念sama閱讀 34,409評論 4 333
  • 正文 年R本政府宣布厨内,位于F島的核電站,受9級特大地震影響雏胃,放射性物質發(fā)生泄漏请毛。R本人自食惡果不足惜方仿,卻給世界環(huán)境...
    茶點故事閱讀 40,052評論 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望仙蚜。 院中可真熱鬧,春花似錦厂汗、人聲如沸委粉。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,815評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽艳丛。三九已至匣掸,卻和暖如春趟紊,著一層夾襖步出監(jiān)牢的瞬間氮双,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,043評論 1 266
  • 我被黑心中介騙來泰國打工霎匈, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留戴差,地道東北人。 一個月前我還...
    沈念sama閱讀 46,503評論 2 361
  • 正文 我出身青樓铛嘱,卻偏偏與公主長得像暖释,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子墨吓,可洞房花燭夜當晚...
    茶點故事閱讀 43,627評論 2 350

推薦閱讀更多精彩內容