python编程快速上手(持续更新中…)
推荐系统基础
文章目录
2.1.Model-Based 协同过滤算法
随着机器学习技术的逐渐发展与完善,推荐系统也逐渐运用机器学习的思想来进行推荐。将机器学习应用到推荐系统中的方案真是不胜枚举。以下对Model-Based CF算法做一个大致的分类:
- 基于分类算法、回归算法、聚类算法
- 基于矩阵分解的推荐
- 基于神经网络算法
- 基于图模型算法
接下来我们重点学习以下几种应用较多的方案:
- 基于回归模型的协同过滤推荐
- 基于矩阵分解的协同过滤推荐
2.2基于回归模型的协同过滤推荐
如果我们将评分看作是一个连续的值而不是离散的值,那么就可以借助线性回归思想来预测目标用户对某物品的评分。其中一种实现策略被称为Baseline(基准预测)。
Baseline基准预测:
Baseline设计思想基于以下的假设:
-
有些用户的评分普遍高于其他用户,有些用户的评分普遍低于其他用户。比如有些用户天生愿意给别人好评,心慈手软,比较好说话,而有的人就比较苛刻,总是评分不超过3分(5分满分)
-
一些物品的评分普遍高于其他物品,一些物品的评分普遍低于其他物品。比如一些物品一被生产便决定了它的地位,有的比较受人们欢迎,有的则被人嫌弃。
这个用户或物品普遍高于或低于平均值的差值,我们称为偏置(bias)
Baseline目标:
- 找出每个用户普遍高于或低于他人的偏置值
- 找出每件物品普遍高于或低于其他物品的偏置值
- 我们的目标也就转化为寻找最优的
使用Baseline的算法思想预测评分的步骤如下:
- 计算所有电影的平均评分(即全局平均评分)
- 计算每个用户评分与平均评分
- 计算每部电影所接受的评分与平均评分
预测用户对电影的评分:
举例:通过Baseline来预测用户A对电影“阿甘正传”的评分 - 首先计算出整个评分数据集的平均评分是3.5分
- 用户A比较苛刻,普遍比平均评分低0.5分,即用户A的偏置值是-0.5;
- “阿甘正传”比较热门且备受好评,评分普遍比平均评分要高1.2分,“阿甘正传”的偏置是+1.2
- 因此就可以预测出用户A对电影“阿甘正传”的评分为:,也就是4.2分。
对于所有电影的平均评分是直接能计算出的,因此问题在于要测出每个用户的评分偏置和每部电影的得分偏置。对于线性回归问题,我们可以利用平方差构建损失函数如下:
方法一:随机梯度下降法优化
方法一:随机梯度下降法优化
step 1:梯度下降法推导
step 2:随机梯度下降
由于随机梯度下降法本质上利用每个样本的损失来更新参数,而不用每次求出全部的损失和,因此使用SGD时:
step 3:算法实现
tips pandas 版本不要过低 pandas 0.24.2
数据加载
import pandas as pd
import numpy as np
dtype = {"userId": np.int32, "movieId": np.int32, "rating": np.float32}
dataset = pd.read_csv('../data/ml-latest-small/ratings.csv', dtype=dict(dtype), usecols=range(3))
dataset
2.根据userid聚合
# 根据userid聚合
users_ratings = dataset.groupby('userId').agg([list])
users_ratings
3.根据umovieId聚合
# 根据umovieId聚合
items_ratings = dataset.groupby('movieId').agg([list])
items_ratings
4.计算全局平均分
# 计算全局平均分
global_mean = dataset['rating'].mean()
global_mean
3.5015569
5.初始化 bu bi
# 初始化 bu bi
bu = dict(zip(users_ratings.index, np.zeros(len(users_ratings))))
bi = dict(zip(items_ratings.index, np.zeros(len(items_ratings))))
bu
list(dataset.itertuples(index=False))
6.利用梯度下降优化
# 利用梯度下降优化
#number_epochs 迭代次数 alpha学习率 reg 正则化系数
number_epochs = 10
alpha = 0.1
reg = 0.1
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])
bu
7.预测评分
# 预测评分
def predict(uid, iid):
predict_rating = global_mean + bu[uid] + bi[iid]
return predict_rating
# 1号用户对1号商品
predict(1, 1)
4.368831380758709
Step 4: 准确性指标评估
添加test方法,然后使用之前实现accuary方法计算准确性指标
1.数据集拆分
# 数据集拆分
def data_split(data_path, x=0.8, random=False):
'''
切分数据集, 这里为了保证用户数量保持不变,将每个用户的评分数据按比例进行拆分
:param data_path: 数据集路径
:param x: 训练集的比例,如x=0.8,则0.2是测试集
:param random: 是否随机切分,默认False
:return: 用户-物品评分矩阵
'''
print("开始切分数据集...")
# 设置要加载的数据字段的类型
dtype = {"userId": np.int32, "movieId": np.int32, "rating": np.float32}
# 加载数据,我们只用前三列数据,分别是用户ID,电影ID,已经用户对电影的对应评分
ratings = pd.read_csv(data_path, dtype=dtype, usecols=range(3))
testset_index = []
# 为了保证每个用户在测试集和训练集都有数据,因此按userId聚合
for uid in ratings.groupby("userId").any().index:
user_rating_data = ratings.where(ratings["userId"]==uid).dropna()
if random:
# 因为不可变类型不能被 shuffle方法作用,所以需要强行转换为列表
index = list(user_rating_data.index)
np.random.shuffle(index) # 打乱列表
_index = round(len(user_rating_data) * x)
testset_index += list(index[_index:])
else:
# 将每个用户的x比例的数据作为训练集,剩余的作为测试集
index = round(len(user_rating_data) * x)
testset_index += list(user_rating_data.index.values[index:])
testset = ratings.loc[testset_index]
trainset = ratings.drop(testset_index)
print("完成数据集切分...")
return trainset, testset
trainset, testset = data_split(’…/data/ml-latest-small/ratings.csv’)
开始切分数据集…
完成数据集切分…
2.trainset
trainset
3.预测测试集数据
# 预测测试集数据
def test(testset):
'''预测测试集数据'''
for uid, iid, real_rating in testset.itertuples(index=False):
try:
pred_rating = predict(uid, iid)
except Exception as e:
print(e)
else:
yield uid, iid, real_rating, pred_rating
4.准确性指标计算方法
# 准确性指标计算方法
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)
5.testset
testset
6.预测
# 预测
test_result = test(testset)
accuray(test_result)
(0.7841, 0.5922)
方法二:交替最小二乘法优化
使用交替最小二乘法优化算法预测Baseline偏置值
step 1: 交替最小二乘法推导
- 最小二乘法和梯度下降法一样,可以用于求极值。
- 最小二乘法思想:对损失函数求偏导,然后再使偏导为0
step 2: 交替最小二乘法应用
通过最小二乘推导,我们最终分别得到了bu和bibu和bi的表达式,但他们的表达式中却又各自包含对方,因此这里我们将利用一种叫交替最小二乘的方法来计算他们的值:
- 计算其中一项,先固定其他未知参数,即看作其他未知参数为已知
- 如求bu时,将bi看作是已知;求bi时,将bubu时,将bi看作是已知;求bi时,将bu看作是已知;如此反复交替,不断更新二者的值,求得最终的结果。这就是交替最小二乘法(ALS)
step 3: 算法实现
- 数据加载初始化与之前完全相同
- 迭代更新bu bi
1.初始化bu、bi的值,全部设为0
# 初始化bu、bi的值,全部设为0
bu = dict(zip(users_ratings.index, np.zeros(len(users_ratings))))
bi = dict(zip(items_ratings.index, np.zeros(len(items_ratings))))
for i in range(15):
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 / (0.1 + 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 / (0.1 + len(iids))
2.predict(1, 1)
predict(1, 1)
4.670576814116479
2.3基于矩阵分解的CF算法
矩阵分解发展史
Traditional 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基础上,认为用户对于项目的历史浏览记录、购买记录、收听记录等可以从侧面反映用户的偏好。
2.4基于矩阵分解的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∗QP∗Q还原出User-Item评分矩阵RR。整个过程相当于降维处理,其中:
矩阵值P11P11表示用户1对隐含特征1的权重值
矩阵值Q11Q11表示隐含特征1在物品1上的权重值
矩阵值R11就表示预测的用户1对物品1的评分,且
利用LFM预测用户对物品的评分,
k
k
k表示隐含特征数量:
因此最终,我们的目标也就是要求出P矩阵和Q矩阵及其当中的每一个值,然后再对用户-物品的评分进行预测。
损失函数
同样对于评分预测我们利用平方差来构建损失函数:
加入L2正则化:
随机梯度下降法优化
算法实现
1.数据加载
users_ratings = dataset.groupby('userId').agg([list])
items_ratings = dataset.groupby('movieId').agg([list])
2.初始化Q和P
# 初始化Q和P
# User-LF 10 代表 隐含因子个数是10个
P = dict(zip(users_ratings.index,np.random.rand(len(users_ratings),10).astype(np.float32)))
P
Q = dict(zip(items_ratings.index,np.random.rand(len(items_ratings),10).astype(np.float32)))
Q
3.梯度下降优化损失函数
# 梯度下降优化损失函数
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
4.评分预测
# 评分预测
def predict2(uid, iid):
# 如果uid或iid不在,我们使用全剧平均分作为预测结果返回
if uid not in users_ratings.index or iid not in items_ratings.index:
return global_mean
p_u = P[uid]
q_i = Q[iid]
return np.dot(p_u, q_i)
5.预测
predict2(1, 1)
4.9007773
2.5基于内容的推荐算法(Content-Based)
简介
基于内容的推荐方法是非常直接的,它以物品的内容描述信息为依据来做出的推荐,本质上是基于对物品和用户自身的特征或属性的直接分析和计算。
例如,假设已知电影A是一部喜剧,而恰巧我们得知某个用户喜欢看喜剧电影,那么我们基于这样的已知信息,就可以将电影A推荐给该用户。
基于内容的推荐实现步骤
画像构建,顾名思义,画像就是刻画物品或用户的特征。本质上就是给用户或物品贴标签。
物品画像:例如给电影《战狼2》贴标签,可以有哪些?
“动作”、“吴京”、“吴刚”、“张翰”、“大陆电影”、“国产”、“爱国”、"军事"等等一系列标签是不是都可以贴上
用户画像:例如已知用户的观影历史是:"《战狼1》"、"《战狼2》"、"《建党伟业》"、"《建军大业》"、"《建国大业》"、"《红海行动》"、"《速度与激情1-8》"等,我们是不是就可以分析出该用户的一些兴趣特征如:“爱国”、“战争”、“赛车”、“动作”、“军事”、“吴京”、"韩三平"等标签。
问题:物品的标签来自哪儿?
PGC 物品画像–冷启动
- 物品自带的属性(物品一产生就具备的):如电影的标题、导演、演员、类型等等
- 服务提供方设定的属性(服务提供方为物品附加的属性):如短视频话题、微博话题(平台拟定)
- 其他渠道:如爬虫
UGC 冷启动问题
- 用户在享受服务过程中提供的物品的属性:如用户评论内容,微博话题(用户拟定)
根据PGC内容构建的物品画像的可以解决物品的冷启动问题
基于内容推荐的算法流程:
物品冷启动处理
2.6基于内容的电影推荐:物品画像
1.物品画像构建步骤:
2.基于TF·IDF提取TOP-N关键词,构建电影画像
TF-IDF算法便是其中一种在自然语言处理领域中应用比较广泛的一种算法。可用来提取目标文档中,并得到关键词用于计算对于目标文档的权重,并将这些权重组合到一起得到特征向量。
算法原理
TF-IDF自然语言处理领域中计算文档中词或短语的权值的方法,是词频(Term Frequency,TF)和逆转文档频率(Inverse Document Frequency,IDF)的乘积。
代码实现
1、加载数据
import pandas as pd
import numpy as np
# 加载基于所有电影的标签
# 由于ml-latest-small中标签数据太多,因此借助其来扩充
_tags = pd.read_csv("../data/ml-latest-small/all-tags.csv", usecols=range(1, 3)).dropna()
tags = _tags.groupby("movieId").agg(list)
tags
# 加载电影列表数据集
movies = pd.read_csv("../data/ml-latest-small/movies.csv", index_col="movieId")
movies
2.将类别词分开
# 将类别词分开
movies["genres"] = movies["genres"].apply(lambda x: x.split("|"))
movies
3.为每部电影匹配对应的标签数据,如果没有将会是NAN
# 为每部电影匹配对应的标签数据,如果没有将会是NAN
movies_index = set(movies.index) & set(tags.index)
new_tags = tags.loc[list(movies_index)]
ret = movies.join(new_tags)
ret
list(ret.itertuples())
4.构建电影数据集,包含电影Id、电影名称、类别、标签四个字段
# 构建电影数据集,包含电影Id、电影名称、类别、标签四个字段
temp = 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())
# 执行了就没了数据
# list(temp)
# 如果电影没有标签数据,那么就替换为空列表
# map(fun,可迭代对象)
movie_dataset = pd.DataFrame(temp, columns=["movieId", "title", "genres","tags"])
movie_dataset
5.添加标签
movie_dataset.set_index("movieId", inplace=True)
movie_dataset
6.获取tags
# 获取tags
dataset1 = movie_dataset['tags'].values
dataset1
7.创建dict对象
from gensim.corpora import Dictionary
from gensim.models import TfidfModel
# 创建dict对象
# 根据数据集建立词袋,并统计词频,将所有词放入一个词典,使用索引进行获取
dct = Dictionary(dataset1)
# 单词出现频率
dct.doc2bow(['Comedy', 'Romance', 'moldy', 'old', 'Ann Margaret', 'Burgess Meredith', 'Daryl Hannah', 'fishing', 'good soundtrack', 'fishing', 'good soundtrack'])
8.根据将每条数据,返回对应的词索引和词频
# 根据将每条数据,返回对应的词索引和词频
corpus = [dct.doc2bow(line) for line in dataset1]
corpus
9.训练TF-IDF模型,即计算TF-IDF值
# 训练TF-IDF模型,即计算TF-IDF值
model = TfidfModel(corpus)
# vector = model[corpus[1]]
# vector
# 按照TF-IDF值得到top-n的关键词
movie_tags = sorted(vector, key=lambda x: x[1], reverse=True)[:30]
movie_tags
10.根据关键词提取对应的名称
# 根据关键词提取对应的名称
dict(map(lambda x:(dct[x[0]], x[1]), movie_tags))
11.通过标签找到对应的电影名
# 通过标签找到对应的电影名
def create_movie_profile(movie_dataset):
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
create_movie_profile(movie_dataset)
12.使用tfidf,分析提取topn关键词
# 使用tfidf,分析提取topn关键词
def create_movie_profile2(movie_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_profile = create_movie_profile2(movie_dataset)
movie_profile
13.建立tag-物品的倒排索引
# 建立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)
inverted_table
2.7基于内容的电影推荐:用户画像
1.用户画像构建步骤
2.代码实现
watch_record = pd.read_csv("../data/ml-latest-small/ratings.csv", usecols=range(2), dtype={"userId":np.int32, "movieId": np.int32})
watch_record = watch_record.groupby("userId").agg(list)
watch_record
movie_profile.head()
record_movie_prifole = movie_profile.loc[[1, 3, 6, 47, 50, 70, 101, 110, 151, 157, 163]]
record_movie_prifole
from functools import reduce
import collections
temp = reduce(lambda x,y : list(x)+list(y), record_movie_prifole['profile'].values)
temp
# 统计
counter = collections.Counter(temp)
counter
# 取出出现次数最多的词 出现的次数
maxcount = interest_words[0][1]
maxcount
5
# 利用次数计算权重 出现次数最多的词权重为1
interest_words = [(w,round(c/maxcount, 4)) for w,c in interest_words]
interest_words
2.9基于内容的电影推荐:物品冷启动处理
1.词向量
2.文档向量
3.代码实现