推薦系統-隱因子模型(LFM)

今天我們來聊一聊LFM(Latent Factor Model)的故事,這也算是我們在推薦系統裏第一個用到的學習算法了吧,前面講的兩個協同過濾都是基於統計來的。

協同過濾的思路就是基於用戶和物品的交互行爲,要麼計算用戶間的相似度,推薦相似度高的用戶喜歡的物品,因爲這兩個用戶可能興趣相投;要麼就是計算物品間的相似度,推薦和歷史記錄相似度很高的物品,因爲他們可能屬於同一類別的商品。我們做決策的基礎都是默認了商品是有類別的,可能有的用戶都喜歡某一類商品,所以這些用戶之間相似度高,可能有的商品是屬於同一類別的,因此這些商品的相似度很高。那既然這樣,有沒有可能直接得到商品的類別呢?這樣我們就可以直接根據類別去進行推薦了~

LFM就是基於這樣的想法,假設商品存在若干個種類,那麼每個用戶對每個類會有一個興趣度,同樣的,每個類內又有若干種商品,每個商品在這個類內又會有一個對應的權重。這樣,對於任何一個用戶-商品對,我們都可以用下述公式來表達

OK,那麼下一步就是去計算矩陣P和Q,最常見的方法就是在訓練集上不停迭代更新參數直至參數收斂。既然需要迭代,那我們得有一個損失函數或者目標函數作爲我們迭代的依據,這裏我們選用的是最常見的預測值和實際值差值的平方和,並且加上了正則項防止過擬合,具體如下

熟悉機器學習的同學應該很清楚,接下來就是基於梯度下降去優化這個損失函數,這裏就不贅述了。但是我們還有一個問題,對於顯式反饋的數據,我們直接用評分數據作爲訓練集,但是對於隱式反饋而言,只有正樣本,我們可以把這些數據標註爲1,但同時我們也需要負樣本,所以往往需要我們從所有樣本里選擇樣本構建負樣本集。這裏在構建負樣本的時候有一個小小的trick,要儘量選擇那些非常熱門但是用戶卻沒有發生交互的物品,因爲這樣構建的樣本往往更具有代表性,也有利於我們的training。在movielens數據集中,我們以樣本出現的次數作爲權重,隨機選擇樣本構建負樣本集,實現如下

def Random_Negative_Sampling(self, items):
        ret = {}
        for i in items:
            ret[i] = 1
        count = 0
        for i in range(0, len(items)*5):
            item = self.items_pool[random.randint(0, len(self.items_pool)-1)]
            if item in items:
                continue
            ret[item] = 0
            count += 1
            if count > 2*len(items):
                break
        return ret

在實際實現LFM算法的過程中,自己遇到了一個大坑,提出來希望大家注意。

先問問你,如何去計算一個用戶對一個物品的喜愛程度?是不是直接用上面那個公式,用用戶對類的喜愛程度和該物品在此類中的權重乘積的累加呀。那麼恭喜你,如果這樣做,效果非常不好,一是太難收斂,二是預測效果總是不好。爲啥呢?實際預測的矩陣裏只有0和1兩個值,而Preference的計算則是一個實數,在梯度下降的時候要用到這兩者的差值,而這個值變動的範圍太大,一方面導致你基本很難收斂,另一方面超參數的選擇是個非常頭疼的問題。所以,在計算喜好度的時候,一定要給結果

加個sigmoid!!!

加個sigmoid!!!

加個sigmoid!!!

這樣把你的結果約束到0和1之間,防止在計算誤差的時候出現太大的波動從而影響收斂。這裏我在movielen的數據集上也進行了實驗,《推薦系統實戰》那本書裏選擇的隱類個數爲100,本來是想復現他的結果,不過我發現算起來實在是太慢了,所以我這裏選擇隱類F=10,學習率alpha=0.03,正則項稀疏lambda=0.01,在所有的用戶上迭代了30輪次,最後在測試集上的表現如下

召回率6.91% 精確率 23.2% 覆蓋率 40.8% 流行度 5.20。

