框架设计
具体的设计从三方面来完成:数据载入,算法设计,结果评估。
其中每个部分我们又可以进一步完善,下面分三部分去介绍这三个内容的细节设计,具体的实现后面会随着我的整理逐渐给出。大家也可以参考 surprise 的源码去自己分析,然后写一写。
数据载入
数据载入模块,需要支持特定数据集的自动下载,自动解压和处理;
然后可以支持用户自定义的数据集的载入,以及处理成特定格式;
最后应当包括自动划分训练集,测试集的部分。
自动下载数据
以 movielens 为例,为了避免 Python 导致的问题,选择 six 模块来进行下载,提前设置好 movielens 的下载 URL:
from six.moves.urllib.request import urlretrieve
import zipfile
tmp_file_path = join(get_dataset_dir(), 'tmp.zip')
urlretrieve(dataset.url, tmp_file_path)
with zipfile.ZipFile(tmp_file_path, 'r') as tmp_zip:
tmp_zip.extractall(join(get_dataset_dir(), name))
os.remove(tmp_file_path)
这里以 surprise 源码中的部分代码为例,get_dataset_dir() 是一个获取当前工作路径的函数,然后用 six 模块中的 urlretrieve 函数进行下载,接着利用 zipfile 进行解压,最后删除下载的临时解压文件。
这是一个简单的自动下载数据集的方式,我们可以进一步优化该模块。如多个数据集的设置,由用户选择,判断数据集是否存在等。
自定义数据载入
自定义的数据我们默认为三列数据,每行按照 user item rating 为序。那么将其读取进来的方式就很简单:
with open(os.path.expanduser(file_name)) as f:
raw_ratings = [line.strip().split() for line in f.readlines()]
但是到这一步还有许多需要优化的内容,如用户输入的格式不是 user item rating 的情况下,或者需要跳过前几行等。
划分训练集和测试集
训练集和测试集的划分,大家应该都很理解。在大多数情况下,我们都是把训练集和测试集按照 4:1 进行划分,参考 surprise 中的源码,其设计的方案是 k-折交叉验证法,默认设置的 k 值是 5,其实也相当于按照 4:1 进行划分。
start, stop = 0, 0
for fold_i in range(self.n_splits):
start = stop
stop += len(indices) // self.n_splits
if fold_i < len(indices) % self.n_splits:
stop += 1
raw_trainset = [data.raw_ratings[i] for i in chain(indices[:start],
indices[stop:])]
raw_testset = [data.raw_ratings[i] for i in indices[start:stop]]
trainset = data.construct_trainset(raw_trainset)
testset = data.construct_testset(raw_testset)
yield trainset, testset
这段代码从源码中取得,是在构建一个生成器,每次生成 k 折交叉验证中的一组训练集和测试集。
算法设计
算法设计中,surprise 提供了各种 knn 方法,也就是基于邻域的协同过滤,另外还提供了 SVD,SVD++ 等矩阵分解的方法。
对于算法的设计,主要是接口清晰,统一,具体内部的实现我们可以根据自己的需要,进行特定的设计。感兴趣的同学也可以看一下源码的方法,后面的文章中我就是参考源码的实现,结合自己的理解去进行 coding。
我希望感兴趣的朋友也可以自己独立完成编码,整个工作量不大,所有代码总计看了一下,几千行的样子,自己完整的写一遍有利于自己对模块之间调用进行熟悉。
结果评估
在 surprise 当中提供了 rmse,mae 等指标进行判断,因为原始数据集中的标签是 rating,所以这里选择了直接去学得一个拟合函数,来预估用户的评分。
def rmse(predictions, verbose=True):
if not predictions:
raise ValueError('Prediction list is empty.')
mse = np.mean([float((true_r - est)**2) for (_, _, true_r, est, _) in predictions])
rmse_ = np.sqrt(mse)
if verbose:
print('RMSE: {0:1.4f}'.format(rmse_))
return rmse_
这里就是一个计算 rmse 的方法,prediction 是一个从给定算法中得到的返回值,我们可以清晰的看到真实的标签得分是 true_r,而算法得到的预测分数为 est,然后按照 mse 的计算方式求得结果,在开方得到 rmse。
这是以 rmse 为例,但是从模型的设计方面考虑,如果我们要计算多个指标,或者由用户选择来计算哪些指标,就应该设计的更灵活点。
那么 python 的内置函数 getattr 就可以解决这个问题。
for m in measures:
f = getattr(accuracy, m.lower())
test_measures[m] = f(predictions, verbose=0)
在 measures 中,是一个由需要获得的指标组成的列表,例如 [rmse, mse],这就表示需要计算 rmse 和 mse 两个指标的结果。
总结
今天简单的介绍了一下推荐系统中非常基础的 surprise 库,然后解析了一下它的主体架构,后面我们自己逐渐细化进去,完成一个自己的推荐系统库,在这个过程中也可以锻炼自己的代码结构的设计能力。
推荐阅读:
▼点击成为社区会员 喜欢就点个在看吧