10

image

前言

现在,大多数成功的在线销售型企业为每个用户实现用户画像、为其产生个性化推送内容,进而实现精准营销。例如个性化产品推荐以及促销活动。为了产生最好的产品内容,我们首先需要预测用户的下一步行为。比如,用户会通过浏览一个商品并将其添加进购物车。如果我们在此时此刻推送此类商品的促销信息,那么用户会有更大概率去购买商品。通过对于用户过去的行为以及喜好进行分析,我们可以推断出用户在未来潜在的消费行为倾向从而产生更好的个性化推荐内容(例如邮件推送,广告)。

在亚马逊,我们使用 Apache MXNet 构造了一个多标签分类模型用于在数千类别里预测用户倾向。通过预测的结果,我们可以创造一种个性化的内容,帮助用户去选择最好的商品。这个文章将通过准备数据,模型构造和模型部署三个步骤来介绍在构造模型中我们遇到的各种挑战以及使用 Deep Java Library (DJL)Apache Spark 上进行大规模的深度学习推理任务。因为使用的工具完全开源,你也可以尝试去构建类似的应用。

准备数据

我们需要准备两组数据用于训练,分别是输入数据和标记数据。

输入数据

无论构建什么样的机器学习模型,其中一个很重要的部分就是输入数据。我们在这个模型中选择了多标记分类模型,这样做的好处是我们无需构建多个 Pipeline(管道),只需要一个就可以进行推理任务。这个数据管道采集了来自于多个分类中用户的表现,然后通过这些信息,我们可以推断用户下一步在这些分类中的行为倾向。相比于构造大量的 binary 分类器,这种单模型多标记的设计减少了维护成本。

然后我们开始准备输入数据。我们为几亿亚马逊用户创建了几十万维度的特征向量,因为数据相对稀疏,我们使用了稀疏矩阵的形式来构造我们的输入数据:
image

上图展示了一个稀疏矩阵的表达类型。通过给出数据大小,数据内容和对应的坐标,我们可以构建出一个稀疏矩阵。相比于密集矩阵,稀疏矩阵可以帮助节约内存以及加快推理的速度。

标记数据

这个倾向模型将会判断用户对于一个类别的选择,在不同的地区这些类别的内容也不尽相同。对于每一个地区而言,我们都有几千种类别。每一个类别都使用简单的0或者1表达。1表示用户进行了这个类别的行为,0则反之。这些过去行为的类别,将会被用来评估用户在未来是否会进行同一种选择。下面就是一组以 one-hot 编码呈现的类别:
image

在这个案例中,用户 A 进行了类别1和类别3的动作,然后用户 B 之做了类别2的行为。

构造模型

模型架构

这个倾向模型是通过使用 MXNet Python API 构造的。总体来说,它是一个包含了稀疏输入层,隐藏层,和上千输出层的前馈神经网络。虽然输出结果可以很容易的用逻辑回归的方式表达,但是我们还是选择了使用softmax处理输出以达到多种类别判断结果的平衡。这样不仅可以帮助我们判断用户是否有可能会选择这个类别,也会给出相较于其他类别的相对可能性。下图就是一个这个网络结构的样例,一个简单的输入和4个类别输出:image

下面是一段伪代码展示这个网络结构:

data <- 输入变量,类型 'csr' 矩阵
weight <- 变量,类型 'row\_sparse' 矩阵
bias <- 变量,长度等于第一个隐藏层的大小
first\_hidden\_layer <- 第一个隐藏层,在 sparse.dot(data,weight) 和 bias 之间用了broadcast.add
hidden\_layers <- 后续的隐藏层,activation 和 dropout 交替使用
classification\_layer <- 分类层,N个 FullyConnected 层
output\_layer <- SoftmaxOutput 输出层

模型训练

为了更好的训练模型,我们写了一个自定义的迭代器来处理稀疏输入然后将它转成 MXNet 的 NDArray。在每一次迭代过程中,我们批量读入包含有用户 ID,标记,以及稀疏特征的输入。然后我们使用这些信息构造 MXNet CSR 矩阵来编码非0值以及它们对应的位置信息。下面是一个稀疏矩阵设定的案例,其中批大小为3,特征大小为5:
image

标记数据输入到 MXNet Module 中作为标准的 MXNet NDArray。每一个在表里的数据点都代表了一个类别。然后我们进行了进一步转化,把0和1的值表达成了一个二维数组,以[是,不是]的形式来表达。比如如果数据中类别1的值为1,那么二维数组的表达就是 [1, 0]。最后我们将结果构建成一个二维矩阵,其中第一个维度是类别维度,第二个维度是这个类别的完成度。下面展示了一个批大小为3,类别为4的例子:
image

然后我们把特征和标记包装成 MXNet Databatch 用来训练。训练的损失函数为交叉熵损失。随着 Apache MXNet 技术的演进,我们也在逐步利用基于 MXNet Gluon 的 Block 架构来改进模型构造以及训练过程。

模型部署

部署中的挑战

因为数据量大,使用率高,在生产环境中,我们需要构建一个可以容易扩展且容易去维护的架构。在试过一些工具之后,我们认为 Apache Spark 可以帮助我们在需求的处理时间内扩展完成任务。通过一段时间的深度学习框架比较,我们最终选择了性能和精度最优的 Apache MXNet 作为我们深度学习的平台。

