Surprise简介

**Surprise(Simple Python Recommendation System Engine)**是一款推荐系统库,是scikit系列中的一个,简单易用,同时支持多种推荐算法(基础算法、协同过滤、矩阵分解等)。

Surprise设计时考虑到以下目的:

  • 让用户完美控制他们的实验。为此,特别强调文档,试图通过指出算法的每个细节尽可能清晰和准确。
  • 减轻数据集处理的痛苦。用户可以使用内置数据集(Movielens, Jester)和他们自己的自定义数据集。
  • 提供各种即用型预测算法,例如基线算法,邻域方法,基于矩阵因子分解(SVD,PMF,SVD ++,NMF)等等。此外,内置了各种相似性度量(余弦,MSD,皮尔逊…)。
  • 可以轻松实现新的算法思路。
  • 提供评估,分析和比较算法性能的工具。使用强大的CV迭代器(受scikit-learn优秀工具启发)以及对一组参数的详尽搜索,可以非常轻松地运行交叉验证程序。
    Surprise的主要特点是简单易用,同时支持多种推荐算法:

Surprise安装

1
2
pip install numpy
pip install scikit-surprise

在安装之前首先确认安装了numpy模块。

基本算法

算法类名 说明
random_pred.NormalPredictor 根据训练集的分布特征随机给出一个预测值
baseline_only.BaselineOnly 给定用户和Item,给出基于baseline的估计值
knns.KNNBasic 最基础的协同过滤
knns.KNNWithMeans 将每个用户评分的均值考虑在内的协同过滤实现
knns.KNNBaseline 考虑基线评级的协同过滤
matrix_factorization.SVD SVD实现
matrix_factorization.SVDpp SVD++,即LFM+SVD
matrix_factorization.NMF 基于矩阵分解的协同过滤
slope_one.SlopeOne 一个简单但精确的协同过滤算法
co_clustering.CoClustering 基于协同聚类的协同过滤算法

其中基于近邻的方法(协同过滤)可以设定不同的度量准则

相似度度量标准 度量标准说明
cosine 计算所有用户(或物品)对之间的余弦相似度
msd 计算所有用户(或物品)对之间的均方差异相似度
pearson 计算所有用户(或物品)对之间的Pearson相关系数
pearson_baseline 计算所有用户(或物品)对之间的(缩小的)Pearson相关系数,使用基线进行居中而不是平均值

支持不同的评估准则

评估准则 表头
rmse 计算RMSE(均方根误差)
mae 计算MAE(平均绝对误差)
fcp 计算FCP(协调对的分数)

Surprise使用

(1)载入自带的数据集

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#-*- coding:utf-8 -*-
# 可以使用上面提到的各种推荐系统算法
from surprise import SVD
from surprise import Dataset, print_perf
from surprise.model_selection import cross_validate

# 默认载入movielens数据集
data = Dataset.load_builtin('ml-100k')
# k折交叉验证(k=3),此方法现已弃用
# data.split(n_folds=3)
# 试一把SVD矩阵分解
algo = SVD()
# 在数据集上测试一下效果
perf = cross_validate(algo, data, measures=['RMSE', 'MAE'], cv=3)
#输出结果
print_perf(perf)

(2)载入自己的数据集

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from surprise import SVD
from surprise import Dataset, print_perf, Reader
from surprise.model_selection import cross_validate
import os

# 指定文件所在路径
file_path = os.path.expanduser('data.csv')
# 告诉文本阅读器,文本的格式是怎么样的
reader = Reader(line_format='user item rating', sep=',')
# 加载数据
data = Dataset.load_from_file(file_path, reader=reader)
algo = SVD()
# 在数据集上测试一下效果
perf = cross_validate(algo, data, measures=['RMSE', 'MAE'], cv=3)
#输出结果
print_perf(perf)

需要注意:

1.无法识别中文,如果有中文,需要将其转换成ID号再进行操作(以下列出一种简单的转换方式)

2.不能有表头,需要去掉表头和元数据中有中文的列

3.需要修改Reader,line_format 就是数据的列,sep 是分隔方式(表格格式初始分割方式是‘,’)

一种简单的数据转换方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#-*- coding:utf-8 -*-
# 构建物品id

import pandas as pd

df = pd.read_csv('train_score.csv', encoding="gbk")
# 读取第二列的数据
item_name = df.iloc[:, 1]
item = {}
item_id = []
num = 0
# 将每个不同的物品与id号进行关联
for i in item_name:
if i in item:
item_id.append(item[i])
else:
item[i] = num
item_id.append(num)
num += 1
print item_id
df['itemId'] = item_id
df.to_csv("data.csv", encoding="gbk", index=False)

算法调参

这里实现的算法用到的算法无外乎也是SGD等,因此也有一些超参数会影响最后的结果,我们同样可以用sklearn中常用到的网格搜索交叉验证(GridSearchCV)来选择最优的参数。简单的例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 定义好需要优选的参数网格
param_grid = {'n_epochs': [5, 10], 'lr_all': [0.002, 0.005],
'reg_all': [0.4, 0.6]}
# 使用网格搜索交叉验证
grid_search = GridSearch(SVD, param_grid, measures=['RMSE', 'FCP'])
# 在数据集上找到最好的参数
data = Dataset.load_builtin('ml-100k')
data.split(n_folds=3)
grid_search.evaluate(data)
# 输出调优的参数组
# 输出最好的RMSE结果
print(grid_search.best_score['RMSE'])
# >>> 0.96117566386

# 输出对应最好的RMSE结果的参数
print(grid_search.best_params['RMSE'])
# >>> {'reg_all': 0.4, 'lr_all': 0.005, 'n_epochs': 10}

