一、基本原理
1. 自编码结构
矩阵分解本质上只通过一次分解来对原矩阵进行逼近,特征挖掘层次不够深入,也没有运用到物品本身的特征。随着神经网络的兴起,多层感知机可以得到更加深度的特征表示,并对内容分类特征加以利用。该方法试图直接学习数据的特征集,利用与此特征集相应的基向量,将学习得到的特征集从特征空间转换到样本数据空间,这样我们就可以用学习得到的特征集重构样本数据。其根本是一种数据降维的方法。
大型推荐系统,物品的数量级为千万,用户的数量级为亿。所以用户对物品的打分基本不可能靠离线计算完成,只能依靠在线计算。而在线计算能更快地响应最近的事件和用户交互,但必须实时完成。这又会限制使用算法的复杂性和处理的数据量。所以个性化推荐实时架构的关键问题,就是如何以无缝方式结合、管理在线和离线计算过程。使用稀疏编码进行数据降维后,用户或者物品均可用一组低维基向量表征,便于存储计算,可供在线层实时调用。
稀疏自编码神经网络是一种无监督学习算法。假设我们只有一个没有带类别标签的训练样本集合:,其中,稀疏编码使用反向传播算法,并让目标值等于输入值,同时中间层维度远低于输入层和输出层,如下图所示,这样就得到了第一层特征压缩。
简言之,自编码神经网络尝试学习一个恒等函数
如果网络的输入数据是完全随机的,比如每一个输入都是一个跟其它特征完全无关的独立同分布高斯随机变量,那么这一压缩表示将会非常难学习。但是如果输入数据中隐含着一些特定的结构,比如某些输入特征是彼此相关的,那么这一算法就可以发现输入数据中的这些相关性。
以上是自编码最基础的结构,另外我们也可以用深度学习的一些思想,学习到高层抽象特征。其中一种方法是栈式自编码,其采用逐层贪婪训练法进行训练。即先利用原始输入来训练网络的第一层,得到其参数
然后网络第一层将原始输入转化成为由隐藏单元激活值组成的向量,接着将其作为第二层的输入,继续训练得到第二层的参数
最后,对后面的各层同样采用的策略,即将前层的输出作为下一层输入的方式依次训练。
假设我们用原始输入 ,训练第一个自编码器,它能够学习得到原始输入的一阶特征表示 ,然后再用这些一阶特征作为另一个稀疏自编码器的输入,使用它们来学习二阶特征,接下来,可以把这些二阶特征作为softmax分类器的输入,训练得到一个能将二阶特征映射到数字标签的模型。最终,可以将这三层结合起来构建一个包含两个隐藏层和一个最终softmax分类器层的栈式自编码网络。
2. 推荐系统中的应用
在推荐系统中,主要使用稀疏编码的方法,输入用户点击/收藏/购买数据,训练出物品及用户的特征向量,具体构造自编码网络的方法如下:
输入层,每首物品的输入向量为,其中表示用户是否点击/收藏/购买该物品,输入矩阵(包含一个截距项),为用户数量,为物品数量。
输出层,指定为和输出层一致(无截距项)。
隐藏层,强制指定神经元的数量为个,此时隐藏层其实就是物品的低维特征向量,矩阵为,为特征维数(包含一个截距项1,之所以保留,是为了可以重构出输出层),为物品数量。
隐藏层到输出层的连接。一般的神经网络中,往往会忽略隐藏层到输出层的连接权重
的意义,只是将其作为一个输出预测的分类器;但在自编码网络中,连接层是有实际意义的。这些权重作用是将物品特征向量映射到用户是否听过/喜欢该物品,其实可就是用户的低维特征,所以该稀疏网络同样可以学习到用户的特征矩阵。值得注意的是,当网络结构为3层时,其目标函数与svd基本一致,算法上是相通的。
二、算法实现
采用GroupLens提供的MovieLens数据集,https://grouplens.org/datasets/movielens/ ,实现上述算法。
1.数据加载
import numpy as np
import pandas as pd
import torch
import torch.nn as nn
import torch.nn.parallel
import torch.optim as optim
import torch.utils.data
from torch.autograd import Variable
training_set = pd.read_csv('ml-100k/u1.base', delimiter='\t', header=None)
test_set = pd.read_csv('ml-100k/u1.test', delimiter='\t', header=None)
training_set = np.array(training_set,dtype='int')
test_set = np.array(test_set,dtype='int')
n_users = int(max(max(training_set[:,0]),max(test_set[:,0])))
n_movies = int(max(max(training_set[:,1]),max(test_set[:,1])))
print('Number of users:{}, number of movies:{}'.format(n_users,n_movies))
training_set,test_set数据的基本格式为userId,movieId,rating,timestamp,下图为部分数据举例。
据统计,训练集共80000条记录,测试集共20000条记录,其中用户总数为943,电影总数为1682.
Number of users:943, number of movies:1682
2. 数据转换
将原始数据转化为矩阵形式,用户为行,电影为列,每一行为用户对其看过的电影的评分,列为每个电影不同用户的评分,矩阵大小为。
def convert(data):
new_data = []
for id_user in range(1,n_users+1):
id_movie = data[:,1][data[:,0]==id_user]
id_rating = data[:,2][data[:,0]==id_user]
ratings = np.zeros(n_movies)
ratings[id_movie-1] = id_rating
new_data.append(list(ratings))
return new_data
training_set = convert(training_set)
test_set = convert(test_set)
training_set = torch.FloatTensor(training_set)
test_set = torch.FloatTensor(test_set)
进而将其转化为Pytorch的FloatTensor的数据格式,结果如下:
In [3]: training_set
Out[3]:
tensor([[5., 3., 4., ..., 0., 0., 0.],
[4., 0., 0., ..., 0., 0., 0.],
[0., 0., 0., ..., 0., 0., 0.],
...,
[5., 0., 0., ..., 0., 0., 0.],
[0., 0., 0., ..., 0., 0., 0.],
[0., 5., 0., ..., 0., 0., 0.]])
In [4]: test_set
Out[4]:
tensor([[0., 0., 0., ..., 0., 0., 0.],
[0., 0., 0., ..., 0., 0., 0.],
[0., 0., 0., ..., 0., 0., 0.],
...,
[0., 0., 0., ..., 0., 0., 0.],
[0., 0., 0., ..., 0., 0., 0.],
[0., 0., 0., ..., 0., 0., 0.]])
3. 自编码网络的搭建与实例化
本文的自编码结构图如下所示,损失函数采用MSE,优化器采用RMSprop。
class SAE(nn.Module):
def __init__(self):
super(SAE,self).__init__()
self.fc1 = nn.Linear(n_movies,20)
self.fc2 = nn.Linear(20,10)
self.fc3 = nn.Linear(10,20)
self.fc4 = nn.Linear(20,n_movies)
def forward(self,x):
x = nn.Sigmoid()(self.fc1(x))
x = nn.Sigmoid()(self.fc2(x))
x = nn.Sigmoid()(self.fc3(x))
x = self.fc4(x)
return x
sae = SAE()
criterion = nn.MSELoss()
optimizer = optim.RMSprop(sae.parameters(),lr=0.01,weight_decay=0.5)
4. 训练及测试过程
代码的具体含义如注释所示:
epochs = 200
for epoch in range(1,epochs+1):
train_loss = 0
s = 0
for id_user in range(n_users): #对于每个用户
input = Variable(training_set[id_user]).unsqueeze(0) # 输入其电影评分
target = input.clone() #目标与输入相同
if torch.sum(target.data>0)>0: # 至少有一个评分
output = sae(input) #调用自编码网络
target.require_grad = False #目标不允许梯度
output[target==0] = 0 #未评分输出仍为0
loss = criterion(output,target) #损失函数
mean_corrector = n_movies/float(torch.sum(target.data>0) + 1e-10) #均分
loss.backward() #误差反向传播
train_loss += np.sqrt(loss.data*mean_corrector)
s += 1
optimizer.step()
print('epoch:'+str(epoch)+' training loss:'+str(train_loss/s))
test_loss = 0
s = 0
for id_user in range(n_users):
input = Variable(training_set[id_user]).unsqueeze(0)
target = Variable(test_set[id_user]).unsqueeze(0)
if torch.sum(target.data>0)>0:
output = sae(input)
target.require_grad = False
output[target==0] = 0
loss = criterion(output,target)
mean_corrector = n_movies/float(torch.sum(target.data>0) + 1e-10)
test_loss += np.sqrt(loss.data*mean_corrector)
s += 1
print('test loss:'+str(test_loss/s))
训练过程如下:
epoch:1 training loss:tensor(1.7715)
epoch:2 training loss:tensor(1.0967)
epoch:3 training loss:tensor(1.0535)
epoch:4 training loss:tensor(1.0383)
epoch:5 training loss:tensor(1.0310)
epoch:6 training loss:tensor(1.0268)
epoch:7 training loss:tensor(1.0241)
epoch:8 training loss:tensor(1.0219)
epoch:9 training loss:tensor(1.0209)
epoch:10 training loss:tensor(1.0200)
...
epoch:191 training loss:tensor(0.9185)
epoch:192 training loss:tensor(0.9186)
epoch:193 training loss:tensor(0.9181)
epoch:194 training loss:tensor(0.9186)
epoch:195 training loss:tensor(0.9176)
epoch:196 training loss:tensor(0.9184)
epoch:197 training loss:tensor(0.9177)
epoch:198 training loss:tensor(0.9179)
epoch:199 training loss:tensor(0.9170)
epoch:200 training loss:tensor(0.9176)
test loss:tensor(0.9560)
5. 效果预测
首先找到电影及其对应的原本的名称
movies = pd.read_csv('ml-100k/u.item', sep = '|', engine = 'python', encoding = 'latin-1', header = None)
movie_title = movies.iloc[:n_movies, 1:2]
user_id = 10
user_rating = training_set.data.numpy()[user_id - 1, :].reshape(-1,1)
user_target = test_set.data.numpy()[user_id, :].reshape(-1,1)
user_input = Variable(training_set[user_id]).unsqueeze(0)
predicted = sae(user_input)
predicted = predicted.data.numpy().reshape(-1,1)
result_array = np.hstack([movie_title, user_target, predicted])
result_array = result_array[result_array[:, 1] > 0]
result_df = pd.DataFrame(data=result_array, columns=['Movie', 'Target Rating', 'Predicted'])
进一步对第10号用户,进行预测并只保留非0项
参考资料
[1]. 推荐系统与深度学习. 黄昕等. 清华大学出版社. 2019.
[2]. 美团机器学习实践. 美团算法团队. 人民邮电出版社. 2018.
[3]. 推荐系统算法实践. 黄美灵. 电子工业出版社. 2019.
[4]. 推荐系统算法. 项亮. 人民邮电出版社. 2012.
[5]. https://github.com/fredkron/SAE_Recommendation_System
[6]. https://github.com/devalindey/Recommendation-System-with-SAE-using-Pytorch
[7]. https://zhuanlan.zhihu.com/p/33801415
惟此独立之精神,自由之思想,历千万祀,与天壤而同久,共三光而永光。——陈寅恪 题王国维碑