我们是一个由科研人员以及开发人员组成的开发团队。但是一些问题也由此产生:科学家主推 Python(易用性)用于研究以及训练,而开发者则会选择采用基于 Spark Java/Scala(稳定性)语言的开发平台。我们经过很长时间的讨论,无论是全部用 Java 进行,还是全用 Python 都不是合理解决方案。现在,通过使用 DJL,我们可以无缝的将两者的优势结合在一起。因为其易用性以及稳定性的特色,我们基本不需要对代码作出太大改变就可以完成推理任务。然后考虑到它不受限制于深度学习引擎,使得日后我们部署基于其他平台的模型(TensorFlow, PyTorch) 也易如反掌。

推理任务部署

完成完整的推理任务十分简单,只需如下几步:

环境配置

通过添加如下依赖项,便可以完成在 gradle 上的设置

dependencies {
 compile group: 'ai.djl', name: 'repository', version: '0.4.1'
 compile group: 'ai.djl.mxnet', name: 'mxnet-engine', version: '0.4.1'
 runtime group: 'ai.djl.mxnet', name: 'mxnet-native-auto', version: '1.6.0'
}

推理逻辑

DJL 使用 NDList (List of NDArray)作为数据在推理任务中的传输格式。它提供了一个 Translator 接口(翻译器)作为前处理和后处理的模式:前处理将输入数据转化成 NDArray 添加进 NDList,后处理将 NDArray 转化成输出数据。值得一提的是 DJL 支持稀疏矩阵输入也同时支持批量输入。

首先,我们从本地路径读入模型:

Path modelDir \= Paths.get("/Your/Model/Directory");
String modelName \= "your\_model\_name";
Model model \= Model.newInstance();
model.load(modelDir, modelName);

我们设定了 Translator 来把特征向量转化为 NDList 然后把输出结果转成

class InputOutputTranslator
 extends Translator\[Array\[SparseVector\], Array\[Array\[Float\]\]\] {

 override def processInput(translatorContext: TranslatorContext,
 input: Array\[SparseVector\]): NDList \= {
 // 转化 Array\[SparseVector\] 到 CSR NDArray
 val indices: Array\[Long\] \= ...
 val indptr: Array\[Long\] \= ....
 val data: Buffer \= ....
 val shape: Shape \= ....
 val csrFeatures: NDArray \= model.getNDManager.createCSR(data, indptr, indices, shape)
 new NDList(csrFeatures)
 }

 override def processOutput(translatorContext: TranslatorContext,
 predictionNDList: NDList): Array\[Array\[Float\]\] \= {
 // 因为我们是批处理对多类别分类, 所以输出格式为:Array\[Array\[Float\]\]
 // 每组 Array\[Float\] 代表了每个类别的分类结果. 内部每个值代表了每个标记的预测结果
 ....
 }

}

然后我们将 Translator 输入进 model 产生 Predictor。Predictor 是一个线程安全,可进行推理的结构体。

val predictor: Predictor\[Array\[SparseVector\], Array\[Array\[Float\]\]\] \=

 model.newPredictor(new InputOutputTranslator)

val featureVectorArray : Array\[SparseVector\] \= ... 

val predictions: Array\[Array\[Float\]\] \= predictor.predict(featureVectorArray)

最后我们将 label 对应的类别应用在数据上产生如下数据:

{

 \[

 "customerId": 1

 "predictions": \[

 "category\_a": 0.813611214,

 "category\_b": 0.580259696,

 "category\_c": 7.5886305E-4,

 "category\_d": 0.7010947181,

 ....

 \]

 \],

 \[

 "customerId": 2

 "predictions": \[

 "category\_a": 0.0066125533,

 "category\_b": 0.304356237,

 "category\_c": 0.908850298,

 "category\_d": 2.3412544E-6,

 ....

 \]

 \],

 ...

}

性能表现

在使用 DJL 之前,完成一次批量推理任务大概需要24小时并且附带着很多内存管理的问题。DJL 显著缩短了推理时间,只需要几小时。曾经我们需要对每一个模型花费两周时间进行内存管理调试。使用 DJL 之后,我们无需在这上面花费更多的时间。上线模型的速度也从曾经的一个月,缩短到两周。

关于 DJL

DJL 是亚马逊云服务在2019年 re:Invent 大会推出的专为 Java 开发者量身定制的深度学习框架,现已运行在亚马逊数以百万的推理任务中。如果要总结 DJL 的主要特色,那么就是如下三点:

  • DJL 不设限制于后端引擎:用户可以轻松的使用 MXNet, PyTorch, TensorFlow 和 fastText 来在 Java 上做模型训练和推理。
  • DJL 的算子设计无限趋近于 numpy:它的使用体验上和 numpy 基本是无缝的,切换引擎也不会造成结果改变。
  • DJL 优秀的内存管理以及效率机制:DJL 拥有自己的资源回收机制,100个小时连续推理也不会内存溢出。

想了解更多,请参见下面几个链接:

https://djl.ai

https://github.com/awslabs/djl

也欢迎加入 DJL 的 slack 论坛

image


亚马逊云开发者
2.9k 声望9.6k 粉丝

亚马逊云开发者社区是面向开发者交流与互动的平台。在这里,你可以分享和获取有关云计算、人工智能、IoT、区块链等相关技术和前沿知识,也可以与同行或爱好者们交流探讨,共同成长。