随着机器学习技术的逐渐发展与完善,推荐系统也逐渐运用机器学习的思想来进行推荐。将机器学习应用到推荐系统中的方案真是不胜枚举。以下对Model-Based CF算法做一个大致的分类:
接下来重点学习以下几种应用较多的方案:
如果我们将评分看作是一个连续的值而不是离散的值,那么就可以借助线性回归思想来预测目标用户对某物品的评分。其中一种实现策略被称为Baseline(基准预测)。
Baseline设计思想基于以下的假设:
这个用户或物品普遍高于或低于平均值的差值,我们称为偏置(bias)
Baseline目标:
使用Baseline的算法思想预测评分的步骤如下:
计算所有电影的平均评分 μ \mu μ(即全局平均评分)
计算每个用户评分与平均评分 μ 的 偏 置 值 b u \mu的偏置值b_u μ的偏置值bu
计算每部电影所接受的评分与平均评分 μ 的 偏 置 值 b i \mu的偏置值b_i μ的偏置值bi
预测用户对电影的评分:
r ^ u i = b u i = μ + b u + b i \hat{r}_{ui} = b_{ui} = \mu + b_u + b_i r^ui=bui=μ+bu+bi
举例:通过Baseline来预测用户A对电影“阿甘正传”的评分
对于所有电影的平均评分是直接能计算出的,因此问题在于要测出每个用户的评分偏置和每部电影的得分偏置。对于线性回归问题,我们可以利用平方差构建损失函数如下:
加入L2正则化:
C o s t = ∑ u , i ∈ R ( r u i − μ − b u − b i ) 2 + λ ∗ ( ∑ u b u 2 + ∑ i b i 2 ) 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∑bu2+i∑bi2)
公式解析:
对于最小过程的求解,我们一般采用随机梯度下降法或者交替最小二乘法来优化实现。
使用随机梯度下降优化算法预测Baseline偏置值
梯度下降参数更新原始公式:(公式中α为学习率)
θ j : = θ j − α ∂ ∂ θ j J ( θ ) \theta_j:=\theta_j-\alpha\cfrac{\partial }{\partial \theta_j}J(\theta) θj:=θj−α∂θj∂J(θ)
梯度下降更新 b u b_u bu:
b u b_u bu更新(因为学习率alpha可以人为控制,所以2可以省略掉):
同理可得,梯度下降更新 b i b_i bi:
b i : = b i + α ∗ ( ∑ u , i ∈ R ( r u i − μ − b u − b i ) − λ ∗ b i ) 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)
由于随机梯度下降法本质上利用每个样本的损失来更新参数,而不用每次求出全部的损失和,因此使用SGD时:
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))
数据初始化
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])
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))
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聚合
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, 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偏置值
最小二乘法和梯度下降法一样,可以用于求极值。
最小二乘法思想:对损失函数求偏导,然后再使偏导为0
同样,损失函数:
J ( θ ) = ∑ u , i ∈ R ( r u i − μ − b u − b i ) 2 + λ ∗ ( ∑ u b u 2 + ∑ i b i 2 ) 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∑bu2+i∑bi2)
经过交替最小二乘
b u : = ∑ u , i ∈ R ( r u i − μ − b i ) λ 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)∣表示用户 u u u的有过评分数量
同理可得:
b i : = ∑ u , i ∈ R ( r u i − μ − b u ) λ 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收到的评分数量
b u 和 b i b_u和b_i bu和bi分别属于用户和物品的偏置,因此他们的正则参数可以分别设置两个独立的参数
通过最小二乘推导,我们最终分别得到了 b u 和 b i b_u和b_i 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))
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聚合
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, 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)
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基础上,添加了用户的隐式反馈信息。(显示反馈指的用户的评分这样的行为,隐式反馈指用户的浏览记录、购买记录、收听记录等。)
LFM也就是前面提到的Funk SVD矩阵分解
LFM(latent factor model)隐语义模型核心思想是通过隐含特征联系用户和物品,如下图:
利用矩阵分解技术,将原始User-Item的评分矩阵(稠密/稀疏)分解为P和Q矩阵,然后利用 P ∗ Q P*Q P∗Q还原出User-Item评分矩阵 R R R。整个过程相当于降维处理,其中:
矩阵值 P 11 P_{11} P11表示用户1对隐含特征1的权重值
矩阵值 Q 11 Q_{11} Q11表示隐含特征1在物品1上的权重值
利用LFM预测用户对物品的评分, k k k表示隐含特征数量:
因此最终,我们的目标也就是要求出P矩阵和Q矩阵及其当中的每一个值,然后再对用户-物品的评分进行预测。
加入L2正则化:
C o s t = ∑ u , i ∈ R ( r u i − ∑ k = 1 k p u k q i k ) 2 + λ ( ∑ U p u k 2 + ∑ I q i k 2 ) 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∑kpukqik)2+λ(U∑puk2+I∑qik2)
梯度下降更新参数 p u k 和 q i k 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))
数据初始化
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_puk
Q[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.globalMean
p_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 = columns
def fit(self, dataset):
'''
fit 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.globalMean = self.dataset[self.columns[2]].mean()
self.P, self.Q = self.sgd()
def _init_matrix(self):
'''
初始化P和Q矩阵,同时为设置0,1之间的随机值作为初始值
:return:
'''
# User-LF
P = dict(zip(
self.users_ratings.index,
np.random.rand(len(self.users_ratings), self.number_LatentFactors).astype(np.float32)
))
# Item-LF
Q = dict(zip(
self.items_ratings.index,
np.random.rand(len(self.items_ratings), self.number_LatentFactors).astype(np.float32)
))
return P, Q
def 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 Q
v_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, Q
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.globalMean
p_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_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))
# 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]]]
BiasSvd其实就是前面提到的Funk SVD矩阵分解基础上加上了偏置项。
利用BiasSvd预测用户对物品的评分,k表示隐含特征数量:
同样对于评分预测我们利用平方差来构建损失函数:
加入L2正则化:
C o s t = ∑ u , i ∈ R ( r u i − μ − b u − b i − ∑ k = 1 k p u k q i k ) 2 + λ ( ∑ U b u 2 + ∑ I b i 2 + ∑ U p u k 2 + ∑ I q i k 2 ) 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∑kpukqik)2+λ(U∑bu2+I∑bi2+U∑puk2+I∑qik2)
梯度下降更新参数 p u k p_{uk} puk:
同理:
b u : = b u + α [ ∑ u , i ∈ R ( r u i − μ − b u − b i − ∑ k = 1 k p u k q i k ) − λ b u ] 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∑kpukqik)−λbu]
b i : = b i + α [ ∑ u , i ∈ R ( r u i − μ − b u − b i − ∑ k = 1 k p u k q i k ) − λ b i ] 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∑kpukqik)−λbi]
随机梯度下降:
b u : = b u + α [ ( r u i − μ − b u − b i − ∑ k = 1 k p u k q i k ) − λ 3 b u ] 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∑kpukqik)−λ3bu]
b i : = b i + α [ ( r u i − μ − b u − b i − ∑ k = 1 k p u k q i k ) − λ 4 b i ] 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∑kpukqik)−λ4bi]
由于P矩阵和Q矩阵是两个不同的矩阵,通常分别采取不同的正则参数,如 λ 1 和 λ 2 \lambda_1和\lambda_2 λ1和λ2
算法实现
'''
BiasSvd Model
'''
import math
import random
import pandas as pd
import numpy as np
class 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_p
self.reg_q = reg_q
self.reg_bu = reg_bu
self.reg_bi = reg_bi
self.number_LatentFactors = number_LatentFactors # 隐式类别数量
self.number_epochs = number_epochs
self.columns = columns
def 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-LF
P = dict(zip(
self.users_ratings.index,
np.random.rand(len(self.users_ratings), self.number_LatentFactors).astype(np.float32)
))
# Item-LF
Q = dict(zip(
self.items_ratings.index,
np.random.rand(len(self.items_ratings), self.number_LatentFactors).astype(np.float32)
))
return P, Q
def sgd(self):
'''
使用随机梯度下降,优化结果
:return:
'''
P, Q = self._init_matrix()
# 初始化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)
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_qi
bu[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, bi
def predict(self, uid, iid):
if uid not in self.users_ratings.index or iid not in self.items_ratings.index:
return self.globalMean
p_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)))
基于内容的推荐方法是非常直接的,它以物品的内容描述信息为依据来做出的推荐,本质上是基于对物品和用户自身的特征或属性的直接分析和计算。
例如,假设已知电影A是一部喜剧,而恰巧我们得知某个用户喜欢看喜剧电影,那么我们基于这样的已知信息,就可以将电影A推荐给该用户。
画像构建。顾名思义,画像就是刻画物品或用户的特征。本质上就是给用户或物品贴标签。
根据PGC内容构建的物品画像的可以解决物品的冷启动问题
物品画像构建步骤:
前面提到,物品画像的特征标签主要都是指的如电影的导演、演员、图书的作者、出版社等结构话的数据,也就是他们的特征提取,尤其是体征向量的计算是比较简单的,如直接给作品的分类定义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指的是一个词语在文档中的出现频率。假设文档集包含的文档数为 N N N,文档集中包含关键词 k i k_i ki的文档数为 n i n_i ni, f i j f_{ij} fij表示关键词 k i k_i ki在文档 d j d_j dj中出现的次数, f d j f_{dj} fdj表示文档 d j d_j dj中出现的词语总数, k i k_i ki在文档dj中的词频 T F i j TF_{ij} TFij定义为:
T F i j = f i j f d j TF_{ij}=\frac {f_{ij}}{f_{dj}} TFij=fdjfij
并且注意,这个数字通常会被正规化,以防止它偏向长的文件(指同一个词语在长文件里可能会比短文件有更高的词频,而不管该词语重要与否)。
IDF是一个词语普遍重要性的度量。表示某一词语在整个文档集中出现的频率,由它计算的结果取对数得到关键词 k i k_i ki的逆文档频率 I D F i IDF_i IDFi:
I D F i = l o g N n i IDF_i=log\frac {N}{n_i} IDFi=logniN
由TF和IDF计算词语的权重为:
w i j = T F i j w_{ij}=TF_{ij} wij=TFij· I D F i = f i j f d j IDF_{i}=\frac {f_{ij}}{f_{dj}} IDFi=fdjfij· l o g N n i log\frac {N}{n_i} logniN
结论:TF-IDF与词语在文档中的出现次数成正比,与该词在整个文档集中的出现次数成反比。
用途:在目标文档中,提取关键词(特征标签)的方法就是将该文档所有词语的TF-IDF计算出来并进行对比,取其中TF-IDF值最大的k个数组成目标文档的特征向量用以表示文档。
注意:文档中存在的停用词(Stop Words),如“是”、“的”之类的,对于文档的中心思想表达没有意义的词,在分词时需要先过滤掉再计算其他词语的TF-IDF值。
对于计算影评的TF-IDF,以电影“加勒比海盗:黑珍珠号的诅咒”为例,假设它总共有1000篇影评,其中一篇影评的总词语数为200,其中出现最频繁的词语为“海盗”、“船长”、“自由”,分别是20、15、10次,并且这3个词在所有影评中被提及的次数分别为1000、500、100,就这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("|"))
# 为每部电影匹配对应的标签数据,如果没有将会是NAN
movies_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
movie_dataset = get_movie_dataset()
print(movie_dataset)
map函数
描述
map() 会根据提供的函数对指定序列做映射。
第一个参数 function 以参数序列中的每一个元素调用 function 函数,返回包含每次 function 函数返回值的新列表。
语法
map() 函数语法:
map(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]
gensim介绍
gensim基本概念
词袋模型(BOW bag of words)
文本特征提取有两个非常重要的模型:
两者本质上的区别,词袋是在词集的基础上增加了频率的维度,词集只关注有和没有,词袋还要关注有几个。
from gensim.models import TfidfModel
import pandas as pd
import numpy as np
from pprint import pprint
# ......
def create_movie_profile(movie_dataset):
'''
使用tfidf,分析提取topn关键词
:param movie_dataset:
:return:
'''
dataset = movie_dataset["tags"].values
from 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_profile
movie_dataset = get_movie_dataset()
pprint(create_movie_profile(movie_dataset))
算法大致流程
获取电影数据集
1.读取数据
2.把movie.csv 和 tags.csv的内容进行取交集,得到最终的电影数据
3.把movie 和 tags 合并
构建出电影画像
1.获取电影的tags值
2.通过gensim库的api
2.1 获取词
2.2 获取词的词频
2.3 获取词对物品的tfidf
3. 构建电影id和对应电影的词的字典匹配
3.1 构建电影id 和对应电影的词的字典匹配
3.2 构建词和词的tfidf值的匹配
from gensim.models import TfidfModel
import pandas as pd
import numpy as np
from pprint import pprint
# ......
def create_movie_profile(movie_dataset):
'''
使用tfidf,分析提取topn关键词
:param movie_dataset:
:return:
'''
dataset = movie_dataset["tags"].values
from 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.0
for g in genres:
topN_tags_weights[g] = 1.0
topN_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
movie_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_table
inverted_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("|"))
# 为每部电影匹配对应的标签数据,如果没有将会是NAN
movies_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'].values
from 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))
# 将类别词添加进去,并设置权重为1
for g in genres:
topN_tags_weights[g] = 1.0
topN_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_table
movie_dataset = get_movie_dataset()
from pprint import pprint
movie_profile = creat_movie_profile(movie_dataset)
pprint(movie_profile)
pprint(create_inverted_table(movie_profile))
用户画像构建步骤:
import pandas as pd
import numpy as np
from gensim.models import TfidfModel
from functools import reduce
import collections
from 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]
# 利用次数计算权重 出现次数最多的词权重为1
interest_words = [(w,round(c/maxcount, 4)) for w,c in interest_words]
user_profile[uid] = interest_words
return user_profile
user_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])
参数
返回值
返回函数计算结果。
示例
>>>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)
# ......
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天 实时计算