Files
ailearning/tutorials/RecommenderSystems/rs_rating_demo.py

262 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/python
# coding:utf-8
# -------------------------------------------------------------------------------
# Name: 推荐系统
# Purpose: 推荐系统: Item CF/User CF/SVD 对比
# Author: jiangzhonglian
# Create_time: 2020年9月21日
# Update_time: 2020年9月21日
# -------------------------------------------------------------------------------
from __future__ import print_function
import sys
import math
from operator import itemgetter
import numpy as np
import pandas as pd
from scipy.sparse.linalg import svds
from sklearn import model_selection as cv
from sklearn.metrics import mean_squared_error
from sklearn.metrics.pairwise import pairwise_distances
from middleware.utils import TimeStat, Chart
def splitData(dataFile, test_size):
# 加载数据集 (用户ID 电影ID 评分, 时间戳)
header = ['user_id', 'item_id', 'rating', 'timestamp']
df = pd.read_csv(dataFile, sep='\t', names=header)
n_users = df.user_id.unique().shape[0]
n_items = df.item_id.unique().shape[0]
print('>>> 本数据集包含: 总用户数 = %s | 总电影数 = %s' % (n_users, n_items) )
train_data, test_data = cv.train_test_split(df, test_size=test_size)
print(">>> 训练:测试 = %s:%s = %s:%s" % (len(train_data), len(test_data), 1-test_size, test_size))
return df, n_users, n_items, train_data, test_data
def calc_similarity(n_users, n_items, train_data, test_data):
# 创建用户产品矩阵,针对测试数据和训练数据,创建两个矩阵:
"""
line: Pandas(Index=93661, user_id=624, item_id=750, rating=4, timestamp=891961163)
"""
train_data_matrix = np.zeros((n_users, n_items))
for line in train_data.itertuples():
train_data_matrix[line[1] - 1, line[2] - 1] = line[3]
test_data_matrix = np.zeros((n_users, n_items))
for line in test_data.itertuples():
test_data_matrix[line[1] - 1, line[2] - 1] = line[3]
print("1:", np.shape(train_data_matrix)) # 行: 人 | 列: 电影
print("2:", np.shape(train_data_matrix.T)) # 行: 电影 | 列: 人
# 使用sklearn的 pairwise_distances 计算向量距离cosine来计算余弦距离越小越相似
user_similarity = pairwise_distances(train_data_matrix, metric="cosine")
item_similarity = pairwise_distances(train_data_matrix.T, metric="cosine")
# print("<<< %s \n %s" % (np.shape(user_similarity), user_similarity) )
# print("<<< %s \n %s" % (np.shape(item_similarity), item_similarity) )
print('开始统计流行item的数量...', file=sys.stderr)
item_popular = {}
# 统计同一个电影,观看的总人数(也就是所谓的流行度!)
for i_index in range(n_items):
if np.sum(train_data_matrix[:, i_index]) != 0:
item_popular[i_index] = np.sum(train_data_matrix[:, i_index] != 0)
# save the total number of items
item_count = len(item_popular)
print('总共流行 item 数量 = %d' % item_count, file=sys.stderr)
return train_data_matrix, test_data_matrix, user_similarity, item_similarity, item_popular
def predict(rating, similarity, type='user'):
"""
:param rating: 训练数据
:param similarity: 向量距离
:return:
"""
print("+++ %s" % type)
print(" rating=", np.shape(rating))
print(" similarity=", np.shape(similarity))
if type == 'item':
"""
综合打分:
rating.dot(similarity) 表示:
某1个人所有的电影组合 X ·电影*电影·距离第1列都是关于第1部电影和其他的电影的距离计算出 第一个人对第1/2/3部电影的 总评分 1*n
某2个人所有的电影组合 X ·电影*电影·距离第1列都是关于第1部电影和其他的电影的距离计算出 第一个人对第1/2/3部电影的 总评分 1*n
...
某n个人所有的电影组合 X ·电影*电影·距离第1列都是关于第1部电影和其他的电影的距离计算出 第一个人对第1/2/3部电影的 总评分 1*n
= 人-电影-评分(943, 1682) * 电影-电影-距离(1682, 1682)
= 人-电影-总评分距离(943, 1682)
np.array([np.abs(similarity).sum(axis=1)]) 表示: 横向求和: 1 表示某一行所有的列求和
第1列表示某个A电影对于所有电影计算出A的总距离
第2列表示某个B电影对于所有电影的综出B的总距离
...
第n列表示某个N电影对于所有电影的综出N的总距离
= 每一个电影的总距离 (1, 1682)
pred = 人-电影-平均评分 (943, 1682)
"""
pred = rating.dot(similarity) / np.array([np.abs(similarity).sum(axis=1)])
elif type == 'user':
# 每个样本上减去数据的统计平均值可以移除共同的部分,凸显个体差异。
# 求出每一个用户,所有电影的综合评分
# 横向求平均: 1 表示某一行所有的列求平均
mean_user_rating = rating.mean(axis=1)
# numpy中包含的 newaxis 可以给原数组增加一个维度
rating_diff = (rating - mean_user_rating[:, np.newaxis])
# 均分 +
# 人-人-距离(943, 943)*人-电影-评分diff(943, 1682)=结果-人-电影(每个人对同一电影的综合得分)(943, 1682) 再除以 个人与其他人总的距离 = 人-电影综合得分
"""
综合打分:
similarity.dot(rating_diff) 表示:
第1列第1个人与其他人的相似度 * 人与电影的相似度,得到 第1个人对第1/2/3列电影的 总得分 1*n
第2列第2个人与其他人的相似度 * 人与电影的相似度,得到 第2个人对第1/2/3列电影的 总得分 1*n
...
第n列第n个人与其他人的相似度 * 人与电影的相似度,得到 第n个人对第1/2/3列电影的 总得分 1*n
= 人-人-距离(943, 943) * 人-电影-评分(943, 1682)
= 人-电影-总评分距离(943, 1682)
np.array([np.abs(similarity).sum(axis=1)]) 表示: 横向求和: 1 表示某一行所有的列求和
第1列表示第A个人对于所有人计算出A的总距离
第2列表示第B个人对于所有人计算出B的总距离
...
第n列表示第N个人对于所有人计算出N的总距离
= 每一个电影的总距离 (1, 943)
pred = 均值 + 人-电影-平均评分 (943, 1682)
"""
pred = mean_user_rating[:, np.newaxis] + similarity.dot(rating_diff) / np.array([np.abs(similarity).sum(axis=1)]).T
return pred
def rmse(prediction, ground_truth):
prediction = prediction[ground_truth.nonzero()].flatten()
ground_truth = ground_truth[ground_truth.nonzero()].flatten()
return math.sqrt(mean_squared_error(prediction, ground_truth))
def evaluate(prediction, item_popular, name):
hit = 0
rec_count = 0
test_count = 0
popular_sum = 0
all_rec_items = set()
for u_index in range(n_users):
items = np.where(train_data_matrix[u_index, :] == 0)[0]
pre_items = sorted(
dict(zip(items, prediction[u_index, items])).items(),
key=itemgetter(1),
reverse=True)[:20]
test_items = np.where(test_data_matrix[u_index, :] != 0)[0]
# 对比测试集和推荐集的差异 item, w
for item, _ in pre_items:
if item in test_items:
hit += 1
all_rec_items.add(item)
# popular_sum是对所有的item的流行度进行加和
if item in item_popular:
popular_sum += math.log(1 + item_popular[item])
rec_count += len(pre_items)
test_count += len(test_items)
precision = hit / (1.0 * rec_count)
# 召回率,相对于测试推荐集合的数据
recall = hit / (1.0 * test_count)
# 覆盖率,相对于训练集合的数据
coverage = len(all_rec_items) / (1.0 * len(item_popular))
popularity = popular_sum / (1.0 * rec_count)
print('--- %s: precision=%.4f \t recall=%.4f \t coverage=%.4f \t popularity=%.4f' % (
name, precision, recall, coverage, popularity), file=sys.stderr)
def recommend(u_index, prediction):
items = np.where(train_data_matrix[u_index, :] == 0)[0]
pre_items = sorted(
dict(zip(items, prediction[u_index, items])).items(),
key=itemgetter(1),
reverse=True)[:10]
test_items = np.where(test_data_matrix[u_index, :] != 0)[0]
result = [key for key, value in pre_items]
result.sort(reverse=False)
print('原始结果(%s): %s' % (len(test_items), test_items) )
print('推荐结果(%s): %s' % (len(result), result) )
def main():
global n_users, train_data_matrix, test_data_matrix
# 基于内存的协同过滤
# ...
# 拆分数据集
# http://files.grouplens.org/datasets/movielens/ml-100k.zip
path_root = "/Users/jiangzl/work/data/机器学习"
dataFile = '%s/16.RecommenderSystems/ml-100k/u.data' % path_root
df, n_users, n_items, train_data, test_data = splitData(dataFile, test_size=0.25)
# 计算相似度
train_data_matrix, test_data_matrix, user_similarity, item_similarity, item_popular = calc_similarity(
n_users, n_items, train_data, test_data)
item_prediction = predict(train_data_matrix, item_similarity, type='item')
user_prediction = predict(train_data_matrix, user_similarity, type='user')
# # 评估: 均方根误差
print('>>> Item based CF RMSE: ' + str(rmse(item_prediction, test_data_matrix)))
print('>>> User based CF RMSE: ' + str(rmse(user_prediction, test_data_matrix)))
# 基于模型的协同过滤
# ...
# 计算MovieLens数据集的稀疏度 n_usersn_items 是常量,所以,用户行为数据越少,意味着信息量少;越稀疏,优化的空间也越大)
sparsity = round(1.0 - len(df) / float(n_users * n_items), 3)
print('\nMovieLen100K的稀疏度: %s%%\n' % (sparsity * 100))
# # 计算稀疏矩阵的最大k个奇异值/向量
# minrmse = math.inf
# index = 1
# for k in range(1, 30, 1):
# u, s, vt = svds(train_data_matrix, k=k)
# # print(">>> ", s)
# s_diag_matrix = np.diag(s)
# svd_prediction = np.dot(np.dot(u, s_diag_matrix), vt)
# r_rmse = rmse(svd_prediction, test_data_matrix)
# if r_rmse < minrmse:
# index = k
# minrmse = r_rmse
index = 11
minrmse = 2.6717213264389765
u, s, vt = svds(train_data_matrix, k=index)
# print(">>> ", s)
s_diag_matrix = np.diag(s)
svd_prediction = np.dot(np.dot(u, s_diag_matrix), vt)
r_rmse = rmse(svd_prediction, test_data_matrix)
print("+++ k=%s, svd-shape: %s" % (index, np.shape(svd_prediction)) )
print('>>> Model based CF RMSE: %s\n' % minrmse)
# """
# 在信息量相同的情况下,矩阵越小,那么携带的信息越可靠。
# 所以: user-cf 推荐效果高于 item-cf 而svd分解后发现15个维度效果就能达到90%以上,所以信息更可靠,效果也更好。
# item-cf: 1682
# user-cf: 943
# svd: 15
# """
evaluate(item_prediction, item_popular, 'item')
evaluate(user_prediction, item_popular, 'user')
evaluate(svd_prediction, item_popular, 'svd')
# 推荐结果
# recommend(1, item_prediction)
# recommend(1, user_prediction)
recommend(1, svd_prediction)
if __name__ == "__main__":
main()