推荐系统算法

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

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

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

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

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

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

Baseline:基准预测

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

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

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

Baseline目标:

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

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

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

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

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

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

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

    • 首先计算出整个评分数据集的平均评分μ​\mu​μ​是3.5分
    • 用户A比较苛刻,普遍比平均评分低0.5分,即用户A的偏置值bub_ubu​是-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分。

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


加入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,i∈R∑​(rui​−μ−bu​−bi​)2+λ∗(u∑​bu​2+i∑​bi​2)
公式解析:

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

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

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

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

step 1:梯度下降法推导

损失函数: ( λ 为正则化系数)

梯度下降参数更新原始公式:(公式中α为学习率)
θj:=θj−α∂∂θjJ(θ)\theta_j:=\theta_j-\alpha\cfrac{\partial }{\partial \theta_j}J(\theta) θj​:=θj​−α∂θj​∂​J(θ)
梯度下降更新bub_ubu​:

​ 损失函数偏导推导:

​ bu​b_u​bu​​更新(因为学习率alpha可以人为控制,所以2可以省略掉):

同理可得,梯度下降更新bi​b_i​bi​​:
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,i∈R∑​(rui​−μ−bu​−bi​)−λ∗bi​)

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))
  • 数据初始化

    • tips 更多关于groupby的 API 详见 http://pandas.pydata.org/pandas-docs/stable/reference/groupby.html
# 用户评分数据  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])
  • itertuples 解释

  • 预测评分

def predict(uid, iid):predict_rating = global_mean + bu[uid] + bi[iid]return predict_rating
  • 整体封装
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# 用户评分数据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):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 = 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))trainset_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)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]testset = ratings.drop(trainset_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)
方法二:交替最小二乘法优化

使用交替最小二乘法优化算法预测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,i∈R∑​(rui​−μ−bu​−bi​)2+λ∗(u∑​bu​2+i∑​bi​2)

经过交替最小二乘
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,i∈R​(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,i∈R​(rui​−μ−bu​)​

其中∣R(i)∣|R(i)|∣R(i)∣表示物品i​i​i​收到的评分数量

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

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

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

  • 计算其中一项,先固定其他未知参数,即看作其他未知参数为已知
  • 如求bub_ubu​时,将bib_ibi​看作是已知;求bib_ibi​时,将bu​b_u​bu​​看作是已知;如此反复交替,不断更新二者的值,求得最终的结果。这就是交替最小二乘法(ALS)
step 3: 算法实现
  • 数据加载初始化与之前完全相同
  • 迭代更新bub_ubu​ bib_ibi​
for i in range(number_epochs):print("iter%d" % i)for iid, uids, ratings in items_ratings.itertuples(index=True):_sum = 0for 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 = 0for 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 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))trainset_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)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]testset = ratings.drop(trainset_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)

基于矩阵分解的CF算法

矩阵分解发展史

Traditional SVD:
SVD

通常SVD矩阵分解指的是SVD(奇异值)分解技术,在这我们姑且将其命名为Traditional SVD(传统并经典着)其公式如下:


Traditional SVD分解的形式为3个矩阵相乘,中间矩阵为奇异值矩阵。如果想运用SVD分解的话,有一个前提是要求矩阵是稠密的,即矩阵里的元素要非空,否则就不能运用SVD分解。

很显然我们的数据其实绝大多数情况下都是稀疏的,因此如果要使用Traditional SVD,一般的做法是先用均值或者其他统计学方法来填充矩阵,然后再运用Traditional SVD分解降维,但这样做明显对数据的原始性造成一定影响。

FunkSVD(LFM)

刚才提到的Traditional SVD首先需要填充矩阵,然后再进行分解降维,同时存在计算复杂度高的问题,因为要分解成3个矩阵,所以后来提出了Funk SVD的方法,它不在将矩阵分解为3个矩阵,而是分解为2个用户-隐含特征,项目-隐含特征的矩阵,Funk SVD也被称为最原始的LFM模型


借鉴线性回归的思想,通过最小化观察数据的平方来寻求最优的用户和项目的隐含向量表示。同时为了避免过度拟合(Overfitting)观测数据,又提出了带有L2正则项的FunkSVD,上公式:

以上两种最优化函数都可以通过梯度下降或者随机梯度下降法来寻求最优解。

BiasSVD:

在FunkSVD提出来之后,出现了很多变形版本,其中一个相对成功的方法是BiasSVD,顾名思义,即带有偏置项的SVD分解:

它基于的假设和Baseline基准预测是一样的,但这里将Baseline的偏置引入到了矩阵分解中

SVD++:

人们后来又提出了改进的BiasSVD,被称为SVD++,该算法是在BiasSVD的基础上添加了用户的隐式反馈信息:

显示反馈指的用户的评分这样的行为,隐式反馈指用户的浏览记录、购买记录、收听记录等。
SVD++是基于这样的假设:在BiasSVD基础上,认为用户对于项目的历史浏览记录、购买记录、收听记录等可以从侧面反映用户的偏好。

Traditional SVD:
将矩阵分解为3个矩阵,中间为奇异值矩阵。被分解矩阵必须为稠密矩阵,在工厂中无法应用
LFM FUNK SVD:
将一个矩阵分解为两个矩阵,其中一个是用户-隐含特征矩阵,另一个是物品-隐含特征矩阵
Bias SVD:
在原来的Funk SVD基础上,加入了偏置项。
SVD++:
SVD++是基于这样的假设:在BiasSVD基础上,认为用户对于项目的历史浏览记录、购买记录、收听记录等可以从侧面反映用户的偏好。
在Bias SVD基础上,添加了用户的隐式反馈信息。(显示反馈指的用户的评分这样的行为,隐式反馈指用户的浏览记录、购买记录、收听记录等。)

基于矩阵分解的CF算法实现(一):LFM

LFM也就是前面提到的Funk SVD矩阵分解

LFM原理解析