按照常理和書上所說,基於學習的這種方法的表現應該比基於統計的協同過濾表現要好點,但是這裏的結果明顯比之前的最好結果要差些,主要原因可能是這裏的隱類選的太少了,書中的實驗實際選擇的隱類數目爲100。但我發現隱類100的時候不僅每一輪計算的速度大大提升,並且收斂的速度也遠遠慢於隱類數目較少的時候,這也很容易理解,畢竟對應的需要優化的參數數目多了10倍,因此需要的訓練樣本也要更多,收斂自然更慢,有興趣的或者有時間的同學可以去嘗試一下。

然後在整個實現LFM的過程中,還有其他幾個小地方大家可以注意一下

  1. 參數的初始化。所有參數初始化爲0好像不是一個好選擇,總是無法收斂。一般的做法都是選擇在-1到1之間的隨機數。
  2. 構建負樣本和正樣本的比例。書中說這是一個影響模型效果很重要的參數,實際上可以這麼理解,相同輪次的迭代,負樣本比例越高,其實越多的訓練數據,自然模型的表達能力可能會更好。
  3. 超參數的選擇。在實際使用的過程中,我發現超參數的選擇對於LFM模型的表現真實至關重要,學習率過高直接導致沒法收斂,但是太低了收斂的速度又太慢了,所以超參數的選擇真的是一個非常頭疼的事。
  4. LFM的計算速度是真的很慢。每一輪迭代,要在每個用戶上迭代其所有的行爲記錄和我們構造的負樣本,導致計算真的非常耗時。而隱類個數的增加不僅導致需要優化的參數增加,對於數據的需求同樣提高,這樣直接導致收斂速度變得非常非常慢。

===============================================================================================最後是對LFM算法的一些思考,它的本質其實就是矩陣分解,將之前的用戶物品矩陣分解成了用戶-隱類矩陣和隱類-物品矩陣,然後目標是讓這兩個矩陣的乘積與原矩陣的殘差儘可能得小,同時引入了一些正則項防止過擬合。後來我發現其實LFM算法就是FunkSVD算法,是典型的基於矩陣分解的方法,後來又出現了基於它進行改進的方法。

BiasSVD。在FunkSVD的基礎上又引入了平均得分、用戶偏置項和物品偏置項,相比於FunkSVD又多了兩個需要優化的參數,但優化方法啥的都是一樣的。事實表明,由於考慮了這些偏置項,令BiasSVD在某些場景下變現會優異很多。

SVD++。在BiasSVD的基礎上又引入了用戶的隱式反饋,也就是用戶之前如果對該物品產生過交互,則對它的評分要進行修正。我的媽呀,越來越複雜,算得也是越來越慢,之前在知乎上看到說雖然SVD++之前在比賽裏大放異彩,但實際好像根本沒人用……計算代價太大,沒法實時推薦,並且可解釋性也不強,真是扎心了。

OK,LFM就聊到這裏,下一章我們講Item2Vec,終於要到深度部分了啊,蛤蛤蛤~

===============================================================================================

剛剛發佈了一下發現內容有點短啊,那我把LFM的完整代碼發出來吧,希望對大家有用

