基于K最近邻的协同过滤推荐

基于K最近邻的协同过滤推荐其实本质上就是MemoryBased CF,只不过在选取近邻的时候,加上K最近邻的限制。

这里我们直接根据MemoryBased CF的代码实现

修改以下地方

class CollaborativeFiltering(object):based = Nonedef __init__(self, k=40, rules=None, use_cache=False, standard=None):''':param k: 取K个最近邻来进行预测:param rules: 过滤规则,四选一,否则将抛异常:"unhot", "rated", ["unhot","rated"], None:param use_cache: 相似度计算结果是否开启缓存:param standard: 评分标准化方法,None表示不使用、mean表示均值中心化、zscore表示Z-Score标准化'''self.k = 40self.rules = rulesself.use_cache = use_cacheself.standard = standard

修改所有的选取近邻的地方的代码,根据相似度来选取K个最近邻

similar_users = self.similar[uid].drop([uid]).dropna().sort_values(ascending=False)[:self.k]similar_items = self.similar[iid].drop([iid]).dropna().sort_values(ascending=False)[:self.k]

但由于我们的原始数据较少,这里我们的KNN方法的效果会比纯粹的MemoryBasedCF要差.## 基于K最近邻的协同过滤推荐

基于K最近邻的协同过滤推荐其实本质上就是MemoryBased CF,只不过在选取近邻的时候,加上K最近邻的限制。

这里我们直接根据MemoryBased CF的代码实现

修改以下地方

class CollaborativeFiltering(object):based = Nonedef __init__(self, k=40, rules=None, use_cache=False, standard=None):''':param k: 取K个最近邻来进行预测:param rules: 过滤规则,四选一,否则将抛异常:"unhot", "rated", ["unhot","rated"], None:param use_cache: 相似度计算结果是否开启缓存:param standard: 评分标准化方法,None表示不使用、mean表示均值中心化、zscore表示Z-Score标准化'''self.k = 40self.rules = rulesself.use_cache = use_cacheself.standard = standard

修改所有的选取近邻的地方的代码,根据相似度来选取K个最近邻

similar_users = self.similar[uid].drop([uid]).dropna().sort_values(ascending=False)[:self.k]similar_items = self.similar[iid].drop([iid]).dropna().sort_values(ascending=False)[:self.k]

但由于我们的原始数据较少,这里我们的KNN方法的效果会比纯粹的MemoryBasedCF要差。


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

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

Baseline:基准预测

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

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

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

Baseline目标:

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

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

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

  • 计算每个用户评分与平均评分μ\muμ的偏置值bub_ubu

  • 计算每部电影所接受的评分与平均评分μ\muμ的偏置值bib_ibi

  • 预测用户对电影的评分:
    r^ui=bui=μ+bu+bi \hat{r}_{ui} = b_{ui} = \mu + b_u + b_i r^ui=bui=μ+bu+bi

举例:

​ 比如我们想通过Baseline来预测用户A对电影“阿甘正传”的评分,那么首先计算出整个评分数据集的平均评分μ\muμ是3.5分;而用户A是一个比较苛刻的用户,他的评分比较严格,普遍比平均评分低0.5分,即用户A的偏置值bib_ibi是-0.5;而电影“阿甘正传”是一部比较热门而且备受好评的电影,它的评分普遍比平均评分要高1.2分,那么电影“阿甘正传”的偏置值bib_ibi是+1.2,因此就可以预测出用户A对电影“阿甘正传”的评分为:3.5+(−0.5)+1.23.5+(-0.5)+1.23.5+(0.5)+1.2,也就是4.2分。

对于所有电影的平均评分μ\muμ是直接能计算出的,因此问题关键在于要测出每个用户的bub_ubu值和每部电影的bib_ibi的值。对于线性回归问题,我们可以利用平方差构建损失函数如下:
Cost=∑u,i∈R(rui−r^ui)2=∑u,i∈R(rui−μ−bu−bi)2 \begin{split} Cost &= \sum_{u,i\in R}(r_{ui}-\hat{r}_{ui})^2 \\&=\sum_{u,i\in R}(r_{ui}-\mu-b_u-b_i)^2 \end{split} Cost=u,iR(ruir^ui)2=u,iR(ruiμbubi)2

