推荐系统之AutoRec模型原理以及代码实践

简介

本文要介绍的AutoRec模型是由澳大利亚国立大学在2015年提出的,它将自编码器(AutoEncoder)的思想与协同过滤(Collaborative Filter)的思想结合起来,提出了一种单隐层的简单神经网络推荐模型。可以说这个模型的提出,拉开了使用深度学习解决推荐系统问题的序幕,为复杂深度学习网络的构建提供了思路。原论文只有2页,非常简洁明了,比较适合作为深度学习推荐系统的入门模型来学习,原文地址在这里


前言

本文会介绍AutoRec模型的基本原理,包括网络模型、损失函数、推荐过程、实验结果等,并且会给出基于PyTorch的代码。

AutoRec模型介绍

AutoRec模型跟MLP(多层感知器)类似,是一个标准的3层(包含输入层)神经网络,只不过它结合其结合了自编码器(AutoEncoder)和协同过滤(Collaborative Filtering)的思想。其实再确切一点说,AutoRec模型就是一个标准的自编码器结构,它的基本原理是利用协同过滤中的共现矩阵,完成物品向量或者用户向量的自编码。再利用自编码结果得到用户对所有物品的评分,结果通过排序之后就可以用于物品推荐。

这里先简要地介绍一下自编码器:
自编码器是一种无监督的数据维度压缩和数据特征表达方法,它是神经网络的一种,经过训练后能尝试将输入复制到输出。自编码器由编码器和解码器组成,结构如下:

自编码器结构

其中输入为x,输出为rS代表所有的输入数据向量,h=f(x)表示编码器,r=g(h)=g(f(x))表示解码器,自编码器的目标便是优化损失函数\mathop {argmin} \sum_{r \in S} \| r - g(f(x)) \|^2_{2},也就是令图中的红色部分的Error的值最小。
那使得输出向量与输入向量误差最小有什么意义呢?直接使用原始输入向量不行么?干嘛需要进行编码和解码这些操作?这些问题不是本文关注的点,感兴趣的童鞋可以参考链接
我这里提一下,为什么自编码器可以与协同过滤算法结合起来用于推荐系统呢?
假设有m个用户,n个物品,那么协同过滤算法中的共现矩阵保存了每个用户u对每个物品i的评分,但是实际上这个矩阵是一个巨大的稀疏矩阵,因为用户不可能对每个物品都评过分,因此矩阵中存在大量的缺失值。如果我们将所有用户对物品i的评分数据单独拿出来,可以得到一个m维的向量,这个向量毫无疑问也是稀疏的。我们将其传入自编码器,得到一个输出向量。由于经过了自编码器的"泛化"过程,故输出向量与输入向量不完全相同,即某些缺失值会被补上,故自编码器具备了一定缺失维度的预测能力。如果我们能得到全部的缺失值,那么我们就得到了所有用户对物品i的评分数据,就可以用于物品推荐了。

在基于评分数据的协同过滤算法当中,假设我们有m个用户,n个物品,则有用户-物品评分矩阵R \in \mathbb R^{m \times n}。对于一个用户u来说,他对所有n个物品的评分数据可以形成一个n维的向量r^{(u)} = (R_{u1}, ...,R_{un})。同理,对于一个物品i而言,所有m个用户对它的评分可以构成一个m维的向量r^{(i)} = (R_{1i}, ...,R_{mi})。其中R_{ui}代表的是含义是用户u对物品i的评分。
下面是AutoRec的整体模型框图:

基于物品的AutoRec模型

可以看到整个模型只有3层,蓝色的圆点代表的是隐层神经元,红色方框代表的是模型的输入r^{(i)},经过权重矩阵V到达隐藏层,再经过权重矩阵W到达输出层,我们的目的是通过训练模型,找到合适的权重矩阵VW,以及偏置\mub,使得输入值和输出值的误差最小。
令模型的重建函数为h(r; \theta ),其中r \in\mathbb R^d,其定义如下:

其中f(\cdot)代表的是输出层的神经元的激活函数,g(\cdot)代表的是隐层神经元的激活函数。这里的参数\theta = \{ W, V, \mu, b\}VW分别是隐层和输出层的权重矩阵,\mub分别是隐层和输出层的偏置,权重矩阵W \in \mathbb R^{d \times k},V \in \mathbb R^{k \times d},偏置\mu \in \mathbb R^{k}b \in \mathbb R^d。故在基于物品的AutoRec模型中,参数总量为2mk + m+ k个。

损失函数

首先给出自编码器的损失函数,如下:


其中Sd维向量的集合。

AutoRec模型的损失函数中考虑到了对参数的限制,因此加入了L2正则来防止过拟合,损失函数变化为:


其中\| \cdot \|_F 为Frobenius范数。
定义好损失函数之后,就可以使用反向传播和梯度下降等方法来进行模型训练了。基于物品的AutoRec模型的训练过程如下:

  1. 输入物品i的评分向量r^{(i)},即所有用户对物品i的评分向量。
  2. 得到模型的评分预测输出向量h(r^{(i)}; \theta)
  3. 根据评分预测向量和真实评分向量的误差进行训练,最小化损失函数,得到最终的模型参数\theta