LFM(latent factor model)隐语义模型核心思想是通过隐含特征联系用户和物品,如下图:

  • P矩阵是User-LF矩阵,即用户和隐含特征矩阵。LF有三个,表示共总有三个隐含特征。
  • Q矩阵是LF-Item矩阵,即隐含特征和物品的矩阵
  • R矩阵是User-Item矩阵,由P*Q得来
  • 能处理稀疏评分矩阵

利用矩阵分解技术,将原始User-Item的评分矩阵(稠密/稀疏)分解为P和Q矩阵,然后利用P∗Q​P*Q​P∗Q​还原出User-Item评分矩阵R​R​R​。整个过程相当于降维处理,其中:

  • 矩阵值P11​P_{11}​P11​​表示用户1对隐含特征1的权重值

  • 矩阵值Q11​Q_{11}​Q11​​表示隐含特征1在物品1上的权重值

  • 矩阵值R11R_{11}R11​就表示预测的用户1对物品1的评分,且

利用LFM预测用户对物品的评分,k​k​k​表示隐含特征数量:

因此最终,我们的目标也就是要求出P矩阵和Q矩阵及其当中的每一个值,然后再对用户-物品的评分进行预测。

损失函数

同样对于评分预测我们利用平方差来构建损失函数:

加入L2正则化:
Cost=∑u,i∈R(rui−∑k=1kpukqik)2+λ(∑Upuk2+∑Iqik2)Cost = \sum_{u,i\in R} (r_{ui}-{\sum_{k=1}}^k p_{uk}q_{ik})^2 + \lambda(\sum_U{p_{uk}}^2+\sum_I{q_{ik}}^2) Cost=u,i∈R∑​(rui​−k=1∑​kpuk​qik​)2+λ(U∑​puk​2+I∑​qik​2)

随机梯度下降法优化

梯度下降更新参数puk和qik​p_{uk}和q_{ik}​puk​和qik​​:(α学习率 λ正则化系数)

随机梯度下降: 向量乘法 每一个分量相乘 求和

由于P矩阵和Q矩阵是两个不同的矩阵,通常分别采取不同的正则参数,如λ1和λ2\lambda_1和\lambda_2λ1​和λ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))
  • 数据初始化

    • tips 更多关于groupby的 API 详见 http://pandas.pydata.org/pandas-docs/stable/reference/groupby.html
# 用户评分数据  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()
# 初始化P Q  610  9700   K值  610*K    9700*K
# User-LF  10 代表 隐含因子个数是10个
P = dict(zip(users_ratings.index,np.random.rand(len(users_ratings),10).astype(np.float32)))
# Item-LF
Q = dict(zip(items_ratings.index,np.random.rand(len(items_ratings),10).astype(np.float32)))
  • 梯度下降优化损失函数
#梯度下降优化损失函数
for i in range(15):print('*'*10,i)for uid,iid,real_rating in dataset.itertuples(index = False):#遍历 用户 物品的评分数据 通过用户的id 到用户矩阵中获取用户向量v_puk = P[uid]# 通过物品的uid 到物品矩阵里获取物品向量v_qik = Q[iid]#计算损失error = real_rating-np.dot(v_puk,v_qik)# 0.02学习率 0.01正则化系数v_puk += 0.02*(error*v_qik-0.01*v_puk)v_qik += 0.02*(error*v_puk-0.01*v_qik)P[uid] = v_pukQ[iid] = v_qik
  • 评分预测
def predict(self, uid, iid):# 如果uid或iid不在,我们使用全剧平均分作为预测结果返回if uid not in self.users_ratings.index or iid not in self.items_ratings.index:return self.globalMeanp_u = self.P[uid]q_i = self.Q[iid]return np.dot(p_u, q_i)
'''
LFM Model
'''
import pandas as pd
import numpy as np# 评分预测    1-5
class LFM(object):def __init__(self, alpha, reg_p, reg_q, number_LatentFactors, number_epochs, columns):self.alpha = alpha # 学习率self.reg_p = reg_p    # P矩阵正则self.reg_q = reg_q    # Q矩阵正则self.number_LatentFactors = number_LatentFactors  # 隐式类别数量self.number_epochs = number_epochs    # 最大迭代次数self.columns = columnsdef fit(self, dataset):'''fit dataset:param dataset: uid, iid, rating:return:'''self.dataset = datasetself.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.globalMean = self.dataset[self.columns[2]].mean()self.P, self.Q = self.sgd()def _init_matrix(self):'''初始化P和Q矩阵,同时为设置0,1之间的随机值作为初始值:return:'''# User-LFP = dict(zip(self.users_ratings.index,np.random.rand(len(self.users_ratings), self.number_LatentFactors).astype(np.float32)))# Item-LFQ = dict(zip(self.items_ratings.index,np.random.rand(len(self.items_ratings), self.number_LatentFactors).astype(np.float32)))return P, Qdef sgd(self):'''使用随机梯度下降,优化结果:return:'''P, Q = self._init_matrix()for i in range(self.number_epochs):print("iter%d"%i)error_list = []for uid, iid, r_ui in self.dataset.itertuples(index=False):# User-LF P## Item-LF Qv_pu = P[uid] #用户向量v_qi = Q[iid] #物品向量err = np.float32(r_ui - np.dot(v_pu, v_qi))v_pu += self.alpha * (err * v_qi - self.reg_p * v_pu)v_qi += self.alpha * (err * v_pu - self.reg_q * v_qi)P[uid] = v_pu Q[iid] = v_qi# for k in range(self.number_of_LatentFactors):#     v_pu[k] += self.alpha*(err*v_qi[k] - self.reg_p*v_pu[k])#     v_qi[k] += self.alpha*(err*v_pu[k] - self.reg_q*v_qi[k])error_list.append(err ** 2)print(np.sqrt(np.mean(error_list)))return P, Qdef predict(self, uid, iid):# 如果uid或iid不在,我们使用全剧平均分作为预测结果返回if uid not in self.users_ratings.index or iid not in self.items_ratings.index:return self.globalMeanp_u = self.P[uid]q_i = self.Q[iid]return np.dot(p_u, q_i)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_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))# def __init__(self, alpha, reg_p, reg_q, number_LatentFactors, num_epoches, columns):lfm = LFM(0.02, 0.01, 0.01, 10, 15,['userId', 'movieId', 'rating'])lfm.fit(dataset)while True:uid = input("uid: ")iid = input("iid: ")print(lfm.predict(int(uid), int(iid)))