在这里插入图片描述

加入L2正则化:
Cost=∑u,i∈R(rui−μ−bu−bi)2+λ∗(∑ubu2+∑ibi2) Cost=\sum_{u,i\in R}(r_{ui}-\mu-b_u-b_i)^2 + \lambda*(\sum_u {b_u}^2 + \sum_i {b_i}^2) Cost=u,iR(ruiμbubi)2+λ(ubu2+ibi2)
公式解析:

  • 公式第一部分∑u,i∈R(rui−μ−bu−bi)2\sum_{u,i\in R}(r_{ui}-\mu-b_u-b_i)^2u,iR(ruiμbubi)2是用来寻找与已知评分数据拟合最好的bub_ububib_ibi
  • 公式第二部分λ∗(∑ubu2+∑ibi2)\lambda*(\sum_u {b_u}^2 + \sum_i {b_i}^2)λ(ubu2+ibi2)是正则化项,用于避免过拟合现象

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


我们一步步来解释你提到的两个问题:


一、损失函数 Cost 第二部分的来源与含义

损失函数是:

Cost=∑u,i∈R(rui−μ−bu−bi)2+λ(∑ubu2+∑ibi2) Cost = \sum_{u,i \in R}(r_{ui} - \mu - b_u - b_i)^2 + \lambda \left( \sum_u b_u^2 + \sum_i b_i^2 \right) Cost=u,iR(ruiμbubi)2+λ(ubu2+ibi2)

✅ 第一部分:

∑u,i∈R(rui−μ−bu−bi)2 \sum_{u,i \in R}(r_{ui} - \mu - b_u - b_i)^2 u,iR(ruiμbubi)2

这部分是拟合误差的平方和,目标是拟合训练集中用户对物品的评分,也就是最小化预测值与真实值之间的差距。


✅ 第二部分(你的问题重点):

λ(∑ubu2+∑ibi2) \lambda \left( \sum_u b_u^2 + \sum_i b_i^2 \right) λ(ubu2+ibi2)

这是L2 正则化项(L2 Regularization Term),用于防止过拟合(overfitting)。下面逐一解释来源与含义:

1. 为什么加这部分?
  • 如果只最小化拟合误差(第一部分),模型可能会为了降低误差而“过度拟合”训练数据,使得 $b_u$ 和 $b_i$ 的值变得很大。
  • 为了避免这种情况,我们要对参数的大小加以惩罚。
  • 加入正则项就是为了惩罚偏置项的过大取值,促使它们保持小的数值。
2. L2正则化含义:
  • ∑ubu2\sum_u b_u^2ubu2​ 表示所有用户偏置的平方和

  • ∑ibi2\sum_i b _i^2ibi2 表示所有物品偏置的平方和

  • λ(lambda)是正则化系数,控制“惩罚力度”

    • λ 越大,模型更偏向简单(小的参数)
    • λ 越小,模型更偏向拟合训练集
✅ 总结:

正则项的作用是:防止模型为训练数据拟合得太好而导致泛化能力差(即对新数据表现不好)


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

SGD: Stochastic Gradient Descent

​ Stochastic /stɒˈkæstɪk/ adj. [数] 随机的;猜测的

​ Gradient /ˈɡreɪdiənt/ n.斜坡/斜率

​ Descent /dɪˈsent/ n. 下降,降落;

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

step 1:梯度下降法推导

损失函数:
J(θ)=Cost=f(bu,bi)J(θ)=∑u,i∈R(rui−μ−bu−bi)2+λ∗(∑ubu2+∑ibi2) \begin{split} &J(\theta)=Cost=f(b_u, b_i)\\ \\ &J(\theta)=\sum_{u,i\in R}(r_{ui}-\mu-b_u-b_i)^2 + \lambda*(\sum_u {b_u}^2 + \sum_i {b_i}^2) \end{split} J(θ)=Cost=f(bu,bi)J(θ)=u,iR(ruiμbubi)2+λ(ubu2+ibi2)
梯度下降参数更新原始公式:
θj:=θj−α∂∂θjJ(θ) \theta_j:=\theta_j-\alpha\cfrac{\partial }{\partial \theta_j}J(\theta) θj:=θjαθjJ(θ)
梯度下降更新bub_ubu:

