项目实训记录

LFM的代码实现:
发现使用LFM后,

①:我们不需要关心分类的角度,结果都是基于用户行为统计自动聚类的,全凭数据自己说了算。
②:不需要关心分类粒度的问题,通过设置LFM的最终分类数就可控制粒度,分类数越大,粒度约细。
③: 对于一个item,并不是明确的划分到某一类,而是计算其属于每一类的概率,是一种标准的软分类。
④:对于一个user,我们可以得到他对于每一类的兴趣度,而不是只关心可见列表中的那几个类。
⑤:对于每一个class,我们可以得到类中每个item的权重,越能代表这个类的item,权重越高。

import os
import time

import pandas as pd
import pickle
import numpy as np
import math
import random

class Corpus:
    # 数据字典位置
    items_dict_path = '../data/lfm_items.dict'

    @classmethod
    def pre_process(cls):
        '''
        预处理数据,需要存在ratings.csv文件
        获得所有的用户id
        获得所有的商品id
        获得所有的商品字典{userid1:{itemid1:1,itemid2:0},...}
        :return:
        '''
        print('Process 语料生成')
        file_path = '../data/ratings.csv'
        # 文件数据
        cls.frame = pd.read_csv(file_path)
        # 所有的用户id
        cls.user_ids = set(cls.frame['UserID'])
        # 所有的商品id
        cls.item_ids = set(cls.frame['MovieID'])
        # 所有的商品字典,存储形式为{user_id:{item_id1:0,item_id2:1}}
        cls.items_dict = {user_id: cls._get_pos_neg_item(user_id) for user_id in list(cls.user_ids)}
        # 保存商品字典
        cls.save()
        print('Process 语料生成完毕->', cls.items_dict_path)

    @classmethod
    def save(cls):
        '''
        保存items_dict到文件
        :return:
        '''
        f = open(cls.items_dict_path, 'wb')
        pickle.dump(cls.items_dict, f)
        f.close()

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


    @classmethod
    def _get_pos_neg_item(cls, user_id):
        '''
        获得等量的用户喜欢的商品和不喜欢的商品
        :param user_id:
        :return:
        '''
        pos_items_ids = set(cls.frame[cls.frame['UserID']
                                     == user_id]['MovieID'])
        neg_items_ids = cls.item_ids ^ pos_items_ids
        neg_items_ids = list(neg_items_ids)[:len(pos_items_ids)]
        item_dict = {}
        for i in pos_items_ids:
            item_dict[i]=1
        for i in neg_items_ids:
            item_dict[i]=0

        return item_dict


class LFM:

    # 模型矩阵位置
    model_path = '../data/lfm.model'

    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):
        '''
        获得语料,初始化隐类矩阵
        :return:
        '''
        file_path = '../data/ratings.csv'
        self.frame = pd.read_csv(file_path)
        self.user_ids = set(self.frame['UserID'])
        self.item_ids = set(self.frame['MovieID'])
        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)

        # 带索引的隐类矩阵,行是user_id和item_id
        self.p = pd.DataFrame(array_p, columns=range(self.class_count),
                              index=list(self.user_ids))
        self.q = pd.DataFrame(array_q, columns=range(self.class_count),
                              index=list(self.item_ids))

    def save(self):
        '''
        保存模型参数
        :return:
        '''
        f = open(self.model_path, 'wb')
        pickle.dump(self.p, self.q, f)
        f.close()


    def load(self):
        '''
        加载模型参数
        :return:
        '''
        f = open(self.model_path, 'rb')
        self.p, self.q = pickle.load(f)
        f.close()

    def _predict(self, user_id, item_id):
        '''
        计算用户对商品的兴趣
        :param user_id:
        :param item_id:
        :return:
        '''
        p = np.mat(self.p.loc[user_id].values)
        q = np.mat(self.q.loc[item_id].values).T
        r = sum(p*q)
        logit = 1/(1+math.exp(-r))
        return logit


    def predict(self, user_id, top_n=20):
        '''
        给用户推荐
        :param user_id:
        :param top_n:
        :return:
        '''
        # 加载参数矩阵
        self.load()
        # print(self.p)
        # print(self.q)
        # 获得用户看过的电影
        pos_items_ids = set(self.frame[self.frame['UserID']
                                      == user_id]['MovieID'])
        # 获得用户没有看过的电影
        neg_items_ids = self.item_ids ^ pos_items_ids
        # 兴趣得分列表
        interest_list = []
        for i in neg_items_ids:
            interest_list.append(self._predict(user_id, i))
        # 排序
        candidates = sorted(zip(list(neg_items_ids), interest_list),
                            key=lambda x: x[1], reverse=True)
        # 返回前top_n个
        return candidates[:top_n]

    def _loss(self, user_id, item_id, y, step):
        '''
        实际的损失值
        :param user_id:
        :param item_id:
        :param y: 实际的喜好
        :param step: 迭代到第几步
        :return:
        '''
        e = y - self._predict(user_id, item_id)
        print('setp', step, 'user_id', user_id, 'item_id', item_id,
              '实际值', y, 'loss', e)
        return e

    def _optimize(self, user_id, item_id, e):
        '''
        使用梯度下降的方法对模型进行优化
        已知用户对商品的偏差值,调整模型
        方法如下,调整第p行和第q行:
        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))
        :param user_id:
        :param item_id:
        :param e:
        :return:
        '''
        # 梯度
        gradient_p = -e * self.q.loc[item_id].values
        gradient_q = -e * self.p.loc[user_id].values
        # 正则项
        l2_p = self.lam*self.q.loc[user_id].values
        l2_q = self.lam*self.p.loc[item_id].values
        # 乘以学习率
        delta_p = self.lr*(gradient_p + l2_p)
        delta_q = self.lr*(gradient_q + l2_q)
        # 调整
        self.p -= delta_p
        self.q -= delta_q

    def train(self):
        '''
        训练模型,定期完成
        :return:
        '''
        # 迭代iter_count次
        for step in range(0, self.iter_count):
            # 遍历,字典中的每一key,value,
            for user_id, item_dict in self.items_dict.items():
                item_ids = list(item_dict.keys())
                # 随机打乱
                random.shuffle(item_ids)
                # 遍历每一个user_id对应的item_id
                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 run_(l,userid):
    print('Start..')
    start = time.time()
    if not os.path.exists('../data/lfm_items.dict'):
        Corpus.pre_process()
    if not os.path.exists('../data/lfm.model'):
        LFM().train()
    movies = LFM().predict(user_id=userid)
    for movie in movies:
        l.append(movie[0])
    print('Cost time: %f' % (time.time() - start))

if __name__ == '__main__':
    l=[]
    run_(l,15)
    print(l)

上一篇:vue连个数组对比


下一篇:mysql中删除字段中逗号隔开的某个值