LFM(latent factor model)隱語義模型,這也是在推薦系統中應用相當普遍的一種模型。那這種模型跟ItemCF或UserCF的不同在於:
- 對於UserCF,我們可以先計算和目標用戶興趣相似的用戶,之后再根據計算出來的用戶喜歡的物品給目標用戶推薦物品。
- 而ItemCF,我們可以根據目標用戶喜歡的物品,尋找和這些物品相似的物品,再推薦給用戶。
- 我們還有一種方法,先對所有的物品進行分類,再根據用戶的興趣分類給用戶推薦該分類中的物品,LFM就是用來實現這種方法。
如果要實現最后一種方法,需要解決以下的問題:
- 給物品分類
- 確定用戶興趣屬於哪些類及感興趣程度
- 對於用戶感興趣的類,如何推薦物品給用戶
對分類,很容易想到人工對物品進行分類,但是人工分類是一種很主觀的事情,比如一部電影用戶可能因為這是喜劇片去看了,但也可能因為他是周星馳主演的看了,也有可能因為這是一部屬於西游類型的電影,不同的人可以得到不同的分類。
而且對於物品分類的粒度很難控制,究竟需要把物品細分到個程度,比如一本線性代數,可以分類到數學中,也可以分類到高等數學,甚至根據線性代數主要適用的領域再一次細分,但對於非專業領域的人來說,想要對這樣的物品進行小粒度細分無疑是一件費力不討好的事情。
而且一個物品屬於某個類,但是這個物品相比其他物品,是否更加符合這個類呢?這也是很難人工確定的事情。解決這個問題,就需要隱語義模型。隱語義模型,可以基於用戶的行為自動進行聚類,並且這個類的數量,即粒度完全由可控。
對於某個物品是否屬與一個類,完全由用戶的行為確定,我們假設兩個物品同時被許多用戶喜歡,那么這兩個物品就有很大的幾率屬於同一個類。而某個物品在類所占的權重,也完全可以由計算得出。
以下公式便是隱語義模型計算用戶u對物品i興趣的公式:
其中,pu,k度量了用戶u的興趣和第k個隱類的關系,而qi,k度量了第k個隱類和物品i之間的關系
接下的問題便是如何計算這兩個參數p和q了,對於這種線性模型的計算方法,這里使用的是梯度下降法。大概的思路便是使用一個數據集,包括用戶喜歡的物品和不喜歡的物品,根據這個數據集來計算p和q。
如果沒有負樣本,則對於一個用戶,從他沒有過行為的物品采樣出一些物品作為負樣本,但采樣時,保證每個用戶的正負樣本數目相當。
下面給出公式,對於正樣本,我們規定r=1,負樣本r=0,需要優化如下損失函數來找到最合適的參數p和參數q:
損失函數里邊有兩組參數puk和qik,隨機梯度下降法,需要對他們分別求偏導數,可得:
然后,根據隨機梯度下降法,需要將參數沿着最速下降方向前進,因此可以得到如下遞推公式:
其中α是學習速率,它的選取需要通過反復試驗獲得。
后面的lambda是為了防止過擬合的正則化項,下面給出Python代碼。
from multiprocessing import Pool, Manager from math import exp import pandas as pd import numpy as np import pickle import time def getResource(csvPath): ''' 獲取原始數據 :param csvPath: csv原始數據路徑 :return: frame ''' frame = pd.read_csv(csvPath) return frame def getUserNegativeItem(frame, userID): ''' 獲取用戶負反饋物品:熱門但是用戶沒有進行過評分 與正反饋數量相等 :param frame: ratings數據 :param userID:用戶ID :return: 負反饋物品 ''' userItemlist = list(set(frame[frame['UserID'] == userID]['MovieID'])) #用戶評分過的物品 otherItemList = [item for item in set(frame['MovieID'].values) if item not in userItemlist] #用戶沒有評分的物品 itemCount = [len(frame[frame['MovieID'] == item]['UserID']) for item in otherItemList] #物品熱門程度 series = pd.Series(itemCount, index=otherItemList) series = series.sort_values(ascending=False)[:len(userItemlist)] #獲取正反饋物品數量的負反饋物品 negativeItemList = list(series.index) return negativeItemList def getUserPositiveItem(frame, userID): ''' 獲取用戶正反饋物品:用戶評分過的物品 :param frame: ratings數據 :param userID: 用戶ID :return: 正反饋物品 ''' series = frame[frame['UserID'] == userID]['MovieID'] positiveItemList = list(series.values) return positiveItemList def initUserItem(frame, userID=1): ''' 初始化用戶正負反饋物品,正反饋標簽為1,負反饋為0 :param frame: ratings數據 :param userID: 用戶ID :return: 正負反饋物品字典 ''' positiveItem = getUserPositiveItem(frame, userID) negativeItem = getUserNegativeItem(frame, userID) itemDict = {} for item in positiveItem: itemDict[item] = 1 for item in negativeItem: itemDict[item] = 0 return itemDict def initPara(userID, itemID, classCount): ''' 初始化參數q,p矩陣, 隨機 :param userCount:用戶ID :param itemCount:物品ID :param classCount: 隱類數量 :return: 參數p,q ''' arrayp = np.random.rand(len(userID), classCount) arrayq = np.random.rand(classCount, len(itemID)) p = pd.DataFrame(arrayp, columns=range(0,classCount), index=userID) q = pd.DataFrame(arrayq, columns=itemID, index=range(0,classCount)) return p,q def work(id, queue): ''' 多進程slave函數 :param id: 用戶ID :param queue: 隊列 ''' print(id) itemDict = initUserItem(frame, userID=id) queue.put({id:itemDict}) def initUserItemPool(userID): ''' 初始化目標用戶樣本 :param userID:目標用戶 :return: ''' pool = Pool() userItem = [] queue = Manager().Queue() for id in userID: pool.apply_async(work, args=(id,queue)) pool.close() pool.join() while not queue.empty(): userItem.append(queue.get()) return userItem def initModel(frame, classCount): ''' 初始化模型:參數p,q,樣本數據 :param frame: 源數據 :param classCount: 隱類數量 :return: ''' userID = list(set(frame['UserID'].values)) itemID = list(set(frame['MovieID'].values)) p, q = initPara(userID, itemID, classCount) userItem = initUserItemPool(userID) return p, q, userItem def sigmod(x): ''' 單位階躍函數,將興趣度限定在[0,1]范圍內 :param x: 興趣度 :return: 興趣度 ''' y = 1.0/(1+exp(-x)) return y def lfmPredict(p, q, userID, itemID): ''' 利用參數p,q預測目標用戶對目標物品的興趣度 :param p: 用戶興趣和隱類的關系 :param q: 隱類和物品的關系 :param userID: 目標用戶 :param itemID: 目標物品 :return: 預測興趣度 ''' p = np.mat(p.ix[userID].values) q = np.mat(q[itemID].values).T r = (p * q).sum() r = sigmod(r) return r def latenFactorModel(frame, classCount, iterCount, alpha, lamda): ''' 隱語義模型計算參數p,q :param frame: 源數據 :param classCount: 隱類數量 :param iterCount: 迭代次數 :param alpha: 步長 :param lamda: 正則化參數 :return: 參數p,q ''' p, q, userItem = initModel(frame, classCount) for step in range(0, iterCount): for user in userItem: for userID, samples in user.items(): for itemID, rui in samples.items(): eui = rui - lfmPredict(p, q, userID, itemID) for f in range(0, classCount): print('step %d user %d class %d' % (step, userID, f)) p[f][userID] += alpha * (eui * q[itemID][f] - lamda * p[f][userID]) q[itemID][f] += alpha * (eui * p[f][userID] - lamda * q[itemID][f]) alpha *= 0.9 return p, q def recommend(frame, userID, p, q, TopN=10): ''' 推薦TopN個物品給目標用戶 :param frame: 源數據 :param userID: 目標用戶 :param p: 用戶興趣和隱類的關系 :param q: 隱類和物品的關系 :param TopN: 推薦數量 :return: 推薦物品 ''' userItemlist = list(set(frame[frame['UserID'] == userID]['MovieID'])) otherItemList = [item for item in set(frame['MovieID'].values) if item not in userItemlist] predictList = [lfmPredict(p, q, userID, itemID) for itemID in otherItemList] series = pd.Series(predictList, index=otherItemList) series = series.sort_values(ascending=False)[:TopN] return series if __name__ == '__main__': frame = getResource('ratings.csv') p, q = latenFactorModel(frame, 5, 10, 0.02, 0.01) l = recommend(frame, 1, p, q) print(l)