尽管矩阵分解模型在评级预测任务上实现了不错的性能,但它本质上是线性模型。因此,此类模型无法捕获可能预示用户偏好的复杂非线性和复杂关系。在本节中,我们介绍了非线性神经网络协同过滤模型AutoRec。它使用自动编码器架构识别协作过滤(CF),并旨在基于显式反馈将非线性变换集成到CF中。神经网络已被证明能够逼近任何连续函数,使其适合于解决矩阵分解的局限性并丰富矩阵分解的表达能力:
- AutoRec具有与自动编码器相同的结构,自动编码器由输入层,隐藏层和重构(输出)层组成。自动编码器是一种神经网络,可以学习将其输入复制到其输出,以便将输入编码为隐藏(通常是低维)表示形式。在AutoRec中,不是将用户/项目显式嵌入到低维空间中,而是使用交互矩阵的列/行作为输入,然后在输出层中重建交互矩阵。
- AutoRec与传统的自动编码器不同:AutoRec而不是学习隐藏的表示,而是专注于学习/重建输出层。它使用部分观察到的交互矩阵作为输入,旨在重建完整的评分矩阵。同时,出于推荐的目的,通过重构将输入的缺失条目填充在输出层中。
AutoRec有两种变体:基于用户和基于项目。为简便起见,这里我们仅介绍基于项目的AutoRec。可以相应地导出基于用户的AutoRec。
1. AutoRec模型
让 表示评分矩阵第
列,默认情况下未知评分设置为0,神经系统结构定义为:
%20%3D%20f(%5Cmathbf%7BW%7D%20%5Ccdot%20g(%5Cmathbf%7BV%7D%20%5Cmathbf%7BR%7D%7B*i%7D%20%2B%20%5Cmu)%20%2B%20b)%0A#card=math&code=h%28%5Cmathbf%7BR%7D%7B%2Ai%7D%29%20%3D%20f%28%5Cmathbf%7BW%7D%20%5Ccdot%20g%28%5Cmathbf%7BV%7D%20%5Cmathbf%7BR%7D_%7B%2Ai%7D%20%2B%20%5Cmu%29%20%2B%20b%29%0A)
#card=math&code=f%28%5Ccdot%29) 和
#card=math&code=g%28%5Ccdot%29) 代表激活函数,
和
是权重矩阵,
和
是偏差。让
#card=math&code=h%28%20%5Ccdot%20%29) AutoRec的整个网络。
#card=math&code=h%28%5Cmathbf%7BR%7D_%7B%2Ai%7D%29)输出是重建
列的矩阵。
以下目标函数旨在最大程度地减少重构误差:
%5Cparallel%7B%5Cmathcal%7BO%7D%7D%5E2%7D%20%2B%5Clambda(%5C%7C%20%5Cmathbf%7BW%7D%20%5C%7C_F%5E2%20%2B%20%5C%7C%20%5Cmathbf%7BV%7D%5C%7C_F%5E2)%0A#card=math&code=%5Cunderset%7B%5Cmathbf%7BW%7D%2C%5Cmathbf%7BV%7D%2C%5Cmu%2C%20b%7D%7B%5Cmathrm%7Bargmin%7D%7D%20%5Csum%7Bi%3D1%7D%5EM%7B%5Cparallel%20%5Cmathbf%7BR%7D%7B%2Ai%7D%20-%20h%28%5Cmathbf%7BR%7D%7B%2Ai%7D%29%5Cparallel_%7B%5Cmathcal%7BO%7D%7D%5E2%7D%20%2B%5Clambda%28%5C%7C%20%5Cmathbf%7BW%7D%20%5C%7C_F%5E2%20%2B%20%5C%7C%20%5Cmathbf%7BV%7D%5C%7C_F%5E2%29%0A)
只考虑到评分的贡献, 也就是说在反向传播期间只更新和观察输入关联的权重。
from d2l import mxnet as d2lfrom mxnet import autograd, gluon, np, npxfrom mxnet.gluon import nnfrom plotly import express as pximport pandas as pdimport mxnet as mximport sysnpx.set_np()
2. 模型实现
典型的自动编码器由编码器和解码器组成。编码器将输入投影到隐藏表示中,而解码器将隐藏层映射到重建层。我们遵循这种做法,并创建具有密集层的编码器和解码器。编码器的激活函数默认为sigmoid,并且不对解码器应用任何激活。编码转换后包括了Dropout,以减少过度拟合。掩盖未观察到的输入的梯度,以确保仅观察到的等级有助于模型学习过程。
class AutoRec(nn.Block):def __init__(self, num_hidden, num_users, dropout=0.05):super(AutoRec, self).__init__()self.encoder = nn.Dense(num_hidden, activation='sigmoid', use_bias=True)self.decoder = nn.Dense(num_users, use_bias=True)self.dropout = nn.Dropout(dropout)def forward(self, input):hidden = self.dropout(self.encoder(input))pred = self.decoder(hidden)if autograd.is_training(): # 在训练过程中遮盖梯度return pred * np.sign(input)else:return pred
3. 评估器
由于输入和输出已更改,因此我们需要重新实现评估功能,同时我们仍将RMSE用作准确性度量。
def evaluator(network, inter_matrix, test_data, devices):scores = []for values in inter_matrix:feat = gluon.utils.split_and_load(values, devices, even_split=False)scores.extend([network(i).asnumpy() for i in feat])recons = np.array([item for sublist in scores for item in sublist])rmse = np.sqrt(np.sum(np.square(test_data - np.sign(test_data) * recons))/ np.sum(np.sign(test_data)))return float(rmse)
4. 训练模型
现在,让我们在MovieLens数据集上训练和评估AutoRec。使用之前的训练函数:
def train_recsys_rating(net, train_iter, test_iter, loss, trainer, num_epochs, devices=d2l.try_all_gpus(), evaluator=None,**kwargs):timer = d2l.Timer()data = []for epoch in range(num_epochs):metric, l = d2l.Accumulator(3), 0.for i, values in enumerate(train_iter):timer.start()input_data = []values = values if isinstance(values, list) else [values]for v in values:input_data.append(gluon.utils.split_and_load(v, devices))train_feat = input_data[0:-1] if len(values) > 1 else input_datatrain_label = input_data[-1]with autograd.record():preds = [net(*t) for t in zip(*train_feat)]ls = [loss(p, s) for p, s in zip(preds, train_label)][l.backward() for l in ls]l += sum([l.asnumpy() for l in ls]).mean() / len(devices)trainer.step(values[0].shape[0])metric.add(l, values[0].shape[0], values[0].size)timer.stop()if len(kwargs) > 0: # AutoRec中会用到这个test_rmse = evaluator(net, test_iter, kwargs['inter_mat'], devices)else:test_rmse = evaluator(net, test_iter, devices)train_l = l / (i + 1)data.append((epoch+1, train_l, test_rmse))print(f'train loss {metric[0] / metric[1]:.3f}, test RMSE {test_rmse:.3f}')print(f'{metric[2] * num_epochs / timer.sum():.1f} examples/sec on {str(devices)}')fig = px.line(pd.DataFrame(data, columns=['epoch', 'train loss', 'test RMSE']), x='epoch', y=['train loss', 'test RMSE'], width=580, height=400)fig.show()
我们可以清楚地看到,测试RMSE低于矩阵分解模型,从而证明了神经网络在评级预测任务中的有效性。
devices = d2l.try_all_gpus()# 加载数据df, num_users, num_items = d2l.read_data_ml100k()train_data, test_data = d2l.split_data_ml100k(df, num_users, num_items)_, _, _, train_inter_mat = d2l.load_data_ml100k(train_data, num_users,num_items)_, _, _, test_inter_mat = d2l.load_data_ml100k(test_data, num_users,num_items)train_iter = gluon.data.DataLoader(train_inter_mat, shuffle=True,last_batch="rollover", batch_size=256,num_workers=d2l.get_dataloader_workers())test_iter = gluon.data.DataLoader(np.array(train_inter_mat), shuffle=False,last_batch="keep", batch_size=1024,num_workers=d2l.get_dataloader_workers())# 设置net以及loss进行训练net = AutoRec(500, num_users)net.initialize(ctx=devices, force_reinit=True, init=mx.init.Normal(0.01))lr, num_epochs, wd, optimizer = 0.002, 25, 1e-5, 'adam'loss = gluon.loss.L2Loss()trainer = gluon.Trainer(net.collect_params(), optimizer, {"learning_rate": lr, 'wd': wd})train_recsys_rating(net, train_iter, test_iter, loss, trainer, num_epochs,devices, evaluator, inter_mat=test_inter_mat)