​ 损失函数偏导推导:
∂∂buJ(θ)=∂∂buf(bu,bi)=2∑u,i∈R(rui−μ−bu−bi)(−1)+2λbu=−2∑u,i∈R(rui−μ−bu−bi)+2λ∗bu \begin{split} \cfrac{\partial}{\partial b_u} J(\theta)&=\cfrac{\partial}{\partial b_u} f(b_u, b_i) \\&=2\sum_{u,i\in R}(r_{ui}-\mu-b_u-b_i)(-1) + 2\lambda{b_u} \\&=-2\sum_{u,i\in R}(r_{ui}-\mu-b_u-b_i) + 2\lambda*b_u \end{split} buJ(θ)=buf(bu,bi)=2u,iR(ruiμbubi)(1)+2λbu=2u,iR(ruiμbubi)+2λbu
bub_ubu更新(因为alpha可以人为控制,所以2可以省略掉):
bu:=bu−α∗(−∑u,i∈R(rui−μ−bu−bi)+λ∗bu):=bu+α∗(∑u,i∈R(rui−μ−bu−bi)−λ∗bu) \begin{split} b_u&:=b_u - \alpha*(-\sum_{u,i\in R}(r_{ui}-\mu-b_u-b_i) + \lambda * b_u)\\ &:=b_u + \alpha*(\sum_{u,i\in R}(r_{ui}-\mu-b_u-b_i) - \lambda* b_u) \end{split} bu:=buα(u,iR(ruiμbubi)+λbu):=bu+α(u,iR(ruiμbubi)λbu)
同理可得,梯度下降更新bib_ibi:
bi:=bi+α∗(∑u,i∈R(rui−μ−bu−bi)−λ∗bi) b_i:=b_i + \alpha*(\sum_{u,i\in R}(r_{ui}-\mu-b_u-b_i) -\lambda*b_i) bi:=bi+α(u,iR(ruiμbubi)λbi)

step 2:随机梯度下降

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

单样本损失值:
error=rui−r^ui=rui−(μ+bu+bi)=rui−μ−bu−bi \begin{split} error &=r_{ui}-\hat{r}_{ui} \\&= r_{ui}-(\mu+b_u+b_i) \\&= r_{ui}-\mu-b_u-b_i \end{split} error=ruir^ui=rui(μ+bu+bi)=ruiμbubi
参数更新:
bu:=bu+α∗((rui−μ−bu−bi)−λ∗bu):=bu+α∗(error−λ∗bu)bi:=bi+α∗((rui−μ−bu−bi)−λ∗bi):=bi+α∗(error−λ∗bi) \begin{split} b_u&:=b_u + \alpha*((r_{ui}-\mu-b_u-b_i) -\lambda*b_u) \\ &:=b_u + \alpha*(error - \lambda*b_u) \\ \\ b_i&:=b_i + \alpha*((r_{ui}-\mu-b_u-b_i) -\lambda*b_i)\\ &:=b_i + \alpha*(error -\lambda*b_i) \end{split} bubi:=bu+α((ruiμbubi)λbu):=bu+α(errorλbu):=bi+α((ruiμbubi)λbi):=bi+α(errorλbi)

step 3:算法实现

