之前有写过推荐的相关博客,通过皮尔森相关性、knn、以及矩阵分解进行处理,当时使用的矩阵分解为sklearn的集成方法,这里

1. 矩阵分解

矩阵分解是推荐系统公认的算法。矩阵分解是一种协同过滤模型。协同过滤从广义上讲,它是使用涉及多个用户、代理和数据源之间协作的技术对信息或模式进行过滤的过程。

详细的说,矩阵分解模型通过将用户-项目交互的矩阵(例如,评级矩阵)分解为两个相较低等级的矩阵的乘机,从而获取用户-项目交互的低等级结构。

Mxnet (42): 推荐系统之矩阵分解 - 图1 用来表示Mxnet (42): 推荐系统之矩阵分解 - 图2个用户 和 Mxnet (42): 推荐系统之矩阵分解 - 图3个项目的交互矩阵, Mxnet (42): 推荐系统之矩阵分解 - 图4中的值代表评分。 用户-项目交互将分解为用户潜在矩阵 Mxnet (42): 推荐系统之矩阵分解 - 图5和项目潜在矩阵 Mxnet (42): 推荐系统之矩阵分解 - 图6,其中 Mxnet (42): 推荐系统之矩阵分解 - 图7是潜在因子的大小。让 Mxnet (42): 推荐系统之矩阵分解 - 图8 表示Mxnet (42): 推荐系统之矩阵分解 - 图9的第 Mxnet (42): 推荐系统之矩阵分解 - 图10行以及 Mxnet (42): 推荐系统之矩阵分解 - 图11 表示Mxnet (42): 推荐系统之矩阵分解 - 图12的第 Mxnet (42): 推荐系统之矩阵分解 - 图13 行 。对于给定的项目Mxnet (42): 推荐系统之矩阵分解 - 图14, Mxnet (42): 推荐系统之矩阵分解 - 图15用于衡量项目具有的特征比如电影的流派和语言。对于给定的用户 Mxnet (42): 推荐系统之矩阵分解 - 图16, Mxnet (42): 推荐系统之矩阵分解 - 图17衡量用户对项目相应特征的兴趣程度。这些潜在的因素可能是这些示例中明显的维度也许这些维度完全没法解释。 可以通过一下方程估计评级:

Mxnet (42): 推荐系统之矩阵分解 - 图18

Mxnet (42): 推荐系统之矩阵分解 - 图19是形状与 Mxnet (42): 推荐系统之矩阵分解 - 图20相同的预测评级矩阵。该预测规则的一个主要问题是无法对用户/项目偏差进行建模。例如, 一些用户更加倾向于给出较高的分数,或者某些项目由于质量问题总是获得较低的评分,这些偏差在显示中很常见。为了捕获这些偏差,引入用户的特定偏差以及项目的特定偏差。具体来说,用户 Mxnet (42): 推荐系统之矩阵分解 - 图21 给项目 Mxnet (42): 推荐系统之矩阵分解 - 图22预测评分为:

Mxnet (42): 推荐系统之矩阵分解 - 图23

然后,我们通过最小化预测评分和真实评分之间的均方误差来训练矩阵分解模型。目标函数定义如下:

Mxnet (42): 推荐系统之矩阵分解 - 图24%20%5Cin%20%5Cmathcal%7BK%7D%7D%20%5C%7C%20%5Cmathbf%7BR%7D%7Bui%7D%20-%0A%5Chat%7B%5Cmathbf%7BR%7D%7D%7Bui%7D%20%5C%7C%5E2%20%2B%20%5Clambda%20(%5C%7C%20%5Cmathbf%7BP%7D%20%5C%7C%5E2F%20%2B%20%5C%7C%20%5Cmathbf%7BQ%7D%0A%5C%7C%5E2_F%20%2B%20b_u%5E2%20%2B%20b_i%5E2%20)%0A#card=math&code=%5Cunderset%7B%5Cmathbf%7BP%7D%2C%20%5Cmathbf%7BQ%7D%2C%20b%7D%7B%5Cmathrm%7Bargmin%7D%7D%20%5Csum%7B%28u%2C%20i%29%20%5Cin%20%5Cmathcal%7BK%7D%7D%20%5C%7C%20%5Cmathbf%7BR%7D%7Bui%7D%20-%0A%5Chat%7B%5Cmathbf%7BR%7D%7D%7Bui%7D%20%5C%7C%5E2%20%2B%20%5Clambda%20%28%5C%7C%20%5Cmathbf%7BP%7D%20%5C%7C%5E2_F%20%2B%20%5C%7C%20%5Cmathbf%7BQ%7D%0A%5C%7C%5E2_F%20%2B%20b_u%5E2%20%2B%20b_i%5E2%20%29%0A)