# 最好的FCP得分
print(grid_search.best_score['FCP'])
# >>> 0.702279736531

# 对应最高FCP得分的参数
print(grid_search.best_params['FCP'])
# >>> {'reg_all': 0.6, 'lr_all': 0.005, 'n_epochs': 10}

GridSearchCV 方法:

1
2
3
4
5
6
7
8
9
10
11
12
# 定义好需要优选的参数网格
param_grid = {'n_epochs': [5, 10], 'lr_all': [0.002, 0.005],
'reg_all': [0.4, 0.6]}
# 使用网格搜索交叉验证
grid_search = GridSearchCV(SVD, param_grid, measures=['RMSE', 'FCP'], cv=3)
# 在数据集上找到最好的参数
data = Dataset.load_builtin('ml-100k')
# pref = cross_validate(grid_search, data, cv=3)
grid_search.fit(data)
# 输出调优的参数组
# 输出最好的RMSE结果
print(grid_search.best_score)

使用不同的推荐系统算法进行建模比较

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
from surprise import Dataset, print_perf
from surprise.model_selection import cross_validate
data = Dataset.load_builtin('ml-100k')
### 使用NormalPredictor
from surprise import NormalPredictor
algo = NormalPredictor()
perf = cross_validate(algo, data, measures=['RMSE', 'MAE'], cv=3)
print_perf(perf)

### 使用BaselineOnly
from surprise import BaselineOnly
algo = BaselineOnly()
perf = cross_validate(algo, data, measures=['RMSE', 'MAE'], cv=3)
print_perf(perf)

### 使用基础版协同过滤
from surprise import KNNBasic, evaluate
algo = KNNBasic()
perf = cross_validate(algo, data, measures=['RMSE', 'MAE'], cv=3)
print_perf(perf)

### 使用均值协同过滤
from surprise import KNNWithMeans, evaluate
algo = KNNWithMeans()
perf = cross_validate(algo, data, measures=['RMSE', 'MAE'], cv=3)
print_perf(perf)

### 使用协同过滤baseline
from surprise import KNNBaseline, evaluate
algo = KNNBaseline()
perf = cross_validate(algo, data, measures=['RMSE', 'MAE'], cv=3)
print_perf(perf)

### 使用SVD
from surprise import SVD, evaluate
algo = SVD()
perf = cross_validate(algo, data, measures=['RMSE', 'MAE'], cv=3)
print_perf(perf)

### 使用SVD++
from surprise import SVDpp, evaluate
algo = SVDpp()
perf = cross_validate(algo, data, measures=['RMSE', 'MAE'], cv=3)
print_perf(perf)

### 使用NMF
from surprise import NMF
algo = NMF()
perf = cross_validate(algo, data, measures=['RMSE', 'MAE'], cv=3)
print_perf(perf)

movielens推荐实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
#-*- coding:utf-8 -*-
from __future__ import (absolute_import, division, print_function,
unicode_literals)
import os
import io
from surprise import KNNBaseline
from surprise import Dataset

import logging

logging.basicConfig(level=logging.INFO,
format='%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s',
datefmt='%a, %d %b %Y %H:%M:%S')


# 训练推荐模型 步骤:1
def getSimModle():
# 默认载入movielens数据集
data = Dataset.load_builtin('ml-100k')
trainset = data.build_full_trainset()
#使用pearson_baseline方式计算相似度 False以item为基准计算相似度 本例为电影之间的相似度
sim_options = {'name': 'pearson_baseline', 'user_based': False}
##使用KNNBaseline算法
algo = KNNBaseline(sim_options=sim_options)
#训练模型
algo.fit(trainset)
return algo


# 获取id到name的互相映射 步骤:2
def read_item_names():
"""
获取电影名到电影id 和 电影id到电影名的映射
"""
file_name = (os.path.expanduser('~') +
'/.surprise_data/ml-100k/ml-100k/u.item')
rid_to_name = {}
name_to_rid = {}
with io.open(file_name, 'r', encoding='ISO-8859-1') as f:
for line in f:
line = line.split('|')
rid_to_name[line[0]] = line[1]
name_to_rid[line[1]] = line[0]
return rid_to_name, name_to_rid


# 基于之前训练的模型 进行相关电影的推荐 步骤:3
def showSimilarMovies(algo, rid_to_name, name_to_rid):
# 获得电影Toy Story (1995)的raw_id
toy_story_raw_id = name_to_rid['Toy Story (1995)']
logging.debug('raw_id=' + toy_story_raw_id)
#把电影的raw_id转换为模型的内部id
toy_story_inner_id = algo.trainset.to_inner_iid(toy_story_raw_id)
logging.debug('inner_id=' + str(toy_story_inner_id))
#通过模型获取推荐电影 这里设置的是10
toy_story_neighbors = algo.get_neighbors(toy_story_inner_id, 10)
logging.debug('neighbors_ids=' + str(toy_story_neighbors))
#模型内部id转换为实际电影id
neighbors_raw_ids = [algo.trainset.to_raw_iid(inner_id) for inner_id in toy_story_neighbors]
#通过电影id列表 或得电影推荐列表
neighbors_movies = [rid_to_name[raw_id] for raw_id in neighbors_raw_ids]
print('The 10 nearest neighbors of Toy Story are:')
for movie in neighbors_movies:
print(movie)


if __name__ == '__main__':
# 获取id到name的互相映射
rid_to_name, name_to_rid = read_item_names()

# 训练推荐模型
algo = getSimModle()

##显示相关电影
showSimilarMovies(algo, rid_to_name, name_to_rid)

参考文章:
利用Surprise包进行电影推荐