# 导包
import pandas as pd
import numpy as npclass 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 = columnsdef fit(self, dataset):''':param dataset: uid, iid, rating:return:'''self.dataset = dataset# 用户评分数据:'''① 对 dataset 进行操作。dataset 是一个 Pandas DataFrame,它包含了用户ID(userId)、物品ID(movieId)和评分(rating).② 对数据集dataset按照self.columns[0]进行分组,分组后,每个分组代表一个用户的数据。③ 分组后对数据进行聚合操作,list 表示将每个分组中的元素转换为列表。具体来说,self.columns[1] 和 self.columns[2]    分别是 movieId 和 rating,这意味着对于每个用户(userId),我们将会把该用户评分的所有 movieId 和相应的 rating 收集到一个列表中.④ 最终结果是:对每个用户(userId),movieId和rating的列表将作为该用户的评分数据。userId        movieId       rating1001         [1, 2, 3]    [4.0, 5.0, 3.5]'''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'''① Python 中的 zip() 函数用于将多个可迭代对象作为参数,将每个对象中对应的元素打包成一个元组,然后返回这些元组组成的列表。如果传入的迭代器长度不一致,zip() 函数会以最短的迭代器长度为准,返回相同长度的列表。② self.users_ratings.index:用户ID列表(如 [1, 2, 3, ...])self.items_ratings.index:物品ID列表(如 [10, 11, 12, ...])③ np.zeros(len(self.users_ratings)):返回一个长度为用户数量的全零数组。np.zeros(len(self.items_ratings)):返回一个长度为物品数量的全零数组。④ zip()将用户Id列表和全0数组进行合并,合成一个这两个集合的元素组成的元组的列表,如:[(1,0),(2,0)]  dict()函数将zip产生的数据,转换成字典类型,比如bu = {1: 0.0, 2: 0.0, 3: 0.0}bi = {10: 0.0, 11: 0.0, 12: 0.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, bidef predict(self, uid, iid):predict_rating = self.global_mean + self.bu[uid] + self.bi[iid]return predict_ratingif __name__ == '__main__':dtype = [("userId", np.int32), ("movieId", np.int32), ("rating", np.float32)]dataset = pd.read_csv("./data/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 npdef 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, testsetdef 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 = 0for uid, iid, real_rating, pred_rating in predict_results:length += 1_rmse_sum += (pred_rating - real_rating) ** 2return round(np.sqrt(_rmse_sum / length), 4)def mae(predict_results):'''mae评估指标:param predict_results::return: mae'''length = 0_mae_sum = 0for 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 = 0for 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 = columnsdef 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的值,全部设为0bu = 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, bidef 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_ratingdef 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_ratingif __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)

梯度下降算法的公式理解

在整个机器学习优化过程中的核心:梯度下降更新公式是怎么来的?
我们一步一步推导并解释它的原理,确保你从根上理解它。


✅ 1. 背景:我们想做什么?

我们有一个损失函数 J(θ)J(\theta)J(θ),目标是:

找到使损失函数最小的参数 θ \text{找到使损失函数最小的参数 } \theta 找到使损失函数最小的参数 θ

例如,在你说的 Baseline 预测问题中,损失函数是:

J(θ)=∑(u,i)∈R(rui−μ−bu−bi)2+λ(∑ubu2+∑ibi2) J(\theta) = \sum_{(u,i)\in R}(r_{ui} - \mu - b_u - b_i)^2 + \lambda \left(\sum_u b_u^2 + \sum_i b_i^2\right) J(θ)=(u,i)R(ruiμbubi)2+λ(ubu2+ibi2)

我们的目标是:

找到一组 bub_ububib_ibi,让这个损失函数最小。


✅ 2. 关键思想:用梯度指明下降方向

梯度(偏导数)告诉我们:

损失函数对每个参数的“变化率”,也就是哪边更陡、更快上升。

为了让损失变小,我们应该往反方向走,这就是“梯度下降法”的思想。


✅ 3. 梯度下降更新公式

假设你要优化的参数是 θ\thetaθ,学习率是 α\alphaα,那么基本的梯度下降更新公式是:

θ:=θ−α⋅∂J∂θ \theta := \theta - \alpha \cdot \frac{\partial J}{\partial \theta} θ:=θαθJ

解释:

  • ∂J∂θ\frac{\partial J}{\partial \theta}θJ:损失函数对参数 θ\thetaθ 的偏导数(梯度)
  • α\alphaα:学习率(控制步长)
  • −α⋅∂J∂θ-\alpha \cdot \frac{\partial J}{\partial \theta}αθJ:表示“朝着使损失减少的方向移动一点”
  • := 表示“更新”,也就是新的 θ\thetaθ 用旧的 θ\thetaθ 减去这个变化值

✅ 4. 应用于你的例子:更新 bub_ubu

损失函数对 bub_ubu 的偏导是:

∂J∂bu=−2∑i∈Ru(rui−μ−bu−bi)+2λbu \frac{\partial J}{\partial b_u} = -2\sum_{i\in R_u}(r_{ui} - \mu - b_u - b_i) + 2\lambda b_u buJ=2iRu(ruiμbubi)+2λbu