class LFM():
    def __init__(self, data, F, seed):
        self.train, self.test = self.train_test_split(data, seed)
        self.F = F
        self.items_pool, self.all_items, self.all_users = self.Helper()
        self.item_popularity=self.Item_popularity()
        self.P, self.Q = self.Initial_LFM_Par()

    def train_test_split(self, data, seed):
        train_set = {}
        test_set = {}
        for user, movies in data.groupby('user_id'):
            movies = movies.sample(
                frac=1, random_state=seed).reset_index(drop=True)
            train = movies[:int(0.8*len(movies))]
            test = movies[int(0.8*len(movies)):]
            train_set[user] = set(train['movies_id'].tolist())
            test_set[user] = set(test['movies_id'].tolist())
        print('Data preparation finished')
        return train_set, test_set

    def data_update(self, data, seed):
        self.train, self.test = self.train_test_split(data, seed)

    def Helper(self):
        items_pool = []
        all_items = set()
        all_users = set()
        for u, items in self.train.items():
            all_users.add(u)
            for i in items:
                items_pool.append(i)
                all_items.add(i)
        return items_pool, all_items, all_users
    
    def Item_popularity(self):
        item_popularity = {}
        for item in self.items_pool:
            if item not in item_popularity.keys():
                item_popularity[item] = 0
            item_popularity[item] += 1
        return item_popularity

    def Random_Negative_Sampling(self, items):
        ret = {}
        for i in items:
            ret[i] = 1
        count = 0
        for i in range(0, len(items)*5):
            item = self.items_pool[random.randint(0, len(self.items_pool)-1)]
            if item in items:
                continue
            ret[item] = 0
            count += 1
            if count > 2*len(items):
                break
        return ret

    def Initial_LFM_Par(self):
        P = {}
        Q = {}
        for user in self.all_users:
            P[user] = {}
            for k in range(self.F):
                P[user][k] = random.random()
        for k in range(self.F):
            Q[k] = {}
            for item in self.all_items:
                Q[k][item] = random.random()
        return P, Q

    def Predict(self, user, item):
        pre = sum(self.P[user][k]*self.Q[k][item] for k in range(self.F))
        return 1.0/(1+np.exp(-pre))
    
    def Generate_sample(self):
        sample={}
        for user, items in self.train.items():     
            sample[user] = self.Random_Negative_Sampling(items) 
        return sample
    
    def Parameter_Update2(self, N_step, alpha, lam, N):
        sample=self.Generate_sample()
        for step in range(N_step):
            print('Now step %i' % step)
            for user in self.all_users:                     
                for item, rui in sample[user].items():
                    eui = rui-self.Predict(user, item)
                    for k in range(self.F):
                        self.P[user][k] += (eui*self.Q[k]
                                            [item]-lam*self.P[user][k])*alpha
                        self.Q[k][item] += (eui*self.P[user]
                                            [k]-lam*self.Q[k][item])*alpha           
            if (step+1)%10==0:
                recall, precision, coverage, popularity=self.eval_fun(N)
            alpha *= 0.95

    def Parameter_Update(self, N_step, alpha, lam, N):
         for step in range(N_step):
            print('Now step %i' % step)
            for user, items in self.train.items():     
                sample = self.Random_Negative_Sampling(items)                    
                for item, rui in sample.items():
                    eui = rui-self.Predict(user, item)
                    for k in range(self.F):
                        self.P[user][k] += (eui*self.Q[k]
                                            [item]-lam*self.P[user][k])*alpha
                        self.Q[k][item] += (eui*self.P[user]
                                            [k]-lam*self.Q[k][item])*alpha           
            if (step+1)%10==0:
                recall, precision, coverage, popularity=self.eval_fun(N)
            alpha *= 0.95


    def Recommend(self, user):
        rank = {}
        already_items = self.train[user]
        for item in self.all_items:
            if item in already_items:
                continue
            rank[item]=self.Predict(user,item)
        return rank

    def Get_Recommendation(self, user,N):
        rank = self.Recommend(user)
        recommend_list = []
        for i, score in sorted(rank.items(), key=itemgetter(1), reverse=True)[:N]:
            recommend_list.append(i)
        return recommend_list

    def eval_fun(self,N):
        hit = 0
        all_recall = 0
        all_precision = 0
        recommend_items = set()
        ret = 0
        n = 0
        for user in self.all_users:
            tu = self.test[user]
            recommend_list = self.Get_Recommendation(user,N)
            for item in recommend_list:
                recommend_items.add(item)
                ret += np.log(1+self.item_popularity[item])
                n += 1
                if item in tu:
                    hit += 1
            all_recall += len(tu)
            all_precision += N
        print(hit)
        recall = hit/(all_recall*1.0)
        precision = hit/(all_precision*1.0)
        coverage = len(recommend_items)/(len(self.all_items)*1.0)
        popularity = ret/(n*1.0)
        print(recall, precision, coverage, popularity)
        return recall, precision, coverage, popularity

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章