对于这里的解释,可以参考jupyter的结果

self.users_ratings = dataset.groupby(self.columns[0]).agg([list])[[self.columns[1], self.columns[2]]]

基于矩阵分解的CF算法实现(二):BiasSvd

BiasSvd其实就是前面提到的Funk SVD矩阵分解基础上加上了偏置项。

BiasSvd

利用BiasSvd预测用户对物品的评分,k表示隐含特征数量:

损失函数

同样对于评分预测我们利用平方差来构建损失函数:

加入L2正则化:

Cost=∑u,i∈R(rui−μ−bu−bi−∑k=1kpukqik)2+λ(∑Ubu2+∑Ibi2+∑Upuk2+∑Iqik2)Cost = \sum_{u,i\in R} (r_{ui}-\mu - b_u - b_i-{\sum_{k=1}}^k p_{uk}q_{ik})^2 \\+ \lambda(\sum_U{b_u}^2+\sum_I{b_i}^2+\sum_U{p_{uk}}^2+\sum_I{q_{ik}}^2) Cost=u,i∈R∑​(rui​−μ−bu​−bi​−k=1∑​kpuk​qik​)2+λ(U∑​bu​2+I∑​bi​2+U∑​puk​2+I∑​qik​2)

随机梯度下降法优化

梯度下降更新参数pukp_{uk}puk​:

同理:

bu:=bu+α[∑u,i∈R(rui−μ−bu−bi−∑k=1kpukqik)−λbu]b_u:=b_u + \alpha[\sum_{u,i\in R} (r_{ui}-\mu - b_u - b_i-{\sum_{k=1}}^k p_{uk}q_{ik}) - \lambda b_u] bu​:=bu​+α[u,i∈R∑​(rui​−μ−bu​−bi​−k=1∑​kpuk​qik​)−λbu​]

bi:=bi+α[∑u,i∈R(rui−μ−bu−bi−∑k=1kpukqik)−λbi]b_i:=b_i + \alpha[\sum_{u,i\in R} (r_{ui}-\mu - b_u - b_i-{\sum_{k=1}}^k p_{uk}q_{ik}) - \lambda b_i] bi​:=bi​+α[u,i∈R∑​(rui​−μ−bu​−bi​−k=1∑​kpuk​qik​)−λbi​]

随机梯度下降:

bu:=bu+α[(rui−μ−bu−bi−∑k=1kpukqik)−λ3bu]b_u:=b_u + \alpha[(r_{ui}-\mu - b_u - b_i-{\sum_{k=1}}^k p_{uk}q_{ik}) - \lambda_3 b_u] bu​:=bu​+α[(rui​−μ−bu​−bi​−k=1∑​kpuk​qik​)−λ3​bu​]

bi:=bi+α[(rui−μ−bu−bi−∑k=1kpukqik)−λ4bi]b_i:=b_i + \alpha[(r_{ui}-\mu - b_u - b_i-{\sum_{k=1}}^k p_{uk}q_{ik}) - \lambda_4 b_i] bi​:=bi​+α[(rui​−μ−bu​−bi​−k=1∑​kpuk​qik​)−λ4​bi​]

由于P矩阵和Q矩阵是两个不同的矩阵,通常分别采取不同的正则参数,如λ1和λ2\lambda_1和\lambda_2λ1​和λ2​

算法实现

'''
BiasSvd Model
'''
import math
import random
import pandas as pd
import numpy as npclass BiasSvd(object):def __init__(self, alpha, reg_p, reg_q, reg_bu, reg_bi, number_LatentFactors=10, number_epochs=10, columns=["uid", "iid", "rating"]):self.alpha = alpha # 学习率self.reg_p = reg_pself.reg_q = reg_qself.reg_bu = reg_buself.reg_bi = reg_biself.number_LatentFactors = number_LatentFactors  # 隐式类别数量self.number_epochs = number_epochsself.columns = columnsdef fit(self, dataset):'''fit dataset:param dataset: uid, iid, rating:return:'''self.dataset = pd.DataFrame(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.globalMean = self.dataset[self.columns[2]].mean()self.P, self.Q, self.bu, self.bi = self.sgd()def _init_matrix(self):'''初始化P和Q矩阵,同时为设置0,1之间的随机值作为初始值:return:'''# User-LFP = dict(zip(self.users_ratings.index,np.random.rand(len(self.users_ratings), self.number_LatentFactors).astype(np.float32)))# Item-LFQ = dict(zip(self.items_ratings.index,np.random.rand(len(self.items_ratings), self.number_LatentFactors).astype(np.float32)))return P, Qdef sgd(self):'''使用随机梯度下降,优化结果:return:'''P, Q = self._init_matrix()# 初始化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)error_list = []for uid, iid, r_ui in self.dataset.itertuples(index=False):v_pu = P[uid]v_qi = Q[iid]err = np.float32(r_ui - self.globalMean - bu[uid] - bi[iid] - np.dot(v_pu, v_qi))v_pu += self.alpha * (err * v_qi - self.reg_p * v_pu)v_qi += self.alpha * (err * v_pu - self.reg_q * v_qi)P[uid] = v_pu Q[iid] = v_qibu[uid] += self.alpha * (err - self.reg_bu * bu[uid])bi[iid] += self.alpha * (err - self.reg_bi * bi[iid])error_list.append(err ** 2)print(np.sqrt(np.mean(error_list)))return P, Q, bu, bidef predict(self, uid, iid):if uid not in self.users_ratings.index or iid not in self.items_ratings.index:return self.globalMeanp_u = self.P[uid]q_i = self.Q[iid]return self.globalMean + self.bu[uid] + self.bi[iid] + np.dot(p_u, q_i)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))bsvd = BiasSvd(0.02, 0.01, 0.01, 0.01, 0.01, 10, 20)bsvd.fit(dataset)while True:uid = input("uid: ")iid = input("iid: ")print(bsvd.predict(int(uid), int(iid)))

