推荐系统开发实践:Surprise库基本用法

首先是构建数据集

这里我使用了随机生成用户和项目,以及用户对项目的评价,喜欢(1)与不喜欢(0)

import random
import pandas
import numpy as np

def build_dataframe() -> pandas.DataFrame:
    datasets = {
        'userID': [],
        'itemID': [],
        'rating': []
    }
    # 生成所有用户
    users = [user for user in np.arange(1, 1000)]
    # 从数据库取出所有文章
    items = [item for item in np.arange(1, 10000)]
    # 模拟评分
    for user in users:
        # 选n篇用户看过的文章
        viewed_items = random.sample(items, 100)
        for item in viewed_items:
            rating = random.randint(0, 1)
            datasets['userID'].append(user)
            datasets['itemID'].append(item)
            datasets['rating'].append(rating)

    return pandas.DataFrame(datasets)

准备训练模型

自动交叉验证

Surprise工具库拥有一系列内置函数和数据集供你使用,它强大到你只需要简单地写几行代码就能使用交叉验证法。

为了从pandas数据帧中加载数据集,需要使用load_frim_df()方法,也要使用Reader类,但是只有rating_scale参数必须指定。数据帧必须有三列,对应于用户(原始)ID,项目(原始)ID以及此顺序中的评级。因此,每行对应于给定的评级。因为可以轻松地重新排序数据帧的列。

这里使用我自己生成随机数据集,效果很差,还是用MovieLens的数据集比较好

from surprise import NormalPredictor,SVD,KNNBasic
from surprise import Dataset
from surprise import Reader
from surprise.model_selection import cross_validate

print('构建数据集')
df = build_dataframe()

# 同样需要定义 Reader 对象,但只需指定 rating_scale 参数。
reader = Reader(rating_scale=(0, 1))

# 传入的列必须对应着 userID,itemID 和 rating(严格按此顺序)。
data = Dataset.load_from_df(df[['userID', 'itemID', 'rating']], reader)

# 使用MovieLens-100k数据集
data = Dataset.load_builtin('ml-100k')

# 然后即可根据需要来操作此数据集,例如调用cross_validate
print('交叉验证')
cross_validate(KNNBasic(), data, measures=['RMSE', 'MAE'], cv=5, verbose=True)
运行结果
构建数据集
交叉验证
Computing the msd similarity matrix...
Done computing similarity matrix.
Computing the msd similarity matrix...
Done computing similarity matrix.
Computing the msd similarity matrix...
Done computing similarity matrix.
Computing the msd similarity matrix...
Done computing similarity matrix.
Computing the msd similarity matrix...
Done computing similarity matrix.
Evaluating RMSE, MAE of algorithm KNNBasic on 5 split(s).

Fold 1  Fold 2  Fold 3  Fold 4  Fold 5  Mean    Std     
RMSE (testset)    0.9861  0.9848  0.9806  0.9759  0.9639  0.9783  0.0080  
MAE (testset)     0.7755  0.7777  0.7767  0.7726  0.7614  0.7728  0.0059  
Fit time          0.59    0.67    0.61    0.58    0.62    0.61    0.03    
Test time         3.59    3.78    3.74    3.48    3.46    3.61    0.13    

{'test_rmse': array([0.98610945, 0.98482529, 0.98062869, 0.97590553, 0.96393148]),
'test_mae': array([0.7754694 , 0.77774512, 0.77666541, 0.77260546, 0.76140967]),
'fit_time': (0.5875513553619385,
0.6657052040100098,
0.614863395690918,
0.5803155899047852,
0.6159660816192627),
'test_time': (3.592573404312134,
3.7826573848724365,
3.736438751220703,
3.481112241744995,
3.4585647583007812)}

训练集-测试集划分和拟合函数fit()

不想进行交叉验证法,也可以使用train_test_split()将测试集与训练集划按你给定大小进行划分,并使用选定的精度评价指标。fit()函数将在训练集上使用算法,test()函数返回在测试集中的预测

from surprise import SVD
from surprise import Reader
from surprise import Dataset
from surprise import accuracy
from surprise.model_selection import train_test_split

df = build_dataframe()
reader = Reader(rating_scale=(0, 1))
data = Dataset.load_from_df(df[['userID', 'itemID', 'rating']], reader)

# 随机抽取训练集和测试集,测试集占总数据集的25%
trainset, testset = train_test_split(data, test_size=.25)

# 将使用SVD算法
algo = SVD()

# 用训练集训练模型,用测试集来得到评级结果
algo.fit(trainset)
predictions = algo.test(testset)

# 计算均方根误差RMSE
accuracy.rmse(predictions)

运行结果

RMSE: 0.5205
0.5204505667249893

也可以使用一行代码来实现训练和测试算法:

predictions = algo.fit(trainset).test(testset)
accuracy.rmse(predictions)

运行结果

RMSE: 0.5194
0.5194365387027055

训练整个数据集和predict()预测函数

显然,我们也可以简单地将算法拟合到整个数据集。使用build_full_trainset()方法来实现,然后通过直接调用predict()方法来预测正确率,假如使用:“user id:196, item id:302,是否喜欢:1”来验证(确保训练集中包含这个)。

from surprise import KNNBasic
from surprise import Dataset

df = build_dataframe()
reader = Reader(rating_scale=(0, 1))
data = Dataset.load_from_df(df[['userID', 'itemID', 'rating']], reader)

# 取回训练集
trainset = data.build_full_trainset()

# 构建算法并训练
algo = KNNBasic()
algo.fit(trainset)

uid = 1
iid = 1

# 获取指定用户和电影的评级结果
pred = algo.predict(uid, iid, r_ui=1, verbose=True)

运行结果