Mxnet (42): 推荐系统之矩阵分解 - 图25 表示正则化率。正则项Mxnet (42): 推荐系统之矩阵分解 - 图26#card=math&code=%5Clambda%20%28%5C%7C%20%5Cmathbf%7BP%7D%20%5C%7C%5E2F%20%2B%20%5C%7C%20%5Cmathbf%7BQ%7D%0A%5C%7C%5E2_F%20%2B%20b_u%5E2%20%2B%20b_i%5E2%20%29) 用于通过惩罚参数的大小来避免过度拟合。 每对Mxnet (42): 推荐系统之矩阵分解 - 图27#card=math&code=%28u%2C%20i%29) 的![](https://g.yuque.com/gr/latex?%5Cmathbf%7BR%7D%7Bui%7D#card=math&code=%5Cmathbf%7BR%7D%7Bui%7D)被存储于Mxnet (42): 推荐系统之矩阵分解 - 图28%20%5Cmid%20%5Cmathbf%7BR%7D%7Bui%7D%20%5Ctext%7B%20is%20known%7D%5C%7D#card=math&code=%5Cmathcal%7BK%7D%3D%5C%7B%28u%2C%20i%29%20%5Cmid%20%5Cmathbf%7BR%7D_%7Bui%7D%20%5Ctext%7B%20is%20known%7D%5C%7D)集合中。模型可以使用优化算法学习,比如SGD和Adam.

下图直观展示矩阵分解:

Mxnet (42): 推荐系统之矩阵分解 - 图29

使用MovieLens数据集进行矩阵分解的训练。

2. MovieLens数据集

有许多可用于推荐研究的数据集。其中,MovieLens 数据集可能是最受欢迎的数据集之一。MovieLens是基于网络的非商业性电影推荐系统。它创建于1997年,由明尼苏达大学的研究实验室GroupLens运营,目的是收集电影收视率数据用于研究目的。MovieLens数据对于包括个性化推荐和社会心理学在内的多项研究至关重要。
我们将使用MovieLens 100K数据集,该数据集包括 100,000 收视率从1到5星不等,在1682部电影中有943位用户。已对其进行清理,以便每个用户至少对20部电影评分。还提供一些简单的人口统计信息,例如用户的年龄,性别,体裁和物品。

  1. from d2l import mxnet as d2l
  2. from mxnet import gluon, np, autograd, npx, init
  3. import mxnet as mx
  4. from mxnet.gluon import nn
  5. from plotly import express as px, graph_objs as go
  6. import os
  7. import pandas as pd
  8. npx.set_np()

然后,我们下载MovieLens 100k数据集,并将加载为pandas的DataFrame

  1. d2l.DATA_HUB['ml-100k'] = ('http://files.grouplens.org/datasets/movielens/ml-100k.zip',
  2. 'cd4dcac4241c8a4ad7badc7ca635da8a69dddb83')
  3. def read_data_ml100k():
  4. data_dir = d2l.download_extract('ml-100k')
  5. names = ['user_id', 'item_id', 'rating', 'timestamp']
  6. data = pd.read_csv(os.path.join(data_dir, 'u.data'), '\t', names=names, engine='python')
  7. num_users = data.user_id.unique().shape[0]
  8. num_items = data.item_id.unique().shape[0]
  9. return data, num_users, num_items

2.1 数据集统计

加载数据用于观察。

  1. data, num_users, num_items = read_data_ml100k()
  2. sparsity = 1 - len(data) / (num_users * num_items)
  3. print(f'number of users: {num_users}, number of items: {num_items}')
  4. print(f'matrix sparsity: {sparsity:f}')
  5. data.head(5)

