1. 程式人生 > >【知識發現】隱語義模型LFM演算法python實現(三)

【知識發現】隱語義模型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

表示的是user i對class j的興趣度;Q矩陣式class-item矩陣,矩陣值Qij表示的是item j在class i中的權重,權重越高越能作為該類的代表。所以LFM根據如下公式來計算使用者U對物品I的興趣度:

使用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