初学者指南:利用SVD创建推荐系统

2020-08-28 14:42:59 浏览数 (1)

作者:Mayukh Bhattacharyya

翻译:老齐

序言

你是否有过这样的经历:前一天晚上登录Netflix,观看了《星际穿越》,他们会建议你看《地心引力》。或者你在亚马逊上购买了东西,看到了网站推荐给你可能感兴趣的产品。你是否想知道在线广告代理商是如何根据我们的浏览习惯向我们推送广告的?这一切都归结为一种被称为推荐系统的东西,它根据我们与产品互动的历史,预测我们可能对哪些产品感兴趣。

本文中,我们将建立一个很酷的推荐系统。我们将使用SVD(Sigular Vector Decomposition)技术,这比基于内容的基本推荐系统要高级得多。

SVD是一种协同过滤算法,它能捕获志趣相投的用户的潜在兴趣模式,并根据相似用户的选择和偏好来推送新产品。

数据集

我们肯定需要一个数据集,本文中将使用著名的Movielens数据集,可以在 http://grouplens.org/datasets/movielens/ 网页上下载 movielens100k 数据集。

这个数据集包含了不同用户对各种电影的大约10万个评价。我们研究一下数据集。创建新exploration.py文件并添加以下代码块。注意:这里我们将使用脚本文件,你也可以在 IPython notebook 中运行下面的程序。

代码语言:javascript复制
import pandas as pd
import numpy as np

data = pd.read_csv('movielens100k.csv')
data['userId'] = data['userId'].astype('str')
data['movieId'] = data['movieId'].astype('str')

users = data['userId'].unique()                # list of all users
movies = data['movieId'].unique()          #list of all movies

print("Number of users", len(users))
print("Number of movies", len(movies))
print(data.head())

做得不错!你将看到数据集中有718个用户和8915部电影。

代码语言:javascript复制
Number of users 718
Number of movies 8915
 ---- ---------- ----------- ---------- ------------- 
|    |   userId |   movieId |   rating |   timestamp |
|---- ---------- ----------- ---------- -------------|
|  0 |        1 |         1 |        5 |   847117005 |
|  1 |        1 |         2 |        3 |   847642142 |
|  2 |        1 |        10 |        3 |   847641896 |
|  3 |        1 |        32 |        4 |   847642008 |
|  4 |        1 |        34 |        4 |   847641956 |
 ---- ---------- ----------- ---------- ------------- 

划分训练集和测试集

我们本可以按通常的随机方式将数据集划分为训练集和测试集。但是既然数据集中有时间戳(timestamp特征),那就让我们做一些更奇特吧。创建一个新脚本workspace.py ,在脚本完成所有的工作。

代码语言:javascript复制
import pandas as pd
import numpy as np
import scipy

data = pd.read_csv('movielens100k.csv')
data['userId'] = data['userId'].astype('str')
data['movieId'] = data['movieId'].astype('str')

users = data['userId'].unique() #list of all users
movies = data['movieId'].unique() #list of all moviestest = pd.DataFrame(columns=data.columns)

train = pd.DataFrame(columns=data.columns)
test_ratio = 0.2           #fraction of data to be used as test set.

for u in users:
    temp = data[data['userId'] == u]
    n = len(temp)
    test_size = int(test_ratio*n)
    temp = temp.sort_values('timestamp').reset_index()

temp.drop('index', axis=1, inplace=True)
    
dummy_test = temp.ix[n-1-test_size :]
dummy_train = temp.ix[: n-2-test_size]
    
test = pd.concat([test, dummy_test])
train = pd.concat([train, dummy_train])

这样做的目的是,根据这些评级的时间戳对数据进行排序,以使最近的评级保持在底部,并且从底部开始对每个用户取20%的评级作为测试集。所以,我们用最近的评级作为测试集,而不是随机选择。这样更符合逻辑,因为推荐系统的目标是:以类似产品的历史评级为基础,然后对未遇到过的产品进行评级。

效用矩阵

当前形式的数据集对我们毫无用处。为了将数据用于推荐系统,我们需要将数据集转换为一种效用矩阵(Utility Matrix)的形式。下面的脚本中创建了函数create_utility_matrix,并且把新脚本文件命名为recsys.py,使用此脚本中的函数来处理训练和测试集数据。

代码语言:javascript复制
import numpy as np
import pandas as pd
from scipy.linalg import sqrtm

def create_utility_matrix(data, formatizer = {'user':0, 'item': 1, 'value': 2}):
    """
        :param data:      Array-like, 2D, nx3
        :param formatizer:pass the formatizer
        :return:          utility matrix (n x m), n=users, m=items
    """
        
    itemField = formatizer['item']
    userField = formatizer['user']
    valueField = formatizer['value']
    userList = data.ix[:,userField].tolist()
    itemList = data.ix[:,itemField].tolist()
    valueList = data.ix[:,valueField].tolist()
    users = list(set(data.ix[:,userField]))
    items = list(set(data.ix[:,itemField]))
    users_index = {users[i]: i for i in range(len(users))}
    pd_dict = {item: [np.nan for i in range(len(users))] for item in items}
    for i in range(0,len(data)):
        item = itemList[i]
        user = userList[i]
        value = valueList[i]
        pd_dict[item][users_index[user]] = value
        X = pd.DataFrame(pd_dict)
    X.index = users
        
    itemcols = list(X.columns)
    items_index = {itemcols[i]: i for i in range(len(itemcols))}
    # users_index gives us a mapping of user_id to index of user
    # items_index provides the same for items
    
    return X, users_index, items_index

