基于回归模型的协同过滤推荐

日萌社

人工智能AI:Keras PyTorch MXNet TensorFlow PaddlePaddle 深度学习实战(不定时更新)


Model-Based 协同过滤算法

随着机器学习技术的逐渐发展与完善,推荐系统也逐渐运用机器学习的思想来进行推荐。将机器学习应用到推荐系统中的方案真是不胜枚举。以下对Model-Based CF算法做一个大致的分类:

  • 基于分类算法、回归算法、聚类算法
  • 基于矩阵分解的推荐
  • 基于神经网络算法
  • 基于图模型算法

接下来我们重点学习以下几种应用较多的方案:

  • 基于回归模型的协同过滤推荐
  • 基于矩阵分解的协同过滤推荐

函数求导:


基于回归模型的协同过滤推荐

如果我们将评分看作是一个连续的值而不是离散的值,那么就可以借助线性回归思想来预测目标用户对某物品的评分。其中一种实现策略被称为Baseline(基准预测)。

Baseline:基准预测

Baseline设计思想基于以下的假设:

  • 有些用户的评分普遍高于其他用户,有些用户的评分普遍低于其他用户。比如有些用户天生愿意给别人好评,心慈手软,比较好说话,而有的人就比较苛刻,总是评分不超过3分(5分满分)
  • 一些物品的评分普遍高于其他物品,一些物品的评分普遍低于其他物品。比如一些物品一被生产便决定了它的地位,有的比较受人们欢迎,有的则被人嫌弃。

这个用户或物品普遍高于或低于平均值的差值,我们称为偏置(bias)

Baseline目标:

  • 找出每个用户普遍高于或低于他人的偏置值bu
  • 找出每件物品普遍高于或低于其他物品的偏置值
  • 我们的目标也就转化为寻找最优的

使用Baseline的算法思想预测评分的步骤如下:

  • 计算所有电影的平均评分(即全局平均评分)

  • 计算每个用户评分与平均评分

  • 计算每部电影所接受的评分与平均评分

  • 预测用户对电影的评分:

  • 举例:通过Baseline来预测用户A对电影“阿甘正传”的评分

    • 首先计算出整个评分数据集的平均评分是3.5分
    • 用户A比较苛刻,普遍比平均评分低0.5分,即用户A的偏置值是-0.5;
    • “阿甘正传”比较热门且备受好评,评分普遍比平均评分要高1.2分,“阿甘正传”的偏置是+1.2
    • 因此就可以预测出用户A对电影“阿甘正传”的评分为:​,也就是4.2分。

对于所有电影的平均评分是直接能计算出的,因此问题在于要测出每个用户的评分偏置和每部电影的得分偏置。对于线性回归问题,我们可以利用平方差构建损失函数如下:

加入L2正则化: Cost=∑u,i∈R(rui−μ−bu−bi)2+λ∗(∑ubu2+∑ibi2) 公式解析: 公式第一部分∑u,i∈R(rui−μ−bu−bi)2是用来寻找与已知评分数据拟合最好的bu和bi​ 公式第二部分λ∗(∑ubu2+∑ibi2)​是正则化项,用于避免过拟合现象加入L2正则化: Cost=∑u,i∈R(rui−μ−bu−bi)2+λ∗(∑ubu2+∑ibi2) 公式解析: 公式第一部分∑u,i∈R(rui−μ−bu−bi)2是用来寻找与已知评分数据拟合最好的bu和bi​ 公式第二部分λ∗(∑ubu2+∑ibi2)​是正则化项,用于避免过拟合现象

对于最小过程的求解,我们一般采用随机梯度下降法或者交替最小二乘法来优化实现。

方法一:随机梯度下降法优化

使用随机梯度下降优化算法预测Baseline偏置值

step 1:梯度下降法推导

step 2:随机梯度下降

由于随机梯度下降法本质上利用每个样本的损失来更新参数,而不用每次求出全部的损失和,因此使用SGD时:

step 3:算法实现

  • tips pandas 版本不要过低 pandas 0.24.2

  • 数据加载

import pandas as pd
import numpy as np
dtype = [("userId", np.int32), ("movieId", np.int32), ("rating", np.float32)]
dataset = pd.read_csv("ml-latest-small/ratings.csv", usecols=range(3), dtype=dict(dtype))
# 用户评分数据  groupby 分组  groupby('userId') 根据用户id分组 agg(aggregation聚合)
users_ratings = dataset.groupby('userId').agg([list])
# 物品评分数据
items_ratings = dataset.groupby('movieId').agg([list])
# 计算全局平均分
global_mean = dataset['rating'].mean()
# 初始化bu bi
bu = dict(zip(users_ratings.index, np.zeros(len(users_ratings))))
bi = dict(zip(items_ratings.index, np.zeros(len(items_ratings))))
  • 关于zip

    • zip() 函数用于将可迭代的对象作为参数,将对象中对应的元素打包成一个个元组,然后返回由这些元组组成的对象,这样做的好处是节约了不少的内存。

      我们可以使用 list() 转换来输出列表。

      如果各个迭代器的元素个数不一致,则返回列表长度与最短的对象相同,利用 * 号操作符,可以将元组解压为列表。

    • 语法 zip([iterable, ...])

    • 示例:

    a = [1,2,3]
    b = [4,5,6]
    c = [4,5,6,7,8]
    zipped = zip(a,b)     # 返回一个对象
    >>> zipped
    <zip object at 0x103abc288>
    >>> list(zipped)  # list() 转换为列表
    [(1, 4), (2, 5), (3, 6)]
    >>> list(zip(a,c))              # 元素个数与最短的列表一致
    [(1, 4), (2, 5), (3, 6)]
    
    a1, a2 = zip(*zip(a,b))          # 与 zip 相反,zip(*) 可理解为解压,返回二维矩阵式
    >>> list(a1)
    [1, 2, 3]
    >>> list(a2)
    [4, 5, 6]
    
  • 更新bu bi
#number_epochs 迭代次数 alpha学习率  reg 正则化系数
for i in range(number_epochs):
    print("iter%d" % i)
    for uid, iid, real_rating in dataset.itertuples(index=False):
        error = real_rating - (global_mean + bu[uid] + bi[iid])
        bu[uid] += alpha * (error - reg * bu[uid])
        bi[iid] += alpha * (error - reg * bi[iid])
  • 预测评分
def predict(uid, iid):
    predict_rating = global_mean + bu[uid] + bi[iid]
    return predict_rating
  • 整体封装
import pandas as pd
import numpy as np