代入梯度下降公式:

bu:=bu−α⋅(−2∑i∈Ru(rui−μ−bu−bi)+2λbu) b_u := b_u - \alpha \cdot \left( -2\sum_{i\in R_u}(r_{ui} - \mu - b_u - b_i) + 2\lambda b_u \right) bu:=buα(2iRu(ruiμbubi)+2λbu)

提出 2 并简化后(省略 2):

bu:=bu+α(∑i∈Ru(rui−μ−bu−bi)−λbu) b_u := b_u + \alpha \left( \sum_{i\in R_u}(r_{ui} - \mu - b_u - b_i) - \lambda b_u \right) bu:=bu+α(iRu(ruiμbubi)λbu)

这就是你看到的更新式。


✅ 总结
步骤意义
写出损失函数表达你要最小化的目标
对每个参数求偏导得到损失函数对参数的影响方向
代入更新公式 θ:=θ−α⋅∂J∂θ\theta := \theta - \alpha \cdot \frac{\partial J}{\partial \theta}θ:=θαθJ实现参数向“减少损失”的方向移动
简化公式可省略常数(如 2),吸收进学习率

校验评估的代码部分的理解
⭐ RMSE 和 MAE 是什么?

它们是 预测评分与真实评分之间误差的评估指标,常用于推荐系统、回归模型的性能评估。


📐 RMSE:Root Mean Squared Error(均方根误差)
公式:

RMSE=1n∑i=1n(r^i−ri)2 \text{RMSE} = \sqrt{ \frac{1}{n} \sum_{i=1}^{n} ( \hat{r}_i - r_i )^2 } RMSE=n1i=1n(r^iri)2

含义:
  • r^i\hat{r}_ir^i:模型预测的评分
  • rir_iri:实际的评分
  • nnn:测试集中有评分的数据条数
特点:
  • 大误差更加敏感(因为平方了差值)。
  • 如果你想惩罚预测偏离大的情况,选择 RMSE。

📐 MAE:Mean Absolute Error(平均绝对误差)
公式:

MAE=1n∑i=1n∣r^i−ri∣ \text{MAE} = \frac{1}{n} \sum_{i=1}^{n} | \hat{r}_i - r_i | MAE=n1i=1nr^iri

  • 含义:

  • 衡量预测评分与实际评分之间平均的“绝对差值”

  • 相比 RMSE,MAE 对异常值(大误差)不太敏感


✅ 对比总结
指标全称敏感度数学操作常用场景
RMSE均方根误差对大误差敏感差值平方开根关注预测偏差的严重程度
MAE平均绝对误差均匀对待每个误差直接取绝对值关注整体平均误差

📊 在推荐系统中的用途

在这段代码中,RMSE 和 MAE 被用来衡量训练好的推荐模型在测试集上的预测准确性。

pred_results = bcf.test(testset)    # 模型预测每个 (uid, iid) 的评分
rmse, mae = accuray(pred_results)   # 分别计算 RMSE 和 MAE
print("rmse: ", rmse, "mae: ", mae) # 输出误差
  • RMSE 越小 → 模型整体预测越准确(尤其是少有偏离大的情况)。
  • MAE 越小 → 模型在平均意义下更稳定。

🧪 举个简单例子

假设某个用户对电影的真实评分是 4,模型预测是:

  • 预测1:3.9 → 误差是 0.1
  • 预测2:2.0 → 误差是 2.0

MAE:平均误差 = 0.1+2.02=1.05\frac{0.1 + 2.0}{2} = 1.0520.1+2.0=1.05

RMSE:平方平均 = (0.1)2+(2.0)22=2.005≈1.415\sqrt{ \frac{(0.1)^2 + (2.0)^2}{2} } = \sqrt{2.005} \approx 1.4152(0.1)2+(2.0)2=2.0051.415

注意:RMSE > MAE,因为 RMSE 惩罚了那个大的 2.0 的误差。


✅ 总结一句话

RMSE 和 MAE 是评价推荐系统预测评分准确性最常用的两个指标,能量化你模型的“靠谱程度”。
选择哪个要看你是更关注大误差(用 RMSE)还是整体平均误差(用 MAE)。

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

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

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

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

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

