尽管还没进入六月,可各大电商的暑期大促早已如火如荼地开始了。优惠、满减、秒杀、返券……当然,在你成功清空购物车后,还少不了人民群众们喜闻乐见的「猜你喜欢」环节。经常使用就会发现,这类推荐内容通常都会很合你自己的口味,电商到底是怎么做到的?这就涉及到「推荐系统」这个概念了。
商品推荐系统,到底是怎样的原理?
推荐系统,是利用电子商务网站向客户提供商品信息和建议,帮助用户决定应该购买什么产品,并模拟销售人员帮助客户完成购买过程。个性化推荐则是根据用户兴趣特点和购买行为,向用户推荐用户感兴趣的信息和商品。
最早的推荐算法通常是用关联规则建立的,如著名的啤酒尿不湿的故事,就是利用关联规则推荐商品促进成交的经典案例。为之而生的算法,如 Apriori 算法就是亚马逊所发明的。
当我们在 Amazon 上购买图书时,会经常看到下面两个提示:
- 这些书会被消费者一起购买,并且价格上有一定的折扣;
- 购买了这本书的人,也会购买其他书。
Amazon 会对平台中海量用户记录进行挖掘,发现这些规律,然后将这些规律应用于实际销售工作当中。而结果也显示,使用这种算法优化,对于当时亚马逊的业绩提升起到了很大作用。
今天,随着电子商务规模的不断扩大,商品种类快速增长,顾客需要花费大量的时间才能找到自己想买的商品。这种浏览大量无关信息和产品的过程无疑会使淹没在信息过载问题中的消费者不断流失。为解决这些问题,个性化推荐系统应运而生。
个性化推荐系统是建立在海量数据挖掘基础上的一种高级商务智能平台,可帮助电子商务网站为其顾客购物提供完全个性化的决策支持和信息服务。区别于传统的规则推荐,个性化推荐算法通常使用机器学习甚至深度学习算法,对于用户信息与其行为信息充分挖掘,进而进行有效的推荐。
常用的推荐算法有很多,其中最为经典的就是基于 Matrix Factorization(矩阵分解)的推荐。矩阵分解的思想简单来说就是:每个用户和每个物品都会有自己的一些特性,用矩阵分解的方法可以从评分矩阵中分解出用户—特性矩阵,以及特性—物品矩阵等,这样做的好处是得到了用户的偏好和每件物品的特性。矩阵分解的思想也被扩展和泛化到深度学习及 Embedding 中,这样构建模型能够增强模型的准确率及灵活易用性。
试试看,使用 Amazon SageMaker 构建基于 Gluon 的推荐系统
下文介绍的方法将会用到 Amazon SageMaker,它可以帮助开发人员和数据科学家构建、训练并部署 ML 模型。Amazon SageMaker 是一项完全托管的服务,涵盖了 ML 的整个工作流,可以标记和准备数据、选择算法、训练模型、调整和优化模型以便部署、预测和执行操作。
同时本方案基于 Gluon API,Gluon 是微软联合亚马逊推出的一个开源深度学习库,这是一个清晰、简洁、简单但功能强大的深度学习 API,该规范可以提升开发人员学习深度学习的速度,而无需关心所选择的深度学习框架。Gluon API 提供了灵活的接口来简化深度学习原型设计、创建、训练以及部署,而且不会牺牲数据训练的速度。
下文将介绍如何使用 Amazon SageMaker 的自定义脚本(Bring Your Own Script,简称 BYOS)方式来运行 Gluon 程序(MXNet 后端)的训练任务,并且进行部署调用。
首先一起看看如何在 Amazon SageMaker Notebook 上运行这个项目,在本地运行训练任务,然后再进行部署,直接利用 Amazon SageMaker 的相关接口调用。
解决方案概览
在此示例中,我们将使用 Amazon SageMaker 执行以下操作:
- 环境准备
- 使用 Jupyter Notebook 下载数据集并将其进行数据预处理
- 使用本地机器训练
- 使用 Amazon SageMaker BYOS 进行模型训练
- 托管部署及推理测试
1. 环境准备
首先要创建一个 Amazon SageMaker Notebook,笔记本实例类型最好选择 ml.p3.2xlarge,因为本例中用到了本地机器训练的部分用来测试我们的代码,卷大小建议改成 10GB 或更大,因为运行该项目需要下载一些额外数据。
笔记本启动后,打开页面上的终端并执行下列命令下载代码;或者也可以通过 Sagemaker Examples 找到 Introduction to Applying Machine Learning/gluon_recommender_system.ipynb,点击 Use 运行代码。
cd ~/SageMaker
git clone https://github.com/awslabs/amazon-sagemaker-examples/tree/master/introduction_to_applying_machine_learning/gluon_recommender_system
2. 使用 Jupyter Notebook 下载数据集并将其进行数据预处理
本文使用了亚马逊官方开源数据集,其中包含 2000 位亚马逊电商用户对 160k 个视频的评论打分,打分分数为 1 -5,您可以访问该数据集主页查看完整的数据说明并下载。由于本数据集很大,我们将使用临时目录进行存储。
!mkdir /tmp/recsys/
!aws s3 cp s3://amazon-reviews-pds/tsv/amazon_reviews_us_Digital_Video_Download_v1_00.tsv.gz /tmp/recsys/
3. 数据预处理
下载好数据后,可以通过 Python 的 Pandas 库进行数据的读取,浏览和预处理。
首先,运行如下代码加载数据:
df=pd.read_csv('/tmp/recsys/amazon_reviews_us_Digital_Video_Download_v1_00.tsv.gz', delimiter='\t',error_bad_lines=False)
df.head()
随后可以看到如下结果,因为列比较多,这里截图显示的并不完整:
我们可以看到,该数据集包含了很多特征(列),其中每列具体的含义如下:
- marketplace:两位数的国家编码,此处都是「US」
- customer_id:一个代表发表评论用户的随机编码,对于每个用户唯一
- review_id:对于评论的唯一编码
- product_id:亚马逊通用的产品编码
- product_parent:母产品编码,很多产品有同属于一个母产品
- product_title:产品的描述
- product_category:产品品类
- star_rating:评论星数,从 1 到 5
- helpful_votes:有用评论数
- total_votes:总评论数
- vine:是否为 vine 项目中的评论
- verified_purchase:该评论是否来源于已购买该产品的客户
- review_headline:评论标题
- review_body:评论内容
- review_date:评论时间
在这个例子中,我们只准备使用 custermor_id、product_id 和 star_rating 三列构建模型。这也是我们构建一个推荐系统所需要最少的三列数据。其余特征列如果在构建模型时添加,可以有效提高模型的准确率,但本文不会包括这部分内容。同时我们会保留 product_title 列用于结果验证。
df = df[['customer_id', 'product_id', 'star_rating', 'product_title']]
同时,因为大部分的视频对大部分人而言都没有看过,所以我们的数据是很稀疏的。一般来说,推荐系统的模型对于稀疏数据可以很好地处理,但这一般需要大规模的数据来训练模型。为了实验示例运行更加顺畅,这里我们将把这种数据稀疏的场景进行验证,并且进行清洗,并使用一个较为稠密的 reduced_df 进行模型的训练。
随后可以通过如下代码进行稀疏(「长尾效应」)的验证
customers = df['customer_id'].value_counts()
products = df['product_id'].value_counts()
quantiles = [0, 0.01, 0.02, 0.03, 0.04, 0.05, 0.1, 0.25, 0.5, 0.75, 0.9, 0.95, 0.96, 0.97, 0.98, 0.99, 1]
print('customers\n', customers.quantile(quantiles))
print('products\n', products.quantile(quantiles))
可以看到,只有 5% 的客户评论了 5 个及以上的视频,同时只有 25% 的视频被超过 10 个用户评论。
接下来我们将把数据进行过滤,去掉长尾用户和产品:
customers = customers[customers >= 5]
products = products[products >= 10]
reduced_df = df.merge(pd.DataFrame({'customer_id': customers.index})).merge(pd.DataFrame({'product_id': products.index}))
随后对用户与产品进行重新编码:
customer_index = pd.DataFrame({'customer_id': customers.index, 'user': np.arange(customers.shape[0])})
product_index = pd.DataFrame({'product_id': products.index,
'item': np.arange(products.shape[0])})
reduced_df = reduced_df.merge(customer_index).merge(product_index)
接下来,我们将准备好的数据集切割为训练集和验证集。其中验证集将作为模型效果的验证使用,并不会在训练中使用:
test_df = reduced_df.groupby('customer_id').last().reset_index()
train_df = reduced_df.merge(test_df[['customer_id', 'product_id']],
on=['customer_id', 'product_id'],
how='outer',
indicator=True)
train_df = train_df[(train_df['_merge'] == 'left_only')]
最后,我们将数据集由 Pandas DataFrame 转换为 MXNet NDArray,这是因为即将使用基于 MXNe 的 Gluon 接口进行模型训练:
b
atch_size = 1024
train = gluon.data.ArrayDataset(nd.array(train_df['user'].values, dtype=np.float32),
nd.array(train_df['item'].values, dtype=np.float32),
nd.array(train_df['star_rating'].values, dtype=np.float32))
test = gluon.data.ArrayDataset(nd.array(test_df['user'].values, dtype=np.float32),
nd.array(test_df['item'].values, dtype=np.float32),
nd.array(test_df['star_rating'].values, dtype=np.float32))
train_iter = gluon.data.DataLoader(train, shuffle=True, num_workers=4, batch_size=batch_size, last_batch='rollover')
test_iter = gluon.data.DataLoader(train, shuffle=True, num_workers=4, batch_size=batch_size, last_batch='rollover')
4. 使用本地机器训练
首先通过自己定义一个简单的网络结构在本地进行训练,这里我们会继承 Gluon 接口构建 MFBlock 类,需要设置如下的主要网络结构。更多关于如何使用 Gluon 构建自定义网络模型的信息,可以参考动手深度学习。简单说明如下:
- embeddings 将输入的稠密向量转换为固定长度。这里我们选取「64」,可调节
- dense layers 全链接层,使用 ReLU 激活函数
- dropout layer 用于防止过拟合
class MFBlock(gluon.HybridBlock):
def __init__(self, max_users, max_items, num_emb, dropout_p=0.5):
super(MFBlock, self).__init__()
self.max_users = max_users
self.max_items = max_items
self.dropout_p = dropout_p
self.num_emb = num_emb
with self.name_scope():
self.user_embeddings = gluon.nn.Embedding(max_users, num_emb)
self.item_embeddings = gluon.nn.Embedding(max_items, num_emb)
self.dropout_user = gluon.nn.Dropout(dropout_p)
self.dropout_item = gluon.nn.Dropout(dropout_p)
self.dense_user = gluon.nn.Dense(num_emb, activation='relu')
self.dense_item = gluon.nn.Dense(num_emb, activation='relu')
def hybrid_forward(self, F, users, items):
a = self.user_embeddings(users)
a = self.dense_user(a)
b = self.item_embeddings(items)
b = self.dense_item(b)
predictions = self.dropout_user(a) * self.dropout_item(b)
predictions = F.sum(predictions, axis=1)
return predictions
##set up network
num_embeddings = 64
net = MFBlock(max_users=customer_index.shape[0],
max_items=product_index.shape[0],
num_emb=num_embeddings,
dropout_p=0.5)
# Initialize network parameters
ctx = mx.gpu()
net.collect_params().initialize(mx.init.Xavier(magnitude=60),
ctx=ctx,
force_reinit=True)
net.hybridize()
# Set optimization parameters
opt = 'sgd'
lr = 0.02
momentum = 0.9
wd = 0.
trainer = gluon.Trainer(net.collect_params(),
opt,
{'learning_rate': lr,
'wd': wd,
'momentum': momentum})
同时我们还需要构建一个评估函数用来评价模型。这里将使用 MSE:
def eval_net(data, net, ctx, loss_function):
acc = MSE()
for i, (user, item, label) in enumerate(data):
user = user.as_in_context(ctx)
item = item.as_in_context(ctx)
label = label.as_in_context(ctx)
predictions = net(user, item).reshape((batch_size, 1))
acc.update(preds=[predictions], labels=[label])
return acc.get()[1]
接下来定义训练的代码并且示例一下进行几个轮次的训练:
def execute(train_iter, test_iter, net, epochs, ctx):
loss_function = gluon.loss.L2Loss()
for e in range(epochs):
print("epoch: {}".format(e))
for i, (user, item, label) in enumerate(train_iter):
user = user.as_in_context(ctx)
item = item.as_in_context(ctx)
label = label.as_in_context(ctx)
with mx.autograd.record():
output = net(user, item)
loss = loss_function(output, label)
loss.backward()
trainer.step(batch_size)
print("EPOCH {}: MSE ON TRAINING and TEST: {}. {}".format(e,
eval_net(train_iter, net, ctx, loss_function),
eval_net(test_iter, net, ctx, loss_function)))
print("end of training")
return net
%%time
epochs = 3
trained_net = execute(train_iter, test_iter, net, epochs, ctx)
可以看到,打印出来的训练日志如下图所示,显示训练成功进行,并且 Loss 随着迭代次数的增加在下降:
5. 使用 Amazon SageMaker BYOS 进行模型训练
在上文范例中,我们使用本地环境一步步训练了一个较小的模型,验证了我们的代码。现在,我们需要把代码进行整理,在 Amazon SageMaker 上进行可扩展至分布式的托管训练任务。
首先要将上文的代码整理至一个 Python 脚本,然后使用 SageMaker 上预配置的 MXNet 容器,我们可以通过很多灵活的使用方式来使用该容器,详情可参考 mxnet-sagemaker-estimators。
接下来将执行如下几步操作:
- 将所有数据预处理的工作封装至一个函数,本文中为 prepare_train_data
- 将所有训练相关的代码(函数或类)复制粘贴
- 定义一个名为 Train 的函数,用于:
➢ 添加一段 Sagemaker 训练集群读取数据的代码
➢ 定义超参数为一个字典作为入参,在之前的例子我们是全局定义的
➢ 创建网络并且执行训练
我们可以在下载的代码目录中看到 Recommender.py 脚本,是编辑后的范例。
现在,我们需要将数据从本地上传至 S3,这样 Amazon SageMaker 后台运行时可以直接读取。这种方式通常对于大数据量的场景和生产环境的实践十分常见。
boto3.client('s3').copy({'Bucket': 'amazon-reviews-pds',
'Key': 'tsv/amazon_reviews_us_Digital_Video_Download_v1_00.tsv.gz'},
bucket,
prefix + '/train/amazon_reviews_us_Digital_Video_Download_v1_00.tsv.gz')
最后,我们可以通过 SageMaker Python SDK 创建一个 MXNet estimator,需要传入以下设置:
- 训练的实例类型和实例个数,SageMaker 提供的 MXNet 容器支持单机训练也支持多 GPU 训练,只需要指定训练机型个数即可切换
- 模型存储的 S3 路径及其对应的权限设置
- 模型对应的超参数,这里我们将 Embedding 的个数提升,后面可以看到这个结果会优于之前的结果,这里的超参数配置可以进一步进行调优,从而得到更为准确的模型
完成以上配置后,可以用.fit () 来开启训练任务,这会创建一个 SageMaker 训练任务加载数据和程序,运行我们 Recommender.py 脚本中的 Train 函数,将模型结果保存至传入的 S3 路径。
m = MXNet('recommender.py',
py_version='py3',
role=role,
train_instance_count=1,
train_instance_type="ml.p2.xlarge",
output_path='s3://{}/{}/output'.format(bucket, prefix),
hyperparameters={'num_embeddings': 512,
'opt': opt,
'lr': lr,
'momentum': momentum,
'wd': wd,
'epochs': 10},
framework_version='1.1')
m.fit({'train': 's3://{}/{}/train/'.format(bucket, prefix)})
训练启动后,我们可以在 Amazon SageMaker 控制台看到这个训练任务,点进详情可以看到训练的日志输出,以及监控机器的 GPU、CPU、内存等使用率等情况,以确认程序可以正常工作。
6. 托管部署及推理测试
在本地完成训练后,即可轻松地将上述模型部署成一个实时可在生产环境中调用的端口:
predictor = m.deploy(initial_instance_count=1,
instance_type='ml.m4.xlarge')
predictor.serializer = None
<!-- /\* Font Definitions \*/ @font-face {font-family:"Cambria Math"; panose-1:2 4 5 3 5 4 6 3 2 4; mso-font-charset:0; mso-generic-font-family:roman; mso-font-pitch:variable; mso-font-signature:3 0 0 0 1 0;} @font-face {font-family:"Segoe UI"; panose-1:2 11 5 2 4 2 4 2 2 3; mso-font-charset:0; mso-generic-font-family:swiss; mso-font-pitch:variable; mso-font-signature:-469750017 -1073683329 9 0 511 0;} @font-face {font-family: 微软雅黑; panose-1:2 11 5 3 2 2 4 2 2 4; mso-font-charset:134; mso-generic-font-family:swiss; mso-font-pitch:variable; mso-font-signature:-2147483001 718224464 22 0 262175 0;} @font-face {font-family:Consolas; panose-1:2 11 6 9 2 2 4 3 2 4; mso-font-charset:0; mso-generic-font-family:modern; mso-font-pitch:fixed; mso-font-signature:-536869121 64767 1 0 415 0;} @font-face {font-family:"\\@微软雅黑"; mso-font-charset:134; mso-generic-font-family:swiss; mso-font-pitch:variable; mso-font-signature:-2147483001 718224464 22 0 262175 0;} /\* Style Definitions \*/ p.MsoNormal, li.MsoNormal, div.MsoNormal {mso-style-unhide:no; mso-style-qformat:yes; mso-style-parent:""; margin-top:2.5pt; margin-right:0cm; margin-bottom:2.5pt; margin-left:0cm; mso-para-margin-top:.5gd; mso-para-margin-right:0cm; mso-para-margin-bottom:.5gd; mso-para-margin-left:0cm; text-align:justify; text-justify:inter-ideograph; mso-pagination:none; layout-grid-mode:char; word-break:break-all; font-size:10.5pt; mso-bidi-font-size:11.0pt; font-family:"Segoe UI",sans-serif; mso-fareast-font-family: 微软雅黑; mso-bidi-font-family:"Times New Roman"; mso-bidi-theme-font:minor-bidi; mso-font-kerning:1.0pt; layout-grid-mode:line;} p.a, li.a, div.a {mso-style-name: 代码; mso-style-update:auto; mso-style-unhide:no; mso-style-qformat:yes; mso-style-link:" 代码 字符 "; margin:0cm; margin-bottom:.0001pt; text-align:justify; text-justify:inter-ideograph; mso-pagination:none; layout-grid-mode:char; background:#BFBFBF; mso-background-themecolor:background1; mso-background-themeshade:191; word-break:break-all; font-size:10.5pt; mso-bidi-font-size:11.0pt; font-family:Consolas; mso-fareast-font-family: 微软雅黑; mso-bidi-font-family:"Times New Roman"; mso-bidi-theme-font:minor-bidi; mso-font-kerning:1.0pt; layout-grid-mode:line;} span.a0 {mso-style-name:" 代码 字符 "; mso-style-unhide:no; mso-style-locked:yes; mso-style-link: 代码; font-family:Consolas; mso-ascii-font-family:Consolas; mso-fareast-font-family: 微软雅黑; mso-hansi-font-family:Consolas; background:#BFBFBF; mso-shading-themecolor:background1; mso-shading-themeshade:191; layout-grid-mode:both;} .MsoChpDefault {mso-style-type:export-only; mso-default-props:yes; font-family: 等线; mso-bidi-font-family:"Times New Roman"; mso-bidi-theme-font:minor-bidi;} /\* Page Definitions \*/ @page {mso-page-border-surround-header:no; mso-page-border-surround-footer:no;} @page WordSection1 {size:612.0pt 792.0pt; margin:72.0pt 90.0pt 72.0pt 90.0pt; mso-header-margin:36.0pt; mso-footer-margin:36.0pt; mso-paper-source:0;} div.WordSection1 {page:WordSection1;} -->
上述命令运行后,我们会在控制面板中看到相应的终结点配置。当成功创建后,可以测试一下,为此可以发出一个 HTTP Post 请求,也可以简单地调用 SDK 的.predict () 直接调用,随后会得到返回的预测结果:[5.446407794952393, 1.6258208751678467]
predictor.predict(json.dumps({'customer\_id': customer\_index\[customer\_index\['user'\] == 6\]\['customer\_id'\].values.tolist(),
'product\_id': \['B00KH1O9HW', 'B00M5KODWO'\]}))
在此基础上,我们还可以计算模型在测试集上的误差,结果为 1.27。这个结果优于我们之前本地 Embedding 设置为 64 时的 1.65,这也体现了通过调节网络结构,我们可以不断优化模型。
test_preds = []
for array in np.array_split(test_df[['customer_id', 'product_id']].values, 40):
test_preds += predictor.predict(json.dumps({'customer_id': array[:, 0].tolist(),
'product_id': array[:, 1].tolist()}))
test_preds = np.array(test_preds)
print('MSE:', np.mean((test_df['star_rating'] - test_preds) ** 2))
总结
本文介绍了如何利用 Amazon SageMaker 基于 Gluon 构建一个简单的推荐系统,并且将它进行部署调用。这可以是大家入手推荐系统的很好的入门教程。但值得注意的是:本文作为基础示例,并没有包含超参数调优,网络结构的优化,多特征的引入等工作,这都是后续提升准确率构建一个完备推荐系统所必需的工作。如果需要使用更复杂深入的推荐系统模型,或是基于 Gluon 构建其他应用,请关于我们后续发布地相关内容,或参阅 Amazon SageMaker 官方文档。
参考资料
- 本文涉及的代码:https://github.com/awslabs/am…
- Apriori 算法:https://en.wikipedia.org/wiki…
- MXNet:https://mxnet.incubator.apach…
- Amazon SageMaker 官方文档:https://docs.aws.amazon.com/s…
- 动手深度学习:http://zh.d2l.ai/chapter_pref…