class BaselineCFBySGD(object):

    def __init__(self, number_epochs, alpha, reg, columns=["uid", "iid", "rating"]):
        # 梯度下降最高迭代次数
        self.number_epochs = number_epochs
        # 学习率
        self.alpha = alpha
        # 正则参数
        self.reg = reg
        # 数据集中user-item-rating字段的名称
        self.columns = columns

    def fit(self, dataset):
        '''
        :param dataset: uid, iid, rating
        :return:
        '''
        self.dataset = dataset
        # 用户评分数据
        self.users_ratings = dataset.groupby(self.columns[0]).agg([list])[[self.columns[1], self.columns[2]]]
        # 物品评分数据
        self.items_ratings = dataset.groupby(self.columns[1]).agg([list])[[self.columns[0], self.columns[2]]]
        # 计算全局平均分
        self.global_mean = self.dataset[self.columns[2]].mean()
        # 调用sgd方法训练模型参数
        self.bu, self.bi = self.sgd()

    def sgd(self):
        '''
        利用随机梯度下降,优化bu,bi的值
        :return: bu, bi
        '''
        # 初始化bu、bi的值,全部设为0
        bu = dict(zip(self.users_ratings.index, np.zeros(len(self.users_ratings))))
        bi = dict(zip(self.items_ratings.index, np.zeros(len(self.items_ratings))))

        for i in range(self.number_epochs):
            print("iter%d" % i)
            for uid, iid, real_rating in self.dataset.itertuples(index=False):
                error = real_rating - (self.global_mean + bu[uid] + bi[iid])

                bu[uid] += self.alpha * (error - self.reg * bu[uid])
                bi[iid] += self.alpha * (error - self.reg * bi[iid])

        return bu, bi

    def predict(self, uid, iid):
        predict_rating = self.global_mean + self.bu[uid] + self.bi[iid]
        return predict_rating


if __name__ == '__main__':
    dtype = [("userId", np.int32), ("movieId", np.int32), ("rating", np.float32)]
    dataset = pd.read_csv("datasets/ml-latest-small/ratings.csv", usecols=range(3), dtype=dict(dtype))

    bcf = BaselineCFBySGD(20, 0.1, 0.1, ["userId", "movieId", "rating"])
    bcf.fit(dataset)

    while True:
        uid = int(input("uid: "))
        iid = int(input("iid: "))
        print(bcf.predict(uid, iid))

Step 4: 准确性指标评估

  • 添加test方法,然后使用之前实现accuary方法计算准确性指标
import pandas as pd
import numpy as np

def data_split(data_path, x=0.8, random=False):
    '''
    切分数据集, 这里为了保证用户数量保持不变,将每个用户的评分数据按比例进行拆分
    :param data_path: 数据集路径
    :param x: 训练集的比例,如x=0.8,则0.2是测试集
    :param random: 是否随机切分,默认False
    :return: 用户-物品评分矩阵
    '''
    print("开始切分数据集...")
    # 设置要加载的数据字段的类型
    dtype = {"userId": np.int32, "movieId": np.int32, "rating": np.float32}
    # 加载数据,我们只用前三列数据,分别是用户ID,电影ID,已经用户对电影的对应评分
    ratings = pd.read_csv(data_path, dtype=dtype, usecols=range(3))

    testset_index = []
    # 为了保证每个用户在测试集和训练集都有数据,因此按userId聚合
    for uid in ratings.groupby("userId").any().index:
        user_rating_data = ratings.where(ratings["userId"]==uid).dropna()
        if random:
            # 因为不可变类型不能被 shuffle方法作用,所以需要强行转换为列表
            index = list(user_rating_data.index)
            np.random.shuffle(index)    # 打乱列表
            _index = round(len(user_rating_data) * x)
            testset_index += list(index[_index:])
        else:
            # 将每个用户的x比例的数据作为训练集,剩余的作为测试集
            index = round(len(user_rating_data) * x)
            testset_index += list(user_rating_data.index.values[index:])

    testset = ratings.loc[testset_index]
    trainset = ratings.drop(testset_index)
    print("完成数据集切分...")
    return trainset, testset

def accuray(predict_results, method="all"):
    '''
    准确性指标计算方法
    :param predict_results: 预测结果,类型为容器,每个元素是一个包含uid,iid,real_rating,pred_rating的序列
    :param method: 指标方法,类型为字符串,rmse或mae,否则返回两者rmse和mae
    :return:
    '''

    def rmse(predict_results):
        '''
        rmse评估指标
        :param predict_results:
        :return: rmse
        '''
        length = 0
        _rmse_sum = 0
        for uid, iid, real_rating, pred_rating in predict_results:
            length += 1
            _rmse_sum += (pred_rating - real_rating) ** 2
        return round(np.sqrt(_rmse_sum / length), 4)

    def mae(predict_results):
        '''
        mae评估指标
        :param predict_results:
        :return: mae
        '''
        length = 0
        _mae_sum = 0
        for uid, iid, real_rating, pred_rating in predict_results:
            length += 1
            _mae_sum += abs(pred_rating - real_rating)
        return round(_mae_sum / length, 4)

    def rmse_mae(predict_results):
        '''
        rmse和mae评估指标
        :param predict_results:
        :return: rmse, mae
        '''
        length = 0
        _rmse_sum = 0
        _mae_sum = 0
        for uid, iid, real_rating, pred_rating in predict_results:
            length += 1
            _rmse_sum += (pred_rating - real_rating) ** 2
            _mae_sum += abs(pred_rating - real_rating)
        return round(np.sqrt(_rmse_sum / length), 4), round(_mae_sum / length, 4)

    if method.lower() == "rmse":
        rmse(predict_results)
    elif method.lower() == "mae":
        mae(predict_results)
    else:
        return rmse_mae(predict_results)

class BaselineCFBySGD(object):

    def __init__(self, number_epochs, alpha, reg, columns=["uid", "iid", "rating"]):
        # 梯度下降最高迭代次数
        self.number_epochs = number_epochs
        # 学习率
        self.alpha = alpha
        # 正则参数
        self.reg = reg
        # 数据集中user-item-rating字段的名称
        self.columns = columns

    def fit(self, dataset):
        '''
        :param dataset: uid, iid, rating
        :return:
        '''
        self.dataset = dataset
        # 用户评分数据
        self.users_ratings = dataset.groupby(self.columns[0]).agg([list])[[self.columns[1], self.columns[2]]]
        # 物品评分数据
        self.items_ratings = dataset.groupby(self.columns[1]).agg([list])[[self.columns[0], self.columns[2]]]
        # 计算全局平均分
        self.global_mean = self.dataset[self.columns[2]].mean()
        # 调用sgd方法训练模型参数
        self.bu, self.bi = self.sgd()

    def sgd(self):
        '''
        利用随机梯度下降,优化bu,bi的值
        :return: bu, bi
        '''
        # 初始化bu、bi的值,全部设为0
        bu = dict(zip(self.users_ratings.index, np.zeros(len(self.users_ratings))))
        bi = dict(zip(self.items_ratings.index, np.zeros(len(self.items_ratings))))

        for i in range(self.number_epochs):
            print("iter%d" % i)
            for uid, iid, real_rating in self.dataset.itertuples(index=False):
                error = real_rating - (self.global_mean + bu[uid] + bi[iid])

                bu[uid] += self.alpha * (error - self.reg * bu[uid])
                bi[iid] += self.alpha * (error - self.reg * bi[iid])

        return bu, bi

    def predict(self, uid, iid):
        '''评分预测'''
        if iid not in self.items_ratings.index:
            raise Exception("无法预测用户<{uid}>对电影<{iid}>的评分,因为训练集中缺失<{iid}>的数据".format(uid=uid, iid=iid))

        predict_rating = self.global_mean + self.bu[uid] + self.bi[iid]
        return predict_rating

    def test(self,testset):
        '''预测测试集数据'''
        for uid, iid, real_rating in testset.itertuples(index=False):
            try:
                pred_rating = self.predict(uid, iid)
            except Exception as e:
                print(e)
            else:
                yield uid, iid, real_rating, pred_rating