基于内容的推荐算法(Content-Based)

简介

基于内容的推荐方法是非常直接的,它以物品的内容描述信息为依据来做出的推荐,本质上是基于对物品和用户自身的特征或属性的直接分析和计算。

例如,假设已知电影A是一部喜剧,而恰巧我们得知某个用户喜欢看喜剧电影,那么我们基于这样的已知信息,就可以将电影A推荐给该用户。

基于内容的推荐实现步骤

  • 画像构建。顾名思义,画像就是刻画物品或用户的特征。本质上就是给用户或物品贴标签。

    • 物品画像:例如给电影《战狼2》贴标签,可以有哪些?

      “动作”、“吴京”、“吴刚”、“张翰”、“大陆电影”、“国产”、“爱国”、"军事"等等一系列标签是不是都可以贴上

    • 用户画像:例如已知用户的观影历史是:"《战狼1》"、"《战狼2》"、"《建党伟业》"、"《建军大业》"、"《建国大业》"、"《红海行动》"、"《速度与激情1-8》"等,我们是不是就可以分析出该用户的一些兴趣特征如:“爱国”、“战争”、“赛车”、“动作”、“军事”、“吴京”、"韩三平"等标签。

问题:物品的标签来自哪儿?
  1. PGC 物品画像–冷启动

    • 物品自带的属性(物品一产生就具备的):如电影的标题、导演、演员、类型等等
    • 服务提供方设定的属性(服务提供方为物品附加的属性):如短视频话题、微博话题(平台拟定)
    • 其他渠道:如爬虫
  2. UGC 冷启动问题
    • 用户在享受服务过程中提供的物品的属性:如用户评论内容,微博话题(用户拟定)

根据PGC内容构建的物品画像的可以解决物品的冷启动问题

基于内容推荐的算法流程:
  • 根据PGC/UGC内容构建物品画像
  • 根据用户行为记录生成用户画像
  • 根据用户画像从物品中寻找最匹配的TOP-N物品进行推荐
物品冷启动处理:
  • 根据PGC内容构建物品画像
  • 利用物品画像计算物品间两两相似情况
  • 为每个物品产生TOP-N最相似的物品进行相关推荐:如与该商品相似的商品有哪些?与该文章相似文章有哪些?

基于内容的电影推荐:物品画像

物品画像构建步骤:

  • 利用tags.csv中每部电影的标签作为电影的候选关键词
  • 利用TF·IDF计算每部电影的标签的tfidf值,选取TOP-N个关键词作为电影画像标签
  • 将电影的分类词直接作为每部电影的画像标签

基于TF-IDF的特征提取技术

前面提到,物品画像的特征标签主要都是指的如电影的导演、演员、图书的作者、出版社等结构话的数据,也就是他们的特征提取,尤其是体征向量的计算是比较简单的,如直接给作品的分类定义0或者1的状态。

但另外一些特征,比如电影的内容简介、电影的影评、图书的摘要等文本数据,这些被称为非结构化数据,首先他们本应该也属于物品的一个特征标签,但是这样的特征标签进行量化时,也就是计算它的特征向量时是很难去定义的。

因此这时就需要借助一些自然语言处理、信息检索等技术,将如用户的文本评论或其他文本内容信息的非结构化数据进行量化处理,从而实现更加完善的物品画像/用户画像。

TF-IDF算法便是其中一种在自然语言处理领域中应用比较广泛的一种算法。可用来提取目标文档中,并得到关键词用于计算对于目标文档的权重,并将这些权重组合到一起得到特征向量。

算法原理

TF-IDF自然语言处理领域中计算文档中词或短语的权值的方法,是词频(Term Frequency,TF)和逆转文档频率(Inverse Document Frequency,IDF)的乘积。TF指的是某一个给定的词语在该文件中出现的次数。这个数字通常会被正规化,以防止它偏向长的文件(同一个词语在长文件里可能会比短文件有更高的词频,而不管该词语重要与否)。IDF是一个词语普遍重要性的度量,某一特定词语的IDF,可以由总文件数目除以包含该词语之文件的数目,再将得到的商取对数得到。

TF-IDF算法基于一个这样的假设:若一个词语在目标文档中出现的频率高而在其他文档中出现的频率低,那么这个词语就可以用来区分出目标文档。这个假设需要掌握的有两点:

  • 在本文档出现的频率高;
  • 在其他文档出现的频率低。

因此,TF-IDF算法的计算可以分为词频(Term Frequency,TF)和逆转文档频率(Inverse Document Frequency,IDF)两部分,由TF和IDF的乘积来设置文档词语的权重。

TF指的是一个词语在文档中的出现频率。假设文档集包含的文档数为NNN,文档集中包含关键词kik_iki​的文档数为nin_ini​,fijf_{ij}fij​表示关键词kik_iki​在文档djd_jdj​中出现的次数,fdjf_{dj}fdj​表示文档djd_jdj​中出现的词语总数,kik_iki​在文档dj中的词频TFijTF_{ij}TFij​定义为:
TFij=fijfdjTF_{ij}=\frac {f_{ij}}{f_{dj}}TFij​=fdj​fij​​
并且注意,这个数字通常会被正规化,以防止它偏向长的文件(指同一个词语在长文件里可能会比短文件有更高的词频,而不管该词语重要与否)。

