推荐算法之LFM模型及python

本文转自:推荐系统之隐语义模型(LFM)

一 基本概念

LFM(latent factor model)隐语义模型,这也是在推荐系统中应用相当普遍的一种模型。那这种模型跟ItemCF或UserCF有什么不同呢?这里可以做一个对比:

对于UserCF,我们可以先计算和目标用户兴趣相似的用户,之后再根据计算出来的用户喜欢的物品给目标用户推荐物品。

而ItemCF,我们可以根据目标用户喜欢的物品,寻找和这些物品相似的物品,再推荐给用户。

我们还有一种方法,先对所有的物品进行分类,再根据用户的兴趣分类给用户推荐该分类中的物品,LFM就是用来实现这种方法。

如果要实现最后一种方法,需要解决以下的问题:

(1)给物品分类

(2)确定用户兴趣属于哪些类及感兴趣程度

(3)对于用户感兴趣的类,如何推荐物品给用户

对分类,很容易想到人工对物品进行分类,但是人工分类是一种很主观的事情,比如一部电影用户可能因为这是喜剧片去看了,但也可能因为他是周星驰主演的看了,也有可能因为这是一部属于西游类型的电影,不同的人可以得到不同的分类。

而且对于物品分类的粒度很难控制,究竟需要把物品细分到个程度,比如一本线性代数,可以分类到数学中,也可以分类到高等数学,甚至根据线性代数主要适用的领域再一次细分,但对于非专业领域的人来说,想要对这样的物品进行小粒度细分无疑是一件费力不讨好的事情。

而且一个物品属于某个类,但是这个物品相比其他物品,是否更加符合这个类呢?这也是很难人工确定的事情。

对于上述需要解决的问题,我们的隐语义模型就派上用场了。隐语义模型,可以基于用户的行为自动进行聚类,并且这个类的数量,即粒度完全由可控。

对于某个物品是否属与一个类,完全由用户的行为确定,我们假设两个物品同时被许多用户喜欢,那么这两个物品就有很大的几率属于同一个类。

而某个物品在类所占的权重,也完全可以由计算得出。

以下公式便是隐语义模型计算用户u对物品i兴趣的公式:

其中,p为用户兴趣和第k个隐类的关系,q为第k个隐类和物品i的关系,F为隐类的数量,r便是用户对物品的兴趣度。

接下的问题便是如何计算这两个参数p和q了,对于这种线性模型的计算方法,这里使用的是梯度下降法,详细的推导过程可以看一下我的另一篇博客。大概的思路便是使用一个数据集,包括用户喜欢的物品和不喜欢的物品,根据这个数据集来计算p和q。

下面给出公式,对于正样本,我们规定r=1,负样本r=0:

后面的lambda是为了防止过拟合的正则化项,下面给出python代码。

二 实战

我们这里依旧使用movielen的1M数据集

1 首先我们需要计算包含用户喜欢与不喜欢物品的数据集,采用不计算评分的隐反馈方式,只要用户评过分均认为用户对该物品有兴趣,而没有评分则可能没兴趣。

(1)用户正反馈数据


 
 
  1. def getUserPositiveItem(frame, userID):
  2. '''
  3. 获取用户正反馈物品:用户评分过的物品
  4. :param frame: ratings数据
  5. :param userID: 用户ID
  6. :return: 正反馈物品
  7. '''
  8. series = frame[frame[ 'UserID'] == userID][ 'MovieID']
  9. positiveItemList = list(series.values)
  10. return positiveItemList
(2)用户负反馈数据,根据用户无评分物品进行推荐,越热门的物品用户却没有进行过评分,认为用户越有可能对这物品没有兴趣


 
 
  1. def getUserNegativeItem(frame, userID):
  2. '''
  3. 获取用户负反馈物品:热门但是用户没有进行过评分 与正反馈数量相等
  4. :param frame: ratings数据
  5. :param userID:用户ID
  6. :return: 负反馈物品
  7. '''
  8. userItemlist = list(set(frame[frame[ 'UserID'] == userID][ 'MovieID'])) #用户评分过的物品
  9. otherItemList = [item for item in set(frame[ 'MovieID'].values) if item not in userItemlist] #用户没有评分的物品
  10. itemCount = [len(frame[frame[ 'MovieID'] == item][ 'UserID']) for item in otherItemList] #物品热门程度
  11. series = pd.Series(itemCount, index=otherItemList)
  12. series = series.sort_values(ascending= False)[:len(userItemlist)] #获取正反馈物品数量的负反馈物品
  13. negativeItemList = list(series.index)
  14. return negativeItemList