if __name__ == '__main__':

    trainset, testset = data_split("datasets/ml-latest-small/ratings.csv", random=True)

    bcf = BaselineCFBySGD(20, 0.1, 0.1, ["userId", "movieId", "rating"])
    bcf.fit(trainset)

    pred_results = bcf.test(testset)

    rmse, mae = accuray(pred_results)

    print("rmse: ", rmse, "mae: ", mae)

方法二:交替最小二乘法优化

使用交替最小二乘法优化算法预测Baseline偏置值

step 1: 交替最小二乘法推导

最小二乘法和梯度下降法一样,可以用于求极值。

最小二乘法思想:对损失函数求偏导,然后再使偏导为0

step 2: 交替最小二乘法应用

step 3: 算法实现

  • 数据加载初始化与之前完全相同
  • 迭代更新bu bi
for i in range(number_epochs):
    print("iter%d" % i)
    for iid, uids, ratings in items_ratings.itertuples(index=True):
        _sum = 0
        for uid, rating in zip(uids, ratings):
            _sum += rating - global_mean - bu[uid]
        bi[iid] = _sum / (reg_bi + len(uids))

    for uid, iids, ratings in users_ratings.itertuples(index=True):
        _sum = 0
        for iid, rating in zip(iids, ratings):
            _sum += rating - global_mean - bi[iid]
        bu[uid] = _sum / (reg_bu + len(iids))
import pandas as pd
import numpy as np


class BaselineCFByALS(object):

    def __init__(self, number_epochs, reg_bu, reg_bi, columns=["uid", "iid", "rating"]):
        # 梯度下降最高迭代次数
        self.number_epochs = number_epochs
        # bu的正则参数
        self.reg_bu = reg_bu
        # bi的正则参数
        self.reg_bi = reg_bi
        # 数据集中user-item-rating字段的名称
        self.columns = columns

    def fit(self, dataset):
        '''
        :param dataset: uid, iid, rating
        :return:
        '''
        self.dataset = dataset
        # 用户评分数据
        self.users_ratings = dataset.groupby(self.columns[0]).agg([list])[[self.columns[1], self.columns[2]]]
        # 物品评分数据
        self.items_ratings = dataset.groupby(self.columns[1]).agg([list])[[self.columns[0], self.columns[2]]]
        # 计算全局平均分
        self.global_mean = self.dataset[self.columns[2]].mean()
        # 调用sgd方法训练模型参数
        self.bu, self.bi = self.als()

    def als(self):
        '''
        利用随机梯度下降,优化bu,bi的值
        :return: bu, bi
        '''
        # 初始化bu、bi的值,全部设为0
        bu = dict(zip(self.users_ratings.index, np.zeros(len(self.users_ratings))))
        bi = dict(zip(self.items_ratings.index, np.zeros(len(self.items_ratings))))

        for i in range(self.number_epochs):
            print("iter%d" % i)
            for iid, uids, ratings in self.items_ratings.itertuples(index=True):
                _sum = 0
                for uid, rating in zip(uids, ratings):
                    _sum += rating - self.global_mean - bu[uid]
                bi[iid] = _sum / (self.reg_bi + len(uids))

            for uid, iids, ratings in self.users_ratings.itertuples(index=True):
                _sum = 0
                for iid, rating in zip(iids, ratings):
                    _sum += rating - self.global_mean - bi[iid]
                bu[uid] = _sum / (self.reg_bu + len(iids))
        return bu, bi

    def predict(self, uid, iid):
        predict_rating = self.global_mean + self.bu[uid] + self.bi[iid]
        return predict_rating


if __name__ == '__main__':
    dtype = [("userId", np.int32), ("movieId", np.int32), ("rating", np.float32)]
    dataset = pd.read_csv("datasets/ml-latest-small/ratings.csv", usecols=range(3), dtype=dict(dtype))

    bcf = BaselineCFByALS(20, 25, 15, ["userId", "movieId", "rating"])
    bcf.fit(dataset)

    while True:
        uid = int(input("uid: "))
        iid = int(input("iid: "))
        print(bcf.predict(uid, iid))

Step 4: 准确性指标评估

import pandas as pd
import numpy as np

def data_split(data_path, x=0.8, random=False):
    '''
    切分数据集, 这里为了保证用户数量保持不变,将每个用户的评分数据按比例进行拆分
    :param data_path: 数据集路径
    :param x: 训练集的比例,如x=0.8,则0.2是测试集
    :param random: 是否随机切分,默认False
    :return: 用户-物品评分矩阵
    '''
    print("开始切分数据集...")
    # 设置要加载的数据字段的类型
    dtype = {"userId": np.int32, "movieId": np.int32, "rating": np.float32}
    # 加载数据,我们只用前三列数据,分别是用户ID,电影ID,已经用户对电影的对应评分
    ratings = pd.read_csv(data_path, dtype=dtype, usecols=range(3))

    testset_index = []
    # 为了保证每个用户在测试集和训练集都有数据,因此按userId聚合
    for uid in ratings.groupby("userId").any().index:
        user_rating_data = ratings.where(ratings["userId"]==uid).dropna()
        if random:
            # 因为不可变类型不能被 shuffle方法作用,所以需要强行转换为列表
            index = list(user_rating_data.index)
            np.random.shuffle(index)    # 打乱列表
            _index = round(len(user_rating_data) * x)
            testset_index += list(index[_index:])
        else:
            # 将每个用户的x比例的数据作为训练集,剩余的作为测试集
            index = round(len(user_rating_data) * x)
            testset_index += list(user_rating_data.index.values[index:])

    testset = ratings.loc[testset_index]
    trainset = ratings.drop(testset_index)
    print("完成数据集切分...")
    return trainset, testset