Mxnet (42): 推荐系统之矩阵分解 - 图30

评分矩阵中的大多数值都是未知的,因为用户尚未对大多数电影进行评分。我们还显示了该数据集的稀疏性。稀疏度定义为 :Mxnet (42): 推荐系统之矩阵分解 - 图31#card=math&code=1%20-%20number%20of%20nonzero%20entries%20%2F%20%28%20number%20of%20users%20%2A%20number%20of%20items%29)。显然,交互矩阵非常稀疏(即稀疏度= 93.695%)。现实世界的数据集可能会遭受更大程度的稀疏性,并且一直是构建推荐系统的长期挑战。可行的解决方案是使用其他辅助信息(例如用户/项目功能)来减轻稀疏性。下面通过crosstab(或使用pivot)进行数据交叉,获取用户-项目交互矩阵形式,0都是没有数据的,可见数据非常稀疏。

  1. pd.crosstab(data.user_id, data.item_id, values=data.rating, aggfunc=sum).fillna(0).iloc[:10,:20]

Mxnet (42): 推荐系统之矩阵分解 - 图32

绘制评分的计数分布,如预期相仿,近视正太,大多数集中在3~4.

  1. px.histogram(data, x='rating', width=580, height=400, opacity=0.7, title="MovieLens 100K 评分分布")

image-20201012133412065.png

对每个用户求其平均评分,绘制平均评分的计数分布图,可见同样符合正太分布。

  1. px.histogram(data.groupby('user_id')['rating'].mean(), width=580, height=400, opacity=0.7, title="MovieLens 100K 用户平均评分分布", labels={"value":"rating"})

image-20201012133429714.png

对每个电影求其平均评分,绘制平均评分的计数分布图,除去极好(全5分)以及极坏的(全5分)基本符合正太分布。

  1. px.histogram(data.groupby('item_id')['rating'].mean(), width=580, height=400, opacity=0.7, title="MovieLens 100K 电影平均评分分布", labels={"value":"rating"})

Mxnet (42): 推荐系统之矩阵分解 - 图35

2.2 分割数据集

我们将数据集分为训练集和测试集。以下功能提供了两种分割模式,包括random和seq-aware。在此 random模式下,该函数在不考虑时间戳的情况下随机拆分100k交互,默认情况下将90%的数据用作训练样本,其余10%用作测试样本。在该 seq-aware模式下,我们忽略了用户最近为测试评分的项目,以及用户的历史交互作为训练集。用户历史交互根据时间戳从最早到最新进行分类。此模式将在序列感知推荐部分中使用。

  1. def split_data_ml100k(data, num_users, num_items, split_mode='random', test_ratio=0.1):
  2. if split_mode == 'seq-aware':
  3. train_items, test_items, train_list = {}, {}, []
  4. for line in data.itertuples():
  5. u, i, rating, time = line[1], line[2], line[3], line[4]
  6. train_items.setdefault(u, []).append((u, i, rating, time))
  7. if u not in test_items or test_items[u][-1] < time:
  8. test_items[u] = (i, rating, time)
  9. for u in range(1, num_users + 1):
  10. train_list.extend(sorted(train_items[u], key=lambda k: k[3]))
  11. test_data = [(key, *value) for key, value in test_items.items()]
  12. train_data = [item for item in train_list if item not in test_data]
  13. train_data = pd.DataFrame(train_data)
  14. test_data = pd.DataFrame(test_data)
  15. else:
  16. mask = [True if x == 1 else False for x in np.random.uniform(0, 1, (len(data))) < 1 - test_ratio]
  17. neg_mask = [not x for x in mask]
  18. train_data, test_data = data[mask], data[neg_mask]
  19. return train_data, test_data

2.3 加载数据

分割数据集后,为了方便起见,我们将训练集和测试集转换为列表和字典/矩阵。以下函数逐行读取数据帧,并枚举从零开始的用户/项索引。然后,该函数返回用户,项目,等级和记录交互的字典/矩阵的列表。我们可以将反馈的类型指定为explicit 或implicit。

  1. def load_data_ml100k(data, num_users, num_items, feedback='explicit'):
  2. users, items, scores = [], [], []
  3. inter = np.zeros((num_items, num_users)) if feedback == 'explicit' else {}
  4. for line in data.itertuples():
  5. user_index, item_index = int(line[1] - 1), int(line[2] - 1)
  6. score = int(line[3]) if feedback == 'explicit' else 1
  7. users.append(user_index)
  8. items.append(item_index)
  9. scores.append(score)
  10. if feedback == 'implicit':
  11. inter.setdefault(user_index, []).append(item_index)
  12. else:
  13. inter[item_index, user_index] = score
  14. return users, items, scores, inter

