前言
一开始了解SVD的是因为降维,但后面发现有人用它来做推荐,所以我去了解了一下,但我觉得推荐系统跟SVD好像关系并不大,本质上没有用SVD的理论,只是用了SVD的思想:矩阵分解。
矩阵分解
矩阵分解很简单,线性代数里两个矩阵相乘得到了一个矩阵:
P
2
∗
3
Q
3
∗
4
=
R
2
∗
4
P^{2*3} Q^{3*4}=R^{2*4}
P2∗3Q3∗4=R2∗4
P
2
∗
3
P^{2*3}
P2∗3表示矩阵是2x3维的,这样一个矩阵相乘的思想可以被我们反过来利用,将一个矩阵R n * m 分解为一个n * f 的矩阵P和 f * m 的矩阵Q,这样做的目的是什么?可以将一个大的矩阵分解为两个小矩阵,至少存储比较简单,类似压缩数据的技巧,降维也是这个思想。
推荐系统为什么需要矩阵分解?
因为大型网站的用户太多了,而且推荐的商品也多,你想象一下3亿的用户对上300万商品,这是个什么概念,这个维度得多大,所以,直接求解这个大矩阵不现实,所以通过求解两个小矩阵还是比较方便存储。
另一个是我们想挖掘用户和物品的隐藏关联,但这个关联是未知的,隐藏的关联,比如:用户看电影,用户与电影类型的关系,电影与电影类型的关系,那么电影类型就是一个隐藏因子,通过隐藏因子将用户和物品关联起来。
如何落地?
实际类似word2vec的思想,我们一开始初始化这两个矩阵P、Q,把它们作为需要训练的参数,然后通过样本来训练模型,构建损失函数,通过梯度下降来更新参数,这就是一个完整的落地流程,拆解一下详情大概就是:
-
正负样本
因为是做训练,就需要正负样本,正样本还是比较好找的,用户对这个商品评分或者购买等行为就是一个正样本,但负样本怎么选择?一般是选择那些热门商品、销量高的商品,但用户不感兴趣的作为负样本 -
构建模型
模型表达式:
R n , m = ∑ k = 1 K P n , k Q k , m R_{n,m}=\sum_{k=1}^{K}P_{n,k}Q_{k,m} Rn,m=k=1∑KPn,kQk,m
其实这个表达式表明的是第n个用户对第m个物品的预测评分,不是所有的样本。 -
损失函数
一般选用RMSE作为损失函数,
m e s = ∑ ( R n , m ^ − ∑ k = 1 K P n , k Q k , m ) 2 + α ∣ ∣ P ∣ ∣ 2 + β ∣ ∣ Q ∣ ∣ 2 mes=\sum(\hat{R_{n,m}} - \sum_{k=1}^{K}P_{n,k}Q_{k,m})^2+\alpha||P||^2+\beta||Q||^2 mes=∑(Rn,m^−k=1∑KPn,kQk,m)2+α∣∣P∣∣2+β∣∣Q∣∣2
R n , m ^ \hat{R_{n,m}} Rn,m^是第n个用户对第m个物品的真实评分。
尾项有两个,这两个是正则化表达式。 -
梯度更新
这一块主要是对矩阵每个元素做更新
存在的问题:
- 这样每次来一个用户,是不是都要训练一次模型?
因为这个用户的之前都不在样本集中,增加一个样本也就修改了一个矩阵的行数,似乎这个问题挺恶心的。 - 同样增加一个新的物品,也有同样的问题
于是基于物品的、用户的推荐似乎可以解决冷启动的问题。
代码
网上给你很多代码通过评分来构建模型,一个用户对物品的评分,这个评分的高低就说明了用户对物品的喜爱程度,通过样本来训练模型。
#-*- coding:utf-8 -*-
# 可以使用上面提到的各种推荐系统算法
from surprise import SVD
from surprise import Dataset
from surprise.model_selection import cross_validate
from surprise import Reader
from surprise import BaselineOnly, KNNBasic, NormalPredictor
from surprise import accuracy
from surprise.model_selection import KFold, split
from surprise import SVD,SVDpp
import numpy as np
import pandas as pd
from collections import defaultdict
import os
# 指定文件所在路径
file_path = os.path.expanduser('mydata.csv')
# 告诉文本阅读器,文本的格式是怎么样的
reader = Reader(line_format='user item rating', sep=',')
# 加载数据
data = Dataset.load_from_file(file_path, reader=reader)
trainset = data.build_full_trainset()
algo = SVD()
algo.fit(trainset)
def get_top_n(predictions, n=10):
# First map the predictions to each user.
top_n = defaultdict(list)
# uid: 用户ID
# iid: item ID
# true_r: 真实得分
# est:估计得分
for uid, iid, true_r, est, _ in predictions:
top_n[uid].append((iid, est))
# Then sort the predictions for each user and retrieve the k highest ones.
# 为每一个用户都寻找K个得分最高的item
for uid, user_ratings in top_n.items():
user_ratings.sort(key=lambda x: x[1], reverse=True)
top_n[uid] = user_ratings[:n]
return top_n
testset = [
('5','1',0),# 想获取第5个用户对第1个item的得分
('5','4',0),# 0这个位置是真实得分,不知道时可以写0
('5','5',0),# 但写0后,就没法进行算法评估了,因为不知道真实值
]
predictions = algo.test(testset)
top_n = get_top_n(predictions,10)
# Print the recommended items for each user
for uid, user_ratings in top_n.items():
print(uid, [iid for (iid, _) in user_ratings])
其中数据集部分,非常简单:
1,1,1
1,2,2
1,3,3
1,4,4
1,5,5
2,1,1
2,2,2
2,3,3
2,4,4
2,5,5
3,1,1
3,2,2
3,3,3
3,4,4
3,5,5
4,1,1
4,2,2
4,3,3
4,4,4
4,5,5
5,2,2
5,3,3
当然大家也可以主动去学习:
# 默认载入movielens数据集
data = Dataset.load_builtin('ml-100k')
# 试一把SVD矩阵分解
algo = SVD()
# 在数据集上测试一下效果
perf = cross_validate(algo, data, measures=['RMSE', 'MAE'], cv=3)
#输出结果
print(perf)