推荐系统(四):基于稀疏自编码的推荐方法

一、基本原理

1. 自编码结构

矩阵分解本质上只通过一次分解来对原矩阵进行逼近,特征挖掘层次不够深入,也没有运用到物品本身的特征。随着神经网络的兴起,多层感知机可以得到更加深度的特征表示,并对内容分类特征加以利用。该方法试图直接学习数据的特征集,利用与此特征集相应的基向量,将学习得到的特征集从特征空间转换到样本数据空间,这样我们就可以用学习得到的特征集重构样本数据。其根本是一种数据降维的方法。
大型推荐系统,物品的数量级为千万,用户的数量级为亿。所以用户对物品的打分基本不可能靠离线计算完成,只能依靠在线计算。而在线计算能更快地响应最近的事件和用户交互,但必须实时完成。这又会限制使用算法的复杂性和处理的数据量。所以个性化推荐实时架构的关键问题,就是如何以无缝方式结合、管理在线和离线计算过程。使用稀疏编码进行数据降维后,用户或者物品均可用一组低维基向量表征,便于存储计算,可供在线层实时调用。
稀疏自编码神经网络是一种无监督学习算法。假设我们只有一个没有带类别标签的训练样本集合:\{x^{1},x^2,x^3,\dots\},其中x^i \in R^n,稀疏编码使用反向传播算法,并让目标值等于输入值y^{(i)}=x^{(i)},同时中间层维度远低于输入层和输出层,如下图所示,这样就得到了第一层特征压缩。

自编码神经网络模型

简言之,自编码神经网络尝试学习一个恒等函数

如果网络的输入数据是完全随机的,比如每一个输入都是一个跟其它特征完全无关的独立同分布高斯随机变量,那么这一压缩表示将会非常难学习。但是如果输入数据中隐含着一些特定的结构,比如某些输入特征是彼此相关的,那么这一算法就可以发现输入数据中的这些相关性。

以上是自编码最基础的结构,另外我们也可以用深度学习的一些思想,学习到高层抽象特征。其中一种方法是栈式自编码,其采用逐层贪婪训练法进行训练。即先利用原始输入来训练网络的第一层,得到其参数
W^{(1,1)},W^{(1,2)},b^{(1,1)},b^{(1,2)}

然后网络第一层将原始输入转化成为由隐藏单元激活值组成的向量,接着将其作为第二层的输入,继续训练得到第二层的参数
W^{(2,1)},W^{(2,2)},b^{(2,1)},b^{(2,2)}

最后,对后面的各层同样采用的策略,即将前层的输出作为下一层输入的方式依次训练。
假设我们用原始输入x^{(k)} ,训练第一个自编码器,它能够学习得到原始输入的一阶特征表示 h^{(1)(k)},然后再用这些一阶特征作为另一个稀疏自编码器的输入,使用它们来学习二阶特征h^{(2)(k)},接下来,可以把这些二阶特征作为softmax分类器的输入,训练得到一个能将二阶特征映射到数字标签的模型。最终,可以将这三层结合起来构建一个包含两个隐藏层和一个最终softmax分类器层的栈式自编码网络。

完整的栈式自编码网络

2. 推荐系统中的应用

在推荐系统中,主要使用稀疏编码的方法,输入用户点击/收藏/购买数据,训练出物品及用户的特征向量,具体构造自编码网络的方法如下:
输入层,每首物品的输入向量为(u_1,u_2,u_3,\dots),其中u_i表示用户i是否点击/收藏/购买该物品,输入矩阵(m+1)\times n(包含一个截距项),m为用户数量,n为物品数量。
输出层,指定为和输出层一致(无截距项)。
隐藏层,强制指定神经元的数量为k+1个,此时隐藏层其实就是物品的低维特征向量,矩阵为(k+1)\times nk+1为特征维数(包含一个截距项1,之所以保留,是为了可以重构出输出层),n为物品数量。
隐藏层到输出层的连接。一般的神经网络中,往往会忽略隐藏层到输出层的连接权重
W^{(1,1)},W^{(1,2)},b^{(1,1)},b^{(1,2)}

的意义,只是将其作为一个输出预测的分类器;但在自编码网络中,连接层是有实际意义的。这些权重作用是将物品特征向量映射到用户是否听过/喜欢该物品,其实可就是用户的低维特征,所以该稀疏网络同样可以学习到用户的特征矩阵m\times(k+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,下图为部分数据举例。


training_set

test_set

据统计,训练集共80000条记录,测试集共20000条记录,其中用户总数为943,电影总数为1682.

Number of users:943, number of movies:1682

2. 数据转换

将原始数据转化为矩阵形式,用户为行,电影为列,每一行为用户对其看过的电影的评分,列为每个电影不同用户的评分,矩阵大小为943\times 1682

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. 效果预测

首先找到电影及其对应的原本的名称


movie_title 举例
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

惟此独立之精神,自由之思想,历千万祀,与天壤而同久,共三光而永光。——陈寅恪 题王国维碑

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容