通过本文,你可以了解
《Advances in Collaborative Filtering》
配置方式参考上一篇BLOG。
pandas直接用pip安装即可。
因为在windows环境下直接采用pip安装会出现错误,所以本文推荐在此下载网 1下载numpy+scpiy+sklearn的二进制文件到目录下采用
pip install ***.whl
安装即可。
在矩阵M的奇异值分解中
在基于SVD算法的推荐系统中,我们可以发现,对于一个打分矩阵M,可以将其分解成P,Q的两个矩阵的乘积,而我们可以分别认为P与Q两个矩阵为用户和电影对每一个因子的影响程度。
了解了以上的思想,我们发现,我们要做的就是根据一个不完整的评分矩阵(通常是如此,因为用户不会对所有的电影打分,因此才需要我们做出推荐)来得出 P 和 Q 两个因子矩阵即可。但是我们发现,根据传统的SVD理论我们很难根据不完整的矩阵得到 P 与 Q 两个矩阵,通常的做法例如置0或者随机赋值经过尝试后发现对于这种稀疏矩阵对结果的影响很大所以放弃,于是我们想到了采用机器学习的思想来解决这个问题。
在论文《Advances in Collaborative Filtering》中,我们找到了一种通过学习来得到 P 和 Q 两个矩阵的方法,我们通过以下这个方程来进行打分
rui=μ+bi+bu+qTipu
其中
* μ 表示所有电影的平均分
* bi 表示用户评分偏离电影平均分的程度
* bu 表示电影评分偏离电影平均分的程度
* pu 表示第u个用户对各种因子的爱好程度
* qTi 表示第i部电影包含每个因子的程度
由此我们发现,问题的关键在于如何获得 P 与 Q 两个矩阵了,在论文中提到了一种方法,即通过不断的学习,让下面这个方程
minb∗,q∗,p∗∑(u,i)∈K(rui−μ−bi−bu−qTipu)2+λ4(b2i+b2u+∥qi∥2)+∥pu∥2)
取到最小值。同样该方法无法直接实现,于是论文中采用了梯度向下的方法进行学习,通过如下四个公式优化 bi,bu,qi,pu 四个参数
bu←bu+γ⋅(eui−λ4⋅bu)
bi←bi+γ⋅(eui−λ4⋅bi)
qi←qi+γ⋅(eui⋅pu−λ4⋅qi)
pu←pu+γ⋅(eui⋅qi−λ4⋅pu)
来得到 bi,bu,qi,pu 的值从而带到
rui=μ+bi+bu+qTipu
中去,来对每一段电影进行打分,其中当 γ=0.005,λ4=0.02 时我们可以得到较好的结果。
得到了具体的实现方法,我们需要一个合理的架构来实现它。通过多次迭代的方式来梯度下降学习到四个参数的值是一个比较好的思路,因此我们采用RMSE(均方根误差)来评价每次得到的结果如何。
到此我们已经基本阐述了一种简单的基于SVD的推荐系统,在接下来的部分我们主要介绍如何通过python来实现它。
通过研究数据集我们发现,数据集的结构为
用户ID::电影ID::打分::时间戳
这样的结构,我们采用pandas的read_csv方法将其读取进来。
file = '../DATA/Movie Lens/ratings.dat'
xls_data = pd.read_csv(file, sep='::', header=None)
其中参数sep='::'
表示以’::’分割数据,header=None
则表示该数据没有头行,即第一行就是数据。
python的sklearn库提供了非常方便的方法来划分测试集和训练集
train_X, test_X = sklearn.cross_validation.train_test_split(xls_data, test_size=0.2)
其中参数test_size=0.2
表示测试集占总的比例为0.2
我们通过如下的方式来初始化推荐系统
def __init__(self,data,vector_length=20):
#总共有多少用户,多少电影
self.user_num=6040
self.movie_num=3952
# 数据集
self.data=np.array(data)
# 向量的长度
self.vector_length=vector_length
# 电影的平均分
self.movie_average=np.mean(self.data[:,2])
# 损失函数的四个需要优化的参数 bi,bu,qi,pu
self.Bi = {}
self.Bu = {}
self.Qi = {}
self.Pu = {}
# 电影-用户矩阵
self.movie_user_mat={}
# 用户-电影矩阵
self.user_movie_mat={}
# 遍历数据,初始化数据集
for i in range(self.data.shape[0]):
user_id = self.data[i][0]
movie_id = self.data[i][1]
rating = self.data[i][2]
#初始化两个矩阵的每一行并赋值
self.movie_user_mat.setdefault(movie_id,{})
self.user_movie_mat.setdefault(user_id,{})
self.movie_user_mat[movie_id][user_id]=rating
self.user_movie_mat[user_id][movie_id]=rating
#通过随机赋值的方式初始化四个参数
self.Bi.setdefault(movie_id,0)
self.Bu.setdefault(user_id,0)
t=np.random.random((self.vector_length,1))/10*(np.sqrt(self.vector_length))
self.Qi.setdefault(movie_id,t)
t=np.random.random((self.vector_length,1))/10*(np.sqrt(self.vector_length))
self.Pu.setdefault(user_id,t)
以上代码的注释比较齐全就不在此赘述,只提一点,向量的长度就是因子数量
训练的方法也比较简洁,贴出代码如下所示
def train_model(self,steps=20,alpha=0.15,beta=0.04):
for step in range(steps):
sum_Rmse=0
for i in range(self.data.shape[0]):
user_id=self.data[i][0]
movie_id=self.data[i][1]
rating=self.data[i][2]
# 计算均方根误差
temp_Rmse=rating-self.recommend(user_id,movie_id)
sum_Rmse+=temp_Rmse**2
# 根据结果采用随机梯度下降进行优化
self.Bu[user_id]+=beta*(temp_Rmse-alpha*self.Bu[user_id])
self.Bi[movie_id]+=beta*(temp_Rmse-alpha*self.Bi[movie_id])
temp=self.Qi[movie_id]
self.Qi[movie_id]+=beta*(temp_Rmse*self.Pu[user_id]-alpha*self.Qi[movie_id])
self.Pu[user_id]+=beta*(temp_Rmse*temp-alpha*self.Pu[user_id])
print('第'+str(step)+'步迭代的RMSE值为'+str(np.sqrt(sum_Rmse/self.data.shape[0])))
alpha=alpha*0.93
具体的打分函数recommend
为
self.Bi.setdefault(movie_id,0)
self.Bu.setdefault(user_id,0)
self.Qi.setdefault(movie_id,np.zeros((self.vector_length,1)))
self.Pu.setdefault(user_id,np.zeros((self.vector_length,1)))
if (self.Qi[movie_id]==None):
self.Qi[movie_id]=np.zeros((self.vector_length,1))
if (self.Pu[user_id]==None):
self.Pu[user_id]=np.zeros((self.vector_length,1))
score=self.movie_average+self.Bi[movie_id]+self.Bu[user_id]+np.sum(self.Qi[movie_id]*self.Pu[user_id])
if score>5:
return 5
elif score<1:
return 1
return score
测试的思想比较简单,通过读取测试数据进行打分再与真实分数进行比较并计算RMSE即可
test_data_rmse=0
test_data=np.array(test_data)
result_list=[]
for i in range(test_data.shape[0]):
my_rating=self.recommend(test_data[i][0],test_data[i][1])
result_list.append((test_data[i][0],test_data[i][1],my_rating))
test_data_rmse+=(my_rating-test_data[i][2])**2
print('测试集的RMSE为'+str(np.sqrt(test_data_rmse/test_data.shape[0])))
return result_list
推荐的思路为首先遍历整个数据寻找仍未打分的数据,然后对该用户所有未打分的电影进行打分并排序,将得分最高的五个返回即可。
#寻找已经评分的电影
rated_num=[]
for i in range(self.data.shape[0]):
i_user_id=self.data[i][0]
i_movie_id=self.data[i][1]
if (i_user_id==user_id):
rated_num.append(i_movie_id)
#对未评分的电影打分
rating_list=[]
for i in range(self.movie_num):
this_movie_id=i+1
if (this_movie_id not in rated_num):
score=self.recommend(user_id,this_movie_id)
rating_list.append((this_movie_id,score))
# 对得分进行排序
rating_list.sort(key=operator.itemgetter(1),reverse=True)
# 选出得分最高的五部电影
return rating_list[:5]
在最初由于只考虑到了SVD的数学意义即可把任意一个矩阵分解成三个矩阵的乘积,结果在完成后发现由于评分矩阵中存在着大量的未打分矩阵所以无法从数学意义上进行分解而需要采用机器学习的方法学习得到两个因子矩阵。而正是为此翻阅论文也是花费了不少时间。
python的pip是真的坑啊,python的对齐也是真的坑啊!
机器学习的公式推导全是数学上的计算,感觉自己脆弱的数学根基受到了极大地摧残。
实验终于结束啦,耶!