2 接下来是初始化参数p和q,这里我们采用随机初始化的方式,将p和q取值在[0,1]之间:


 
 
  1. def initPara(userID, itemID, classCount):
  2. '''
  3. 初始化参数q,p矩阵, 随机
  4. :param userCount:用户ID
  5. :param itemCount:物品ID
  6. :param classCount: 隐类数量
  7. :return: 参数p,q
  8. '''
  9. arrayp = np.random.rand(len(userID), classCount)
  10. arrayq = np.random.rand(classCount, len(itemID))
  11. p = pd.DataFrame(arrayp, columns=range( 0,classCount), index=userID)
  12. q = pd.DataFrame(arrayq, columns=itemID, index=range( 0,classCount))
  13. return p,q
3 定义函数计算用户对物品的兴趣


 
 
  1. def lfmPredict(p, q, userID, itemID):
  2. '''
  3. 利用参数p,q预测目标用户对目标物品的兴趣度
  4. :param p: 用户兴趣和隐类的关系
  5. :param q: 隐类和物品的关系
  6. :param userID: 目标用户
  7. :param itemID: 目标物品
  8. :return: 预测兴趣度
  9. '''
  10. p = np.mat(p.ix[userID].values)
  11. q = np.mat(q[itemID].values).T
  12. r = (p * q).sum()
  13. r = sigmod(r)
  14. return r
  15. def sigmod(x):
  16. '''
  17. 单位阶跃函数,将兴趣度限定在[0,1]范围内
  18. :param x: 兴趣度
  19. :return: 兴趣度
  20. '''
  21. y = 1.0/( 1+exp(-x))
  22. return y
4 隐语义模型,利用梯度下降迭代计算参数p和q

 
 
  1. def latenFactorModel(frame, classCount, iterCount, alpha, lamda):
  2. '''
  3. 隐语义模型计算参数p,q
  4. :param frame: 源数据
  5. :param classCount: 隐类数量
  6. :param iterCount: 迭代次数
  7. :param alpha: 步长
  8. :param lamda: 正则化参数
  9. :return: 参数p,q
  10. '''
  11. p, q, userItem = initModel(frame, classCount)
  12. for step in range( 0, iterCount):
  13. for user in userItem:
  14. for userID, samples in user.items():
  15. for itemID, rui in samples.items():
  16. eui = rui - lfmPredict(p, q, userID, itemID)
  17. for f in range( 0, classCount):
  18. print( 'step %d user %d class %d' % (step, userID, f))
  19. p[f][userID] += alpha * (eui * q[itemID][f] - lamda * p[f][userID])
  20. q[itemID][f] += alpha * (eui * p[f][userID] - lamda * q[itemID][f])
  21. alpha *= 0.9
  22. return p, q
5 最后根据计算出来的p和q参数对用户进行物品的推荐


 
 
  1. def recommend(frame, userID, p, q, TopN=10):
  2. '''
  3. 推荐TopN个物品给目标用户
  4. :param frame: 源数据
  5. :param userID: 目标用户
  6. :param p: 用户兴趣和隐类的关系
  7. :param q: 隐类和物品的关系
  8. :param TopN: 推荐数量
  9. :return: 推荐物品
  10. '''
  11. userItemlist = list(set(frame[frame[ 'UserID'] == userID][ 'MovieID']))
  12. otherItemList = [item for item in set(frame[ 'MovieID'].values) if item not in userItemlist]
  13. predictList = [lfmPredict(p, q, userID, itemID) for itemID in otherItemList]
  14. series = pd.Series(predictList, index=otherItemList)
  15. series = series.sort_values(ascending= False)[:TopN]
  16. return series
# coding: utf-8 -*-
import random
import pickle
import pandas as pd
import numpy as np
from math import exp


class Corpus:

    items_dict_path = 'data/lfm_items.dict'

    @classmethod
    def pre_process(cls):
        file_path = 'data/ratings.csv'
        cls.frame = pd.read_csv(file_path)
        cls.user_ids = set(cls.frame['UserID'].values)
        cls.item_ids = set(cls.frame['MovieID'].values)
        cls.items_dict = {user_id: cls._get_pos_neg_item(user_id) for user_id in list(cls.user_ids)}
        cls.save()

    @classmethod
    def _get_pos_neg_item(cls, user_id):
        """
        Define the pos and neg item for user.
        pos_item mean items that user have rating, and neg_item can be items
        that user never see before.
        Simple down sample method to solve unbalance sample.
        """
        print('Process: {}'.format(user_id))
        pos_item_ids = set(cls.frame[cls.frame['UserID'] == user_id]['MovieID'])
        neg_item_ids = cls.item_ids ^ pos_item_ids
        # neg_item_ids = [(item_id, len(self.frame[self.frame['MovieID'] == item_id]['UserID'])) for item_id in neg_item_ids]
        # neg_item_ids = sorted(neg_item_ids, key=lambda x: x[1], reverse=True)
        neg_item_ids = list(neg_item_ids)[:len(pos_item_ids)]
        item_dict = {}
        for item in pos_item_ids: item_dict[item] = 1
        for item in neg_item_ids: item_dict[item] = 0
        return item_dict

    @classmethod
    def save(cls):
        f = open(cls.items_dict_path, 'wb')
        pickle.dump(cls.items_dict, f)
        f.close()

    @classmethod
    def load(cls):
        f = open(cls.items_dict_path, 'rb')
        items_dict = pickle.load(f)
        f.close()
        return items_dict