之后,我们将上述步骤放在一起,将在下一部分中使用。结果用Dataset和 包裹DataLoader。请注意,训练数据的last_batch设置为rollover模式(剩余样本将滚动到下一个epoch),并按顺序排序。

  1. def split_and_load_ml100k(split_mode='seq-aware', feedback='explicit', test_ratio=0.1, batch_size=256):
  2. data, num_users, num_items = read_data_ml100k()
  3. train_data, test_data = split_data_ml100k(data, num_users, num_items, split_mode, test_ratio)
  4. train_u, train_i, train_r, _ = load_data_ml100k(train_data, num_users, num_items, feedback)
  5. test_u, test_i, test_r, _ = load_data_ml100k(test_data, num_users, num_items, feedback)
  6. train_set = gluon.data.ArrayDataset(np.array(train_u), np.array(train_i), np.array(train_r))
  7. test_set = gluon.data.ArrayDataset(np.array(test_u), np.array(test_i), np.array(test_r))
  8. train_iter = gluon.data.DataLoader( train_set, shuffle=True, last_batch='rollover',batch_size=batch_size)
  9. test_iter = gluon.data.DataLoader(test_set, batch_size=batch_size)
  10. return num_users, num_items, train_iter, test_iter

3.定义矩阵分解模型

首先,我们实现上述矩阵分解模型。可以使用创建用户和项目潜在因素nn.Embedding。的input_dim是项目/用户的数量,而(output_dim)是潜在因素的维度( k )。我们还可以 nn.Embedding通过将其设置output_dim为1来创建用户/项目偏好 。在该forward函数中,使用用户ID和项目ID来查找嵌入。

  1. class MF(nn.Block):
  2. def __init__(self, num_factors, num_users, num_items, **kwargs):
  3. super(MF, self).__init__(**kwargs)
  4. self.P = nn.Embedding(input_dim=num_users, output_dim=num_factors)
  5. self.Q = nn.Embedding(input_dim=num_items, output_dim=num_factors)
  6. self.user_bias = nn.Embedding(num_users, 1)
  7. self.item_bias = nn.Embedding(num_items, 1)
  8. def forward(self, user_id, item_id):
  9. P_u = self.P(user_id)
  10. Q_i = self.Q(item_id)
  11. b_u = self.user_bias(user_id)
  12. b_i = self.item_bias(item_id)
  13. outputs = (P_u * Q_i).sum(axis=1) + np.squeeze(b_u) + np.squeeze(b_i)
  14. return outputs.flatten()

4. 评估定义

然后,我们实施RMSE(均方根误差)度量,该度量通常用于度量模型预测的评分得分与实际观察到的评分(基本事实)之间的差异:

Mxnet (42): 推荐系统之矩阵分解 - 图36%20%5Cin%20%5Cmathcal%7BT%7D%7D(%5Cmathbf%7BR%7D%7Bui%7D%20-%5Chat%7B%5Cmathbf%7BR%7D%7D%7Bui%7D)%5E2%7D%0A#card=math&code=%5Cmathrm%7BRMSE%7D%20%3D%20%5Csqrt%7B%5Cfrac%7B1%7D%7B%7C%5Cmathcal%7BT%7D%7C%7D%5Csum%7B%28u%2C%20i%29%20%5Cin%20%5Cmathcal%7BT%7D%7D%28%5Cmathbf%7BR%7D%7Bui%7D%20-%5Chat%7B%5Cmathbf%7BR%7D%7D_%7Bui%7D%29%5E2%7D%0A)