代码语言:javascript复制
 ---- ---------- ----------- ---------- 
|    |   userId |   movieId |   rating |
|---- ---------- ----------- ---------- 
|  0 |      mark|     movie1|        5 |
|  1 |      lucy|     movie2|        2 |
|  2 |      mark|     movie3|        3 |
|  3 |     shane|     movie2|        1 |
|  4 |      lisa|     movie3|        4 |
 ---- ---------- ----------- ---------- 

如果将此数据集传到 create_utility_matrix函数,它将返回一个这样的效用矩阵以及辅助字典 user_indexitem_index,如下所示。

代码语言:javascript复制
 ---- ---- ---- 
| 5  | nan| 3  |   # user_index = {mark: 0, lucy:1, shane:2, lisa:3}
 ---- ---- ----    # item_index = {movie1:0, movie2: 1, movie3:2}
| nan| 2  | nan|
 ---- ---- ---- 
| nan| 1  | nan|   # The nan values are for user-item combinations
 ---- ---- ----    # where the ratings are unavailable.
| nan| nan| 4  |
 ---- ---- ---- 

SVD 计算

SVD是奇异向量分解,它的作用是将一个矩阵分解成特征向量的数组,这个数组对应着各行各列。继续编辑文件recsys.py,创建函数svd,这个函数将从create_utility_matrix函数和参数k中得到输出结果。参数k是每个用户和每部电影将被解析到的特征值。

SVD 是由 Brandyn Webb 引入推荐系统领域的,他在Netflix Prize挑战赛中所用的名字 Simon Funk 更加著名。这里我们没有使用 Funk 的迭代版本的SVD或FunkSVD,而是使用numpy中提供的SVD函数实现。

代码语言:javascript复制
def svd(train, k):
    utilMat = np.array(train)    # the nan or unavailable entries are masked
    mask = np.isnan(utilMat)
    masked_arr = np.ma.masked_array(utilMat, mask)
    item_means = np.mean(masked_arr, axis=0)    # nan entries will replaced by the average rating for each item
    utilMat = masked_arr.filled(item_means)
    x = np.tile(item_means, (utilMat.shape[0],1))    # we remove the per item average from all entries.
    # the above mentioned nan entries will be essentially zero now
    
    utilMat = utilMat - x    # The magic happens here. U and V are user and item features
    U, s, V=np.linalg.svd(utilMat, full_matrices=False)
    s=np.diag(s)    # we take only the k most significant features
    s=s[0:k,0:k]
    U=U[:,0:k]
    V=V[0:k,:]    s_root=sqrtm(s)    Usk=np.dot(U,s_root)
    skV=np.dot(s_root,V)
    UsV = np.dot(Usk, skV)    UsV = UsV   x    print("svd done")
    return UsV

组合起来

回到workspace.py文件, 在这个文件中引入上面的函数。我们将使用真实的评级,找出测试集预测评级的均方根误差。除了创建函数,我们还将创建一个列表来保存不同数量的特征,这将有助于后面的分析。

代码语言:javascript复制
from recsys import svd, create_utility_matrix

def rmse(true, pred):
    # this will be used towards the end
    x = true - pred
    return sum([xi*xi for xi in x])/len(x)  # to test the performance over a different number of features

no_of_features = [8,10,12,14,17]
utilMat, users_index, items_index = create_utility_matrix(train)

for f in no_of_features: 
    svdout = svd(utilMat, k=f)
    pred = []           #to store the predicted ratings
    for _,row in test.iterrows():
        user = row['userId']
        item = row['movieId']
        u_index = users_index[user]
        if item in items_index:
            i_index = items_index[item]
            pred_rating = svdout[u_index, i_index]
        else:
            pred_rating = np.mean(svdout[u_index, :])
        pred.append(pred_rating)print(rmse(test['rating'], pred))

对于 test_size = 0.2,RMSE分数约为0.96

这是 Movielens100k 的一个适度的分数。稍加调整,你也许可以超过0.945,但这取决于你。

如果你喜欢这篇文章,请告诉我!以下是3个链接供您阅读:

  • https://github.com/mayukh18/reco(SVD的完整代码以及其他著名RecSys算法的实现)
  • https://paperswithcode.com/sota/collaborative-filtering-on-movielens-100k(Movielens100k的最新结果。这些结果在官方测试集上得到验证)
  • https://sifter.org/~simon/journal/20061211.html(Simon Funk最著名的博客,详细介绍了他的SVD方法)

原文链接:https://towardsdatascience.com/beginners-guide-to-creating-an-svd-recommender-system-1fd7326d1f65

0 人点赞