同样,损失函数:
J(θ)=∑u,i∈R(rui−μ−bu−bi)2+λ∗(∑ubu2+∑ibi2) J(\theta)=\sum_{u,i\in R}(r_{ui}-\mu-b_u-b_i)^2 + \lambda*(\sum_u {b_u}^2 + \sum_i {b_i}^2) J(θ)=u,iR(ruiμbubi)2+λ(ubu2+ibi2)
对损失函数求偏导:
∂∂buf(bu,bi)=−2∑u,i∈R(rui−μ−bu−bi)+2λ∗bu \cfrac{\partial}{\partial b_u} f(b_u, b_i) =-2 \sum_{u,i\in R}(r_{ui}-\mu-b_u-b_i) + 2\lambda * b_u buf(bu,bi)=2u,iR(ruiμbubi)+2λbu
令偏导为0,则可得:
∑u,i∈R(rui−μ−bu−bi)=λ∗bu∑u,i∈R(rui−μ−bi)=∑u,i∈Rbu+λ∗bu \sum_{u,i\in R}(r_{ui}-\mu-b_u-b_i) = \lambda* b_u \\\sum_{u,i\in R}(r_{ui}-\mu-b_i) = \sum_{u,i\in R} b_u+\lambda * b_u u,iR(ruiμbubi)=λbuu,iR(ruiμbi)=u,iRbu+λbu
为了简化公式,这里令∑u,i∈Rbu≈∣R(u)∣∗bu\sum_{u,i\in R} b_u \approx |R(u)|*b_uu,iRbuR(u)bu,即直接假设每一项的偏置都相等,可得:
bu:=∑u,i∈R(rui−μ−bi)λ1+∣R(u)∣ b_u := \cfrac {\sum_{u,i\in R}(r_{ui}-\mu-b_i)}{\lambda_1 + |R(u)|} bu:=λ1+R(u)u,iR(ruiμbi)
其中∣R(u)∣|R(u)|R(u)表示用户uuu的有过评分数量

同理可得:
bi:=∑u,i∈R(rui−μ−bu)λ2+∣R(i)∣ b_i := \cfrac {\sum_{u,i\in R}(r_{ui}-\mu-b_u)}{\lambda_2 + |R(i)|} bi:=λ2+R(i)u,iR(ruiμbu)
其中∣R(i)∣|R(i)|R(i)表示物品iii收到的评分数量

bub_ububib_ibi分别属于用户和物品的偏置,因此他们的正则参数可以分别设置两个独立的参数

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

通过最小二乘推导,我们最终分别得到了bub_ububib_ibi的表达式,但他们的表达式中却又各自包含对方,因此这里我们将利用一种叫交替最小二乘的方法来计算他们的值:

  • 计算其中一项,先固定其他未知参数,即看作其他未知参数为已知
  • 如求bub_ubu时,将bib_ibi看作是已知;求bib_ibi时,将bub_ubu看作是已知;如此反复交替,不断更新二者的值,求得最终的结果。这就是交替最小二乘法(ALS)
step 3: 算法实现
import pandas as pd
import numpy as npclass 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 = columnsdef 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的值,全部设为0bu = 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 = 0for 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 = 0for iid, rating in zip(iids, ratings):_sum += rating - self.global_mean - bi[iid]bu[uid] = _sum / (self.reg_bu + len(iids))return bu, bidef predict(self, uid, iid):predict_rating = self.global_mean + self.bu[uid] + self.bi[iid]return predict_ratingif __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 npdef 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, testsetdef 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 = 0for uid, iid, real_rating, pred_rating in predict_results:length += 1_rmse_sum += (pred_rating - real_rating) ** 2return round(np.sqrt(_rmse_sum / length), 4)def mae(predict_results):'''mae评估指标:param predict_results::return: mae'''length = 0_mae_sum = 0for 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 = 0for 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 = columnsdef 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的值,全部设为0bu = 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 = 0for 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 = 0for iid, rating in zip(iids, ratings):_sum += rating - self.global_mean - bi[iid]bu[uid] = _sum / (self.reg_bu + len(iids))return bu, bidef 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_ratingdef 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_ratingif __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)