def accuray(predict_results, method="all"):
    '''
    准确性指标计算方法
    :param predict_results: 预测结果,类型为容器,每个元素是一个包含uid,iid,real_rating,pred_rating的序列
    :param method: 指标方法,类型为字符串,rmse或mae,否则返回两者rmse和mae
    :return:
    '''

    def rmse(predict_results):
        '''
        rmse评估指标
        :param predict_results:
        :return: rmse
        '''
        length = 0
        _rmse_sum = 0
        for uid, iid, real_rating, pred_rating in predict_results:
            length += 1
            _rmse_sum += (pred_rating - real_rating) ** 2
        return round(np.sqrt(_rmse_sum / length), 4)

    def mae(predict_results):
        '''
        mae评估指标
        :param predict_results:
        :return: mae
        '''
        length = 0
        _mae_sum = 0
        for uid, iid, real_rating, pred_rating in predict_results:
            length += 1
            _mae_sum += abs(pred_rating - real_rating)
        return round(_mae_sum / length, 4)

    def rmse_mae(predict_results):
        '''
        rmse和mae评估指标
        :param predict_results:
        :return: rmse, mae
        '''
        length = 0
        _rmse_sum = 0
        _mae_sum = 0
        for uid, iid, real_rating, pred_rating in predict_results:
            length += 1
            _rmse_sum += (pred_rating - real_rating) ** 2
            _mae_sum += abs(pred_rating - real_rating)
        return round(np.sqrt(_rmse_sum / length), 4), round(_mae_sum / length, 4)

    if method.lower() == "rmse":
        rmse(predict_results)
    elif method.lower() == "mae":
        mae(predict_results)
    else:
        return rmse_mae(predict_results)

class BaselineCFByALS(object):

    def __init__(self, number_epochs, reg_bu, reg_bi, columns=["uid", "iid", "rating"]):
        # 梯度下降最高迭代次数
        self.number_epochs = number_epochs
        # bu的正则参数
        self.reg_bu = reg_bu
        # bi的正则参数
        self.reg_bi = reg_bi
        # 数据集中user-item-rating字段的名称
        self.columns = columns

    def fit(self, dataset):
        '''
        :param dataset: uid, iid, rating
        :return:
        '''
        self.dataset = dataset
        # 用户评分数据
        self.users_ratings = dataset.groupby(self.columns[0]).agg([list])[[self.columns[1], self.columns[2]]]
        # 物品评分数据
        self.items_ratings = dataset.groupby(self.columns[1]).agg([list])[[self.columns[0], self.columns[2]]]
        # 计算全局平均分
        self.global_mean = self.dataset[self.columns[2]].mean()
        # 调用sgd方法训练模型参数
        self.bu, self.bi = self.als()

    def als(self):
        '''
        利用随机梯度下降,优化bu,bi的值
        :return: bu, bi
        '''
        # 初始化bu、bi的值,全部设为0
        bu = dict(zip(self.users_ratings.index, np.zeros(len(self.users_ratings))))
        bi = dict(zip(self.items_ratings.index, np.zeros(len(self.items_ratings))))

        for i in range(self.number_epochs):
            print("iter%d" % i)
            for iid, uids, ratings in self.items_ratings.itertuples(index=True):
                _sum = 0
                for uid, rating in zip(uids, ratings):
                    _sum += rating - self.global_mean - bu[uid]
                bi[iid] = _sum / (self.reg_bi + len(uids))

            for uid, iids, ratings in self.users_ratings.itertuples(index=True):
                _sum = 0
                for iid, rating in zip(iids, ratings):
                    _sum += rating - self.global_mean - bi[iid]
                bu[uid] = _sum / (self.reg_bu + len(iids))
        return bu, bi

    def predict(self, uid, iid):
        '''评分预测'''
        if iid not in self.items_ratings.index:
            raise Exception("无法预测用户<{uid}>对电影<{iid}>的评分,因为训练集中缺失<{iid}>的数据".format(uid=uid, iid=iid))

        predict_rating = self.global_mean + self.bu[uid] + self.bi[iid]
        return predict_rating

    def test(self,testset):
        '''预测测试集数据'''
        for uid, iid, real_rating in testset.itertuples(index=False):
            try:
                pred_rating = self.predict(uid, iid)
            except Exception as e:
                print(e)
            else:
                yield uid, iid, real_rating, pred_rating


if __name__ == '__main__':
    trainset, testset = data_split("datasets/ml-latest-small/ratings.csv", random=True)

    bcf = BaselineCFByALS(20, 25, 15, ["userId", "movieId", "rating"])
    bcf.fit(trainset)

    pred_results = bcf.test(testset)

    rmse, mae = accuray(pred_results)

    print("rmse: ", rmse, "mae: ", mae)


#----------------------------数据加载---------------------------#

# import pandas as pd
# import numpy as np
# dtype = [("userId", np.int32), ("movieId", np.int32), ("rating", np.float32)]
# dataset = pd.read_csv("./ratings.csv", usecols=range(3), dtype=dict(dtype))

#----------------------------数据初始化---------------------------#

# # 用户评分数据:先groupby分组 后aggregation聚合
# #   groupby分组,比如 groupby('userId') 根据用户id分组
# #   agg:aggregation聚合。agg([list]) 以列表方式展示数据
# users_ratings = dataset.groupby('userId').agg([list])
# # print(users_ratings)
# # 物品评分数据
# items_ratings = dataset.groupby('movieId').agg([list])
# # print(items_ratings)
# # 计算全局平均分
# global_mean = dataset['rating'].mean()
# # print(global_mean) #3.501556873321533
"""
users_ratings = dataset.groupby('userId').agg([list])
                                                  movieId                                             rating
                                                     list                                               list
userId                                                                                                      
1       [1, 3, 6, 47, 50, 70, 101, 110, 151, 157, 163,...  [4.0, 4.0, 4.0, 5.0, 5.0, 3.0, 5.0, 4.0, 5.0, ...
2       [318, 333, 1704, 3578, 6874, 8798, 46970, 4851...  [3.0, 4.0, 4.5, 4.0, 4.0, 3.5, 4.0, 4.0, 4.5, ...
3       [31, 527, 647, 688, 720, 849, 914, 1093, 1124,...  [0.5, 0.5, 0.5, 0.5, 0.5, 5.0, 0.5, 0.5, 0.5, ...
4       [21, 32, 45, 47, 52, 58, 106, 125, 126, 162, 1...  [3.0, 2.0, 3.0, 2.0, 3.0, 3.0, 4.0, 5.0, 1.0, ...
5       [1, 21, 34, 36, 39, 50, 58, 110, 150, 153, 232...  [4.0, 4.0, 4.0, 4.0, 3.0, 4.0, 5.0, 4.0, 3.0, ...
...                                                   ...                                                ...
606     [1, 7, 11, 15, 17, 18, 19, 28, 29, 32, 36, 46,...  [2.5, 2.5, 2.5, 3.5, 4.0, 4.0, 2.0, 3.5, 4.5, ...
607     [1, 11, 25, 34, 36, 86, 110, 112, 150, 153, 16...  [4.0, 3.0, 3.0, 3.0, 4.0, 4.0, 5.0, 2.0, 5.0, ...
608     [1, 2, 3, 10, 16, 19, 21, 24, 31, 32, 34, 39, ...  [2.5, 2.0, 2.0, 4.0, 4.5, 2.0, 3.5, 2.0, 3.0, ...
609     [1, 10, 110, 116, 137, 150, 161, 185, 208, 231...  [3.0, 4.0, 3.0, 3.0, 3.0, 3.0, 3.0, 3.0, 3.0, ...
610     [1, 6, 16, 32, 47, 50, 70, 95, 110, 111, 112, ...  [5.0, 5.0, 4.5, 4.5, 5.0, 4.0, 4.0, 3.5, 4.5, ...
[610 rows x 2 columns]
--------------------------------------------------------------------------------------------------
items_ratings = dataset.groupby('movieId').agg([list])
                                                    userId                                             rating
                                                      list                                               list
movieId                                                                                                      
1        [1, 5, 7, 15, 17, 18, 19, 21, 27, 31, 32, 33, ...  [4.0, 4.0, 4.5, 2.5, 4.5, 3.5, 4.0, 3.5, 3.0, ...
2        [6, 8, 18, 19, 20, 21, 27, 51, 62, 68, 82, 91,...  [4.0, 4.0, 3.0, 3.0, 3.0, 3.5, 4.0, 4.5, 4.0, ...
3        [1, 6, 19, 32, 42, 43, 44, 51, 58, 64, 68, 91,...  [4.0, 5.0, 3.0, 3.0, 4.0, 5.0, 3.0, 4.0, 3.0, ...
4                          [6, 14, 84, 162, 262, 411, 600]                [3.0, 3.0, 3.0, 3.0, 1.0, 2.0, 1.5]
5        [6, 31, 43, 45, 58, 66, 68, 84, 103, 107, 111,...  [5.0, 3.0, 5.0, 3.0, 4.0, 4.0, 2.0, 3.0, 4.0, ...
...                                                    ...                                                ...
193581                                               [184]                                              [4.0]
193583                                               [184]                                              [3.5]
193585                                               [184]                                              [3.5]
193587                                               [184]                                              [3.5]
193609                                               [331]                                              [4.0]
[9724 rows x 2 columns]
"""