Mxnet (42): 推荐系统之矩阵分解 - 图37 是由成对的用户和要评估的项目组成的集合。Mxnet (42): 推荐系统之矩阵分解 - 图38 是集合的大小。我们可以使用mx.metric提供的RMSE函数功能。

  1. def evaluator(net, test_iter, devices):
  2. rmse = mx.metric.RMSE() # 获取评估函数
  3. rmse_list = []
  4. for idx, (users, items, ratings) in enumerate(test_iter):
  5. u = gluon.utils.split_and_load(users, devices, even_split=False)
  6. i = gluon.utils.split_and_load(items, devices, even_split=False)
  7. r_ui = gluon.utils.split_and_load(ratings, devices, even_split=False)
  8. r_hat = [net(u, i) for u, i in zip(u, i)]
  9. rmse.update(labels=r_ui, preds=r_hat)
  10. rmse_list.append(rmse.get()[1])
  11. return float(np.mean(np.array(rmse_list)))

5.训练和评估

在训练功能上,我们采用 L2 体重下降引起的损失。重量衰减机制与 L2 正则化。

  1. def train_recsys_rating(net, train_iter, test_iter, loss, trainer, num_epochs, devices=d2l.try_all_gpus(), evaluator=None,
  2. **kwargs):
  3. timer = d2l.Timer()
  4. data = []
  5. for epoch in range(num_epochs):
  6. metric, l = d2l.Accumulator(3), 0.
  7. for i, values in enumerate(train_iter):
  8. timer.start()
  9. input_data = []
  10. values = values if isinstance(values, list) else [values]
  11. for v in values:
  12. input_data.append(gluon.utils.split_and_load(v, devices))
  13. train_feat = input_data[0:-1] if len(values) > 1 else input_data
  14. train_label = input_data[-1]
  15. with autograd.record():
  16. preds = [net(*t) for t in zip(*train_feat)]
  17. ls = [loss(p, s) for p, s in zip(preds, train_label)]
  18. [l.backward() for l in ls]
  19. l += sum([l.asnumpy() for l in ls]).mean() / len(devices)
  20. trainer.step(values[0].shape[0])
  21. metric.add(l, values[0].shape[0], values[0].size)
  22. timer.stop()
  23. if len(kwargs) > 0: # AutoRec中会用到这个
  24. test_rmse = evaluator(net, test_iter, kwargs['inter_mat'], devices)
  25. else:
  26. test_rmse = evaluator(net, test_iter, devices)
  27. train_l = l / (i + 1)
  28. data.append((epoch+1, train_l, test_rmse))
  29. print(f'train loss {metric[0] / metric[1]:.3f}, test RMSE {test_rmse:.3f}')
  30. print(f'{metric[2] * num_epochs / timer.sum():.1f} examples/sec on {str(devices)}')
  31. fig = px.line(pd.DataFrame(data, columns=['epoch', 'train loss', 'test RMSE']), x='epoch', y=['train loss', 'test RMSE'], width=580, height=400)
  32. fig.show()

最后,让我们将所有事物放在一起并训练模型。在这里,我们将潜在因子维度设置为30。

  1. devices = d2l.try_all_gpus()
  2. num_users, num_items, train_iter, test_iter = d2l.split_and_load_ml100k(test_ratio=0.1, batch_size=512)
  3. net = MF(30, num_users, num_items)
  4. net.initialize(ctx=devices, force_reinit=True, init=mx.init.Normal(0.01))
  5. lr, num_epochs, wd, optimizer = 0.002, 20, 1e-5, 'adam'
  6. loss = gluon.loss.L2Loss()
  7. trainer = gluon.Trainer(net.collect_params(), optimizer, {"learning_rate": lr, 'wd': wd})
  8. train_recsys_rating(net, train_iter, test_iter, loss, trainer, num_epochs, devices, evaluator)

Mxnet (42): 推荐系统之矩阵分解 - 图39

Mxnet (42): 推荐系统之矩阵分解 - 图40

下面,我们使用训练过的模型来预测用户(ID 20)可能对商品(ID 30)给予的评分。

  1. scores = net(np.array([20], dtype='int', ctx=devices[0]), np.array([30], dtype='int', ctx=devices[0]))
  2. scores

Mxnet (42): 推荐系统之矩阵分解 - 图41

6. 参考

https://d2l.ai/chapter_recommender-systems/mf.html

7.代码

github