函数求导:

在这里插入图片描述
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/diannao/93138.shtml
繁体地址,请注明出处:http://hk.pswp.cn/diannao/93138.shtml
英文地址,请注明出处:http://en.pswp.cn/diannao/93138.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

望言OCR视频字幕提取2025终极评测:免费版VS专业版提全方位对比(含免费下载)

大家好&#xff0c;欢迎来到程序视点&#xff01;我是你们的老朋友.小二&#xff01;一、产品定位&#xff1a;AI时代的视频字幕处理专家望言OCR作为专业的视频硬字幕提取工具&#xff0c;在AI视频处理领域占据重要地位。最新评测显示&#xff0c;其免费版本依然保持着惊人的97…

Matplotlib(二)- Matplotlib简单绘图

文章目录一、pyplot模块介绍二、Matplotlib简单绘图1. 绘制折线图1.1 折线图介绍1.2 plt.plot()函数介绍1.3 绘制简单折线图1.3.1 绘制单条折线图1.3.2 绘制多条折线图1.4 示例&#xff1a;绘制天气气温折线图2. 绘制柱形图2.1 柱形图介绍2.2 plt.bar()函数介绍2.3 绘制柱形图2…

【世纪龙科技】数字化技术解锁新能源汽车电驱动总成装调与检修

随着新能源汽车产业加速升级&#xff0c;电驱动总成装调与检修技术已成为职业院校汽车专业教学的核心挑战。传统实训模式面临设备投入高、更新周期长、高压操作安全隐患多、教学与产业需求脱节等现实问题&#xff0c;导致学生实践能力培养滞后于行业发展。如何通过数字化手段突…

springboot基于Java与MySQL库的健身俱乐部管理系统设计与实现

用户&#xff1a;注册&#xff0c;登录&#xff0c;健身教练&#xff0c;健身课程&#xff0c;健身器材&#xff0c;健身资讯&#xff0c;课程报名管理&#xff0c;教练预约管理&#xff0c;会员充值管理&#xff0c;个人中心管理员&#xff1a;登录&#xff0c;个人中心&#…

如何修改debian的ip地址

编辑配置文件&#xff1a; sudo nano /etc/network/interfaces修改内容&#xff08;示例将 eth0 设为静态IP&#xff09;&#xff1a; auto eth0 iface eth0 inet static address 192.168.1.100 netmask 255.255.255.0 gateway 192.168.1.1 dns-nameservers 8.8.8.8 8.8.4.4 #…

haproxy七层代理(知识点+相关实验部署)

目录 1.负载均衡介绍 1.1 什么是负载均衡 1.2 为什么用负载均衡 1.3 负载均衡类型 1.3.1 四层负载均衡 1.3.2 七层负载均衡 1.3.3 四层和七层的区别 2.haproxy简介 2.1 haproxy主要特性 2.2 haproxy的优点与缺点 3.haproxy的安装和服务信息 3.1 实验环境 3.1.1 hap…

【集合】JDK1.8 HashMap 底层数据结构深度解析

一、核心数据结构&#xff1a;为什么是 "数组 链表 红黑树"&#xff1f;​HashMap 的底层设计本质是用空间换时间&#xff0c;通过哈希表的快速定位特性&#xff0c;结合链表和红黑树处理冲突&#xff0c;平衡查询与插入效率。​1.1 基础容器&#xff1a;哈希桶数组…

【element-ui】HTML引入本地文件出现font找不到/fonts/element-icons.woff

文章目录目录结构问题复现解决办法目录结构 |-web|- public|- lib|- ...|- index.htmlindex.html <!DOCTYPE html> <html> <head><meta charset"UTF-8"><!-- import CSS --><link rel"stylesheet" href"./public/…

Windows|CUDA和cuDNN下载和安装,默认安装在C盘和不安装在C盘的两种方法

本篇文章将详细介绍在Windows操作系统中配置CUDA和cuDNN的步骤。通过本教程&#xff0c;您将能够轻松完成CUDA和cuDNN的安装、环境变量配置以及与深度学习框架&#xff08;如TensorFlow和PyTorch&#xff09;兼容性测试&#xff0c;从而为您的深度学习项目提供强大的硬件支持。…