基于AutoRec的推荐过程

假如我们现在已经训练好了模型,那基于物品的AutoRec模型的推荐过程是怎样的呢?其实非常简单,只需要2步:

  1. 依次输入物品i的评分向量r^{(i)},得到模型输出的预测评分向量h(r^{(i)}; \theta)
  2. 遍历所有物品预测评分向量的第u维,得到用户u对所有物品的评分,进行排序之后得到用户u的推荐列表。

上述流程是针对基于物品的AutoRec模型,也就是Item-AutoRec,其实只要把AutoRec模型的输入变成用户向量r^{(u)},那么模型其实就变成了基于用户的AutoRec模型,即User-AutoRec。这两者的推荐流程有点不同,在后面的代码实践中,我将两种推荐方式都写下来了,读者可以参考代码。

实验对比

作者分别在MovieLens 1M和10M、以及Netflix数据上进行了对比实验,评测指标为RMSE,即均方根误差。分别与U-RBM、I-RBM、BiasedMF、LLORMA算法进行了对比。结果如下:


对比实验结果1
对比实验结果2

作者还做了消融实验,验证选择不同的激活函数对最终实验结果的影响。
消融实验

除此之外,还评测了不同隐层神经元数量对实验结果的影响, 可以看到随着隐层神经元数量的增加,RMSE稳步下降。

代码实践

代码基于PyTorch编写,主要包含数据预处理和加载文件dataloader.py,网络模型定义network.py,训练器trainer.py,以及测试文件autorec_test.py。
数据预处理部分比较简单,测试的数据为MovieLens 1M数据集,主要定义了m \times n的共现矩阵,并且将数据集划分为训练集和测试集。完整代码如下:

import torch
import numpy as np
import torch.utils.data as Data

def dataProcess(filename, num_users, num_items, train_ratio):
    fp = open(filename, 'r')
    lines = fp.readlines()

    num_total_ratings = len(lines)

    user_train_set = set()
    user_test_set = set()
    item_train_set = set()
    item_test_set = set()

    train_r = np.zeros((num_users, num_items))
    test_r = np.zeros((num_users, num_items))

    train_mask_r = np.zeros((num_users, num_items))
    test_mask_r = np.zeros((num_users, num_items))

    # 生成0~num_total_ratings范围内的的随机序列
    random_perm_idx = np.random.permutation(num_total_ratings)
    # 将数据分为训练集和测试集
    train_idx = random_perm_idx[0:int(num_total_ratings * train_ratio)]
    test_idx = random_perm_idx[int(num_total_ratings * train_ratio):]

    ''' Train '''
    for itr in train_idx:
        line = lines[itr]
        user, item, rating, _ = line.split("::")
        user_idx = int(user) - 1
        item_idx = int(item) - 1
        train_r[user_idx][item_idx] = int(rating)
        train_mask_r[user_idx][item_idx] = 1

        user_train_set.add(user_idx)
        item_train_set.add(item_idx)

    ''' Test '''
    for itr in test_idx:
        line = lines[itr]
        user, item, rating, _ = line.split("::")
        user_idx = int(user) - 1
        item_idx = int(item) - 1
        test_r[user_idx][item_idx] = int(rating)
        test_mask_r[user_idx][item_idx] = 1

        user_test_set.add(user_idx)
        item_test_set.add(item_idx)

    return train_r, train_mask_r, test_r, test_mask_r, user_train_set, item_train_set, user_test_set, item_test_set

def Construct_DataLoader(train_r, train_mask_r, batchsize):
    torch_dataset = Data.TensorDataset(torch.from_numpy(train_r), torch.from_numpy(train_mask_r))
    return Data.DataLoader(dataset=torch_dataset, batch_size=batchsize, shuffle=True)

网络模型部分代码比较简单,基本就是两个全连接层外加一个Sigmoid激活函数就搞定。因为需要加上关于权重矩阵WV的正则化,所以这里的损失函数并没有使用torch自带的,而是根据论文描述自己定义的。同时实现了根据用户对所有物品评分向量r^{(u)}的推荐方法,和根据所有用户对物品i的评分向量r^{(i)}的物品推荐方法,并且加上了在测试集上的评估方法。
代码如下:

import torch
import numpy as np
import torch.nn as nn