# 初始化字典:bu、bi 用于在梯度更新中自动进行更新的权重参数值
#   bu的key为 userId,bi的key为 movieId
#   bu的value 和 bi的value为 在梯度更新中自动进行更新的权重参数值
#   np.zeros(5) 得 array([0., 0., 0., 0., 0.])
# bu = dict(zip(users_ratings.index, np.zeros(len(users_ratings))))
# bi = dict(zip(items_ratings.index, np.zeros(len(items_ratings))))


import pandas as pd
import numpy as np

class BaselineCFBySGD(object):

    def __init__(self, number_epochs, alpha, reg, columns=["uid", "iid", "rating"]):
        # 梯度下降最高迭代次数
        self.number_epochs = number_epochs
        # 学习率
        self.alpha = alpha
        # 正则参数λ
        self.reg = reg
        # 数据集中user-item-rating字段的名称
        self.columns = columns

    def fit(self, dataset):
        '''
        :param dataset: uid, iid, rating
        :return:
        '''
        self.dataset = dataset
        # 用户评分数据:先groupby分组 后aggregation聚合
        #   groupby分组,比如 groupby('userId') 根据用户id分组
        #   agg:aggregation聚合。agg([list]) 以列表方式展示数据

        # 用户评分数据
        # columns=["uid", "iid", "rating"]。[[self.columns[1], self.columns[2]]] 即 [["iid", "rating"]]
        self.users_ratings = dataset.groupby(self.columns[0]).agg([list])[[self.columns[1], self.columns[2]]]
        print(self.users_ratings)
        # print("len(self.users_ratings)",len(self.users_ratings)) #610

        # 物品评分数据
        # columns=["uid", "iid", "rating"]。[[self.columns[0], self.columns[2]]] 即 [["uid", "rating"]]
        self.items_ratings = dataset.groupby(self.columns[1]).agg([list])[[self.columns[0], self.columns[2]]]
        print("items_ratings",self.items_ratings)
        # print("len(self.items_ratings)",len(self.items_ratings)) #9724

        # 计算全局平均分
        self.global_mean = self.dataset[self.columns[2]].mean() #columns[2]即为"rating"列数据

        # 调用sgd方法训练模型参数
        self.bu, self.bi = self.sgd()
        # print(self.bu)
        # print(self.bi)

    def sgd(self):
        '''
        利用随机梯度下降,优化bu,bi的值
        :return: bu, bi
        '''
        # 初始化bu、bi的值,全部设为0
        # 初始化字典:bu、bi 用于在梯度更新中自动进行更新的权重参数值
        #   bu的key为 userId,bi的key为 movieId
        #   bu的value 在梯度更新中自动进行更新的权重参数值,一开始初始化 每个用户给所有电影所打的评分的平均分 与 所有电影的平均分 的偏置值 为0
        #   bi的value为 在梯度更新中自动进行更新的权重参数值,一开始初始化 每部电影得到的所有评分的平均分 与 所有电影的平均分 的偏置值 为0
        bu = dict(zip(self.users_ratings.index, np.zeros(len(self.users_ratings))))
        bi = dict(zip(self.items_ratings.index, np.zeros(len(self.items_ratings))))

        #bu、bi 用于在梯度更新中自动进行更新的权重参数值
        for i in range(self.number_epochs):
            print("iter%d" % i)
            # itertuples 遍历出 dataset中的3列:userId、movieId、rating
            # index=False 表示遍历返回的数据不带index。index=True 表示遍历返回的数据带index,即变成4列index、userId、movieId、rating
            for uid, iid, real_rating in self.dataset.itertuples(index=False):
                #error = 真实评分值 - 预测评分值
                #   error:真实评分值 与 预测评分值 之间的偏差值
                #   real_rating:真实评分值
                #   (self.global_mean + bu[uid] + bi[iid]):预测评分值
                #   global_mean:整个评分数据集的平均评分μ​
                #   bu[uid]:
                #       bu为每个用户评分与平均评分μ的偏置值bu,计算的是 每个用户给所有电影所打的评分的平均分 与 所有电影的平均分 的偏置值。
                #       bu[uid]即为取出该用户的偏置值
                #   bi[iid]:
                #       bi:计算每部电影所接受的评分与平均评分μ的偏置值bi,计算的是 每部电影得到的所有评分的平均分 与 所有电影的平均分 的偏置值。
                #       bi[iid]即为取出该部电影的偏置值
                error = real_rating - (self.global_mean + bu[uid] + bi[iid])
                #alpha:学习率。 error:真实评分值 与 预测评分值 之间的偏差值。 reg:正则参数λ。
                #bu:=bu+α∗(error-λ∗bu)
                #bi:=bi+α∗(error-λ∗bi)
                bu[uid] += self.alpha * (error - self.reg * bu[uid])
                bi[iid] += self.alpha * (error - self.reg * bi[iid])

        return bu, bi

    #预测评分
    def predict(self, uid, iid):
        #训练之后的bu和bi都是已经优化好的值了,bu中已经有每个用户的偏置值,bi中已经有每部电影的偏置值
        predict_rating = self.global_mean + self.bu[uid] + self.bi[iid]
        return predict_rating


if __name__ == '__main__':
    dtype = [("userId", np.int32), ("movieId", np.int32), ("rating", np.float32)]
    dataset = pd.read_csv("./ratings.csv", usecols=range(3), dtype=dict(dtype))

    bcf = BaselineCFBySGD(20, 0.1, 0.1, ["userId", "movieId", "rating"])
    bcf.fit(dataset)

    while True:
        uid = int(input("uid: "))
        iid = int(input("iid: "))
        print(bcf.predict(uid, iid)) 