Vue 项目动态接口获取翻译数据实现方案(前端处理语言翻译 vue-i18n)

在大型多语言项目中&#xff0c;将翻译数据硬编码在项目中往往不够灵活。通过接口动态获取翻译数据&#xff0c;并结合本地缓存提升性能&#xff0c;是更优的国际化实现方式。本文将详细介绍如何在 Vue 项目中实现这一方案。 方案优势 灵活性高&#xff1a;翻译内容更新无需修改…

Mybatis-plus多数据源

适用于多种场景&#xff1a;纯粹多库、 读写分离、 一主多从、 混合模式等目前我们就来模拟一个纯粹多库的一个场景&#xff0c;其他场景类似场景说明&#xff1a;我们创建两个库&#xff0c;分别为&#xff1a; mybatis_plus&#xff08;以前的库不动&#xff09;与my…

广东省省考备考(第五十六天7.25)——常识:科技常识(听课后强化训练)

错题解析解析解析解析解析解析解析解析解析标记题解析解析今日题目正确率&#xff1a;40%

RabbitMQ简述

RabbitMQ简述 RabbitMQ 是一个开源的 消息代理&#xff08;Message Broker&#xff09; 软件&#xff0c;实现了 高级消息队列协议&#xff08;AMQP&#xff09;&#xff0c;用于在分布式系统中存储、转发消息&#xff0c;支持异步通信、解耦服务、负载均衡和消息缓冲。 核心…

skywalking应用性能监控

1.skywalking描述 官方文档 SkyWalking 是一个开源的可观测性平台&#xff0c;用于收集、分析、汇总和可视化来自服务及云原生基础设施的数据。SkyWalking 为维护分布式系统的清晰视图提供了简便的方法&#xff0c;即使是在跨云环境中也能做到。它是一款专为云原生、基于容器的…

如何彻底清除服务器上的恶意软件与后门

清除服务器上的恶意软件与后门 是确保服务器安全的关键步骤。恶意软件和后门可能导致数据泄露、性能下降&#xff0c;甚至服务器被攻击者完全控制。以下是彻底清除恶意软件与后门的详细指南&#xff0c;包括 检测、清理、修复与预防 的步骤。1. 彻底清除恶意软件与后门的步骤1.…

Linux和Windows基于V4L2和TCP的QT监控

最近工作需要用QT做一个网络摄像头测试&#xff0c;简单记录&#xff1a;服务端&#xff0c;主机配置为Ubuntu&#xff0c;通过端口12345采集传输MJPEG格式图片windows客户端&#xff0c;QT Creator通过ip地址连接访问提前准备服务端需要安装QT5sudo apt-get install qt5-defau…

yolo格式

labelimg中的格式yolo格式id 框中心点X对于总图片的比例 框中心点Y对于总图片的比例 框X总长度对于总图片的比例 框Y总长度对于总图片的比例

Day 8-zhou R包批量安装小补充!!!

BiocManager::install(c(“S4Vectors”, “BiocGenerics”)) 以下是使用BiocManager安装S4Vectors和BiocGenerics包的详细步骤。这些步骤基于最新的Bioconductor和R版本&#xff08;R 4.5&#xff09;。 安装步骤安装BiocManager 如果你还没有安装BiocManager&#xff0c;可以使…

电商项目_核心业务_数据归档

无论采用哪种存储系统&#xff0c;数据查询的耗时取决于两个因素查找的时间复杂度数据总量查找的时间复杂度又取决于查找算法数据存储结构以Mysql存储的订单数据为例&#xff0c;随着业务的发展&#xff0c;数据量越来越大&#xff0c;对一些历史归档数据的查询&#xff0c;如果…

第十讲:stack、queue、priority_queue以及deque

目录 1、stack 1.1、stack的使用 1.2、stack的OJ题 1.2.1、最小栈 1.2.2、栈的压入弹出序列 1.2.3、逆波兰表达式求值 1.3、stack的模拟实现 2、queue 2.1、queue的使用 2.2、queue的OJ题 2.2.1、二叉树的层序遍历 2.3、queue的模拟实现 3、priority_queue 3.1、…