IDF是一个词语普遍重要性的度量。表示某一词语在整个文档集中出现的频率,由它计算的结果取对数得到关键词kik_iki​的逆文档频率IDFiIDF_iIDFi​:
IDFi=logNniIDF_i=log\frac {N}{n_i}IDFi​=logni​N​

由TF和IDF计算词语的权重为:
wij=TFijw_{ij}=TF_{ij}wij​=TFij​·IDFi=fijfdjIDF_{i}=\frac {f_{ij}}{f_{dj}}IDFi​=fdj​fij​​·logNnilog\frac {N}{n_i}logni​N​

结论:TF-IDF与词语在文档中的出现次数成正比,与该词在整个文档集中的出现次数成反比。

用途:在目标文档中,提取关键词(特征标签)的方法就是将该文档所有词语的TF-IDF计算出来并进行对比,取其中TF-IDF值最大的k个数组成目标文档的特征向量用以表示文档。

注意:文档中存在的停用词(Stop Words),如“是”、“的”之类的,对于文档的中心思想表达没有意义的词,在分词时需要先过滤掉再计算其他词语的TF-IDF值。

算法举例

对于计算影评的TF-IDF,以电影“加勒比海盗:黑珍珠号的诅咒”为例,假设它总共有1000篇影评,其中一篇影评的总词语数为200,其中出现最频繁的词语为“海盗”、“船长”、“自由”,分别是20、15、10次,并且这3个词在所有影评中被提及的次数分别为1000、500、100,就这3个词语作为关键词的顺序计算如下。

  1. 将影评中出现的停用词过滤掉,计算其他词语的词频。以出现最多的三个词为例进行计算如下:

    • “海盗”出现的词频为20/200=0.1
    • “船长”出现的词频为15/200=0.075
    • “自由”出现的词频为10/200=0.05;
  2. 计算词语的逆文档频率如下:

    • “海盗”的IDF为:log(1000/1000)=0
    • “船长”的IDF为:log(1000/500)=0.3
      “自由”的IDF为:log(1000/100)=1
  3. 由1和2计算的结果求出词语的TF-IDF结果,“海盗”为0,“船长”为0.0225,“自由”为0.05。

通过对比可得,该篇影评的关键词排序应为:“自由”、“船长”、“海盗”。把这些词语的TF-IDF值作为它们的权重按照对应的顺序依次排列,就得到这篇影评的特征向量,我们就用这个向量来代表这篇影评,向量中每一个维度的分量大小对应这个属性的重要性。

将总的影评集中所有的影评向量与特定的系数相乘求和,得到这部电影的综合影评向量,与电影的基本属性结合构建视频的物品画像,同理构建用户画像,可采用多种方法计算物品画像和用户画像之间的相似度,为用户做出推荐。

加载数据集

import pandas as pd
import numpy as np
'''
- 利用tags.csv中每部电影的标签作为电影的候选关键词
- 利用TF·IDF计算每部电影的标签的tfidf值,选取TOP-N个关键词作为电影画像标签
- 并将电影的分类词直接作为每部电影的画像标签
'''def get_movie_dataset():# 加载基于所有电影的标签# all-tags.csv来自ml-latest数据集中# 由于ml-latest-small中标签数据太多,因此借助其来扩充_tags = pd.read_csv("datasets/ml-latest-small/all-tags.csv", usecols=range(1, 3)).dropna()tags = _tags.groupby("movieId").agg(list)# 加载电影列表数据集movies = pd.read_csv("datasets/ml-latest-small/movies.csv", index_col="movieId")# 将类别词分开movies["genres"] = movies["genres"].apply(lambda x: x.split("|"))# 为每部电影匹配对应的标签数据,如果没有将会是NANmovies_index = set(movies.index) & set(tags.index)new_tags = tags.loc[list(movies_index)]ret = movies.join(new_tags)# 构建电影数据集,包含电影Id、电影名称、类别、标签四个字段# 如果电影没有标签数据,那么就替换为空列表# map(fun,可迭代对象) movie_dataset = pd.DataFrame(map(lambda x: (x[0], x[1], x[2], x[2]+x[3]) if x[3] is not np.nan else (x[0], x[1], x[2], []), ret.itertuples()), columns=["movieId", "title", "genres","tags"])movie_dataset.set_index("movieId", inplace=True)return movie_datasetmovie_dataset = get_movie_dataset()
print(movie_dataset)
  • map函数

    • 描述

      map() 会根据提供的函数对指定序列做映射。

      第一个参数 function 以参数序列中的每一个元素调用 function 函数,返回包含每次 function 函数返回值的新列表。

    • 语法

      map() 函数语法:

      map(function, iterable, ...)
      
    • 参数

      • function – 函数
      • iterable – 一个或多个序列
    • 返回值

      Python 2.x 返回列表。

      Python 3.x 返回迭代器。

    • 示例

      >>>def square(x) :            # 计算平方数
      ...     return x ** 2
      ...
      >>> map(square, [1,2,3,4,5])   # 计算列表各个元素的平方
      [1, 4, 9, 16, 25]
      >>> map(lambda x: x ** 2, [1, 2, 3, 4, 5])  # 使用 lambda 匿名函数
      [1, 4, 9, 16, 25]# 提供了两个列表,对相同位置的列表数据进行相加
      >>> map(lambda x, y: x + y, [1, 3, 5, 7, 9], [2, 4, 6, 8, 10])
      [3, 7, 11, 15, 19]
      

基于TF·IDF提取TOP-N关键词,构建电影画像

  • gensim介绍

    • python 三方库 自然语言处理利器
    • 支持包括TF-IDF,word2vec在内的多种主题模型算法
    • 安装 pip install gensim
  • gensim基本概念

    • 语料(Corpus):一组原始文本的集合,在Gensim中,Corpus通常是一个可迭代的对象(比如列表)。每一次迭代返回一个可用于表达文本对象的(稀疏)向量。
    • 向量(Vector):由一组文本特征构成的列表。是一段文本在Gensim中的内部表达。
    • 模型(Model)
  • 词袋模型(BOW bag of words)

    文本特征提取有两个非常重要的模型:

    • 词集模型:单词构成的集合,集合自然每个元素都只有一个,也即词集中的每个单词都只有一个。
    • 词袋模型:在词集的基础上如果一个单词在文档中出现不止一次,统计其出现的次数(频数)。

    两者本质上的区别,词袋是在词集的基础上增加了频率的维度,词集只关注有和没有,词袋还要关注有几个。