import pandas as pd
import numpy as np

def data_split(data_path, x=0.8, random=False):
    '''
    切分数据集, 这里为了保证用户数量保持不变,将每个用户的评分数据按比例进行拆分
    :param data_path: 数据集路径
    :param x: 训练集的比例,如x=0.8,则0.2是测试集
    :param random: 是否随机切分,默认False
    :return: 用户-物品评分矩阵
    '''
    print("开始切分数据集...")
    # 设置要加载的数据字段的类型
    dtype = {"userId": np.int32, "movieId": np.int32, "rating": np.float32}
    # 加载数据,我们只用前三列数据,分别是用户ID,电影ID,已经用户对电影的对应评分
    ratings = pd.read_csv(data_path, dtype=dtype, usecols=range(3))
    trainset_index = []

    # 为了保证每个用户在测试集和训练集都有数据,因此按userId聚合
    # any():取出数据。index:索引
    # 遍历"userId"列 groupby分组之后 每个 用户uid
    for uid in ratings.groupby("userId").any().index:
        # 根据遍历出来的该用户uid 获取对应该用户的数据
        user_rating_data = ratings.where(ratings["userId"]==uid).dropna()
        # print(user_rating_data)
        # print("user_rating_data.index",user_rating_data.index)

        #表示设定了随机种子
        if random:
            # 因为不可变类型不能被 shuffle方法作用,所以需要强行转换为列表,只有列表才能做shuffle操作
            # user_rating_data.index:即为数据集csv文件中的行号
            index = list(user_rating_data.index)
            # 打乱列表中的行号
            np.random.shuffle(index)
            #x:为0.8。len(user_rating_data) * x:表示取出80%的数据
            #round:返回浮点数x的四舍五入值。也即为对len(user_rating_data) * x的计算结果值进行四舍五入。
            _index = round(len(user_rating_data) * x)
            #取出80%的数据 作为 训练集数据的index
            trainset_index += list(index[:_index])
        #顺序获取,不打乱
        else:
            # 将每个用户的x比例的数据作为训练集,剩余的作为测试集
            index = round(len(user_rating_data) * x)
            trainset_index += list(user_rating_data.index.values[:index])

    trainset = ratings.loc[trainset_index] #取出80%的数据 作为 训练集数据的
    testset = ratings.drop(trainset_index) #取出20%的数据 作为 训练集数据的
    # print(len(trainset_index))
    # print(len(trainset))
    # print(len(testset))
    print("完成数据集切分...")
    return trainset, testset

def accuray(predict_results, method="all"):
    '''
    准确性指标计算方法
    :param predict_results: 预测结果,类型为容器,每个元素是一个包含uid,iid,real_rating,pred_rating的序列
    :param method: 指标方法,类型为字符串,rmse或mae,否则返回两者rmse和mae
    :return:
    '''

    """
    均方误差MSE:真实值-预测值的差值进行平方之后再求和再平均
    均方根误差RMSE:
        实际就是在均方误差MSE的基础之上进行开根号,也即真实值-预测值的差值进行平方之后再求和再平均再开根号
        之所以还要在均方误差MSE的基础之上进行开根号,是因为为了保证得出的误差的结果和源数据值是在一个量级单位上。
        就比如标准差是在方差的基础上开根号,目的也是为了保证在同一量纲。
    平均绝对误差:
        真实值-预测值的差值的绝对值再求和再平均
    """
    def rmse(predict_results):
        '''
        rmse评估指标
        :param predict_results:
        :return: rmse
        '''
        length = 0
        _rmse_sum = 0
        for uid, iid, real_rating, pred_rating in predict_results:
            length += 1
            _rmse_sum += (pred_rating - real_rating) ** 2
        return round(np.sqrt(_rmse_sum / length), 4)

    def mae(predict_results):
        '''
        mae评估指标
        :param predict_results:
        :return: mae
        '''
        length = 0
        _mae_sum = 0
        for uid, iid, real_rating, pred_rating in predict_results:
            length += 1
            _mae_sum += abs(pred_rating - real_rating)
        return round(_mae_sum / length, 4)

    def rmse_mae(predict_results):
        '''
        rmse和mae评估指标
        :param predict_results:
        :return: rmse, mae
        '''
        length = 0
        _rmse_sum = 0
        _mae_sum = 0
        for uid, iid, real_rating, pred_rating in predict_results:
            length += 1
            _rmse_sum += (pred_rating - real_rating) ** 2
            _mae_sum += abs(pred_rating - real_rating)
        return round(np.sqrt(_rmse_sum / length), 4), round(_mae_sum / length, 4)

    if method.lower() == "rmse":
        rmse(predict_results)
    elif method.lower() == "mae":
        mae(predict_results)
    else:
        return rmse_mae(predict_results)

class BaselineCFBySGD(object):

    def __init__(self, number_epochs, alpha, reg, columns=["uid", "iid", "rating"]):
        # 梯度下降最高迭代次数
        self.number_epochs = number_epochs
        # 学习率
        self.alpha = alpha
        # 正则参数
        self.reg = reg
        # 数据集中user-item-rating字段的名称
        self.columns = columns

    def fit(self, dataset):
        '''
        :param dataset: uid, iid, rating
        :return:
        '''
        self.dataset = dataset
        # 用户评分数据
        self.users_ratings = dataset.groupby(self.columns[0]).agg([list])[[self.columns[1], self.columns[2]]]
        # 物品评分数据
        self.items_ratings = dataset.groupby(self.columns[1]).agg([list])[[self.columns[0], self.columns[2]]]
        # print(len(self.users_ratings))
        # print(len(self.items_ratings))
        # print(self.users_ratings)
        # print(self.items_ratings)

        # 计算全局平均分
        self.global_mean = self.dataset[self.columns[2]].mean()
        # 调用sgd方法训练模型参数
        self.bu, self.bi = self.sgd()

    def sgd(self):
        '''
        利用随机梯度下降,优化bu,bi的值
        :return: bu, bi
        '''
        # 初始化bu、bi的值,全部设为0
        # 初始化字典:bu、bi 用于在梯度更新中自动进行更新的权重参数值
        #   bu的key为 userId,bi的key为 movieId
        #   bu的value 在梯度更新中自动进行更新的权重参数值,一开始初始化 每个用户给所有电影所打的评分的平均分 与 所有电影的平均分 的偏置值 为0
        #   bi的value为 在梯度更新中自动进行更新的权重参数值,一开始初始化 每部电影得到的所有评分的平均分 与 所有电影的平均分 的偏置值 为0
        bu = dict(zip(self.users_ratings.index, np.zeros(len(self.users_ratings))))
        bi = dict(zip(self.items_ratings.index, np.zeros(len(self.items_ratings))))

        # bu、bi 用于在梯度更新中自动进行更新的权重参数值
        for i in range(self.number_epochs):
            print("iter%d" % i)
            # itertuples 遍历出 dataset中的3列:userId、movieId、rating
            # index=False 表示遍历返回的数据不带index。index=True 表示遍历返回的数据带index,即变成4列index、userId、movieId、rating
            for uid, iid, real_rating in self.dataset.itertuples(index=False):
                # error = 真实评分值 - 预测评分值
                #   error:真实评分值 与 预测评分值 之间的偏差值
                #   real_rating:真实评分值
                #   (self.global_mean + bu[uid] + bi[iid]):预测评分值
                #   global_mean:整个评分数据集的平均评分μ​
                #   bu[uid]:
                #       bu为每个用户评分与平均评分μ的偏置值bu,计算的是 每个用户给所有电影所打的评分的平均分 与 所有电影的平均分 的偏置值。
                #       bu[uid]即为取出该用户的偏置值
                #   bi[iid]:
                #       bi:计算每部电影所接受的评分与平均评分μ的偏置值bi,计算的是 每部电影得到的所有评分的平均分 与 所有电影的平均分 的偏置值。
                #       bi[iid]即为取出该部电影的偏置值
                error = real_rating - (self.global_mean + bu[uid] + bi[iid])
                #alpha:学习率。 error:真实评分值 与 预测评分值 之间的偏差值。 reg:正则参数λ。
                #bu:=bu+α∗(error-λ∗bu)
                #bi:=bi+α∗(error-λ∗bi)
                bu[uid] += self.alpha * (error - self.reg * bu[uid])
                bi[iid] += self.alpha * (error - self.reg * bi[iid])

        return bu, bi

    def predict(self, uid, iid):
        '''评分预测'''
        if iid not in self.items_ratings.index:
            raise Exception("无法预测用户<{uid}>对电影<{iid}>的评分,因为训练集中缺失<{iid}>的数据".format(uid=uid, iid=iid))
        predict_rating = self.global_mean + self.bu[uid] + self.bi[iid]
        return predict_rating

    def test(self,testset):
        '''预测测试集数据'''
        for uid, iid, real_rating in testset.itertuples(index=False):
            try:
                pred_rating = self.predict(uid, iid)
            except Exception as e:
                print(e)
            else:
                yield uid, iid, real_rating, pred_rating

