微软认知工具包深度学习快速启动指南(二)
原文:
annas-archive.org/md5/2a17578844fd3b16114c38533fdf2462译者:飞龙
第五章:处理图像
在本章中,我们将探讨一些使用 CNTK 的深度学习模型。具体来说,我们将重点研究使用神经网络进行图像数据分类。你在前几章中学到的所有内容将在本章中派上用场,因为我们将讨论如何训练卷积神经网络。
本章将涵盖以下主题:
-
卷积神经网络架构
-
如何构建卷积神经网络
-
如何将图像数据输入卷积网络
-
如何通过数据增强提高网络性能
技术要求
我们假设你已经在计算机上安装了最新版本的 Anaconda,并按照第一章《使用 CNTK 入门》中的步骤安装了 CNTK。本章的示例代码可以在我们的 GitHub 仓库找到:github.com/PacktPublishing/Deep-Learning-with-Microsoft-Cognitive-Toolkit-Quick-Start-Guide/tree/master/ch5。
在本章中,我们将使用存储在 Jupyter notebook 中的一个示例。要访问示例代码,请在下载代码的目录中,在 Anaconda 提示符下运行以下命令:
cd ch5
jupyter notebook
我们将在每一部分提到相关的 notebook,以便你可以跟着进行并亲自尝试不同的技术。
本章的数据集在 GitHub 仓库中无法获取,因为它太大,无法存储在那里。请打开 Prepare the dataset.ipynb notebook,并按照其中的说明获取本章的数据。
查看以下视频,看看代码的实际效果:
卷积神经网络架构
在前几章中,我们学习了如何使用常规的前馈网络架构来构建神经网络。在前馈神经网络中,我们假设不同输入特征之间存在相互作用,但我们并未对这些相互作用的性质做出假设。然而,这并不总是正确的做法。
当你处理复杂数据(如图像)时,前馈神经网络并不能很好地完成任务。这是因为我们假设网络输入之间存在相互作用,但我们没有考虑到它们是空间上组织的这一事实。当你观察图像中的像素时,它们之间存在水平和垂直关系。图像中的颜色与某些颜色像素的位置之间也有关系。
卷积网络是一种特殊的神经网络,它明确假设我们处理的数据具有空间关系。这使得它在图像识别方面非常有效。不过,其他具有空间组织的数据也同样适用。让我们来探索一种用于图像分类任务的卷积神经网络的架构。
用于图像分类的网络架构
用于图像分类的卷积网络通常包含一个或多个卷积层,后跟池化层,并且通常以常规的全连接层结束,以提供最终的输出,如下图所示:
此图片来源于:en.wikipedia.org/wiki/File:T…
当你仔细观察卷积网络的结构时,你会发现它以一组卷积层和池化层开始。你可以将这一部分看作是一个复杂的、可训练的照片滤镜。卷积层过滤出分类图像所需的有趣细节,而池化层则总结这些特征,以便网络后端处理的数据点更少。
通常,在用于图像分类的神经网络中,你会发现多个卷积层和池化层的组合。这样做是为了能够从图像中提取更多复杂的细节。网络的第一层提取简单的细节,比如线条。接下来的层会将前一层的输出结合起来,学习更复杂的特征,比如角落或曲线。正如你所想的那样,后续的层会用来学习越来越复杂的特征。
通常,当你构建神经网络时,你希望对图像中的内容进行分类。这时,经典的全连接层就发挥了重要作用。通常,用于图像识别的模型会以一个输出层和一个或多个全连接层结束。
让我们来看看如何使用卷积层和池化层来创建一个卷积神经网络。
使用卷积层
现在,既然你已经了解了卷积网络的基本结构,让我们来看看卷积网络中使用的卷积层:
此图片来源于:en.wikipedia.org/wiki/File:C…
卷积层是卷积网络的核心构建块。你可以将卷积层看作一个可训练的滤镜,用来从输入中提取重要细节并去除被认为是噪声的数据。一个卷积层包含一组权重,这些权重覆盖一个小区域(宽度和高度),但涵盖了输入的所有通道。当你创建卷积层时,你需要指定其神经元的深度。你会发现,大多数框架,包括 CNTK,在讨论层的深度时,会提到滤镜。
当我们执行前向传播时,我们会将该层的卷积核滑动过输入数据,并在每个卷积核上执行输入数据与权重之间的点积运算。滑动的过程由步幅设置来控制。当你指定步幅为 1 时,最终得到的输出矩阵将具有与输入相同的宽度和高度,但深度与该层的卷积核数量相同。你可以设置不同的步幅,这会减少输出矩阵的宽度和高度。
除了输入大小和卷积核数量外,您还可以配置该层的填充。给卷积层添加填充会在处理过的输入数据周围添加零的边框。虽然这听起来像是一件奇怪的事,但在某些情况下,它是非常有用的。
查看卷积层的输出大小时,它将基于输入大小、卷积核数量、步幅和填充来决定。公式如下:
W 是输入大小,F 是卷积核数量或层深,P 是填充,S 是步幅。
不是所有输入大小、卷积核、步幅和填充的组合都是有效的。例如,当输入大小 W = 10,层深 F = 3,步幅 S = 2 时,最终得到的输出体积是 5.5。并不是所有输入都能完美地映射到这个输出大小,因此 CNTK 会抛出一个异常。这就是填充设置的作用。通过指定填充,我们可以确保所有输入都映射到输出神经元。
我们刚才讨论的输入大小和卷积核数量可能感觉有些抽象,但它们是有道理的。设置较大的输入大小会使得该层捕捉到输入数据中的较粗糙的模式。设置较小的输入大小则使得该层能够更好地检测更精细的模式。卷积核的深度或数量决定了能够检测到多少种不同的模式。在高级别上,可以说一个卷积核使用一个滤波器来检测一种模式;例如,水平线。而一个拥有两个滤波器的层则能够检测两种不同的模式:水平线和垂直线。
为卷积网络设定正确的参数可能需要不少工作。幸运的是,CNTK 提供了一些设置,帮助简化这个过程。
训练卷积层的方式与训练常规的密集层相同。这意味着我们将执行一次前向传播,计算梯度,并使用学习器在反向传播时更新参数的值。
卷积层后面通常会跟着一个池化层,用于压缩卷积层学习到的特征。接下来我们来看池化层。
处理池化层
在前一部分中,我们讨论了卷积层以及它们如何用于从像素数据中提取细节。池化层用于总结这些提取的细节。池化层有助于减少数据量,使得分类这些数据变得更加容易。
理解神经网络在面对具有大量不同输入特征的样本时,分类难度较大的问题非常重要。这就是为什么我们使用卷积层和池化层的组合来提取细节并进行总结的原因:
此图像来自: en.wikipedia.org/wiki/File:M…
池化层具有一个下采样算法,你可以通过输入大小和步幅来进行配置。我们将把前一卷积层的每个滤波器的输出传递到池化层。池化层会跨越数据片段,并提取与配置的输入大小相等的小窗口。它从这些小区域的值中选取最大的值作为该区域的输出。就像卷积层一样,它使用步幅来控制它在输入中移动的速度。例如,步幅为 2,大小为 1 时,会将数据的维度减半。通过仅使用最高的输入值,它丢弃了 75%的输入数据。
这种池化技术被称为最大池化,它并不是池化层减少输入数据维度的唯一方式。你还可以使用平均池化。在这种情况下,池化层会使用输入区域的平均值作为输出。
请注意,池化层仅减少输入在宽度和高度方向上的大小。深度保持不变,因此你可以放心,特征只是被下采样,并没有完全丢弃。
由于池化层使用固定算法来下采样输入数据,因此它们没有可训练的参数。这意味着训练池化层几乎不需要时间。
卷积网络的其他用途
我们将重点放在使用卷积网络进行图像分类上,但你也可以将这种神经网络应用于更多的场景,例如:
-
图像中的目标检测。CNTK 网站提供了一个很好的示例,展示了如何构建一个目标检测模型:
docs.microsoft.com/en-us/cognitive-toolkit/Object-Detection-using-Fast-R-CNN -
在照片中检测面部并预测照片中人的年龄
-
使用卷积神经网络和递归神经网络的结合,按照第六章的内容进行图像标题生成,处理时间序列数据
-
预测来自声纳图像的湖底距离
当你开始将卷积网络组合用于不同的任务时,你可以构建一些非常强大的应用;例如,一个安全摄像头,能够检测视频流中的人物,并警告保安有非法入侵者。
中国等国家正在大力投资这种技术。卷积网络被应用于智能城市项目中,用于监控过路口。通过深度学习模型,相关部门能够检测交通信号灯的事故,并自动重新规划交通路线,从而使警察的工作变得更加轻松。
构建卷积网络
现在你已经了解了卷积网络的基本原理以及一些常见的应用场景,让我们来看看如何使用 CNTK 来构建一个卷积网络。
我们将构建一个能够识别手写数字的模型。MNIST 数据集是一个免费的数据集,包含了 60,000 个手写数字样本。还有一个包含 10,000 个样本的测试集。
让我们开始并看看在 CNTK 中构建卷积网络是什么样子。首先,我们将了解如何构建卷积神经网络的结构,然后我们会了解如何训练卷积神经网络的参数。最后,我们将探讨如何通过更改网络结构和不同层设置来改进神经网络。
构建网络结构
通常,当你构建一个用于识别图像中模式的神经网络时,你会使用卷积层和池化层的组合。网络的最后应该包含一个或多个隐藏层,并以 softmax 层结束,用于分类目的。
让我们来构建网络结构:
from cntk.layers import Convolution2D, Sequential, Dense, MaxPooling
from cntk.ops import log_softmax, relu
from cntk.initializer import glorot_uniform
from cntk import input_variable, default_options
features = input_variable((3,28,28))
labels = input_variable(10)
with default_options(initialization=glorot_uniform, activation=relu):
model = Sequential([
Convolution2D(filter_shape=(5,5), strides=(1,1), num_filters=8, pad=True),
MaxPooling(filter_shape=(2,2), strides=(2,2)),
Convolution2D(filter_shape=(5,5), strides=(1,1), num_filters=16, pad=True),
MaxPooling(filter_shape=(3,3), strides=(3,3)),
Dense(10, activation=log_softmax)
])
z = model(features)
按照给定的步骤进行操作:
-
首先,导入神经网络所需的层。
-
然后,导入网络的激活函数。
-
接下来,导入
glorot_uniform initializer函数,以便稍后初始化卷积层。 -
之后,导入
input_variable函数来创建输入变量,以及default_options函数,使得神经网络的配置更加简便。 -
创建一个新的
input_variable来存储输入图像,这些图像将包含3个通道(红、绿、蓝),尺寸为28x28像素。 -
创建另一个
input_variable来存储待预测的标签。 -
接下来,创建网络的
default_options并使用glorot_uniform作为初始化函数。 -
然后,创建一个新的
Sequential层集来构建神经网络的结构 -
在
Sequential层集中,添加一个Convolutional2D层,filter_shape为5,strides设置为1,并将过滤器数量设置为8。启用padding,以便填充图像以保留原始尺寸。 -
添加一个
MaxPooling层,filter_shape为2,strides设置为2,以将图像压缩一半。 -
添加另一个
Convolution2D层,filter_shape为 5,strides设置为 1,使用 16 个过滤器。添加padding以保持由前一池化层产生的图像尺寸。 -
接下来,添加另一个
MaxPooling层,filter_shape为 3,strides设置为 3,将图像尺寸缩小到原来的三分之一。 -
最后,添加一个
Dense层,包含 10 个神经元,用于网络可以预测的 10 个类别。使用log_softmax激活函数,将网络转换为分类模型。
我们使用 28x28 像素的图像作为模型的输入。这个尺寸是固定的,因此当你想用这个模型进行预测时,你需要提供相同尺寸的图像作为输入。
请注意,这个模型仍然非常基础,不会产生完美的结果,但它是一个良好的开始。稍后如果需要,我们可以开始调整它。
使用图像训练网络
现在我们有了卷积神经网络的结构,接下来让我们探讨如何训练它。训练一个处理图像的神经网络需要比大多数计算机可用内存更多的内存。这时,来自第三章的 minibatch 数据源,将数据输入到神经网络中,就派上用场了。我们将设置一组包含两个 minibatch 数据源,用于训练和评估我们刚刚创建的神经网络。让我们首先看看如何为图像构建一个 minibatch 数据源:
import os
from cntk.io import MinibatchSource, StreamDef, StreamDefs, ImageDeserializer, INFINITELY_REPEAT
import cntk.io.transforms as xforms
def create_datasource(folder, max_sweeps=INFINITELY_REPEAT):
mapping_file = os.path.join(folder, 'mapping.bin')
stream_definitions = StreamDefs(
features=StreamDef(field='image', transforms=[]),
labels=StreamDef(field='label', shape=10)
)
deserializer = ImageDeserializer(mapping_file, stream_definitions)
return MinibatchSource(deserializer, max_sweeps=max_sweeps)
按照给定的步骤操作:
-
首先,导入
os包,以便访问一些有用的文件系统功能。 -
接下来,导入必要的组件来创建一个新的
MinibatchSource。 -
创建一个新的函数
create_datasource,该函数接收输入文件夹的路径和一个max_sweeps设置,用来控制我们遍历数据集的频率。 -
在
create_datasource函数中,找到源文件夹中的mapping.bin文件。这个文件包含磁盘上的图像和其关联标签之间的映射。 -
然后创建一组流定义,用来从
mapping.bin文件中读取数据。 -
为图像文件添加一个
StreamDef。确保包括transforms关键字参数,并初始化为空数组。 -
为标签字段添加另一个
StreamDef,该字段包含 10 个特征。 -
创建一个新的
ImageDeserializer,并为其提供mapping_file和stream_definitions变量。 -
最后,创建一个
MinibatchSource并为其提供反序列化器和max_sweeps设置。
请注意,你可以使用Preparing the dataset.ipynb Python 笔记本中的代码创建训练所需的文件。确保你的硬盘上有足够的空间来存储图像。1GB 的硬盘空间足够存储所有训练和验证样本。
一旦我们有了create_datasource函数,就可以创建两个独立的数据源来训练模型:
train_datasource = create_datasource('mnist_train')
test_datasource = create_datasource('mnist_test', max_sweeps=1, train=False)
-
首先,调用
create_datasource函数,并传入mnist_train文件夹,以创建训练数据源。 -
调用
create_datasource函数,并使用mnist_test文件夹,将max_sweeps设置为 1,以创建用于验证神经网络的数据源。
一旦准备好图像,就可以开始训练神经网络了。我们可以使用train方法在loss函数上启动训练过程:
from cntk import Function
from cntk.losses import cross_entropy_with_softmax
from cntk.metrics import classification_error
from cntk.learners import sgd
@Function
def criterion_factory(output, targets):
loss = cross_entropy_with_softmax(output, targets)
metric = classification_error(output, targets)
return loss, metric
loss = criterion_factory(z, labels)
learner = sgd(z.parameters, lr=0.2)
按照给定的步骤进行:
-
首先,从 cntk 包中导入 Function 装饰器。
-
接下来,从 losses 模块中导入
cross_entropy_with_softmax函数。 -
然后,从 metrics 模块中导入
classification_error函数。 -
在此之后,从 learners 模块导入
sgd学习器。 -
创建一个新函数
criterion_factory,带有两个参数:output 和 targets。 -
使用
@Function装饰器标记该函数,将其转换为 CNTK 函数对象。 -
在函数内部,创建一个新的
cross_entropy_with_softmax函数的实例。 -
接下来,创建一个新的
classification_error指标的实例。 -
将损失和指标作为函数的结果返回。
-
在创建
criterion_factory函数后,用它初始化一个新的损失。 -
最后,使用模型的参数和学习率 0.2 设置
sgd学习器。
现在我们已经为神经网络设置了损失和学习器,让我们看看如何训练和验证神经网络:
from cntk.logging import ProgressPrinter
from cntk.train import TestConfig
progress_writer = ProgressPrinter(0)
test_config = TestConfig(test_datasource)
input_map = {
features: train_datasource.streams.features,
labels: train_datasource.streams.labels
}
loss.train(train_datasource,
max_epochs=1,
minibatch_size=64,
epoch_size=60000,
parameter_learners=[learner],
model_inputs_to_streams=input_map,
callbacks=[progress_writer, test_config])
按照给定的步骤进行:
-
首先从
logging模块中导入ProgressPrinter类。 -
接下来,从
train模块中导入TestConfig类。 -
创建一个新的
ProgressPrinter实例,以便我们可以记录训练过程的输出。 -
然后,使用前面创建的
test_datasource作为输入,为神经网络创建TestConfig。 -
创建一个新的字典,将
train_datasource的数据流映射到神经网络的输入变量。 -
最后,在
loss上调用train方法,并提供train_datasource、训练器的设置、learner、input_map和训练期间要使用的回调函数。
当您执行 Python 代码时,您将获得类似于以下输出:
average since average since examples
loss last metric last
------------------------------------------------------
Learning rate per minibatch: 0.2
105 105 0.938 0.938 64
1.01e+07 1.51e+07 0.901 0.883 192
4.31e+06 2 0.897 0.895 448
2.01e+06 2 0.902 0.906 960
9.73e+05 2 0.897 0.893 1984
4.79e+05 2 0.894 0.891 4032
[...]
注意损失随时间减少的情况。达到足够低的值使模型可用确实需要相当长的时间。训练图像分类模型将需要很长时间,因此这是使用 GPU 将大大减少训练时间的情况之一。
选择正确的层次组合
在前面的部分中,我们已经看到如何使用卷积层和池化层构建神经网络。
我们刚刚看到,训练用于图像识别的模型需要相当长的时间。除了长时间的训练时间外,选择卷积网络的正确设置也非常困难,需要很长时间。通常,您需要运行数小时的实验来找到有效的网络结构。这对于有抱负的 AI 开发者来说可能非常沮丧。
幸运的是,许多研究团队正在致力于寻找用于图像分类任务的最佳神经网络架构。已有几种不同的架构在竞赛和现实场景中取得了成功:
-
VGG-16
-
ResNet
-
Inception
还有更多的架构。虽然我们不能详细讨论每种架构的构建方式,但我们可以从功能层面探讨它们的工作原理,这样你可以做出更有根据的选择,决定在自己的应用中尝试哪种网络架构。
VGG 网络架构是由视觉几何组(Visual Geometry Group)发明的,用于将图像分类为 1000 个不同的类别。这项任务非常困难,但该团队成功达到了 70.1%的准确率,考虑到区分 1000 个不同类别的难度,这个结果相当不错。
VGG 网络架构使用了堆叠的卷积层,输入大小为 3x3。层的深度逐渐增加,从 32 个滤波器开始,继续使用 48 个滤波器,一直到 512 个滤波器。数据量的减少是通过使用 2x2 的池化滤波器完成的。VGG 网络架构在 2015 年被发明时是当时的最先进技术,因为它的准确率比之前发明的模型要高得多。
然而,构建用于图像识别的神经网络还有其他方法。ResNet 架构使用了所谓的微架构。它仍然使用卷积层,但这次它们被安排成块。该架构与其他卷积网络非常相似,只是 VGG 网络使用了长链式层,而 ResNet 架构则在卷积层块之间使用了跳跃连接:
ResNet 架构
这就是“微架构”这个术语的来源。每一个块都是一个微型网络,能够从输入中学习模式。每个块有若干卷积层和一个残差连接。这个连接绕过卷积层块,来自残差连接的数据会加到卷积层的输出上。这个设计的理念是,残差连接能够打破网络中的学习过程,使其学习得更好、更快。
与 VGG 网络架构相比,ResNet 架构更深,但更易于训练,因为它需要优化的参数较少。VGG 网络架构占用 599 MB 的内存,而 ResNet 架构只需要 102 MB。
我们将要探讨的最终网络架构是 Inception 架构。这个架构同样属于微架构类别。与 ResNet 架构中使用的残差块不同,Inception 网络使用了 Inception 块:
Inception 网络
Inception 架构中的 Inception 块使用不同输入大小(1x1、3x3 和 5x5)的卷积层,然后沿着通道轴进行拼接。这会生成一个矩阵,其宽度和高度与输入相同,但通道数比输入更多。其思想是,当你这样做时,输入中提取的特征会有更好的分布,从而为执行分类任务提供更高质量的数据。这里展示的 Inception 架构非常浅,通常使用的完整版本可以有超过两个 Inception 块。
当你开始处理其他卷积神经网络架构时,你会很快发现你需要更多的计算能力来训练它们。通常,数据集无法完全加载到内存中,且你的计算机可能会因为训练模型所需的时间过长而变得太慢。此时,分布式训练可以提供帮助。如果你对使用多台机器训练模型感兴趣,绝对应该查看 CNTK 手册中的这一章节:docs.microsoft.com/en-us/cognitive-toolkit/Multiple-GPUs-and-machines。
通过数据增强提升模型性能
用于图像识别的神经网络不仅难以设置和训练,还需要大量数据来进行训练。此外,它们往往会在训练过程中对图像过拟合。例如,当你只使用直立姿势的面部照片时,模型很难识别以其他方向旋转的面部。
为了帮助克服旋转和某些方向上的偏移问题,你可以使用图像增强。CNTK 在创建图像的小批量源时,支持特定的变换。
我们为本章节提供了一个额外的笔记本,演示了如何使用这些变换。你可以在本章节的示例中找到该部分的示例代码,文件名为Recognizing hand-written digits with augmented data.ipynb。
你可以使用多种变换。例如,你可以只用几行代码随机裁剪用于训练的图像。你还可以使用的其他变换包括缩放和颜色变换。你可以在 CNTK 网站上找到有关这些变换的更多信息:cntk.ai/pythondocs/cntk.io.transforms.html。
在本章前面用于创建小批量源的函数中,我们可以通过加入裁剪变换来修改变换列表,代码如下所示:
import os
from cntk.io import MinibatchSource, StreamDef, StreamDefs, ImageDeserializer, INFINITELY_REPEAT
import cntk.io.transforms as xforms
def create_datasource(folder, train=True, max_sweeps=INFINITELY_REPEAT):
mapping_file = os.path.join(folder, 'mapping.bin')
image_transforms = []
if train:
image_transforms += [
xforms.crop(crop_type='randomside', side_ratio=0.8),
xforms.scale(width=28, height=28, channels=3, interpolations='linear')
]
stream_definitions = StreamDefs(
features=StreamDef(field='image', transforms=image_transforms),
labels=StreamDef(field='label', shape=10)
)
deserializer = ImageDeserializer(mapping_file, stream_definitions)
return MinibatchSource(deserializer, max_sweeps=max_sweeps)
我们改进了函数,加入了一组图像变换。在训练时,我们将随机裁剪图像,以获得更多图像的变化。然而,这也会改变图像的尺寸,因此我们还需要加入一个缩放变换,确保图像符合神经网络输入层所期望的大小。
在训练过程中使用这些变换将增加训练数据的变化性,从而减少神经网络因图像的颜色、旋转或大小稍有不同而卡住的几率。
但是需要注意的是,这些变换不会生成新的样本。它们仅仅是在数据输入到训练器之前对其进行更改。你需要增加最大训练轮次,以便在应用这些变换时生成足够的随机样本。需要额外的训练轮次数量将取决于数据集的大小。
同时,需要牢记输入层和中间层的维度对卷积网络的能力有很大影响。较大的图像在检测小物体时自然会表现得更好。将图像缩小到一个更小的尺寸会使较小的物体消失,或者丧失过多细节,以至于网络无法识别。
然而,支持较大图像的卷积网络需要更多的计算能力来进行优化,因此训练这些网络将花费更长的时间,而且更难得到最佳结果。
最终,你需要平衡图像大小、层的维度以及使用的数据增强方法,以获得最佳结果。
总结
在本章中,我们探讨了使用神经网络进行图像分类。与处理普通数据有很大的不同。我们不仅需要更多的训练数据来得到正确的结果,还需要一种更适合图像处理的不同架构。
我们已经看到了卷积层和池化层如何被用来本质上创建一个高级的照片滤镜,从数据中提取重要细节,并总结这些细节,以减少输入的维度,使其变得可管理。
一旦我们使用了卷积滤波器和池化滤波器的高级特性,接下来就是常规的工作了,通过密集层来构建分类网络。
为图像分类模型设计一个良好的结构可能相当困难,因此在开始进行图像分类之前,查看现有的架构总是一个不错的主意。另外,使用合适的增强技术可以在提升性能方面起到相当大的作用。
处理图像只是深度学习强大功能的一个应用场景。在下一章中,我们将探讨如何使用深度学习在时间序列数据上训练模型,如股票信息或比特币等课程信息。我们将学习如何在 CNTK 中使用序列,并构建一个可以在时间上推理的神经网络。下一章见。
第六章:处理时间序列数据
使用神经网络进行图像分类是深度学习中最具代表性的任务之一。但它当然不是神经网络擅长的唯一任务。另一个有大量研究正在进行的领域是循环神经网络。
在本章中,我们将深入探讨循环神经网络,以及它们如何应用于需要处理时间序列数据的场景;例如,在物联网解决方案中,你可能需要预测温度或其他重要的数值。
本章涵盖以下主题:
-
什么是循环神经网络?
-
循环神经网络的应用场景
-
循环神经网络是如何工作的
-
使用 CNTK 构建循环神经网络
技术要求
我们假设你已经在计算机上安装了最新版本的 Anaconda,并且按照第一章《开始使用 CNTK》中的步骤安装了 CNTK。本章的示例代码可以在我们的 GitHub 代码库中找到,网址是github.com/PacktPublishing/Deep-Learning-with-Microsoft-Cognitive-Toolkit-Quick-Start-Guide/tree/master/ch6。
在本章中,我们将处理存储在 Jupyter 笔记本中的示例代码。要访问示例代码,请在 Anaconda 提示符下运行以下命令,前提是你已经下载了代码并进入了该目录:
cd ch6
jupyter notebook
示例代码存储在Training recurrent neural networks.ipynb笔记本中。如果你没有配备 GPU 且无法使用 CNTK 的机器,请注意运行本章的示例代码将需要较长时间。
请观看以下视频,查看代码的实际应用:
什么是循环神经网络?
循环神经网络是一种特殊类型的神经网络,能够进行时间推理。它们主要用于需要处理随时间变化的数值的场景。
在常规神经网络中,你只能提供一个输入,这样就只能得到一个预测结果。这限制了你使用常规神经网络的能力。例如,常规神经网络在文本翻译方面表现不佳,而循环神经网络在翻译任务中却取得了不少成功的实验。
在循环神经网络中,可以提供一系列样本,这将生成一个单一的预测结果。你还可以使用循环神经网络根据一个输入样本预测输出序列。最后,你可以根据输入序列预测输出序列。
和其他类型的神经网络一样,你也可以在分类任务和回归任务中使用循环神经网络,尽管根据网络输出的结果可能很难识别出循环神经网络执行的任务类型。
循环神经网络的变体
递归神经网络可以以多种方式使用。在本节中,我们将探讨递归神经网络的不同变体,以及它们如何用于解决特定类型的问题。具体来说,我们将关注以下几种变体:
-
基于输入序列预测单个输出
-
基于单个输入值预测序列
-
基于其他序列预测序列
最后,我们还将探索如何将多个递归神经网络堆叠在一起,以及如何在处理文本等场景中提高性能。
让我们来看一下递归网络可以使用的场景,因为有多种方法可以利用递归神经网络的独特特性。
基于序列预测单个输出
递归神经网络包含一个反馈连接到输入。当我们输入一系列值时,它将按时间步处理序列中的每个元素。由于反馈连接,它可以将处理一个元素时生成的输出与下一个元素的输入结合起来。通过将前一时间步的输出与下一时间步的输入结合,它将构建一个跨整个序列的记忆,这可以用来进行预测。从示意图来看,基本的递归神经网络如下所示:
当我们将递归神经网络展开成其各个步骤时,这种递归行为变得更加清晰,下面的图示展示了这一点:
要使用这个递归神经网络进行预测,我们将执行以下步骤:
-
首先,我们将输入序列的第一个元素输入,创建一个初始的隐藏状态。
-
然后,我们将初始隐藏状态与输入序列中的第二个元素结合,生成更新后的隐藏状态。
-
最后,我们将输入序列中的第三个元素,生成最终的隐藏状态并预测递归神经网络的输出。
由于这个反馈连接,您可以训练递归神经网络识别随着时间发生的模式。例如,当你想预测明天的气温时,你需要查看过去几天的天气,以发现一个可以用来确定明天气温的模式。
基于单个样本预测序列
递归神经网络的基本模型也可以扩展到其他用例。例如,您可以使用相同的网络架构,基于单个输入预测一系列值,如下图所示:
在这种情况下,我们有三个时间步,每个时间步将根据我们提供的输入预测输出序列中的一个步骤。
-
首先,我们将输入样本送入神经网络,生成初始的隐藏状态,并预测输出序列中的第一个元素。
-
然后,我们将初始隐藏状态与相同的样本结合,生成更新后的隐藏状态和输出,预测输出序列中的第二个元素。
-
最后,我们再次输入样本,进一步更新隐藏状态,并预测输出序列中的最后一个元素。
从一个样本生成一个序列与我们之前的样本非常不同,之前我们收集了输入序列中所有时间步的信息以得到一个单一的预测。而在这种情况下,我们在每个时间步生成输出。
还有一种递归神经网络的变体,它结合了我们刚刚讨论的设置和前一节中讨论的设置,根据一系列值预测一系列值。
基于序列预测序列
现在我们已经了解了如何根据一个序列预测单个值,或根据一个单独的值预测一个序列,让我们看看如何进行序列到序列的预测。在这种情况下,你执行与前面情景中相同的步骤,其中我们是基于单个样本预测一个序列,如下图所示:
在这种情景下,我们有三个时间步,每个时间步接受输入序列中的元素,并预测一个我们想要预测的输出序列中的对应元素。让我们一步一步地回顾这个情景:
-
首先,我们取输入序列中的第一个元素,创建初始的隐藏状态,并预测输出序列中的第一个元素。
-
接下来,我们将初始隐藏状态与输入序列中的第二个元素结合,更新隐藏状态,并预测输出序列中的第二个元素。
-
最后,我们将更新后的隐藏状态和输入序列中的最后一个元素一起,预测输出序列中的最后一个元素。
所以,和我们在前一节中做的那样,我们不再在每一步重复相同的输入样本,而是一次输入序列中的一个元素,并将每一步生成的预测作为模型的输出序列。
堆叠多个递归层
递归神经网络可以拥有多个递归层。这使得递归网络的记忆容量增大,模型能够学习到更复杂的关系。
例如,当你想翻译文本时,你需要堆叠至少两个递归层,一个用于将输入文本编码为中间形式,另一个用于将其解码为你想翻译成的语言。谷歌有一篇有趣的论文,展示了如何使用这种技术进行语言间的翻译,论文地址是arxiv.org/abs/1409.3215。
由于循环神经网络可以以多种方式使用,因此它在处理时间序列数据时具有很强的预测能力。在下一部分,我们将深入了解循环网络如何在内部工作,从而更好地理解隐藏状态的工作原理。
循环神经网络是如何工作的?
为了理解循环神经网络是如何工作的,我们需要仔细看看这些网络中循环层的工作原理。在循环神经网络中,你可以使用几种不同类型的循环层。在我们深入讨论更高级的循环单元之前,让我们首先讨论如何使用标准循环层来预测输出,以及如何训练一个包含循环层的神经网络。
使用循环神经网络进行预测
基本的循环层与神经网络中的常规层非常不同。一般来说,循环层具有一个隐藏状态,作为该层的记忆。该层的输出会通过一个回环连接返回到该层的输入,正如下图所示:
现在我们已经了解了基本的循环层是什么样的,让我们一步一步地看一下这种层类型是如何工作的,我们将使用一个包含三个元素的序列。序列中的每一步称为一个时间步。为了使用循环层预测输出,我们需要用初始的隐藏状态来初始化该层。这通常是通过全零初始化来完成的。隐藏状态的大小与输入序列中单个时间步的特征数量相同。
接下来,我们需要更新序列中第一个时间步的隐藏状态。要更新第一个时间步的隐藏状态,我们将使用以下公式:
在这个公式中,我们通过计算初始隐藏状态(以零初始化)和一组权重之间的点积(即按元素相乘)来计算新的隐藏状态。我们将另加一组权重与该层输入的点积。两个点积的和将通过一个激活函数,就像在常规神经网络层中一样。这样我们就得到了当前时间步的隐藏状态。
当前时间步的隐藏状态将作为序列中下一个时间步的初始隐藏状态。我们将重复在第一个时间步中执行的计算,以更新第二个时间步的隐藏状态。第二个时间步的公式如下所示:
我们将计算隐藏状态权重与步骤 1 中的隐藏状态的点积,并将其与输入和输入权重的点积相加。请注意,我们正在重用前一个时间步的权重。
我们将重复更新隐藏状态的过程,作为序列中的第三个也是最后一个步骤,如下式所示:
当我们处理完序列中的所有步骤后,可以使用第三组权重和最终时间步的隐藏状态来计算输出,如下式所示:
当你使用递归网络预测输出序列时,你需要在每个时间步执行这个最终计算,而不仅仅是在序列的最后一个时间步。
训练递归神经网络
与常规神经网络层一样,你可以通过反向传播来训练递归层。这一次,我们将对常规的反向传播算法应用一个技巧。
在常规神经网络中,你会根据loss函数、输入和模型的期望输出来计算梯度。但这对递归神经网络不起作用。递归层的损失不能仅通过单个样本、目标值和loss函数来计算。因为预测输出是基于网络输入的所有时间步,因此你还需要使用输入序列的所有时间步来计算损失。因此,你得到的不是一组梯度,而是一个梯度序列,当它们加起来时得到最终的损失。
时间上的反向传播比常规反向传播更为困难。为了达到loss函数的全局最优,我们需要更加努力地沿梯度下降。我们梯度下降算法要走的坡度比常规神经网络大得多。除了更高的损失外,它还需要更长时间,因为我们需要处理序列中的每一个时间步,以便为单个输入序列计算和优化损失。
更糟糕的是,由于梯度在多个时间步的累加,反向传播过程中更容易出现梯度爆炸的问题。你可以通过使用有限制的激活函数来解决梯度爆炸问题,例如双曲正切函数(tanh)或sigmoid。这些激活函数将递归层的输出值限制在tanh函数的-1 和 1 之间,以及sigmoid函数的 0 和 1 之间。ReLU激活函数在递归神经网络中不太有用,因为梯度没有限制,这在某个时刻肯定会导致梯度爆炸。
限制激活函数输出值可能会引发另一个问题。记住在第二章,使用 CNTK 构建神经网络中提到的,sigmoid具有一个特定的曲线,梯度在曲线的两端迅速减小到零。我们在本节示例中使用的tanh函数也具有相同类型的曲线,如下图所示:
输入值在 -2 到 +2 之间时,梯度相对较为明确。这意味着我们可以有效地使用梯度下降来优化神经网络中的权重。然而,当递归层的输出低于 -2 或高于 +2 时,梯度会变得较浅。这可能会变得极其低,直到 CPU 或 GPU 开始将梯度四舍五入为零。这意味着我们不再进行学习。
由于涉及多个时间步,递归层比常规神经网络层更容易受到梯度消失或饱和问题的影响。在使用常规递归层时,你无法对此做太多处理。然而,其他递归层类型具有更为先进的设置,能够在一定程度上解决这个问题。
使用其他递归层类型
由于梯度消失问题,基本递归层在学习长期相关性方面表现不佳。换句话说,它在处理长序列时不太有效。当你尝试处理句子或更长的文本序列并试图分类它们的含义时,你会遇到这个问题。在英语和其他语言中,句子中的两个相关词之间有较长的距离,它们共同赋予句子意义。当你的模型仅使用基本递归层时,你很快会发现它在分类文本序列时并不优秀。
然而,还有其他递归层类型更适合处理更长的序列。同时,它们通常能更好地结合长短期相关性。
与门控递归单元(GRU)一起工作
基本递归层的一个替代方案是门控递归单元(GRU)。这种层类型具有两个门,帮助它处理序列中的长距离相关性,如下图所示:
GRU 的形状比常规递归层复杂得多。有更多的连接线将不同的输入与输出相连接。让我们一起看看这个图表,了解这个层类型背后的总体思路。
与常规的递归层不同,GRU 层具有更新门和重置门。重置门和更新门是控制保留多少先前时间步记忆、以及多少新数据用于生成新记忆的阀门。
预测输出与使用常规递归层进行预测非常相似。当我们将数据输入到该层时,先前的隐藏状态将用于计算新隐藏状态的值。当序列中的所有元素都处理完毕时,输出将使用一组额外的权重进行计算,就像我们在常规递归层中所做的那样。
在 GRU 中,计算跨多个时间步的隐藏状态要复杂得多。需要几个步骤来更新 GRU 的隐藏状态。首先,我们需要计算更新门的值,如下所示:
更新门通过两组权重进行控制,一组用于前一个时间步的隐藏状态,另一组用于当前时间步输入到层的值。更新门产生的值控制着多少过去的时间步数据保留在隐藏状态中。
第二步是更新重置门。此操作通过以下公式进行:
重置门也通过两组权重进行控制;一组用于当前时间步的输入值,另一组用于隐藏状态。重置门控制着从隐藏状态中移除多少信息。当我们计算新隐藏状态的初始版本时,这一点会变得更加清晰:
首先,我们将输入与其对应的权重相乘。然后,将前一个隐藏状态与其对应的权重相乘。接着,我们计算重置门与加权隐藏状态之间的逐元素或 Hadamard 积。最后,我们将其与加权输入相加,并对其应用tanh激活函数来计算记忆的隐藏状态。
这个公式中的重置门控制着前一个隐藏状态有多少信息被遗忘。一个较低值的重置门会从前一个时间步移除大量数据。较高的值将帮助层保留更多来自前一个时间步的信息。
但这还没完——一旦我们获得了来自前一个时间戳的信息,并且通过更新门增加并通过重置门调整后,就产生了来自前一个时间步的记忆信息。我们现在可以根据这些记忆信息计算最终的隐藏状态值,如下所示:
首先,我们对前一个隐藏状态与更新门进行逐元素相乘,以确定前一个状态应保留多少信息。然后,我们将更新门与前一个状态记忆信息的逐元素乘积相加。注意,更新门用于引入一定比例的新信息和旧信息。这就是为什么在公式的第二部分使用*1-*操作的原因。
GRU 在涉及计算和记忆长期及短期关系的能力方面,比传统的递归层有了很大的提升。然而,它不能同时处理这两者。
使用长短期记忆单元
另一种使用基本递归层的替代方法是使用长短期记忆(LSTM)单元。这个递归层像我们在上一部分讨论的 GRU 一样,也使用门控机制,区别在于 LSTM 有更多的门控机制。
以下图示展示了 LSTM 层的结构:
LSTM 单元有一个单元状态,这是该层类型工作原理的核心。单元状态在长时间内保持不变,变化很小。LSTM 层还有一个隐藏状态,但这个状态在该层中扮演着不同的角色。
简而言之,LSTM 有一个长期记忆,表现为单元状态,以及一个短期记忆,表现为隐藏状态。对长期记忆的访问是通过多个门来保护的。在 LSTM 层中,有两个门控制长期记忆的访问:
-
遗忘门,控制从单元状态中遗忘什么内容
-
输入门,控制什么内容会从隐藏状态和输入中存储到单元状态中
LSTM 层中还有一个最终的门,控制从单元状态中获取哪些信息到新的隐藏状态。实际上,我们使用输出门来控制从长期记忆中取出什么内容到短期记忆中。让我们一步步了解这一层是如何工作的。
首先,我们来看看遗忘门。遗忘门是当你使用 LSTM 层进行预测时,第一个会被更新的门:
遗忘门控制应该遗忘多少单元状态。它使用以下公式进行更新:
当你仔细查看这个公式时,你会发现它本质上是一个带有sigmoid激活函数的全连接层。遗忘门生成一个在零到一之间的值的向量,用来控制单元状态中多少元素被遗忘。遗忘门的值为一时,表示单元状态中的值被保留。遗忘门的值为零时,表示单元状态中的值被遗忘。
我们将前一步的隐藏状态和新的输入沿列轴拼接成一个矩阵。单元状态本质上会存储输入提供的长期信息,以及层内存储的隐藏状态。
LSTM 层中的第二个门是输入门。输入门控制有多少新数据会被存储在单元状态中。新数据是前一步的隐藏状态和当前时间步的输入的结合,正如以下图示所示:
我们将使用以下公式来确定更新门的值:
就像遗忘门一样,输入门也被建模为 LSTM 层内的一个嵌套全连接层。你可以看到输入门作为前面图示中突出部分的左分支。输入门在以下公式中用于确定要放入单元状态的新的值:
为了更新单元状态,我们还需要一步,下一张图将突出显示这一点:
一旦我们知道遗忘门和输入门的值,就可以使用以下公式计算更新后的单元状态:
首先,我们将遗忘门与上一个单元状态相乘,以遗忘旧的信息。然后,我们将更新门与新值的单元状态相乘,以学习新信息。我们将两个值相加,生成当前时间步的最终单元状态。
LSTM 层中的最后一个门是输出门。这个门控制着从单元状态中有多少信息被用于层的输出和下一个时间步的隐藏状态,具体如下面的示意图所示:
输出门使用以下公式计算:
输出门就像输入门和遗忘门一样,是一个密集层,控制有多少单元状态被复制到输出中。我们现在可以使用以下公式计算层的新隐藏状态或输出:
你可以使用这个新的隐藏状态来计算下一个时间步,或者将其作为层的输出返回。
何时使用其他递归层类型
GRU 和 LSTM 层的复杂度明显高于常规递归层。它们有更多需要训练的参数。这会使得当你遇到问题时,调试模型变得更加困难。
常规递归层在处理较长的数据序列时表现不好,因为它会很快饱和。你可以使用 LSTM 和 GRU 来解决这个问题。GRU 层不需要额外的记忆状态,而 LSTM 使用单元状态来模拟长期记忆。
由于 GRU 的门较少且没有记忆,它的训练时间较短。因此,如果你需要处理较长的序列并且需要一个相对较快的训练网络,使用 GRU 层。
LSTM 层在表达你输入序列中的关系方面有更强的能力。这意味着,如果你有足够的数据来训练它,它的表现会更好。最终,还是需要通过实验来确定哪种层类型最适合你的解决方案。
使用 CNTK 构建递归神经网络
现在我们已经探讨了递归神经网络背后的理论,接下来就该用 CNTK 构建一个递归神经网络了。CNTK 提供了多个构建块用于构建递归神经网络。我们将探索如何使用包含太阳能板功率测量的示例数据集来构建递归神经网络。
太阳能板的功率输出在一天中会发生变化,因此很难预测一个典型家庭能生成多少电力。这使得当地能源公司很难预测他们应该生成多少额外电力以跟上需求。
幸运的是,许多能源公司提供软件,允许客户跟踪太阳能板的功率输出。这将使他们能够基于这些历史数据训练模型,从而预测每天的总功率输出。
我们将使用递归神经网络来训练一个功率输出预测模型,数据集由微软提供,作为 CNTK 文档的一部分。
数据集包含每天多个测量值,每个时间戳下包含当前的功率输出以及截至该时间戳的总功率。我们的目标是根据当天收集的测量数据,预测当天的总功率输出。
你可以使用常规的神经网络,但这意味着我们必须将每个收集的测量值转化为输入特征。这样做假设测量值之间没有相关性。然而,实际上是有的。每一个未来的测量值都依赖于之前的一个测量值。因此,能够进行时间推理的递归模型对于这种情况来说要实用得多。
在接下来的三个部分中,我们将探讨如何在 CNTK 中构建递归神经网络。之后,我们将研究如何使用太阳能板数据集中的数据来训练递归神经网络。最后,我们将了解如何用递归神经网络进行输出预测。
构建神经网络结构
在我们开始预测太阳能板的输出之前,我们需要构建一个递归神经网络。递归神经网络的构建方式与常规神经网络相同。以下是构建方法:
features = sequence.input_variable(1)
with default_options(initial_state = 0.1):
model = Sequential([
Fold(LSTM(15)),
Dense(1)
])(features)
target = input_variable(1, dynamic_axes=model.dynamic_axes)
按照给定步骤操作:
-
首先,创建一个新的输入变量来存储输入序列。
-
然后,初始化神经网络的 default_options,并将 initial_state 设置为 0.1。
-
接下来,创建一个神经网络的 Sequential 层集。
-
在 Sequential 层集里,提供一个带有 15 个神经元的 LSTM 递归层,并将其包装在一个 Fold 层中。
-
最后,添加一个包含一个神经元的 Dense 层。
在 CNTK 中,你可以用两种方式来建模递归神经网络。如果你只关心递归层的最终输出,可以使用Fold层与递归层(例如 GRU、LSTM,甚至是 RNNStep)结合使用。Fold层会收集递归层的最终隐藏状态,并将其作为输出返回,供下一个层使用。
作为Fold层的替代方案,你也可以使用Recurrence模块。这个封装器会返回递归层生成的完整序列。这在你希望用递归神经网络生成序列输出时非常有用。
递归神经网络处理的是顺序输入,这就是为什么我们使用sequence.input_variable函数,而不是常规的input_variable函数。
常规的input_variable函数仅支持固定维度的输入。这意味着我们必须知道每个样本要输入网络的特征数量。这适用于常规模型和处理图像的模型。在图像分类模型中,我们通常使用一个维度表示颜色通道,另外两个维度表示输入图像的宽度和高度。这些维度我们是事先知道的。常规input_variable函数中唯一动态的维度是批量维度。这个维度在你使用特定迷你批次大小设置训练模型时计算出来,进而得出批量维度的固定值。
在递归神经网络中,我们不知道每个序列的长度。我们只知道每个时间步中存储的数据的形状。sequence.input_variable函数允许我们为每个时间步提供维度,并保持模型序列长度的维度动态。与常规的input_variable函数一样,批量维度也是动态的。我们在开始训练时配置此维度,并设置特定的迷你批次大小。
CNTK 在处理序列数据方面独具特色。在像 TensorFlow 这样的框架中,你必须在开始训练之前,预先指定序列长度和批量的维度。由于必须使用固定大小的序列,因此你需要对比模型支持的最大序列长度短的序列添加填充。同时,如果序列较长,你需要截断它们。这会导致模型质量较低,因为你要求模型从序列中的空时间步学习信息。CNTK 对动态序列的处理非常好,因此在使用 CNTK 处理序列时,你不必使用填充。
堆叠多个递归层
在上一部分中,我们只讨论了使用单一的递归层。然而,你可以在 CNTK 中堆叠多个递归层。例如,当我们想堆叠两个递归层时,需要使用以下层的组合:
from cntk import sequence, default_options, input_variable
from cntk.layers import Recurrence, LSTM, Dropout, Dense, Sequential, Fold, Recurrence
features = sequence.input_variable(1)
with default_options(initial_state = 0.1):
model = Sequential([
Recurrence(LSTM(15)),
Fold(LSTM(15)),
Dense(1)
])(features)
请按照以下步骤操作:
-
首先,从
cntk包中导入sequence模块、default_options函数和input_variable函数。 -
接下来,导入递归神经网络的相关层。
-
然后,创建一个新的具有 15 个神经元的
LSTM层,并将其包装在Recurrence层中,以便该层返回一个序列,而不是单一的输出。 -
现在,创建第二个具有 15 个神经元的
LSTM层,但这次将其包装在Fold层中,仅返回最后一个时间步的输出。 -
最后,使用特征变量调用创建的
Sequential层堆栈,以完成神经网络的构建。
这种技术同样可以扩展到超过两层的情况;只需在最后的递归层之前将层包装在Recurrence层中,并将最后一层包装在Fold层中。
对于本章中的示例,我们将只使用一个循环层,正如我们在前一节中构建神经网络结构时所做的那样。在下一节中,我们将讨论如何训练我们创建的循环神经网络。
使用时间序列数据训练神经网络
现在我们有了一个模型,让我们来看看如何在 CNTK 中训练一个循环神经网络。
首先,我们需要定义我们想要优化的损失函数。由于我们在预测一个连续变量——功率输出——我们需要使用均方误差损失函数。我们将把损失函数与均方误差度量标准结合,以衡量模型的表现。请记住,来自第四章,验证模型性能,我们可以使用@Function将损失函数和度量标准结合成一个函数对象:
@Function
def criterion_factory(z, t):
loss = squared_error(z, t)
metric = squared_error(z, t)
return loss, metric
loss = criterion_factory(model, target)
learner = adam(model.parameters, lr=0.005, momentum=0.9)
我们将使用adam学习器来优化模型。这个学习器是随机梯度下降(SGD)算法的扩展。虽然 SGD 使用固定的学习率,但 Adam 会随着时间的推移调整学习率。在开始时,它会使用较高的学习率来快速得到结果。一段时间后,它会开始降低学习率,以提高准确性。adam优化器在优化loss函数时比 SGD 更快。
现在我们有了损失函数和度量标准,我们可以使用内存中的数据和内存外的数据来训练循环神经网络。
循环神经网络的数据需要建模为序列。在我们的例子中,输入数据是每天的功率测量序列,存储在CNTK 文本格式(CTF)文件中。请按照给定的步骤操作:
在第三章,将数据输入神经网络,我们讨论了如何将数据以 CTF 格式存储用于 CNTK 训练。CTF 文件格式不仅支持存储基本样本,还支持存储序列。一个用于序列的 CTF 文件具有以下结构:
<sequence_id> |<input_name> <values> |<input_name> <values>
每一行都以一个独特的编号为前缀,以标识该序列。CNTK 将把具有相同序列标识符的行视为一个序列。所以,你可以将一个序列跨多行存储。每一行可以包含序列中的一个时间步。
在将序列跨多行存储到 CTF 文件时,有一个重要的细节需要记住。存储序列的某一行还应该包含该序列的预期输出。让我们来看一下这在实际操作中的表现:
0 |target 0.837696335078534 |features 0.756544502617801
0 |features 0.7931937172774869
0 |features 0.8167539267015707
0 |features 0.8324607329842932
0 |features 0.837696335078534
0 |features 0.837696335078534
0 |features 0.837696335078534
1 |target 0.4239092495636999 |features 0.24554973821989529
1 |features 0.24554973821989529
1 |features 0.00017225130534296885
1 |features 0.0014886562154347149
1 |features 0.005673647442829338
1 |features 0.01481675392670157
序列的第一行包含target变量,以及序列中第一时间步的数据。target变量用于存储特定序列的预期功率输出。对于同一序列的其他行,只包含features变量。如果你将target变量放在单独的行中,则无法使用输入文件,迷你批次源将无法加载。
你可以像这样将序列数据加载到你的训练代码中:
def create_datasource(filename, sweeps=INFINITELY_REPEAT):
target_stream = StreamDef(field='target', shape=1, is_sparse=False)
features_stream = StreamDef(field='features', shape=1, is_sparse=False)
deserializer = CTFDeserializer(filename, StreamDefs(features=features_stream, target=target_stream))
datasource = MinibatchSource(deserializer, randomize=True, max_sweeps=sweeps)
return datasource
train_datasource = create_datasource('solar_train.ctf')
test_datasource = create_datasource('solar_val.ctf', sweeps=1)
按照给定的步骤:
-
首先,创建一个新函数
create_datasource,它有两个参数:filename和sweeps,其中sweeps的默认值为 INFINITELY_REPEAT,以便我们可以多次迭代相同的数据集。 -
在
create_datasource函数中,为小批量数据源定义两个流,一个用于输入特征,一个用于模型的期望输出。 -
然后使用
CTFDeserializer来读取输入文件。 -
最后,返回一个新的
MinibatchSource,用于提供的输入文件。
为了训练模型,我们需要多次迭代相同的数据以训练多个周期。这就是为什么你应该为小批量数据源使用无限制的max_sweeps设置。测试是通过迭代一组验证样本完成的,所以我们配置小批量数据源时只需要进行一次迭代。
让我们用提供的数据源来训练神经网络,如下所示:
progress_writer = ProgressPrinter(0)
test_config = TestConfig(test_datasource)
input_map = {
features: train_datasource.streams.features,
target: train_datasource.streams.target
}
history = loss.train(
train_datasource,
epoch_size=EPOCH_SIZE,
parameter_learners=[learner],
model_inputs_to_streams=input_map,
callbacks=[progress_writer, test_config],
minibatch_size=BATCH_SIZE,
max_epochs=EPOCHS)
按照给定的步骤:
-
首先,初始化一个
ProgressPrinter来记录训练过程的输出。 -
然后,创建一个新的测试配置,使用来自
test_datasource的数据来验证神经网络。 -
接下来,创建一个映射,将神经网络的输入变量与训练数据源中的流进行关联。
-
最后,在损失函数上调用训练方法以启动训练过程。为它提供
train_datasource、设置、学习器、input_map以及用于记录和测试的回调函数。
由于模型需要训练很长时间,所以在你计划在机器上运行示例代码时,可以准备一到两杯咖啡。
train方法将在屏幕上输出指标和损失值,因为我们将ProgressPrinter作为回调传递给train方法。输出将类似于如下:
average since average since examples
loss last metric last
------------------------------------------------------
Learning rate per minibatch: 0.005
0.66 0.66 0.66 0.66 19
0.637 0.626 0.637 0.626 59
0.699 0.752 0.699 0.752 129
0.676 0.656 0.676 0.656 275
0.622 0.573 0.622 0.573 580
0.577 0.531 0.577 0.531 1150
作为一种良好的实践,你应该使用单独的测试集来验证你的模型。这就是我们之前创建test_datasource函数的原因。要使用这些数据来验证你的模型,你可以将TestConfig对象作为回调传递给train方法。测试逻辑将在训练过程完成后自动调用。
预测输出
当模型最终完成训练后,你可以使用一些样本序列进行测试,这些样本可以在本章的示例代码中找到。记住,CNTK 模型是一个函数,所以你可以使用一个代表你想要预测总输出的序列的 numpy 数组来调用它,如下所示:
import pickle
NORMALIZE = 19100
with open('test_samples.pkl', 'rb') as test_file:
test_samples = pickle.load(test_file)
model(test_samples) * NORMALIZE
按照给定的步骤:
-
首先,导入 pickle 包。
-
接下来,定义设置以规范化数据。
-
之后,打开
test_samples.pkl文件以进行读取。 -
文件打开后,使用 pickle.load 函数加载其内容。
-
最后,将样本通过网络运行,并用 NORMALIZE 常数乘以它们,以获得太阳能电池板的预测输出。
模型输出的值介于零和一之间,因为这正是我们在原始数据集中存储的值。这些值表示太阳能电池板功率输出的规范化版本。我们需要将它们乘以我们用来规范化原始测量值的规范化值,才能得到太阳能电池板的实际功率输出。
模型的最终反规范化输出如下所示:
array([[ 8161.595],
[16710.596],
[13220.489],
...,
[10979.5 ],
[15410.741],
[16656.523]], dtype=float32)
使用递归神经网络进行预测与使用任何其他 CNTK 模型进行预测非常相似,区别在于您需要提供的是序列而不是单一样本。
总结
在本章中,我们探讨了如何使用递归神经网络根据时间序列数据进行预测。递归神经网络在处理财务数据、物联网数据或任何其他随时间收集的信息的场景中非常有用。
递归神经网络的一个重要构建模块是Fold和Recurrence层类型,您可以将它们与任何递归层类型(如 RNNStep、GRU 或 LSTM)结合使用,以构建递归层集。根据您是要预测序列还是单一值,您可以使用Recurrence或Fold层类型来包装递归层。
当您训练递归神经网络时,可以利用存储在 CTF 文件格式中的序列数据,使训练模型变得更容易。但是,您同样可以使用存储为 numpy 数组的序列数据,只要您使用正确的序列输入变量与递归层进行组合。
使用递归神经网络进行预测和使用常规神经网络一样简单。唯一的区别是输入数据格式,和训练时一样,都是一个序列。
在下一章,我们将探讨本书的最后一个主题:将模型部署到生产环境。我们将探讨如何在 C#或 Java 中使用您构建的 CNTK 模型,以及如何使用 Azure 机器学习服务等工具正确管理实验。
第七章:将模型部署到生产环境
在本书的前几章中,我们已经在开发、测试和使用各种深度学习模型方面提高了技能。我们没有过多讨论深度学习在软件工程更广泛背景中的作用。在这一章中,我们将利用这段时间讨论持续交付,以及机器学习在这一背景中的作用。然后,我们将探讨如何以持续交付的思维方式将模型部署到生产环境。最后,我们将讨论如何使用 Azure 机器学习服务来有效管理你开发的模型。
本章将覆盖以下主题:
-
在 DevOps 环境中使用机器学习
-
存储模型
-
使用 Azure 机器学习服务来管理模型
技术要求
我们假设你已在电脑上安装了最新版的 Anaconda,并按照第一章中的步骤,开始使用 CNTK,将 CNTK 安装在你的电脑上。本章的示例代码可以在我们的 GitHub 仓库中找到: github.com/PacktPublishing/Deep-Learning-with-Microsoft-Cognitive-Toolkit-Quick-Start-Guide/tree/master/ch7。
在本章中,我们将处理几个存储在 Jupyter 笔记本中的示例。要访问示例代码,请在你下载代码的目录中,打开 Anaconda 提示符并运行以下命令:
cd ch7
jupyter notebook
本章还包含一个 C# 代码示例,用于演示如何加载开源的 ONNX 格式模型。如果你想运行 C# 代码,你需要在机器上安装 .NET Core 2.2。你可以从以下网址下载最新版本的 .NET Core:dotnet.microsoft.com/download。
查看以下视频,查看代码的实际效果:
在 DevOps 环境中使用机器学习
大多数现代软件开发都以敏捷的方式进行,在一个开发者和 IT 专业人员共同参与的环境中进行。我们所构建的软件通常通过持续集成和持续部署管道部署到生产环境中。我们如何在这种现代环境中集成机器学习?这是否意味着当我们开始构建 AI 解决方案时,我们必须做出很多改变?这些是当你将 AI 和机器学习引入工作流程时,常见的一些问题。
幸运的是,你不需要改变整个构建环境或部署工具栈,就可以将机器学习集成到你的软件中。我们将讨论的大部分内容都可以很好地融入你现有的环境中。
让我们来看一个典型的持续交付场景,这是你在常规敏捷软件项目中可能会遇到的:
如果你曾在 DevOps 环境中工作过,这个概述会让你感觉很熟悉。它从源代码管理开始,连接到持续集成管道。持续集成管道会生成可以部署到生产环境的工件。这些工件通常会被存储在某个地方,以便备份和回滚。这些工件仓库与一个发布管道相连接,发布管道将软件部署到测试、验收,最后到生产环境。
你不需要改变太多的标准设置就能将机器学习集成到其中。然而,开始使用机器学习时,有几个关键点是必须正确处理的。我们将重点讨论四个阶段,并探索如何扩展标准的持续交付设置:
-
如何跟踪你用于机器学习的数据。
-
在持续集成管道中训练模型。
-
将模型部署到生产环境。
-
收集生产反馈
跟踪你的数据
让我们从机器学习的起点开始:用于训练模型的数据。获取好的机器学习数据是非常困难的。几乎 80%的工作将会是数据管理和数据处理。如果每次训练模型时都不得不重做所有工作,那会非常令人沮丧。
这就是为什么拥有某种形式的数据管理系统非常重要的原因。这可以是一个中央服务器,用于存储你知道适合用来训练模型的数据集。如果你有超过几 GB 的数据,它也可以是一个数据仓库。一些公司选择使用像 Hadoop 或 Azure Data Lake 这样的工具来管理他们的数据。无论你使用什么,最重要的是保持数据集的干净,并且以适合训练的格式存储。
要为你的解决方案创建数据管道,你可以使用传统的提取 转换 加载(ETL)工具,如 SQL Server 集成服务,或者你可以在 Python 中编写自定义脚本,并将其作为 Jenkins、Azure DevOps 或 Team Foundation Server 中专用持续集成管道的一部分执行。
数据管道将是你从各种业务来源收集数据的工具,并处理它,以获得足够质量的数据集,作为你模型的主数据集存储。需要注意的是,虽然你可以在不同的模型间重用数据集,但最好不要一开始就以此为目标。你会很快发现,当你尝试将数据集用于太多不同的使用场景时,主数据集会变得脏乱且难以管理。
在持续集成管道中训练模型
一旦你有了基本的数据管道,接下来就是将 AI 模型的训练集成到持续集成环境中的时候了。到目前为止,我们只使用了 Python 笔记本来创建我们的模型。可惜的是,Python 笔记本在生产环境中并不好部署。你不能在构建过程中自动运行它们。
在持续交付环境中,你仍然可以使用 Python 笔记本进行初步实验,以便发现数据中的模式并构建模型的初始版本。一旦你有了候选模型,就必须将代码从笔记本迁移到一个正式的 Python 程序中。
你可以将 Python 训练代码作为持续集成管道的一部分来运行。例如,如果你使用 Azure DevOps、Team Foundation Server 或 Jenkins,你已经拥有了运行训练代码作为持续集成管道的所有工具。
我们建议将训练代码作为与其他软件独立的管道运行。训练深度学习模型通常需要很长时间,你不希望将构建基础设施锁定在这上面。通常,你会看到人们为他们的机器学习模型构建训练管道,使用专用的虚拟机,甚至专用硬件,因为训练模型需要大量的计算能力。
持续集成管道将基于你通过数据管道生成的数据集生成模型。就像代码一样,你也应该为你的模型和用于训练它们的设置进行版本控制。
跟踪你的模型和用于训练它们的设置非常重要,因为这可以让你在生产环境中尝试同一模型的不同版本,并收集反馈。保持已训练模型的备份还可以帮助你在灾难发生后迅速恢复生产,例如生产服务器崩溃。
由于模型是二进制文件,且可能非常大,最好将模型视为二进制工件,就像 .NET 中的 NuGet 包或 Java 中的 Maven 工件一样。
像 Nexus 或 Artifactory 这样的工具非常适合存储模型。在 Nexus 或 Artifactory 中发布你的模型只需要几行代码,并且能节省你数百小时的重新训练模型的工作。
将模型部署到生产环境
一旦你有了模型,你需要能够将其部署到生产环境。如果你将模型存储在诸如 Artifactory 或 Nexus 的仓库中,这将变得更加容易。你可以像创建持续集成管道一样创建专门的发布管道。在 Azure DevOps 和 Team Foundation Server 中,有一个专用的功能来实现这一点。在 Jenkins 中,你可以使用单独的管道将模型部署到服务器。
在发布管道中,你可以从工件仓库中下载模型并将其部署到生产环境。有两种主要的机器学习模型部署方法:你可以将其作为应用程序的额外文件进行部署,或者将其作为一个专用的服务组件进行部署。
如果你将模型作为应用程序的一部分进行部署,通常只会将模型存储在你的工件仓库中。现在,模型变成了一个额外的工件,需要在现有的发布管道中下载,并部署到你的解决方案中。
如果你为你的模型部署一个专用的服务组件,你通常会将模型、使用该模型进行预测的脚本以及模型所需的其他文件存储在工件仓库中,并将其部署到生产环境中。
收集模型反馈
在生产环境中使用深度学习或机器学习模型时,有一个最后需要记住的重要点。你用某个数据集训练了这些模型,你希望这个数据集能很好地代表生产环境中真实发生的情况。但实际情况可能并非如此,因为随着你构建模型,周围的世界也在变化。
这就是为什么向用户征求反馈并根据反馈更新模型非常重要的原因。尽管这不是持续部署环境的正式组成部分,但如果你希望你的机器学习解决方案成功运行,正确设置这一点仍然是非常重要的。
设置反馈循环并不需要非常复杂。例如,当你为欺诈检测分类交易时,你可以通过让员工验证模型的输出结果来设置反馈循环。然后,你可以将员工的验证结果与被分类的输入一起存储。通过这样做,你确保模型不会错误地指控客户欺诈,同时帮助你收集新的观察数据以改进模型。稍后,当你想要改进模型时,你可以使用新收集的观察数据来扩展你的训练集。
存储模型
为了能够将你的模型部署到生产环境中,你需要能够将训练好的模型存储到磁盘上。CNTK 提供了两种在磁盘上存储模型的方法。你可以存储检查点以便稍后继续训练,或者你可以存储一个便携版的模型。这两种存储方法各有其用途。
存储模型检查点以便稍后继续训练
一些模型训练时间较长,有时甚至需要几周时间。你不希望在训练过程中机器崩溃或者停电时丢失所有进度。
这时,检查点功能就变得非常有用。你可以在训练过程中使用CheckpointConfig对象创建一个检查点。你可以通过以下方式修改回调列表,添加此额外的回调到你的训练代码中:
checkpoint_config = CheckpointConfig('solar.dnn', frequency=100, restore=True, preserve_all=False)
history = loss.train(
train_datasource,
epoch_size=EPOCH_SIZE,
parameter_learners=[learner],
model_inputs_to_streams=input_map,
callbacks=[progress_writer, test_config, checkpoint_config],
minibatch_size=BATCH_SIZE,
max_epochs=EPOCHS)
按照以下步骤操作:
-
首先,创建一个新的
CheckpointConfig,并为检查点模型文件提供文件名,设置在创建新检查点之前的小批量数量作为frequency,并将preserve_all设置为False。 -
接下来,使用
loss上的 train 方法,并在callbacks关键字参数中提供checkpoint_config以使用检查点功能。
当你在训练过程中使用检查点时,你会在磁盘上看到额外的文件,名为 solar.dnn 和 solar.dnn.ckp。solar.dnn 文件包含以二进制格式存储的训练模型。solar.dnn.ckp 文件包含在训练过程中使用的小批量源的检查点信息。
当你将 CheckpointConfig 对象的 restore 参数设置为 True 时,最近的检查点会自动恢复给你。这使得在训练代码中集成检查点变得非常简单。
拥有一个检查点模型不仅在训练过程中遇到计算机问题时很有用。如果你在从生产环境收集到额外数据后希望继续训练,检查点也会派上用场。你只需要恢复最新的检查点,然后从那里开始将新的样本输入到模型中。
存储可移植的模型以供其他应用使用
尽管你可以在生产环境中使用检查点模型,但这样做并不聪明。检查点模型以 CNTK 只能理解的格式存储。现在,使用二进制格式是可以的,因为 CNTK 仍然存在,且模型格式将在相当长一段时间内保持兼容。但和所有软件一样,CNTK 并不是为了永恒存在而设计的。
这正是 ONNX 被发明的原因。ONNX 是开放的神经网络交换格式。当你使用 ONNX 时,你将模型存储为 protobuf 兼容的格式,这种格式被许多其他框架所理解。甚至还有针对 Java 和 C# 的原生 ONNX 运行时,这使得你可以在 .NET 或 Java 应用程序中使用 CNTK 创建的模型。
ONNX 得到了许多大型公司的支持,如 Facebook、Intel、NVIDIA、Microsoft、AMD、IBM 和惠普。这些公司中的一些提供了 ONNX 转换器,而另一些甚至支持在其硬件上直接运行 ONNX 模型,而无需使用额外的软件。NVIDIA 目前有多款芯片可以直接读取 ONNX 文件并执行这些模型。
作为示例,我们将首先探索如何将模型存储为 ONNX 格式,并使用 C# 从磁盘加载它来进行预测。首先,我们将看看如何保存一个模型为 ONNX 格式,之后再探索如何加载 ONNX 模型。
存储 ONNX 格式的模型
要将模型存储为 ONNX 格式,你可以在 model 函数上使用 save 方法。当你不提供额外参数时,它将以用于检查点存储的相同格式存储模型。不过,你可以提供额外的参数来指定模型格式,如下所示:
from cntk import ModelFormat
model.save('solar.onnx', format=ModelFormat.ONNX)
按照以下步骤操作:
-
首先,从
cntk包中导入ModelFormat枚举。 -
接下来,在训练好的模型上调用
save方法,指定输出文件名,并将ModelFormat.ONNX作为format关键字参数。
在 C# 中使用 ONNX 模型
一旦模型存储在磁盘上,我们可以使用 C# 加载并使用它。CNTK 版本 2.6 包含了一个相当完整的 C# API,你可以用它来训练和评估模型。
要在 C# 中使用 CNTK 模型,你需要使用一个名为CNTK.GPU或CNTK.CPUOnly的库,它们可以通过 NuGet(.NET 的包管理器)获取。CNTK 的 CPU-only 版本包含了已编译的 CNTK 二进制文件,用于在 CPU 上运行模型,而 GPU 版本则既可以使用 GPU,也可以使用 CPU。
使用 C# 加载 CNTK 模型是通过以下代码片段实现的:
var deviceDescriptor = DeviceDescriptor.CPUDevice;
var function = Function.Load("model.onnx", deviceDescriptor, ModelFormat.ONNX);
按照给定的步骤操作:
-
首先,创建一个设备描述符,以便模型在 CPU 上执行。
-
接下来,使用
Function.Load方法加载先前存储的模型。提供deviceDescriptor,并使用ModelFormat.ONNX将文件加载为 ONNX 模型。
现在我们已经加载了模型,接下来让我们用它进行预测。为此,我们需要编写另一个代码片段:
public IList<float> Predict(float petalWidth, float petalLength, float sepalWidth, float sepalLength)
{
var features = _modelFunction.Inputs[0];
var output = _modelFunction.Outputs[0];
var inputMapping = new Dictionary<Variable, Value>();
var outputMapping = new Dictionary<Variable, Value>();
var batch = Value.CreateBatch(
features.Shape,
new float[] { sepalLength, sepalWidth, petalLength, petalWidth },
_deviceDescriptor);
inputMapping.Add(features, batch);
outputMapping.Add(output, null);
_modelFunction.Evaluate(inputMapping, outputMapping, _deviceDescriptor);
var outputValues = outputMapping[output].GetDenseData<float>(output);
return outputValues[0];
}
按照给定的步骤操作:
-
创建一个新的
Predict方法,该方法接受模型的输入特征。 -
在
Predict方法中,将模型的输入和输出变量存储在两个独立的变量中,方便访问。 -
接下来,创建一个字典,将数据映射到模型的输入和输出变量。
-
然后,创建一个新的批次,包含一个样本,作为模型的输入特征。
-
向输入映射添加一个新条目,将批次映射到输入变量。
-
接下来,向输出映射添加一个新条目,映射到输出变量。
-
现在,使用输入、输出映射和设备描述符在加载的模型上调用
Evaluate方法。 -
最后,从输出映射中提取输出变量并检索数据。
本章的示例代码包含一个基本的 .NET Core C# 项目,演示了如何在 .NET Core 项目中使用 CNTK。你可以在本章的代码示例目录中的 csharp-client 文件夹找到示例代码。
使用存储为 ONNX 格式的模型,可以让你用 Python 训练模型,使用 C# 或其他语言在生产环境中运行模型。这尤其有用,因为像 C# 这样的语言的运行时性能通常比 Python 更好。
在下一节中,我们将介绍如何使用 Azure 机器学习服务来管理训练和存储模型的过程,从而让我们能够更加有条理地处理模型。
使用 Azure 机器学习服务来管理模型
虽然你可以完全手动构建一个持续集成管道,但这仍然是相当费力的工作。你需要购买专用硬件来运行深度学习训练任务,这可能会带来更高的成本。不过,云端有很好的替代方案。Google 提供了 TensorFlow 服务,Microsoft 提供了 Azure 机器学习服务来管理模型。两者都是非常出色的工具,我们强烈推荐。
让我们看看 Azure 机器学习服务,当你希望设置一个完整的机器学习管道时,它可以为你做些什么:
Azure 机器学习服务是一个云服务,提供每个机器学习项目阶段的完整解决方案。它具有实验和运行的概念,允许你管理实验。它还提供模型注册功能,可以存储已训练的模型和这些模型的 Docker 镜像。你可以使用 Azure 机器学习服务工具将这些模型快速部署到生产环境中。
部署 Azure 机器学习服务
要使用此服务,你需要在 Azure 上拥有一个有效账户。如果你还没有账户,可以访问:azure.microsoft.com/en-gb/free/,使用试用账户。这将为你提供一个免费账户,有效期为 12 个月,附带价值 150 美元的信用额度,可以探索各种 Azure 服务。
部署 Azure 机器学习服务有很多种方式。你可以通过门户创建一个新实例,也可以使用云 Shell 创建服务实例。让我们看看如何通过门户创建一个新的 Azure 机器学习服务实例。
使用你最喜欢的浏览器,导航到以下 URL:portal.azure.com/。使用你的凭据登录,你将看到一个门户,展示所有可用的 Azure 资源和一个类似于以下截图的仪表板:
Azure 资源和仪表板
通过此门户,你可以创建新的 Azure 资源,例如 Azure 机器学习工作区。在屏幕左上角点击大号的 + 按钮开始操作。这将显示以下页面,允许你创建一个新的资源:
创建新资源
你可以在此搜索框中搜索不同类型的资源。搜索 Azure 机器学习并从列表中选择 Azure 机器学习工作区资源类型。这将显示以下详细信息面板,允许你启动创建向导:
开始创建向导
该详细信息面板将解释该资源的功能,并指向文档及有关此资源的其他重要信息,例如定价详情。要创建此资源类型的新实例,请点击创建按钮。这将启动创建 Azure 机器学习工作区实例的向导,如下所示:
创建一个新的 Azure 机器学习工作区实例
在创建向导中,您可以配置工作区的名称、它所属的资源组以及它应该创建的数据中心。Azure 资源作为资源组的一部分创建。这些资源组有助于组织您的资源,并将相关的基础设施集中在一个地方。如果您想删除一组资源,可以直接删除资源组,而不需要单独删除每个资源。如果您完成机器学习工作区的测试后想要删除所有内容,这个功能尤其有用。
使用专用的资源组来创建机器学习工作区是一个好主意,因为它将包含多个资源。如果与其他资源混合使用,将使得在完成后清理资源或需要移动资源时变得更加困难。
一旦点击屏幕底部的创建按钮,机器学习工作区就会被创建。这需要几分钟时间。在后台,Azure 资源管理器将根据创建向导中的选择创建多个资源。部署完成后,您将在门户中收到通知。
创建机器学习工作区后,您可以通过门户进行导航。首先,在屏幕左侧的导航栏中进入资源组。接下来,点击您刚刚创建的资源组,以查看机器学习工作区及其相关资源的概况,如下图所示:
了解机器学习工作区和相关资源概况
工作区本身包括一个仪表板,您可以通过它来探索实验并管理机器学习解决方案的某些方面。工作区还包括一个 Docker 注册表,用于存储模型作为 Docker 镜像,以及用于使用模型进行预测所需的脚本。当您在 Azure 门户查看工作区时,您还会找到一个存储帐户,您可以使用它来存储数据集和实验生成的数据。
Azure 机器学习服务环境中的一个亮点是包含了一个应用程序洞察(Application Insights)实例。您可以使用应用程序洞察来监控生产环境中的模型,并收集宝贵的反馈以改进模型。这是默认包含的,因此您不需要为机器学习解决方案手动创建监控解决方案。
探索机器学习工作区
Azure 机器学习工作区包含多个元素。让我们来探索一下这些元素,以便了解当您开始使用工作区时可以使用哪些功能:
机器学习工作区
要进入机器学习工作区,点击屏幕左侧导航栏中的资源组项目。选择包含机器学习工作区项目的资源组,并点击机器学习工作区。它将显示你在创建向导中之前配置的名称。
在工作区中,有一个专门用于实验的部分。这个部分将提供对你在工作区中运行的实验以及作为实验一部分执行的运行的详细信息。
机器学习工作区的另一个有用部分是模型部分。当你训练了一个模型时,你可以将它存储在模型注册表中,以便以后将其部署到生产环境。模型会自动连接到生成它的实验运行,因此你总是可以追溯到使用了什么代码来生成模型,以及训练模型时使用了哪些设置。
模型部分下方是镜像部分。这个部分显示了你从模型创建的 Docker 镜像。你可以将模型与评分脚本一起打包成 Docker 镜像,以便更轻松、可预测地部署到生产环境。
最后,部署部分包含了所有基于镜像的部署。你可以使用 Azure 机器学习服务将模型部署到生产环境,使用单个容器实例、虚拟机,或者如果需要扩展模型部署,还可以使用 Kubernetes 集群。
Azure 机器学习服务还提供了一种技术,允许你构建一个管道,用于准备数据、训练模型并将其部署到生产环境。如果你想构建一个包含预处理步骤和训练步骤的单一过程,这个功能将非常有用。特别是在需要执行多个步骤才能获得训练模型的情况下,它尤其强大。现在,我们将限制自己进行基本的实验并将结果模型部署到生产 Docker 容器实例中。
运行你的第一个实验
现在你已经有了工作区,我们来看看如何在 Python 笔记本中使用它。我们将修改一些深度学习代码,以便将训练后的模型作为实验的输出保存到 Azure 机器学习服务的工作区,并跟踪模型的指标。
首先,我们需要安装azureml包,方法如下:
pip install --upgrade azureml-sdk[notebooks]
azureml包包含了运行实验所需的组件。为了使其工作,你需要在机器学习项目的根目录下创建一个名为config.json的文件。如果你正在使用本章的示例代码,你可以修改azure-ml-service文件夹中的config.json文件。它包含以下内容:
{
"workspace_name": "<workspace name>",
"resource_group": "<resource group>",
"subscription_id": "<your subscription id>"
}
这个文件包含了你的 Python 代码将使用的工作区、包含该工作区的资源组,以及创建工作区的订阅。工作区名称应与之前在向导中创建工作区时选择的名称匹配。资源组应与包含该工作区的资源组匹配。最后,你需要找到订阅 ID。
当你在门户中导航到机器学习工作区的资源组时,你会看到资源组详情面板顶部显示了订阅 ID,如下图所示:
资源组详情面板顶部的订阅 ID
当你将鼠标悬停在订阅 ID 的值上时,门户会显示一个按钮,允许你将该值复制到剪贴板。将此值粘贴到配置文件中的 subscriptionId 字段,并保存。你现在可以通过以下代码片段从任何 Python 笔记本或 Python 程序连接到你的工作区:
from azureml.core import Workspace, Experiment
ws = Workspace.from_config()
experiment = Experiment(name='classify-flowers', workspace=ws)
按照给定的步骤操作:
-
首先,我们基于刚才创建的配置文件创建一个新的工作区。这将连接到 Azure 中的工作区。一旦连接成功,你可以创建一个新的实验,并为它选择一个名称,然后将其连接到工作区。
-
接下来,创建一个新的实验并将其连接到工作区。
在 Azure 机器学习服务中,实验可用于跟踪你正在使用 CNTK 测试的架构。例如,你可以为卷积神经网络创建一个实验,然后再创建一个实验来尝试使用递归神经网络解决相同的问题。
让我们探索如何跟踪实验中的度量和其他输出。我们将使用前几章的鸢尾花分类模型,并扩展训练逻辑以跟踪度量,具体如下:
from cntk import default_options, input_variable
from cntk.layers import Dense, Sequential
from cntk.ops import log_softmax, sigmoid
model = Sequential([
Dense(4, activation=sigmoid),
Dense(3, activation=log_softmax)
])
features = input_variable(4)
z = model(features)
按照给定的步骤操作:
-
首先,导入
default_options和input_variable函数。 -
接下来,从
cntk.layers模块导入模型所需的层类型。 -
然后,从
cntk.ops模块导入log_softmax和sigmoid激活函数。 -
创建一个新的
Sequential层集。 -
向
Sequential层集添加一个新的Dense层,包含 4 个神经元和sigmoid激活函数。 -
添加另一个具有 3 个输出的
Dense层,并使用log_softmax激活函数。 -
创建一个新的
input_variable,大小为 4。 -
使用
features变量调用模型以完成模型。
为了训练模型,我们将使用手动的小批量循环。首先,我们需要加载并预处理鸢尾花数据集,以便它与我们的模型所期望的格式匹配,如下方的代码片段所示:
import pandas as pd
import numpy as np
df_source = pd.read_csv('iris.csv',
names=['sepal_length', 'sepal_width','petal_length','petal_width', 'species'],
index_col=False)
X = df_source.iloc[:, :4].values
y = df_source['species'].values
按照给定的步骤操作:
-
导入
pandas和numpy模块以加载包含训练样本的 CSV 文件。 -
使用 read_csv 函数加载包含训练数据的输入文件。
-
接下来,提取前 4 列作为输入特征。
-
最后,提取物种列作为标签。
标签是以字符串形式存储的,因此我们需要将它们转换为一组独热向量,以便与模型匹配,具体如下:
label_mapping = {
'Iris-setosa': 0,
'Iris-versicolor': 1,
'Iris-virginica': 2
}
def one_hot(index, length):
result = np.zeros(length)
result[index] = 1.
y = [one_hot(label_mapping[v], 3) for v in y]
按照以下步骤操作:
-
创建一个标签到其数值表示的映射。
-
接下来,定义一个新的工具函数
one_hot,将数字值编码为独热向量。 -
最后,使用 Python 列表推导式遍历标签集合中的值,并将它们转换为独热编码向量。
我们需要执行一步操作来准备数据集进行训练。为了能够验证模型是否已正确优化,我们希望创建一个保留集,并在该集上进行测试:
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X,y, test_size=0.2, stratify=y)
使用train_test_split方法,创建一个包含 20%训练样本的小型保留集。使用stratify关键字并提供标签,以平衡拆分。
一旦我们准备好了数据,就可以专注于训练模型。首先,我们需要设置一个loss函数、learner和trainer,如下所示:
from cntk.losses import cross_entropy_with_softmax
from cntk.metrics import classification_error
from cntk.learners import sgd
from cntk.train.trainer import Trainer
label = input_variable(3)
loss = cross_entropy_with_softmax(z, label)
error_rate = classification_error(z, label)
learner = sgd(z.parameters, 0.001)
trainer = Trainer(z, (loss, error_rate), [learner])
-
从
cntk.losses模块导入 cross_entropy_with_softmax 函数。 -
接下来,从
cnkt.metrics模块导入 classification_error 函数。 -
然后,从
cntk.learners模块导入sgd学习器。 -
创建一个新的
input_variable,形状为 3,用于存储标签。 -
接下来,创建一个新的 cross_entropy_with_softmax 损失实例,并为其提供模型变量
z和label变量。 -
然后,使用 classification_error 函数创建一个新的指标,并为其提供网络和
label变量。 -
现在,使用网络的参数初始化
sgd学习器,并将其学习率设置为 0.001。 -
最后,使用网络、
loss、metric和learner初始化Trainer。
通常,我们可以直接在loss函数上使用train方法来优化模型中的参数。然而,这一次,我们希望对训练过程进行控制,以便能够注入逻辑以跟踪 Azure 机器学习工作区中的指标,如以下代码片段所示:
import os
from cntk import ModelFormat
with experiment.start_logging() as run:
for _ in range(10):
trainer.train_minibatch({ features: X_train, label: y_train })
run.log('average_loss', trainer.previous_minibatch_loss_average)
run.log('average_metric', trainer.previous_minibatch_evaluation_average)
test_metric = trainer.test_minibatch( {features: X_test, label: y_test })
按照以下步骤操作:
-
要开始一个新的训练,调用实验的
start_logging方法。这将创建一个新的run。在该运行的范围内,我们可以执行训练逻辑。 -
创建一个新的 for 循环进行 10 个训练周期。
-
在 for 循环内,调用
trainer上的train_minibatch方法进行模型训练。为其提供输入变量与用于训练的数据之间的映射。 -
在此之后,使用来自训练器对象的
previous_minibatch_loss_average值记录average_loss指标。 -
除了平均损失外,还使用训练器对象的
previous_minibatch_evaluation_average属性在运行中记录平均指标。
一旦我们训练完模型,就可以使用test_minibatch方法在测试集上执行测试。该方法返回我们之前创建的metric函数的输出。我们也会将其记录到机器学习工作区中。
运行允许我们跟踪与模型单次训练会话相关的数据。我们可以使用 run 对象上的 log 方法记录度量数据。该方法接受度量的名称和度量的值。你可以使用此方法记录诸如 loss 函数的输出,以监控模型如何收敛到最佳参数集。
还可以记录其他内容,例如用于训练模型的 epoch 数量、程序中使用的随机种子以及其他有用的设置,以便日后复现实验。
在机器学习工作区的实验标签下,导航到实验时,运行期间记录的度量数据会自动显示在门户上,如下图所示。
在机器学习工作区的实验标签下导航到实验
除了 log 方法,还有一个 upload_file 方法用于上传训练过程中生成的文件,示例如下代码片段。你可以使用这个方法存储训练完成后保存的模型文件:
z.save('outputs/model.onnx') # The z variable is the trained model
run.upload_file('model.onnx', 'outputs/model.onnx')
upload_file 方法需要文件的名称(在工作区中可以找到)和文件的本地路径。请注意文件的位置。由于 Azure 机器学习工作区的限制,它只会从输出文件夹中提取文件。这个限制未来可能会被解除。
确保在运行的作用域内执行 upload_file 方法,这样 AzureML 库就会将模型链接到你的实验运行,从而使其可追溯。
在将文件上传到工作区后,你可以在门户中通过运行的输出部分找到它。要查看运行详情,请打开 Azure Portal 中的机器学习工作区,导航到实验,然后选择你想查看详情的运行,如下所示:
选择运行
最后,当你完成运行并希望发布模型时,可以按照以下步骤将其注册到模型注册表:
stored_model = run.register_model(model_name='classify_flowers', model_path='model.onnx')
register_model 方法将模型存储在模型注册表中,以便你可以将其部署到生产环境。当模型先前存储在注册表中时,它将自动作为新版本存储。现在你可以随时回到之前的版本,如下所示:
作为新版本存储的模型
你可以通过前往 Azure Portal 上的机器学习工作区,并在工作区导航菜单中的模型项下找到模型注册表。
模型会自动与实验运行相关联,因此你总能找到用于训练模型的设置。这一点很重要,因为它增加了你能够复现结果的可能性,如果你需要这样做的话。
我们将实验限制在本地运行。如果你愿意,可以使用 Azure Machine Learning 在专用硬件上运行实验。你可以在 Azure Machine Learning 文档网站上阅读更多相关信息:docs.microsoft.com/en-us/azure/machine-learning/service/how-to-set-up-training-targets。
一旦完成实验运行,你就可以将训练好的模型部署到生产环境。在下一部分,我们将探讨如何执行此操作。
将模型部署到生产环境
Azure Machine Learning 的一个有趣功能是其附带的部署工具。该部署工具允许你从模型注册表中提取模型,并将其部署到生产环境中。
在将模型部署到生产环境之前,你需要有一个包含模型和评分脚本的镜像。该镜像是一个包含 Web 服务器的 Docker 镜像,当收到请求时,它会调用评分脚本。评分脚本接受 JSON 格式的输入,并利用它通过模型进行预测。我们针对鸢尾花分类模型的评分脚本如下所示:
import os
import json
import numpy as np
from azureml.core.model import Model
import onnxruntime
model = None
def init():
global model
model_path = Model.get_model_path('classify_flowers')
model = onnxruntime.InferenceSession(model_path)
def run(raw_data):
data = json.loads(raw_data)
data = np.array(data).astype(np.float32)
input_name = model.get_inputs()[0].name
output_name = model.get_outputs()[0].name
prediction = model.run([output_name], { input_name: data})
# Select the first output from the ONNX model.
# Then select the first row from the returned numpy array.
prediction = prediction[0][0]
return json.dumps({'scores': prediction.tolist() })
按照给定步骤操作:
-
首先,导入构建脚本所需的组件。
-
接着,定义一个全局模型变量,用于存储加载的模型。
-
之后,定义 init 函数来初始化脚本中的模型。
-
在 init 函数中,使用
Model.get_model_path函数检索模型的路径。这将自动定位 Docker 镜像中的模型文件。 -
接下来,通过初始化
onnxruntime.InferenceSession类的新实例来加载模型。 -
定义另一个函数
run,该函数接受一个参数raw_data。 -
在
run函数中,将raw_data变量的内容从 JSON 转换为 Python 数组。 -
接着,将
data数组转换为 Numpy 数组,这样我们就可以用它来进行预测。 -
然后,使用加载的模型的
run方法,并将输入特征提供给它。包括一个字典,告诉 ONNX 运行时如何将输入数据映射到模型的输入变量。 -
模型返回一个包含 1 个元素的输出数组,用于模型的输出。该输出包含一行数据。从输出数组中选择第一个元素,再从选中的输出变量中选择第一行,并将其存储在
prediction变量中。 -
最后,返回预测结果作为 JSON 对象。
Azure Machine Learning 服务将自动包含你为特定模型注册的任何模型文件,当你创建容器镜像时。因此,get_model_path也可以在已部署的镜像中使用,并解析为容器中托管模型和评分脚本的目录。
现在我们有了评分脚本,接下来让我们创建一个镜像,并将该镜像部署为云中的 Web 服务。要部署 Web 服务,你可以明确地创建一个镜像,或者你可以让 Azure 机器学习服务根据你提供的配置自动创建一个,方法如下:
from azureml.core.image import ContainerImage
image_config = ContainerImage.image_configuration(
execution_script="score.py",
runtime="python",
conda_file="conda_env.yml")
按照以下步骤操作:
-
首先,从
azureml.core.image模块导入 ContainerImage 类。 -
接下来,使用
ContainerImage.image_configuration方法创建一个新的镜像配置。为其提供 score.py 作为execution_script参数,Pythonruntime,并最终提供 conda_env.yml 作为镜像的conda_file。
我们将容器镜像配置为使用 Python 作为运行时。我们还配置了一个特殊的 Anaconda 环境文件,以便可以像以下这样配置 CNTK 等自定义模块:
name: project_environment
dependencies:
# The python interpreter version.
# Currently Azure ML only supports 3.5.2 and later.
- python=3.6.2
- pip:
# Required packages for AzureML execution, history, and data preparation.
- azureml-defaults
- onnxruntime
按照以下步骤操作:
-
首先,为环境命名。这个步骤是可选的,但在你从此文件本地创建环境进行测试时会很有用。
-
接下来,为你的评分脚本提供 Python 版本 3.6.2。
-
最后,将一个包含
azureml-default和onnxruntime的子列表添加到 pip 依赖列表中。
azureml-default包包含了在 docker 容器镜像中处理实验和模型所需的所有内容。它还包括像 Numpy 和 Pandas 这样的标准包,便于安装。onnxruntime包是必须的,因为我们需要在使用的评分脚本中加载模型。
部署已训练的模型作为 Web 服务还需要一步操作。我们需要设置 Web 服务配置并将模型作为服务部署。机器学习服务支持部署到虚拟机、Kubernetes 集群和 Azure 容器实例,这些都是在云中运行的基本 Docker 容器。以下是如何将模型部署到 Azure 容器实例:
from azureml.core.webservice import AciWebservice, Webservice
aciconfig = AciWebservice.deploy_configuration(cpu_cores=1, memory_gb=1)
service = Webservice.deploy_from_model(workspace=ws,
name='classify-flowers-svc',
deployment_config=aciconfig,
models=[stored_model],
image_config=image_config)
按照以下步骤操作:
-
首先,从
azureml.core.webservice模块导入 AciWebservice 和 Webservice 类。 -
然后,使用 AziWebservice 类上的
deploy_configuration方法创建一个新的AciWebservice配置。为其提供一组资源限制,包括 1 个 CPU 和 1GB 内存。 -
当你为 Web 服务配置完成后,调用
deploy_from_model,使用要部署的工作区、服务名称以及要部署的模型,将已注册的模型部署到生产环境。提供你之前创建的镜像配置。
一旦容器镜像创建完成,它将作为容器实例部署到 Azure。这将为你的机器学习工作区在资源组中创建一个新资源。
一旦新服务启动,你将在 Azure 门户的机器学习工作区中看到新的部署,如下图所示:
在你的机器学习工作区的 Azure 门户上进行新部署
部署包括一个评分 URL,您可以从应用程序中调用该 URL 来使用模型。由于您使用的是 REST 来调用模型,因此您与它在后台运行 CNTK 的事实是隔离的。您还可以使用任何您能想到的编程语言,只要它能够执行 HTTP 请求。
例如,在 Python 中,我们可以使用 requests 包作为基本的 REST 客户端,通过您刚创建的服务进行预测。首先,让我们安装 requests 模块,如下所示:
pip install --upgrade requests
安装了 requests 包后,我们可以编写一个小脚本,通过以下方式对已部署的服务执行请求:
import requests
import json
service_url = "<service-url>"
data = [[1.4, 0.2, 4.9, 3.0]]
response = requests.post(service_url, json=data)
print(response.json())
按照给定的步骤操作:
-
首先,导入 requests 和 json 包。
-
接下来,为 service_url 创建一个新变量,并用 Web 服务的 URL 填充它。
-
然后,创建另一个变量来存储您想要进行预测的数据。
-
之后,使用 requests.post 函数将数据发布到已部署的服务并存储响应。
-
最后,读取响应中返回的 JSON 数据以获取预测值。
可以通过以下步骤获取 service_url:
-
首先,导航到包含机器学习工作区的资源组。
-
然后,选择工作区并在详细信息面板的左侧选择部署部分。
-
选择您想查看详细信息的部署,并从详细信息页面复制 URL。
选择部署
当您运行刚创建的脚本时,您将收到包含输入样本预测类别的响应。输出将类似于以下内容:
{"scores": [-2.27234148979187, -2.486853837966919, -0.20609207451343536]}
总结
在本章中,我们了解了将深度学习和机器学习模型投入生产所需的条件。我们探讨了一些基本原理,这些原理将帮助您在持续交付环境中成功实施深度学习。
我们已经查看了将模型导出到 ONNX 的方法,借助 ONNX 格式的可移植性,使得将训练好的模型部署到生产环境并保持多年运行变得更加容易。接着,我们探讨了如何使用 CNTK API 在其他语言中,如 C#,来进行预测。
最后,我们探讨了如何使用 Azure 机器学习服务,通过实验管理、模型管理和部署工具提升您的 DevOps 体验。尽管您不需要像这样的工具就能入门,但在您计划将更大的项目投入生产时,拥有像 Azure 机器学习服务这样的工具,确实能够为您提供很大帮助。
本章标志着本书的结束。在第一章中,我们开始探索了 CNTK。接着,我们学习了如何构建模型、为模型提供数据并评估其性能。基础内容讲解完毕后,我们研究了两个有趣的应用案例,分别涉及图像和时间序列数据。最后,我们介绍了如何将模型投入生产。现在,你应该已经掌握了足够的信息,可以开始使用 CNTK 构建自己的模型了!