from gensim.models import TfidfModelimport pandas as pd
import numpy as npfrom pprint import pprint# ......def create_movie_profile(movie_dataset):'''使用tfidf,分析提取topn关键词:param movie_dataset: :return: '''dataset = movie_dataset["tags"].valuesfrom gensim.corpora import Dictionary# 根据数据集建立词袋,并统计词频,将所有词放入一个词典,使用索引进行获取dct = Dictionary(dataset)# 根据将每条数据,返回对应的词索引和词频corpus = [dct.doc2bow(line) for line in dataset]# 训练TF-IDF模型,即计算TF-IDF值model = TfidfModel(corpus)movie_profile = {}for i, mid in enumerate(movie_dataset.index):# 根据每条数据返回,向量vector = model[corpus[i]]# 按照TF-IDF值得到top-n的关键词movie_tags = sorted(vector, key=lambda x: x[1], reverse=True)[:30]# 根据关键词提取对应的名称movie_profile[mid] = dict(map(lambda x:(dct[x[0]], x[1]), movie_tags))return movie_profilemovie_dataset = get_movie_dataset()
pprint(create_movie_profile(movie_dataset))

算法大致流程

获取电影数据集1.读取数据2.把movie.csv 和 tags.csv的内容进行取交集,得到最终的电影数据3.把movie 和 tags 合并
构建出电影画像1.获取电影的tags值2.通过gensim库的api2.1 获取词2.2 获取词的词频2.3 获取词对物品的tfidf3. 构建电影id和对应电影的词的字典匹配3.1 构建电影id 和对应电影的词的字典匹配3.2 构建词和词的tfidf值的匹配

完善画像关键词

from gensim.models import TfidfModelimport pandas as pd
import numpy as npfrom pprint import pprint# ......def create_movie_profile(movie_dataset):'''使用tfidf,分析提取topn关键词:param movie_dataset::return:'''dataset = movie_dataset["tags"].valuesfrom gensim.corpora import Dictionary# 根据数据集建立词袋,并统计词频,将所有词放入一个词典,使用索引进行获取dct = Dictionary(dataset)# 根据将每条数据,返回对应的词索引和词频corpus = [dct.doc2bow(line) for line in dataset]# 训练TF-IDF模型,即计算TF-IDF值model = TfidfModel(corpus)_movie_profile = []for i, data in enumerate(movie_dataset.itertuples()):mid = data[0]title = data[1]genres = data[2]vector = model[corpus[i]]movie_tags = sorted(vector, key=lambda x: x[1], reverse=True)[:30]topN_tags_weights = dict(map(lambda x: (dct[x[0]], x[1]), movie_tags))# 将类别词的添加进去,并设置权重值为1.0for g in genres:topN_tags_weights[g] = 1.0topN_tags = [i[0] for i in topN_tags_weights.items()]_movie_profile.append((mid, title, topN_tags, topN_tags_weights))movie_profile = pd.DataFrame(_movie_profile, columns=["movieId", "title", "profile", "weights"])movie_profile.set_index("movieId", inplace=True)return movie_profilemovie_dataset = get_movie_dataset()
pprint(create_movie_profile(movie_dataset))

为了根据指定关键词迅速匹配到对应的电影,因此需要对物品画像的标签词,建立倒排索引

倒排索引介绍

通常数据存储数据,都是以物品的ID作为索引,去提取物品的其他信息数据

而倒排索引就是用物品的其他数据作为索引,去提取它们对应的物品的ID列表