if __name__ == '__main__':
    trainset, testset = data_split("./ratings.csv", random=True)
    bcf = BaselineCFBySGD(20, 0.1, 0.1, ["userId", "movieId", "rating"])
    bcf.fit(trainset)
    pred_results = bcf.test(testset)
    rmse, mae = accuray(pred_results)
    print("rmse: ", rmse, "mae: ", mae)

"""
1.data.std(axis=0)
	std 标准差(均方差):https://ww2.mathworks.cn/help/matlab/ref/std.html?searchHighlight=std&s_tid=doc_srchtitle
		1.标准差(Standard Deviation) ,中文环境中又常称均方差,是离均差平方的算术平均数的平方根,用σ表示。
		  算术平均数的重要特性之一是离均差平方和为最小,样本各观测值与平均数之差的平方和为最小,即离均差平方和为最小。
		  在概率统计中最常使用作为统计分布程度上的测量。
		  标准差(Standard Deviation),在概率统计中最常使用作为统计分布程度(statistical dispersion)上的测量。
		  测量到分布程度的结果,原则上具有两种性质:一个总量的标准差或一个随机变量的标准差,及一个子集合样品数的标准差之间,有所差别。
		2.方差与标准差的区别:
			标准差是方差的算术平方根(平方根)。标准差能反映一个数据集的离散程度。
		  	标准差定义为方差的算术平方根,反映组内个体间的离散程度。
			方差是在概率论和统计方差衡量随机变量或一组数据时离散程度的度量。
			概率论中方差用来度量随机变量和其数学期望(即均值)之间的偏离程度。
			统计中的方差(样本方差)是每个样本值与全体样本值的平均数之差的平方值的平均数。
			在许多实际问题中,研究方差即偏离程度有着重要意义。
			标准差能反映一个数据集的离散程度。平均数相同的两组数据,标准差未必相同。
			方差是衡量源数据和期望值相差的度量值。
		3.平均方差
			样本中各数据与样本平均数的差的平方和的平均数叫做样本方差;样本方差的算术平方根叫做样本标准差。
			样本方差和样本标准差都是衡量一个样本波动大小的量,样本方差或样本标准差越大,样本数据的波动就越大。
 
2.data.mean(axis=0)
	mean 数组的均值:https://ww2.mathworks.cn/help/matlab/ref/mean.html?searchHighlight=mean&s_tid=doc_srchtitle
"""
import pandas as pd
import numpy as np

def data_split(data_path, x=0.8, random=False):
    '''
    切分数据集, 这里为了保证用户数量保持不变,将每个用户的评分数据按比例进行拆分
    :param data_path: 数据集路径
    :param x: 训练集的比例,如x=0.8,则0.2是测试集
    :param random: 是否随机切分,默认False
    :return: 用户-物品评分矩阵
    '''
    print("开始切分数据集...")
    # 设置要加载的数据字段的类型
    dtype = {"userId": np.int32, "movieId": np.int32, "rating": np.float32}
    # 加载数据,我们只用前三列数据,分别是用户ID,电影ID,已经用户对电影的对应评分
    ratings = pd.read_csv(data_path, dtype=dtype, usecols=range(3))

    testset_index = []
    # 为了保证每个用户在测试集和训练集都有数据,因此按userId聚合
    for uid in ratings.groupby("userId").any().index:
        user_rating_data = ratings.where(ratings["userId"]==uid).dropna()
        if random:
            # 因为不可变类型不能被 shuffle方法作用,所以需要强行转换为列表
            index = list(user_rating_data.index)
            np.random.shuffle(index)    # 打乱列表
            _index = round(len(user_rating_data) * x)
            testset_index += list(index[_index:])
        else:
            # 将每个用户的x比例的数据作为训练集,剩余的作为测试集
            index = round(len(user_rating_data) * x)
            testset_index += list(user_rating_data.index.values[index:])

    testset = ratings.loc[testset_index]
    trainset = ratings.drop(testset_index)
    print("完成数据集切分...")
    return trainset, testset

def accuray(predict_results, method="all"):
    '''
    准确性指标计算方法
    :param predict_results: 预测结果,类型为容器,每个元素是一个包含uid,iid,real_rating,pred_rating的序列
    :param method: 指标方法,类型为字符串,rmse或mae,否则返回两者rmse和mae
    :return:
    '''

    def rmse(predict_results):
        '''
        rmse评估指标
        :param predict_results:
        :return: rmse
        '''
        length = 0
        _rmse_sum = 0
        for uid, iid, real_rating, pred_rating in predict_results:
            length += 1
            _rmse_sum += (pred_rating - real_rating) ** 2
        return round(np.sqrt(_rmse_sum / length), 4)

    def mae(predict_results):
        '''
        mae评估指标
        :param predict_results:
        :return: mae
        '''
        length = 0
        _mae_sum = 0
        for uid, iid, real_rating, pred_rating in predict_results:
            length += 1
            _mae_sum += abs(pred_rating - real_rating)
        return round(_mae_sum / length, 4)

    def rmse_mae(predict_results):
        '''
        rmse和mae评估指标
        :param predict_results:
        :return: rmse, mae
        '''
        length = 0
        _rmse_sum = 0
        _mae_sum = 0
        for uid, iid, real_rating, pred_rating in predict_results:
            length += 1
            _rmse_sum += (pred_rating - real_rating) ** 2
            _mae_sum += abs(pred_rating - real_rating)
        return round(np.sqrt(_rmse_sum / length), 4), round(_mae_sum / length, 4)

    if method.lower() == "rmse":
        rmse(predict_results)
    elif method.lower() == "mae":
        mae(predict_results)
    else:
        return rmse_mae(predict_results)