class AutoRec(nn.Module):
    """
    基于物品的AutoRec模型
    """
    def __init__(self, config):
        super(AutoRec, self).__init__()
        self._num_items = config['num_items']
        self._hidden_units = config['hidden_units']
        self._lambda_value = config['lambda']
        self._config = config

        # 定义编码器结构
        self._encoder = nn.Sequential(
            nn.Linear(self._num_items, self._hidden_units),
            nn.Sigmoid()
        )
        # 定义解码器结构
        self._decoder = nn.Sequential(
            nn.Linear(self._hidden_units, self._num_items)
        )

    def forward(self, input):
        return self._decoder(self._encoder(input))

    def loss(self, res, input, mask, optimizer):
        cost = 0
        temp = 0

        cost += ((res - input) * mask).pow(2).sum()
        rmse = cost

        for i in optimizer.param_groups:
            # 找到权重矩阵V和W,并且计算平方和,用于约束项。
            for j in i['params']:
                if j.data.dim() == 2:
                    temp += torch.t(j.data).pow(2).sum()

        cost += temp * self._config['lambda'] * 0.5
        return cost, rmse

    def recommend_user(self, r_u, N):
        """
        :param r_u: 单个用户对所有物品的评分向量
        :param N: 推荐的商品个数
        """
        # 得到用户对所有物品的评分
        predict = self.forward(torch.from_numpy(r_u).float())
        predict = predict.detach().numpy()
        indexs = np.argsort(-predict)[:N]
        return indexs

    def recommend_item(self, user, test_r, N):
        """
        :param r_u: 所有用户对物品i的评分向量
        :param N: 推荐的商品个数
        """
        # 保存给user的推荐列表
        recommends = np.array([])

        for i in range(test_r.shape[1]):
            predict = self.forward(test_r[:, i])
            recommends.append(predict[user])

        # 按照逆序对推荐列表排序,得到最大的N个值的索引
        indexs = np.argsot(-recommends)[:N]
        # 按照用户对物品i的评分降序排序吗,推荐前N个物品给到用户
        return recommends[indexs]

    def evaluate(self, test_r, test_mask_r, user_test_set, user_train_set, item_test_set, item_train_set):
        test_r_tensor = torch.from_numpy(test_r).type(torch.FloatTensor)
        test_mask_r_tensor = torch.from_numpy(test_mask_r).type(torch.FloatTensor)

        res = self.forward(test_r_tensor)

        unseen_user_test_list = list(user_test_set - user_train_set)
        unseen_item_test_list = list(item_test_set - item_train_set)

        for user in unseen_user_test_list:
            for item in unseen_item_test_list:
                if test_mask_r[user, item] == 1:
                    res[user, item] = 3

        mse = ((res - test_r_tensor) * test_mask_r_tensor).pow(2).sum()
        RMSE = mse.detach().cpu().numpy() / (test_mask_r == 1).sum()
        RMSE = np.sqrt(RMSE)
        print('test RMSE : ', RMSE)

    def saveModel(self):
        torch.save(self.state_dict(), self._config['model_name'])

    def loadModel(self, map_location):
        state_dict = torch.load(self._config['model_name'], map_location=map_location)
        self.load_state_dict(state_dict, strict=False)

测试代码主要是包含了模型训练,随机挑选了3个用户并推荐5个商品,以及在测试集上评估RMSE指标等。
代码如下:

import torch
from AutoRec.trainer import Trainer
from AutoRec.network import AutoRec
from AutoRec.dataloader import dataProcess

autorec_config = \
{
    'train_ratio': 0.9,
    'num_epoch': 200,
    'batch_size': 100,
    'optimizer': 'adam',
    'adam_lr': 1e-3,
    'l2_regularization':1e-4,
    'num_users': 6040,
    'num_items': 3952,
    'hidden_units': 500,
    'lambda': 1,
    'device_id': 2,
    'use_cuda': False,
    'data_file': '../Data/ml-1m/ratings.dat',
    'model_name': '../Models/AutoRec.model'
}

if __name__ == "__main__":
    ####################################################################################
    # AutoRec 自编码器协同过滤算法
    ####################################################################################
    train_r, train_mask_r, test_r, test_mask_r, \
    user_train_set, item_train_set, user_test_set, item_test_set = \
        dataProcess(autorec_config['data_file'], autorec_config['num_users'], autorec_config['num_items'], autorec_config['train_ratio'])
    # 实例化AutoRec对象
    autorec = AutoRec(config=autorec_config)

    ####################################################################################
    # 模型训练阶段
    ####################################################################################
    # # 实例化模型训练器
    # trainer = Trainer(model=autorec, config=autorec_config)
    # # 开始训练
    # trainer.train(train_r, train_mask_r)
    # # 保存模型
    # trainer.save()

    ###################################################################################
    # 模型测试阶段
    ###################################################################################
    autorec.loadModel(map_location=torch.device('cpu'))

    # 从测试集中随便抽取几个用户,推荐5个商品
    print("用户1推荐列表: ",autorec.recommend_user(test_r[0], 5))
    print("用户2推荐列表: ",autorec.recommend_user(test_r[9], 5))
    print("用户3推荐列表: ",autorec.recommend_user(test_r[23], 5))

    autorec.evaluate(test_r, test_mask_r, user_test_set=user_test_set, user_train_set=user_train_set, \
                     item_test_set=item_test_set, item_train_set=item_train_set)

测试结果如下:
测试结果

完整代码见https://github.com/HeartbreakSurvivor/RsAlgorithms/tree/main/AutoRec.

总结

AutoRec模型是深度学习方法用于推荐系统中的开山之作,它仅仅使用了一个单隐层的自编码器来泛化用户和物品评分,使模型具有一定的泛化和表达能力。但是由于模型过于简单,也让它在实际使用中显得表征能力不足。

参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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

推荐阅读更多精彩内容