# ......'''
建立tag-物品的倒排索引
'''
def create_inverted_table(movie_profile):inverted_table = {}for mid, weights in movie_profile["weights"].iteritems():for tag, weight in weights.items():#到inverted_table dict 用tag作为Key去取值 如果取不到就返回[]_ = inverted_table.get(tag, [])#将电影的id 和 权重 放到一个tuple中 添加到list中_.append((mid, weight))#将修改后的值设置回去 inverted_table.setdefault(tag, _)return inverted_tableinverted_table = create_inverted_table(movie_profile)
pprint(inverted_table)


完整代码

import numpy as np
import pandas as pd
from gensim.models import TfidfModel'''
- 利用tags.csv中每部电影的标签作为电影的候选关键词
- 利用TF·IDF计算每部电影的标签的tfidf值,选取TOP-N个关键词作为电影画像标签
- 并将电影的分类词直接作为每部电影的画像标签
'''def get_movie_dataset():# 加载基于所有电影的标签# all-tags.csv来自ml-latest数据集中# 由于ml-latest-small中标签数据太多,因此借助其来扩充_tags = pd.read_csv('tags.csv', usecols=range(1, 3)).dropna()tags = _tags.groupby("movieId").agg(list)# 加载电影列表数据集movies = pd.read_csv("movies.csv", index_col="movieId")# 将类别词分开movies["genres"] = movies["genres"].apply(lambda x: x.split("|"))# 为每部电影匹配对应的标签数据,如果没有将会是NANmovies_index = set(movies.index) & set(tags.index)new_tags = tags.loc[list(movies_index)]ret = movies.join(new_tags)# 构建电影数据集,包含电影Id、电影名称、类别、标签四个字段# 如果电影没有标签数据,那么就替换为空列表# map(fun,可迭代对象)movie_dataset = pd.DataFrame(map(lambda x: (x[0], x[1], x[2], x[2] + x[3]) if x[3] is not np.nan else (x[0], x[1], x[2], []),ret.itertuples()), columns=["movieId", "title", "genres", "tags"])movie_dataset.set_index("movieId", inplace=True)return movie_dataset# 完善物品画像关键词
def creat_movie_profile(movie_dataset):"""使用tfidf, 分析提取topN关键词:param movie_dataset::return:"""dataset = movie_dataset['tags'].valuesfrom gensim.corpora import Dictionary# 根据数据集建立词袋,并统计词频,将所有词放入一个词典,使用索引进行获取dct = Dictionary(dataset)# 根据每条数据,返回对应的词索引和词频corpus = [dct.doc2bow(line) for line in dataset]# 计算tf-idf值model = TfidfModel(corpus)_movie_profile = []for i, data in enumerate(movie_dataset.itertuples()):mid = data[0]title = data[1]genres = data[2]vector = model[corpus[i]]movie_tags = sorted(vector, key=lambda x: x[1], reverse=True)[:30]topN_tags_weights = dict(map(lambda x: (dct[x[0]],x[1]), movie_tags))# 将类别词添加进去,并设置权重为1for g in genres:topN_tags_weights[g] = 1.0topN_tags = [i[0] for i in topN_tags_weights.items()]_movie_profile.append((mid, title, topN_tags, topN_tags_weights))movie_profile = pd.DataFrame(_movie_profile, columns=['movieId', 'title', 'profile', 'weights'])movie_profile.set_index('movieId', inplace=True)return movie_profile'''
建立tag-物品倒排索引
'''
def create_inverted_table(movie_profile):inverted_table = {}for mid, weights in movie_profile['weights'].iteritems():for tag, weight in weights.items():# 到inverted_table dict用tag作为key去取值,若取不到就返回[]_ = inverted_table.get(tag, [])# 将电影id 和 权重放到一个tuple中,添加到list中_.append((mid, weight))# 将修改后的值设置回去inverted_table.setdefault(tag, _)return inverted_tablemovie_dataset = get_movie_dataset()
from pprint import pprint
movie_profile = creat_movie_profile(movie_dataset)
pprint(movie_profile)
pprint(create_inverted_table(movie_profile))

基于内容的电影推荐:用户画像

用户画像构建步骤:

  • 根据用户的评分历史,结合物品画像,将有观影记录的电影的画像标签作为初始标签反打到用户身上
  • 通过对用户观影标签的次数进行统计,计算用户的每个初始标签的权重值,排序后选取TOP-N作为用户最终的画像标签

用户画像建立

import pandas as pd
import numpy as np
from gensim.models import TfidfModelfrom functools import reduce
import collectionsfrom pprint import pprint# ......'''
user profile画像建立:
1. 提取用户观看列表
2. 根据观看列表和物品画像为用户匹配关键词,并统计词频
3. 根据词频排序,最多保留TOP-k个词,这里K设为100,作为用户的标签
'''def create_user_profile():watch_record = pd.read_csv("datasets/ml-latest-small/ratings.csv", usecols=range(2), dtype={"userId":np.int32, "movieId": np.int32})watch_record = watch_record.groupby("userId").agg(list)# print(watch_record)movie_dataset = get_movie_dataset()movie_profile = create_movie_profile(movie_dataset)user_profile = {}for uid, mids in watch_record.itertuples():record_movie_profile = movie_profile.loc[list(mids)]# reduce 合并操作,2个参数# [1,2,3,4,5]  reduce(lambda x, y:x+y, [1,2,3,4,5]) x=1时,y=2# 阶乘 reduce(lambda x, y:x*y, range(1,101))# reduce(lambda x, y: list(x)+list(y), record_movie_profile["profile"].values是将record_movie_profile["profile"]中的一行作为x,一行作为y进行拼接counter = collections.Counter(reduce(lambda x, y: list(x)+list(y), record_movie_profile["profile"].values))# 取出出现次数最多的前50个词interest_words = counter.most_common(50)# 取出出现次数最多的词 出现的次数maxcount = interest_words[0][1]# 利用次数计算权重 出现次数最多的词权重为1interest_words = [(w,round(c/maxcount, 4)) for w,c in interest_words]user_profile[uid] = interest_wordsreturn user_profileuser_profile = create_user_profile()
pprint(user_profile)

reduce(lambda x, y: list(x)+list(y), record_movie_profile[“profile”].values是将record_movie_profile[“profile”]中的一行作为x,一行作为y进行拼接

  • reduce函数

    • 描述

      reduce() 函数会对参数序列中元素进行累积。

      函数将一个数据集合(链表,元组等)中的所有数据进行下列操作:用传给 reduce 中的函数 function(有两个参数)先对集合中的第 1、2 个元素进行操作,得到的结果再与第三个数据用 function 函数运算,最后得到一个结果。

    • 语法

      reduce() 函数语法:

      reduce(function, iterable[, initializer])
      
    • 参数

      • function – 函数,有两个参数
      • iterable – 可迭代对象
      • initializer – 可选,初始参数
    • 返回值

      返回函数计算结果。

    • 示例

      >>>def add(x, y) :            # 两数相加
      ...     return x + y
      ...
      >>> reduce(add, [1,2,3,4,5])   # 计算列表和:1+2+3+4+5
      15
      >>> reduce(lambda x, y: x+y, [1,2,3,4,5])  # 使用 lambda 匿名函数
      15
      
  • 使用collections.Counter类统计列表元素出现次数

    from collections import Counter
    names = ["Stanley", "Lily", "Bob", "Well", "Peter", "Bob", "Well", "Peter", "Well", "Peter", "Bob","Stanley", "Lily", "Bob", "Well", "Peter", "Bob", "Bob", "Well", "Peter", "Bob", "Well"]
    names_counts = Counter(names)
    

基于内容的电影推荐:为用户产生TOP-N推荐结果

# ......user_profile = create_user_profile()watch_record = pd.read_csv("datasets/ml-latest-small/ratings.csv", usecols=range(2),dtype={"userId": np.int32, "movieId": np.int32})watch_record = watch_record.groupby("userId").agg(list)for uid, interest_words in user_profile.items():result_table = {} # 电影id:[0.2,0.5,0.7]for interest_word, interest_weight in interest_words:related_movies = inverted_table[interest_word]for mid, related_weight in related_movies:_ = result_table.get(mid, [])_.append(interest_weight)    # 只考虑用户的兴趣程度# _.append(related_weight)    # 只考虑兴趣词与电影的关联程度# _.append(interest_weight*related_weight)    # 二者都考虑result_table.setdefault(mid, _)rs_result = map(lambda x: (x[0], sum(x[1])), result_table.items())rs_result = sorted(rs_result, key=lambda x:x[1], reverse=True)[:100]print(uid)pprint(rs_result)break# 历史数据  ==>  历史兴趣程度 ==>  历史推荐结果       离线推荐    离线计算# 在线推荐 ===>    娱乐(王思聪)   ===>   我 ==>  王思聪 100%  # 近线:最近1天、3天、7天           实时计算

推荐系统lambda架构学习笔记之推荐系统算法(二)相关推荐

  1. 推荐系统lambda架构学习笔记之推荐系统(一)

    推荐系统 个性化推荐(推荐系统)经历了多年的发展,已经成为互联网产品的标配,也是AI成功落地的分支之一,在电商(淘宝/京东).资讯(今日头条/微博).音乐(网易云音乐/QQ音乐).短视频(抖音/快手) ...

  2. 推荐系统lambda架构学习笔记之Hadoop、HDFS、YARNMapReduce(三)

    Hadoop Hadoop介绍 Hadoop名字的由来 作者:Doug cutting Hadoop项目作者的孩子给一个棕黄色的大象样子的填充玩具的命名 Hadoop的概念: Apache™ Hado ...

  3. 点云学习笔记11——VoxelNet算法+代码运行

    点云学习笔记11--VoxelNet算法+代码运行 一.算法分析 摘要 介绍 相关工作 1.2. 贡献 2.VoxelNet 2.1.特征学习网络 2.1.1 特征学习网络 二.代码复现 2.1.环境 ...

  4. R语言与机器学习学习笔记(分类算法)

    转载自:http://www.itongji.cn/article/0P534092014.html 人工神经网络(ANN),简称神经网络,是一种模仿生物神经网络的结构和功能的数学模型或计算模型.神经 ...

  5. MIPS架构学习笔记

    MIPS架构学习笔记 来源: ChinaUnix博客 日期: 2007.03.13 23:18 (共有条评论) 我要评论 MIPS架构学习笔记                              ...

  6. 《基于张量网络的机器学习入门》学习笔记8(Shor算法)

    <基于张量网络的机器学习入门>学习笔记8 Shor算法 来源 Shor算法的大致流程 因数分解 周期求取与量子傅里叶变换(QFT) Shor算法 来源 1994 1994 1994年,应用 ...

  7. 【学习笔记】【算法】【智能优化】粒子群优化(PSO)

    [学习笔记][算法][智能优化]粒子群优化(PSO) 文章目录 [学习笔记][算法][智能优化]粒子群优化(PSO) 1 算法背景 1.1 背景 1.2 基础知识 2 算法原理 2.1 基本原理 2. ...

  8. 【学习笔记】网络流算法简单入门

    [学习笔记]网络流算法简单入门 [大前言] 网络流是一种神奇的问题,在不同的题中你会发现各种各样的神仙操作. 而且从理论上讲,网络流可以处理所有二分图问题. 二分图和网络流的难度都在于问题建模,一般不 ...

  9. 学习笔记------人工蜂群算法

    学习笔记------人工蜂群算法 为了解决多变量函数优化问题Karaboga在2005年提出了人工蜂群算法ABC模型. 1. 蜜蜂采蜜机理 蜜蜂是一种群居昆虫,虽然单个昆虫的行为极其简单,但是由单个简 ...

最新文章

  1. excel 进行二叉树_基础扩展 | 21. 遍历二叉树
  2. 一个能自动搜索源文件并自动推导的Makefile
  3. 计算机回收站怎么设计无法删除,电脑回收站中ai文件删不掉如何解决? ai文件在回收站删不掉的解决办法...
  4. 使用Configuration Manager配置资产智能
  5. MySQL逻辑架构以及调优篇
  6. 状压[Jxoi2012]奇怪的道路
  7. ADO:用代码调用存储过程
  8. Lintcode61 Search for a Range solution 题解
  9. Android Button常用属性
  10. 直方图代码matlab,MATLAB直方图均衡化代码(MATLAB histogram equalization code).doc
  11. 区块链智能合约----Solidity状态修饰符view、pure
  12. CSS基础--absolute与overflow
  13. 安装程序将在重启您的计算机后黑屏,解决Windows 10登陆后黑屏问题
  14. J-LINK放了一晚,不能识别,灯一直闪
  15. ZUCC_数据库系统概论实验_实验五 JDBC进阶 2
  16. mybatis parametertype可以不填么
  17. 腾讯课堂小白训练——仿百度登陆页面
  18. java object取数据_java使用ObjectInputStream从文件中读取对象
  19. python零基础爬虫练习:如何用python爬取高德地图
  20. 希捷原装移动硬盘拆壳

热门文章

  1. 苹果手机桌面找不到计算机,苹果笔记本桌面图标不见了怎么办
  2. CDR VBA X6中Exportbitmap函数的用法(导出图片)
  3. MUTAN:Multimodal Tucker Fusion For Visual Question Answering
  4. convs在MATLAB中,matlab2_MATELAB课程设计_ppt_大学课件预览_高等教育资讯网
  5. 内网穿透工具-venom
  6. tpc ds mysql_tpc-ds测试tidb结果
  7. L2范数-欧几里得范数
  8. html5网页中用video标签无法播放MP4视频的解决方法
  9. 关于连续函数的介值定理
  10. 加密项目是否采用DAO模式 首先考量这8个因素