class BaselineCFByALS(object):

    def __init__(self, number_epochs, reg_bu, reg_bi, columns=["uid", "iid", "rating"]):
        # 梯度下降最高迭代次数
        self.number_epochs = number_epochs
        # bu的正则参数
        self.reg_bu = reg_bu
        # bi的正则参数
        self.reg_bi = reg_bi
        # 数据集中user-item-rating字段的名称
        self.columns = columns

    def fit(self, dataset):
        '''
        :param dataset: uid, iid, rating
        :return:
        '''
        self.dataset = dataset
        # 用户评分数据
        self.users_ratings = dataset.groupby(self.columns[0]).agg([list])[[self.columns[1], self.columns[2]]]
        # 物品评分数据
        self.items_ratings = dataset.groupby(self.columns[1]).agg([list])[[self.columns[0], self.columns[2]]]
        # print(len(self.users_ratings)) #610
        # print(len(self.items_ratings)) #8933
        # print(self.users_ratings) # userId  movieId rating
        # print(self.items_ratings) # movieId userId rating
        # 计算全局平均分
        self.global_mean = self.dataset[self.columns[2]].mean()
        # 调用sgd方法训练模型参数
        self.bu, self.bi = self.als()

    def als(self):
        '''
        利用交替最小二乘法优化,优化bu,bi的值
        :return: bu, bi
        '''
        """
        bu和bi:分别属于用户和物品的偏置,因此他们的正则参数可以分别设置两个独立的参数。
        通过最小二乘推导,我们最终分别得到了bu和bi的表达式,但他们的表达式中却又各自包含对方,
        因此这里我们将利用一种叫交替最小二乘的方法来计算他们的值:
            计算其中一项,先固定其他未知参数,即看作其他未知参数为已知。
            如求bu时,将bi看作是已知;求bi时,将bu看作是已知;当要计算下一轮的bu时,使用上一轮计算完毕的bi;
            当要计算下一轮的bi时,使用上一轮计算完毕的bu;如此反复交替,不断更新二者的值,求得最终的结果。
            这就是交替最小二乘法(ALS)
        """
        # 初始化bu、bi的值,全部设为0
        bu = dict(zip(self.users_ratings.index, np.zeros(len(self.users_ratings))))
        bi = dict(zip(self.items_ratings.index, np.zeros(len(self.items_ratings))))

        for i in range(self.number_epochs):
            print("iter%d" % i)
            # index=False 表示遍历返回的数据不带index。
            # index=True 表示遍历返回的数据带index,即变成3列 movieId/userId/rating
            for iid, uids, ratings in self.items_ratings.itertuples(index=True):
                """
                iid:即movieId,值为一个电影ID
                uids:为userId,值为list列表包含多个用户ID
                ratings:rating,值为list列表包含多个用户评分
                global_mean:整个评分数据集的平均评分μ​
                """
                _sum = 0
                """
                通过最小二乘推导,我们最终分别得到了bi表达式,bi表达式包含bu,因此这里我们将利用一种叫交替最小二乘的方法来计算他们的值。
                此处求bi时,也即是计算bi中每个movieId(key)对应的权重参数(value),那么将bu看作是已知。
                """
                # 遍历每个movieId 对应的 uids(userId的list列表) 和 ratings(用户评分的list列表)
                for uid, rating in zip(uids, ratings):
                    """
                    bu[uid]:
                        此处将bu看作是已知;当要计算下一轮的bi时,使用上一轮计算完毕的bu;当计算第一轮的bi时,使用零初始化好的bu。
                        bu为每个用户评分与平均评分μ的偏置值bu,计算的是 每个用户给所有电影所打的评分的平均分 与 所有电影的平均分 的偏置值。
                        bu[uid]即为取出该用户的偏置值
                    """
                    # 该部电影的用户评分rating - 整个评分数据集的平均评分μ - bu[uid]
                    _sum += rating - self.global_mean - bu[uid]
                # _sum / (reg_bi(bi的正则参数) + 有过用户评分的用户uids的数量)
                bi[iid] = _sum / (self.reg_bi + len(uids))

            # index=False 表示遍历返回的数据不带index。
            # index=True 表示遍历返回的数据带index,即变成3列 userId/movieId/rating
            for uid, iids, ratings in self.users_ratings.itertuples(index=True):
                """
                uid:即userId,值为一个用户ID
                iids:为movieId,值为list列表包含多个电影ID
                ratings:rating,值为list列表包含多个用户评分
                global_mean:整个评分数据集的平均评分μ​
                """
                _sum = 0
                for iid, rating in zip(iids, ratings):
                    """
                   bi[iid]:
                        此处将bi看作是已知;当要计算下一轮的bu时,使用上一轮计算完毕的bi;当计算第一轮的bu时,使用零初始化好的bi。
                        bi:计算每部电影所接受的评分与平均评分μ的偏置值bi,计算的是 每部电影得到的所有评分的平均分 与 所有电影的平均分 的偏置值。
                        bi[iid]即为取出该部电影的偏置值
                    """
                     # 该部电影的用户评分rating - 整个评分数据集的平均评分μ - bi[iid]
                    _sum += rating - self.global_mean - bi[iid]
                # _sum / (reg_bi(bu的正则参数) + 有过用户评分的电影movieId的数量)
                bu[uid] = _sum / (self.reg_bu + len(iids))

        return bu, bi

    def predict(self, uid, iid):
        '''评分预测'''
        if iid not in self.items_ratings.index:
            raise Exception("无法预测用户<{uid}>对电影<{iid}>的评分,因为训练集中缺失<{iid}>的数据".format(uid=uid, iid=iid))

        predict_rating = self.global_mean + self.bu[uid] + self.bi[iid]
        return predict_rating

    def test(self,testset):
        '''预测测试集数据'''
        for uid, iid, real_rating in testset.itertuples(index=False):
            try:
                pred_rating = self.predict(uid, iid)
            except Exception as e:
                print(e)
            else:
                yield uid, iid, real_rating, pred_rating


if __name__ == '__main__':
    trainset, testset = data_split("./ratings.csv", random=True)
    bcf = BaselineCFByALS(20, 25, 15, ["userId", "movieId", "rating"])
    bcf.fit(trainset)
    pred_results = bcf.test(testset)
    rmse, mae = accuray(pred_results)
    print("rmse: ", rmse, "mae: ", mae)

  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

あずにゃん

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值