class LFM:

    def __init__(self):
        self.class_count = 5
        self.iter_count = 5
        self.lr = 0.02
        self.lam = 0.01
        self._init_model()

    def _init_model(self):
        """
        Get corpus and initialize model params.
        """
        file_path = 'data/ratings.csv'
        self.frame = pd.read_csv(file_path)
        self.user_ids = set(self.frame['UserID'].values)
        self.item_ids = set(self.frame['MovieID'].values)
        self.items_dict = Corpus.load()

        array_p = np.random.randn(len(self.user_ids), self.class_count)
        array_q = np.random.randn(len(self.item_ids), self.class_count)
        self.p = pd.DataFrame(array_p, columns=range(0, self.class_count), index=list(self.user_ids))
        self.q = pd.DataFrame(array_q, columns=range(0, self.class_count), index=list(self.item_ids))

    def _predict(self, user_id, item_id):
        """
        Calculate interest between user_id and item_id.
        p is the look-up-table for user's interest of each class.
        q means the probability of each item being classified as each class.
        """
        p = np.mat(self.p.ix[user_id].values)
        q = np.mat(self.q.ix[item_id].values).T
        r = (p * q).sum()
        logit = 1.0 / (1 + exp(-r))
        return logit

    def _loss(self, user_id, item_id, y, step):
        """
        Loss Function define as MSE, the code write here not that formula you think.
        """
        e = y - self._predict(user_id, item_id)
        print('Step: {}, user_id: {}, item_id: {}, y: {}, loss: {}'.
              format(step, user_id, item_id, y, e))
        return e

    def _optimize(self, user_id, item_id, e):
        """
        Use SGD as optimizer, with L2 p, q square regular.
        e.g: E = 1/2 * (y - predict)^2, predict = matrix_p * matrix_q
             derivation(E, p) = -matrix_q*(y - predict), derivation(E, q) = -matrix_p*(y - predict),
             derivation(l2_square,p) = lam * p, derivation(l2_square, q) = lam * q
             delta_p = lr * (derivation(E, p) + derivation(l2_square,p))
             delta_q = lr * (derivation(E, q) + derivation(l2_square, q))
        """
        gradient_p = -e * self.q.ix[item_id].values
        l2_p = self.lam * self.p.ix[user_id].values
        delta_p = self.lr * (gradient_p + l2_p)

        gradient_q = -e * self.p.ix[user_id].values
        l2_q = self.lam * self.q.ix[item_id].values
        delta_q = self.lr * (gradient_q + l2_q)

        self.p.loc[user_id] -= delta_p
        self.q.loc[item_id] -= delta_q

    def train(self):
        """
        Train model.
        """
        for step in range(0, self.iter_count):
            for user_id, item_dict in self.items_dict.items():
                item_ids = list(item_dict.keys())
                random.shuffle(item_ids)
                for item_id in item_ids:
                    e = self._loss(user_id, item_id, item_dict[item_id], step)
                    self._optimize(user_id, item_id, e)
            self.lr *= 0.9
        self.save()

    def predict(self, user_id, top_n=10):
        """
        Calculate all item user have not meet before and return the top n interest items.
        """
        self.load()
        user_item_ids = set(self.frame[self.frame['UserID'] == user_id]['MovieID'])
        other_item_ids = self.item_ids ^ user_item_ids
        interest_list = [self._predict(user_id, item_id) for item_id in other_item_ids]
        candidates = sorted(zip(list(other_item_ids), interest_list), key=lambda x: x[1], reverse=True)
        return candidates[:top_n]

    def save(self):
        """
        Save model params.
        """
        f = open('data/lfm.model', 'wb')
        pickle.dump((self.p, self.q), f)
        f.close()

    def load(self):
        """
        Load model params.
        """
        f = open('data/lfm.model', 'rb')
        self.p, self.q = pickle.load(f)
        f.close()

隐语义模型介绍就到这里了,完整的项目代码可以到我的个人github上面查看:https://github.com/lpty



评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值