Computing the msd similarity matrix...
Done computing similarity matrix.
user: 1          item: 1          r_ui = 0.00   est = 0.65   {'actual_k': 6, 'was_impossible': False}

注意:predict()使用原始id(原始id和内部id的区别以后再详细说明)。如果使用的数据集是从文件中读取的,那么原始ID是字符串(即使它们代表数字),不过我这里用的是dataframe,所以不用这么用。

使用交叉验证法迭代器

为了更好的评估模型,我们还可以实现交叉验证迭代器(p次k折交叉验证)。对split()方法划分的每个迭代器都使用test()方法进行预测。

from surprise import SVD
from surprise import Dataset
from surprise import accuracy
from surprise.model_selection import KFold

df = build_dataframe()
reader = Reader(rating_scale=(0, 1))
data = Dataset.load_from_df(df[['userID', 'itemID', 'rating']], reader)

# 定义几次k折交叉验证迭代
kf = KFold(n_splits=3)

algo = SVD()

for trainset, testset in kf.split(data):
    # 训练和测试算法
    algo.fit(trainset)
    predictions = algo.test(testset)

    # 计算和打印均方根误差
    accuracy.rmse(predictions, verbose=True)

运行结果

RMSE: 0.5188
RMSE: 0.5192
RMSE: 0.5200

其他的交叉验证法如LeaveOneOut(留一法)和 ShuffleSplit(打散组合法)将样本集合随机“打散”后划分为训练集、测试集)也能使用。Surprise的交叉验证工具的设计灵感来自优秀的scikit-learn API。

训练集和测试集分别预定义成文件的情况是交叉验证的一个特例。 例如movielens-100K数据集已经提供了5个训练文件和5个测试文件(u1.base,u1.test,…,u5.base,u5.test)。可以使用surprise.mode_selection.split.PredefinedKFold对象来解决这种情况:

import os
from surprise import SVD
from surprise import Dataset
from surprise import Reader
from surprise import accuracy
from surprise.model_selection import PredefinedKFold

# 数据集文件夹位置
files_dir = os.path.expanduser('~/.surprise_data/ml-100k/ml-100k/')

# 这次,使用内置函数Reader读取数据
reader = Reader('ml-100k')

# 定义folds_files为包含文件路径的元组列表:
# [(u1.base, u1.test), (u2.base, u2.test),...(u5.base, u5.test)]
train_file = files_dir + 'u%d.base'
test_file = files_dir + 'u%d.test'
folds_files = [(train_file % i, test_file % i) for i in (1, 2, 3, 4, 5)]

# 分别载入数据集
data = Dataset.load_from_folds(folds_files, reader=reader)
pkf = PredefinedKFold()

algo = SVD()

for trainset, testset in pkf.split(data):
    # 训练和测试
    algo.fit(trainset)
    predictions = algo.test(testset)

    # 计算和打印均方根误差
    accuracy.rmse(predictions, verbose=True)

运行结果

RMSE: 0.9509
RMSE: 0.9374
RMSE: 0.9333
RMSE: 0.9330
RMSE: 0.9323

当然,也可以只加载单个文件进行训练和加载单个文件进行测试。但是,folds_files参数仍然需要是一个列表的形式。

使用GridSearchCV调参

cross_validate()函数输出给定参数集的交叉验证过程的准确度度量。如果想知道哪个参数组合产生最佳结果,可以使用GridSearchCV类。给定拥有一些参数的字典,该类将详尽地尝试所有参数组合并报告任何精度测量的最佳参数(在不同的分割上取平均值)。

下面是为SVD算法的参数n_epochs,lr_all和reg_all调参的例子:

from surprise import SVD
from surprise import Dataset
from surprise.model_selection import GridSearchCV

# 自动构建数据集
df = build_dataframe()
reader = Reader(rating_scale=(0, 1))
data = Dataset.load_from_df(df[['userID', 'itemID', 'rating']], reader)

# 指定参数选择范围
param_grid = {'n_epochs': [5, 10], 'lr_all': [0.002, 0.005],
              'reg_all': [0.4, 0.6]}

gs = GridSearchCV(SVD, param_grid, measures=['rmse', 'mae'], cv=3)

gs.fit(data)

# 打印最好的均方根误差RMSE
print(gs.best_score['rmse'])

# 打印取得最好RMSE的参数集合
print(gs.best_params['rmse'])

# 现在可以使用产生最佳RMSE的算法
algo = gs.best_estimator['rmse']
algo.fit(data.build_full_trainset())

运行结果

0.5035192355809749
{'n_epochs': 5, 'lr_all': 0.005, 'reg_all': 0.6}
<surprise.prediction_algorithms.matrix_factorization.SVD at 0x23985147a08>

一旦调用了fit()best_estimator属性就会为我们提供一个具有最佳参数集的算法实例。在此使用3折交叉验证法评估平均RMSEMAE,也可以使用任何交叉验证迭代器。

注意:字典参数(例如bsl_options和sim_options)需要特殊处理。 见下面的用法示例:

param_grid = {'k': [10, 20],
              'sim_options': {'name': ['msd', 'cosine'],
                              'min_support': [1, 5],
                              'user_based': [False]}
              }

当然,两者可以组合,例如在KNNBaseline算法中:

param_grid = {'bsl_options': {'method': ['als', 'sgd'],
                              'reg': [1, 2]},
              'k': [2, 3],
              'sim_options': {'name': ['msd', 'cosine'],
                              'min_support': [1, 5],
                              'user_based': [False]}
              }

进一步分析,cv_results属性所有的信息可以导出为pandas数据帧,并保存为.csv文件:

results_df = pd.DataFrame.from_dict(gs.cv_results)
results_df.to_csv('cv_result.csv')