导语
笔者在当年上学刚刚接触物品推荐问题时,使用的数据集就是MovieLens,那时候的课本上,大多使用传统的协同过滤算法,基于相似用户、相似物品,来解决问题。时至今日,市面上涌现了大量的机器学习相关书籍,解决物品推荐问题的算法虽早已物是人非,然而MovieLens数据集,作为物品推荐问题里的“hello world”,却仍然是学习,或者检验一个推荐算法的不二之选。此为笔者个人拙见,仅供参考,敬请指正。
正文
MovieLens,创建于1997年,是一个推荐系统和虚拟社区网站,其主要功能为应用协同过滤技术和用户对电影的喜好,向用户推荐电影。MovieLens保存了用户对电影的评分,其按照用户、电影的数据量大小,提供了多个数据集,如MovieLens 100k、MovieLens 1M、MovieLens 10M等等。注意,本文仅仅使用打分数据,没有另外引入更多的信息,如用户属性、物品属性等,这样的限定有助于我们把物品推荐这个问题抽象化,同时也使得我们的注意力更加收敛。
本文以MovieLens 100k为例,数据集在文章开头附件处u.zip。MovieLens 100k数据集,共10万条样本数据,每个样本记录了用户id、电影id、用户对电影的打分、打分的时间戳(单位:秒),涉及用户943个,电影1682部。
user_id | item_id | rating | timestamp |
---|---|---|---|
1 | 1 | 5 | 874965758 |
2 | 1 | 4 | 888550871 |
5 | 1 | 4 | 875635748 |
6 | 1 | 4 | 883599478 |
10 | 1 | 4 | 877888877 |
13 | 1 | 3 | 882140487 |
15 | 1 | 1 | 879455635 |
16 | 1 | 5 | 877717833 |
17 | 1 | 4 | 885272579 |
18 | 1 | 5 | 880130802 |
20 | 1 | 3 | 879667963 |
21 | 1 | 5 | 874951244 |
23 | 1 | 5 | 874784615 |
25 | 1 | 5 | 885853415 |
26 | 1 | 3 | 891350625 |
38 | 1 | 5 | 892430636 |
41 | 1 | 4 | 890692860 |
42 | 1 | 5 | 881105633 |
43 | 1 | 5 | 875975579 |
…… | …… | …… | …… |
我们使用更为直观的用户物品矩阵来展示这一份数据。
用户1 | 用户2 | 用户3 | 用户4 | 用户5 | …… | |
---|---|---|---|---|---|---|
电影1 | 5 | 4 | 0 | 0 | 4 | …… |
电影2 | 3 | 0 | 0 | 0 | 3 | …… |
电影3 | 4 | 0 | 0 | 0 | 0 | …… |
电影4 | 3 | 0 | 0 | 0 | 0 | …… |
电影5 | 3 | 0 | 0 | 0 | 0 | …… |
…… | …… | …… | …… | …… | …… | …… |
# -*- coding:utf-8 -*-
import pandas as pd
import numpy as np
def main():
header = ['user_id', 'item_id', 'rating', 'timestamp']
dataset = pd.read_csv('u.csv', sep=',', names=header)
# 获取去重用户数
users = dataset.user_id.unique().shape[0]
# 获取去重电影数
items = dataset.item_id.unique().shape[0]
# 生成用户物品矩阵
user_item_matrix = np.zeros((items, users))
for line in dataset.itertuples():
user_item_matrix[line[2] - 1, line[1] - 1] = line[3]
# 持久化
np.savetxt('user_item_matrix.csv', user_item_matrix, delimiter=',')
if __name__ == '__main__':
main()
我们先来简单回顾一下,从前协同过滤算法是如何基于这个用户物品矩阵解决物品推荐问题的。协同过滤(英语:Collaborative Filtering),推荐场景的一个常用思维,"简单来说是利用某兴趣相投、拥有共同经验之群体的喜好来推荐用户感兴趣的信息"--引用自维基百科。
比如说,943个用户中,谁谁谁和用户1的兴趣最接近?显然就是那些个,和用户1看过相同电影并且给出差不多分数的用户,他们的兴趣相投。
又比如说,1682部电影中,哪部哪部和电影1的风格最接近?显然就是和电影1有相同受众的电影,例如小孩都喜欢看电影A和电影1,打高分;大人都不喜欢看电影A和电影1,打低分,那电影A和电影1都偏向低龄化,它们的风格接近。
协同过滤解决问题的核心思想就是,通过计算两两打分向量的距离,找到和用户1兴趣接近的用户并以他们的喜好作为用户1的电影推荐列表(基于用户的协同过滤),或者找到和电影1风格接近的电影并以他们的受众作为电影1的用户推荐列表(基于物品的协同过滤)。文章接下来基于物品的协同过滤展开探讨。
计算两个向量的距离,有挺多种计算方式,如最常见的夹角余弦(Cosine)和欧氏距离,其他如曼哈顿距离、切比雪夫距离、闵可夫斯基距离、巴氏距离等等,各种计算方式侧重点不一,需要结合实际业务进行选择,这里仅以夹角余弦(Cosine)为例。
代码语言:python代码运行次数:0复制# -*- coding:utf-8 -*-
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.metrics.pairwise import pairwise_distances
from sklearn.metrics import mean_squared_error
from math import sqrt
def main():
header = ['user_id', 'item_id', 'rating', 'timestamp']
dataset = pd.read_csv('u.csv', sep=',', names=header)
# 获取去重用户数,共943个用户
users = dataset.user_id.unique().shape[0]
# 获取去重电影数,共1682部电影
items = dataset.item_id.unique().shape[0]
# 以3:1的比例,划分数据集为训练集和测试集
train_data, test_data = train_test_split(dataset, test_size=0.25)
# 生成训练集的用户物品矩阵
train_data_matrix = np.zeros((items, users))
for line in train_data.itertuples():
train_data_matrix[line[2] - 1, line[1] - 1] = line[3]
# 生成测试集的用户物品矩阵
test_data_matrix = np.zeros((items, users))
for line in test_data.itertuples():
test_data_matrix[line[2] - 1, line[1] - 1] = line[3]
# 计算两两物品之间的相似度,item_similarity是一个1682*1682的矩阵
item_similarity = pairwise_distances(train_data_matrix, metric='cosine')
"""
矩阵相乘;
其中item_similarity是一个1682*1682的矩阵,下标为(i,j)的元素表示第i部电影和第j部电影的相似度;
train_data_matrix是一个1682*943的矩阵,下标为(i,j)的元素表示第j个用户对第i部电影的打分;
forecast_data_matrix是一个1682*943的矩阵,下标为(i,j)的元素表示第j个用户对第i部电影的预测打分,其根据第i部电影与其他电影的相似度向量,以及第j个用户对其他电影的实际打分向量,经过向量点乘计算得到;
"""
forecast_data_matrix = item_similarity.dot(train_data_matrix)
# 计算每一部电影与其他电影的相似度的和,作为权重,用于调整forecast_data_matrix
weights = np.array([item_similarity.sum(axis=1)])
# 加权的forecast_data_matrix矩阵,使weighted_forecast_data_matrix的元素取值范围接近1-5
weighted_forecast_data_matrix = forecast_data_matrix / weights.T
'''
基于测试集,评估预测效果;其中,评估指标使用均方差误差;
test_data_matrix是1682*943的矩阵,一个共有25000个样本,评估预测效果时,只计算这部分样本,预测值和实际值的误差;
'''
predicted_value_list = []
actual_value_list = []
for i in range(1682):
for j in range(943):
actual_value = test_data_matrix[i][j]
if actual_value != 0:
actual_value_list.append(actual_value)
predicted_value = weighted_forecast_data_matrix[i][j]
predicted_value_list.append(predicted_value)
MSE = sqrt(mean_squared_error(predicted_value_list, actual_value_list))
print("MSE is {MSE}".format(MSE=MSE))
if __name__ == '__main__':
# 输出:MSE is 3.4505954993448706
main()
可以看到预测结果的均方误差是3.4505954993448706,用户的真实打分和预测打分约莫相差1.857577858218834(打分范围1-5)。传统的协同过滤算法,挖掘用户物品矩阵背后规律的思路是很明朗的,很经典的。我们想想刚才的实现方式中,哪些地方有比较大的改进空间。计算物品向量的距离,这部分属于数学范畴,几百年来牢不可破,我们能做的只有更换另外一个计算公式去衡量向量距离,可操作的空间很小。那物品向量呢,这部分变数很大,基于用户原始打分去构造,显然是比较粗糙的,似乎还有更好的方式去构造。
我们来分析一下这个用户物品矩阵,还有什么信息是可以挖取的?
第一,缺失值的填充不可马虎,能否改进?
试想一下,在实际业务中,我们较难获得像MovieLens这样的高质量数据集。目前市面上,物品推荐的产品设计或许是以下这样的:
1、基于feed流,侧重于推送,用户被动接受信息
2、信息爆炸,用户接触的物品仅是物品池里的一小部分
3、生活节奏很快,用户很忙,用户未必有时间给物品评分
种种原因导致,我们最后收集到的用户物品矩阵,是稀疏的。对待缺失值,除了选择忽略,或者简单替换,应该还有更好的方式去处理。
第二,原始数据里还有一个字段,打分的时间戳(单位:秒),这个信息在转化为用户物品矩阵时给丢掉了,我们能否带上?
第三,自从Mikolov在他2013年的论文“Efficient Estimation of Word Representation in Vector Space”提出词向量的概念后,embedding思想大行其道,万物皆向量,我们能否以一种更好的数据结构去组织这个用户物品矩阵?
我们在接下来的文章中继续讨论,欢迎大家关注我的专栏,您的支持,是我最大的动力。