【知識發現】隱語義模型LFM演算法python實現(三)
http://blog.csdn.net/fjssharpsword/article/details/78257126
基於上篇再優化。
1、回顧LFM原理,可以更好地理解程式碼
對於一個給定的使用者行為資料集(資料集包含的是所有的user, 所有的item,以及每個user有過行為的item列表),使用LFM對其建模後,可得到如下圖所示的模型:(假設資料集中有3個user, 4個item, LFM建模的分類數為4)
R矩陣是user-item矩陣,矩陣值Rij表示的是user i 對item j的興趣度。對於一個user來說,當計算出其對所有item的興趣度後,就可以進行排序並作出推薦。
LFM演算法從資料集中抽取出若干主題,作為user和item之間連線的橋樑,將R矩陣表示為P矩陣和Q矩陣相乘。其中P矩陣是user-class矩陣,矩陣值Pij
使用LFM後, 不需要關心分類的角度,結果都是基於使用者行為統計自動聚類的,全憑資料自己說了算:
l 不需要關心分類粒度的問題,通過設定LFM的最終分類數就可控制粒度,分類數越大,粒度約細
l 對於一個item,並不是明確的劃分到某一類,而是計算其屬於每一類的概率,是一種標準的軟分類
l 對於一個user,我們可以得到他對於每一類的興趣度,而不是隻關心可見列表中的那幾個類。
l 對於每一個class,我們可以得到類中每個item的權重,越能代表這個類的item,權重越高
現在討論如何計算矩陣P和矩陣Q中引數值。一般做法就是最優化損失函式來求引數。在定義損失函式之前,先對資料集和興趣度的取值做一說明。
資料集應該包含所有的user和他們有過行為的(也就是喜歡)的item。所有的這些item構成了一個item全集。對於每個user來說,我們把他有過行為的item稱為正樣本,規定興趣度Rui=1,此外我們還需要從item全集中隨機抽樣,選取與正樣本數量相當的樣本作為負樣本,規定興趣度為Rui=0。因此,興趣的取值範圍為[0,1]。
取樣之後原有的資料集得到擴充,得到一個新的user-item集K={(U,I)},其中如果(U,I)是正樣本,則Rui=1,否則Rui=0。損失函式如下所示:
上式中的
是用來防止過擬合的正則化項,λ需要根據具體應用場景反覆實驗得到。損失函式的優化使用隨機梯度下降演算法:
通過求引數Puk和Qki的偏導確定最快的下降方向;
迭代計算不斷優化引數(迭代次數事先人為設定),直到引數收斂。
其中,α是學習速率,α越大,迭代下降的越快。α和λ一樣,也需要根據實際的應用場景反覆實驗得到。在ratings資料集上進行實驗,取分類數F=100,α=0.02,λ=0.01。
綜上所述,執行LFM需要:根據資料集初始化P和Q矩陣,確定4個引數:隱類數F、迭代次數N、學習速率α、正則化引數λ。
2、基於原理,程式碼構建如下:
# -*- coding: utf-8 -*-
'''
Created on 2017年10月16日
@author: Administrator
'''
'''
實現隱語義模型,對隱式資料進行推薦
1.對正樣本生成負樣本
-負樣本數量相當於正樣本
-物品越熱門,越有可能成為負樣本
2.使用隨機梯度下降法,更新引數
'''
import numpy as np
import pandas as pd
from math import exp
import time
import math
from sklearn import cross_validation
import random
import operator
class LFM:
'''
初始化隱語義模型
引數:*data 訓練資料,要求為pandas的dataframe
*F 隱特徵的個數 *N 迭代次數 *alpha 隨機梯度下降的學習速率
*lamda 正則化引數 *ratio 負樣本/正樣本比例 *topk 推薦的前k個物品
'''
def __init__(self,data,ratio,F=5,N=2,alpha=0.02,lamda=0.01,topk=10):
self.data=data #樣本集
self.ratio =ratio #正負樣例比率,對效能最大影響
self.F = F#隱類數量,對效能有影響
self.N = N#迭代次數,收斂的最佳迭代次數未知
self.alpha =alpha#梯度下降步長
self.lamda = lamda#正則化引數
self.topk =topk #推薦top k項
'''
初始化物品池,物品池中物品出現的次數與其流行度成正比
{item1:次數,item2:次數,...}
'''
def InitItemPool(self):
itemPool=dict()
groups = self.data.groupby([1])
for item,group in groups:
itemPool.setdefault(item,0)
itemPool[item] =group.shape[0]
itemPool=dict(sorted(itemPool.items(), key = lambda x:x[1], reverse = True))
return itemPool
'''
獲取每個使用者對應的商品(使用者購買過的商品)列表,如
{使用者1:[商品A,商品B,商品C],
使用者2:[商品D,商品E,商品F]...}
'''
def user_item(self):
ui = dict()
groups = self.data.groupby([0])
for item,group in groups:
ui[item]=set(group.ix[:,1])
return ui
'''
初始化隱特徵對應的引數
numpy的array儲存引數,使用dict儲存每個使用者(物品)對應的列
'''
def initParam(self):
users=set(self.data.ix[:,0])
items=set(self.data.ix[:,1])
arrayp = np.random.rand(len(users), self.F) #構造p矩陣,[0,1]內隨機值
arrayq = np.random.rand(self.F, len(items)) #構造q矩陣,[0,1]內隨機值
P = pd.DataFrame(arrayp, columns=range(0, self.F), index=users)
Q = pd.DataFrame(arrayq, columns=items, index=range(0,self.F))
return P,Q
'''
self.Pdict=dict()
self.Qdict=dict()
for user in users:
self.Pdict[user]=len(self.Pdict)
for item in items:
self.Qdict[item]=len(self.Qdict)
self.P=np.random.rand(self.F,len(users))
self.Q=np.random.rand(self.F,len(items))
'''
'''
生成負樣本
'''
def RandSelectNegativeSamples(self,items):
ret=dict()
for item in items:
#所有正樣本評分為1
ret[item]=1
#負樣本個數,四捨五入
negtiveNum = int(round(len(items)*self.ratio))
N = 0
#while N<negtiveNum:
#item = self.itemPool[random.randint(0, len(self.itemPool) - 1)]
for item,count in self.itemPool.items():
if N>negtiveNum:
break
if item in items:
#如果在使用者已經喜歡的物品列表中,繼續選
continue
N+=1
#負樣本評分為0
ret[item]=0
return ret
def sigmod(self,x):
# 單位階躍函式,將興趣度限定在[0,1]範圍內
y = 1.0/(1+exp(-x))
return y
def lfmPredict(self,p, q, userID, itemID):
#利用引數p,q預測目標使用者對目標物品的興趣度
p = np.mat(p.ix[userID].values)
q = np.mat(q[itemID].values).T
r = (p * q).sum()
r = self.sigmod(r)
return r
'''
使用隨機梯度下降法,更新引數
'''
def stochasticGradientDecent(self,p,q):
alpha=self.alpha
for i in range(self.N):
for user,items in self.ui.items():
ret=self.RandSelectNegativeSamples(items)
for item,rui in ret.items():
eui = rui - self.lfmPredict(p,q, user, item)
for f in range(0, self.F):
#df[列][行]定位
tmp= alpha * (eui * q[item][f] - self.lamda * p[f][user])
q[item][f] += alpha * (eui * p[f][user] - self.lamda * q[item][f])
p[f][user] +=tmp
'''
p=self.P[:,self.Pdict[user]]
q=self.Q[:,self.Qdict[item]]
eui=rui-sum(p*q)
tmp=p+alpha*(eui*q-self.lamda*p)
self.Q[:,self.Qdict[item]]+=alpha*(eui*p-self.lamda*q)
self.P[:,self.Pdict[user]]=tmp
'''
alpha*=0.9
return p,q
def Train(self):
self.itemPool=self.InitItemPool()#生成物品的熱門度排行
self.ui = self.user_item()#生成使用者-物品
p,q=self.initParam()#生成p,q矩陣
self.P,self.Q=self.stochasticGradientDecent(p,q) #隨機梯度下降訓練
def Recommend(self,user):
items=self.ui[user]
predictList = [self.lfmPredict(self.P, self.Q, user, item) for item in items]
series = pd.Series(predictList, index=items)
series = series.sort_values(ascending=False)[:self.topk]
return series
'''
#items=self.ui[user]
p=self.P[:,self.Pdict[user]]
rank = dict()
for item,id in self.Qdict.items():
#if item in items:
# continue
q=self.Q[:,id];
rank[item]=sum(p*q)
#return sorted(rank.items(),lambda x,y:operator.gt(x[0],y[0]),reverse=True)[0:self.topk-1];
return sorted(rank.items(),key=operator.itemgetter(1),reverse=True)[0:self.topk-1];
'''
def recallAndPrecision(self,test):#召回率和準確率
userID=set(test.ix[:,0])
hit = 0
recall = 0
precision = 0
for userid in userID:
#trueItem = test[test.ix[:,0] == userid]
#trueItem= trueItem.ix[:,1]
trueItem=self.ui[userid]
preitem=self.Recommend(userid)
for item in list(preitem.index):
if item in trueItem:
hit += 1
recall += len(trueItem)
precision += len(preitem)
return (hit / (recall * 1.0),hit / (precision * 1.0))
def coverage(self,test):#覆蓋率
userID=set(test.ix[:,0])
recommend_items = set()
all_items = set()
for userid in userID:
#trueItem = test[test.ix[:,0] == userid]
#trueItem= trueItem.ix[:,1]
trueItem=self.ui[userid]
for item in trueItem:
all_items.add(item)
preitem=self.Recommend(userid)
for item in list(preitem.index):
recommend_items.add(item)
return len(recommend_items) / (len(all_items) * 1.0)
def popularity(self,test):#流行度
userID=set(test.ix[:,0])
ret = 0
n = 0
for userid in userID:
preitem=self.Recommend(userid)
for item in list(preitem.index):
ret += math.log(1+self.itemPool[item])
n += 1
return ret / (n * 1.0)
if __name__ == "__main__":
start = time.clock()
#匯入資料
data=pd.read_csv('D:\\dev\\workspace\\PyRecSys\\demo\\ratings.csv',nrows=10000,header=None)
data=data.drop(0)
data=data.ix[:,0:1]
train,test=cross_validation.train_test_split(data,test_size=0.2)
train = pd.DataFrame(train)
test = pd.DataFrame(test)
print ("%3s%20s%20s%20s%20s" % ('ratio',"recall",'precision','coverage','popularity'))
for ratio in [1,2,3,5]:
lfm = LFM(data,ratio)
lfm.Train()
#rank=lfm.Recommend('1')
#print (rank)
recall,precision = lfm.recallAndPrecision(test)
coverage =lfm.coverage(test)
popularity =lfm.popularity(test)
print ("%3d%19.3f%%%19.3f%%%19.3f%%%20.3f" % (ratio,recall * 100,precision * 100,coverage * 100,popularity))
end = time.clock()
print('finish all in %s' % str(end - start))
執行後的指標:主要觀察ratio比例對結果的影響
ratio recall precision coverage popularity
1 7.001% 100.000% 12.976% 2.003
2 7.001% 100.000% 9.663% 2.412
3 7.001% 100.000% 7.869% 2.627
5 7.001% 100.000% 5.356% 2.809
finish all in 702.2413190975064