微软认知工具包深度学习快速启动指南-全-

微软认知工具包深度学习快速启动指南(全)

原文:annas-archive.org/md5/2a17578844fd3b16114c38533fdf2462

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

人工智能 (AI) 的出现是为了通过自动化我们日常工作中的一些任务来增强人类,使我们能够更多地发挥潜力。我们一直在使用软件程序作为工具来自动化许多较简单的任务。现在是时候挑战自动化更复杂的任务了。

AI 领域正在发生很多变化,比以往任何时候更多的人都希望通过新技术扩展他们的工具包,使软件更加智能化。机器学习,尤其是深度学习,是我们可以增强计算机现有功能的重要工具。

本书旨在帮助您掌握最流行的深度学习工具之一,即 CNTK。我们将探讨这个相对年轻的开源深度学习框架提供了什么。在本书结束时,您将对框架有扎实的理解,并了解可以使用它的一些场景。

本书的受众

本书适合有一定 Java、C# 或 Python 经验的开发人员。我们假设您对机器学习还比较新手。不过,这本书也适合之前已经使用过其他深度学习框架并希望学习另一个优秀深度学习工具的人。

本书内容概述

第一章,CNTK 入门,向您介绍了 CNTK 框架和深度学习的世界。它解释了如何在计算机上安装工具以及如何在 CNTK 中使用 GPU。

第二章,使用 CNTK 构建神经网络,解释了如何使用 CNTK 构建您的第一个神经网络。我们深入探讨基本构建模块,并看看如何用 CNTK 训练神经网络。

第三章,将数据导入您的神经网络,展示了不同的方法来加载用于训练神经网络的数据。您将学习如何处理小数据集和不适合计算机内存的数据集。

第四章,验证模型性能,教您如何使用指标来验证神经网络的性能。您将学习如何验证回归模型和分类模型,并在调试神经网络时要注意什么。

第五章,处理图像,解释了如何使用卷积神经网络对图像进行分类。我们将展示处理空间顺序数据所需的基本组件。我们还将展示一些最著名的用于处理图像的神经网络架构。

第六章,处理时间序列数据,教你如何使用递归神经网络来构建能够进行时间推理的模型。我们将解释构建和验证递归神经网络所需的各种基本模块,基于一个物联网示例。

第七章,部署模型到生产环境,向你展示了将深度学习模型部署到生产环境所需的步骤。我们将探讨一个 DevOps 环境,其中包含 持续集成/持续部署CI/CD)管道,教你在敏捷工程环境中如何训练和部署模型。我们还将向你展示如何使用 Azure 机器学习服务等工具,将你的机器学习工作推向新的高度。

为了最大限度地发挥本书的价值

我们建议你具备 Python 3 的使用经验,这样你就能了解语法的样子。你需要在一台内存和 CPU 性能较好的机器上运行 Linux 或 Windows,因为本书中的示例在较旧的机器上可能需要很长时间才能运行。如果你的机器恰好配备了 NVIDIA 的游戏显卡,我们强烈建议查看如何安装 CNTK 的 GPU 版本的说明,因为这样可以显著加快示例的运行速度。本书中的某些部分假设你对 Java 或 C# 有一定了解。虽然这不是必需的,但了解这些语言的基本语法会很有帮助。

下载示例代码文件

你可以从你的账户在 www.packt.com 下载本书的示例代码文件。如果你在其他地方购买了本书,可以访问 www.packt.com/support 并注册,以便将文件直接发送到你的邮箱。

你可以通过以下步骤下载代码文件:

  1. www.packt.com 上登录或注册。

  2. 选择“SUPPORT”标签。

  3. 点击“代码下载与勘误”。

  4. 在搜索框中输入书名并按照屏幕上的指示操作。

下载文件后,请确保使用最新版本的以下工具解压或提取文件夹:

  • WinRAR/7-Zip for Windows

  • Zipeg/iZip/UnRarX for Mac

  • 7-Zip/PeaZip for Linux

本书的代码包也托管在 GitHub 上,网址是 github.com/PacktPublishing/Deep-Learning-with-Microsoft-Cognitive-Toolkit-Quick-Start-Guide。如果代码有更新,将会在现有的 GitHub 仓库中更新。

我们还提供来自我们丰富书籍和视频目录中的其他代码包,欢迎访问 github.com/PacktPublishing/ 查看!

代码实战

访问以下链接查看运行代码的视频:

bit.ly/2UcIfSe

使用的约定

本书中使用了许多文本约定。

CodeInText:表示文本中的代码词、数据库表名、文件夹名称、文件名、文件扩展名、路径名、虚拟网址、用户输入和 Twitter 用户名。以下是一个示例:“我们使用StreamDef类来实现这个目的。”

一段代码块如下设置:

from cntk.layers import Dense
from cntk import input_variable

features = input_variable(50)
layer = Dense(50)(features)

任何命令行输入或输出如下所示:

cd ch2
jupyter notebook

粗体:表示一个新术语、一个重要词汇或您在屏幕上看到的词汇。例如,菜单或对话框中的词汇在文本中是这样的显示方式。以下是一个示例:“要创建此资源类型的新实例,请单击创建按钮。”

警告或重要提示如下所示。

小贴士和技巧如上所示。

联系我们

我们欢迎读者的反馈。

一般反馈:如果您对本书的任何部分有疑问,请在邮件主题中提及书名,并通过[email protected]联系我们。

勘误表:尽管我们已尽最大努力确保内容的准确性,但错误仍然会发生。如果您发现本书中的错误,欢迎向我们报告。请访问www.packt.com/submit-errata,选择您的书籍,点击“勘误提交表格”链接,并输入详细信息。

盗版:如果您在互联网上遇到任何非法复制的我们的作品,我们将非常感激您提供其位置地址或网站名称。请通过[email protected]与我们联系,并附上该材料的链接。

如果您有兴趣成为作者:如果您在某个领域拥有专业知识,并且有兴趣撰写或参与编写书籍,请访问authors.packtpub.com

评论

请留下评论。一旦您阅读并使用了本书,为什么不在购买平台上留下评论呢?潜在读者可以看到并参考您的公正意见来做出购买决策,Packt 也能了解您对我们产品的看法,我们的作者也能看到您对其书籍的反馈。谢谢!

有关 Packt 的更多信息,请访问packt.com

第一章:开始使用 CNTK

深度学习是一种机器学习技术,正受到公众和研究人员的广泛关注。在本章中,我们将探讨深度学习是什么,以及大公司如何使用它来解决复杂问题。我们将看看是什么使得这种技术如此激动人心,以及推动深度学习的概念是什么。

然后我们将讨论微软认知工具包CNTK),它是什么,以及它在深度学习的更大框架中的作用。我们还将讨论 CNTK 与其他框架相比有什么独特之处。

在本章中,我们还将向你展示如何在你的计算机上安装 CNTK。我们将探讨如何在 Windows 和 Linux 上进行安装。如果你有兼容的显卡,你还需要查看如何配置显卡以便与 CNTK 一起使用,因为这将显著加速训练深度学习模型所需的计算。

在本章中,我们将覆盖以下主题:

  • 人工智能、机器学习和深度学习之间的关系

  • 深度学习是如何工作的?

  • 什么是 CNTK?

  • 安装 CNTK

人工智能、机器学习和深度学习之间的关系

为了理解深度学习是什么,我们必须探索人工智能AI)是什么以及它与机器学习和深度学习的关系。从概念上讲,深度学习是机器学习的一种形式,而机器学习是人工智能的一种形式:

在计算机科学中,人工智能是一种由机器表现出的智能形式。AI 是 20 世纪 50 年代计算机科学研究人员发明的术语。人工智能包括一大类算法,它们表现出的行为比我们为计算机构建的标准软件更智能。

一些算法表现出智能行为,但不能自我改进。一类算法,叫做机器学习算法,可以从你展示的样本数据中学习,并生成模型,然后你可以在类似数据上使用这些模型进行预测。

在机器学习算法的群体中,有一个子类别叫做深度学习算法。这个算法组使用的模型受到人类或动物大脑的结构和功能的启发。

机器学习和深度学习都可以从你提供的样本数据中学习。当我们构建常规程序时,我们通过使用不同的语言构造(如 if 语句、循环和函数)编写业务规则。这些规则是固定的。在机器学习中,我们将样本和期望的答案输入到算法中,算法随后学习将样本与期望答案连接的规则:

机器学习中有两个主要组成部分:机器学习模型和机器学习算法。

当你使用机器学习构建程序时,首先选择一个机器学习模型。机器学习模型是一个包含可训练参数的数学方程,能够将输入转化为预测的答案。这个模型塑造了计算机将要学习的规则。例如:预测汽车的每加仑英里数需要以某种方式建模现实。判断信用卡交易是否欺诈需要一个不同的模型。

输入的表示可以是汽车的属性转化为向量。模型的输出可以是汽车的每加仑英里数。对于信用卡欺诈的情况,输入可以是用户账户的属性和交易信息。输出表示可以是一个介于 0 和 1 之间的分数,其中接近 1 的值表示该交易应该被拒绝。

机器学习模型中的数学变换由一组需要训练的参数控制,只有当这些参数经过训练,变换才能产生正确的输出表示。

这时,第二部分——机器学习算法就派上用场了。为了找到机器学习模型中参数的最佳值,我们需要执行一个多步骤的过程:

  1. 初始时,计算机会为模型中的每个未知参数选择一个随机值。

  2. 然后,模型会使用样本数据进行初步预测。

  3. 这个预测会被输入到loss函数中,与预期的输出一起,以获取关于模型表现的反馈。

  4. 这个反馈然后被机器学习算法用来为模型中的参数找到更好的值。

这些步骤会重复多次,以找到模型中参数的最佳值。如果一切顺利,你最终会得到一个能够对许多复杂情况做出准确预测的模型。

我们能够从示例中学习规则这一事实是一个有用的概念。许多情况下,我们无法使用简单的规则来解决特定问题。例如:信用卡欺诈案件的形式各异。有时,黑客会慢慢地渗透系统,逐步注入小型的攻击,然后盗取资金。其他时候,黑客则会在一次攻击中试图盗取大量资金。基于规则的程序会变得非常难以维护,因为它需要包含大量的代码来处理所有不同的欺诈案件。机器学习是一种优雅的解决方案。它可以在不需要大量代码的情况下理解如何处理不同类型的信用卡欺诈案件。它还能够在合理的范围内对之前未见过的案件做出判断。

机器学习的局限性

机器学习模型非常强大。在许多规则性程序无法解决的场景中,你可以使用它们。机器学习是当你遇到无法用常规规则程序解决的问题时,一个很好的替代方案。然而,机器学习模型也有其局限性。

机器学习模型中的数学变换非常基础。例如:当你想要判断一次信用交易是否应该被标记为欺诈时,你可以使用线性模型。逻辑回归模型是这种用例的一个很好的选择;它创建了一个决策边界函数,将欺诈案例和非欺诈案例分开。大多数欺诈案例将位于边界线的上方,并被正确标记为欺诈。但是没有任何机器学习模型是完美的,正如你在下图中看到的那样,某些案例将无法被模型正确标记为欺诈。

如果你的数据恰好是完全线性可分的,模型会正确分类所有的案例。但当你需要处理更复杂的数据时,基础的机器学习模型就会显得力不从心。而且,机器学习的局限性还有更多原因:

  • 许多算法假设输入特征之间没有交互作用。

  • 机器学习在许多情况下是基于线性算法的,这些算法对非线性处理得并不好。

  • 通常情况下,你需要处理大量的特征,经典的机器学习算法在处理高维输入数据时会面临更大的困难。

深度学习是如何工作的?

机器学习中的局限性促使科学家们寻找其他方法来构建更复杂的模型,使其能够处理非线性关系以及输入之间有大量交互的情况。这导致了人工神经网络的发明。

人工神经网络是由多层人工神经元组成的图结构。它的灵感来源于人类和动物的大脑结构和功能。

要了解深度学习的强大之处以及如何使用 CNTK 构建神经网络,我们需要了解神经网络是如何工作的,以及它是如何被训练来识别你输入的样本中的模式。

神经网络架构

神经网络由不同的层组成。每一层包含多个神经元。

一个典型的神经网络由多个人工神经元的层组成。神经网络的第一层被称为输入层,这是我们将输入数据输入神经网络的地方。神经网络的最后一层被称为输出层,这是神经网络输出变换后数据的地方。神经网络的输出代表了网络做出的预测。

在神经网络的输入层和输出层之间,您可以找到一个或多个隐藏层。在输入和输出之间的层被称为隐藏层,因为我们通常不观察数据通过这些层。

神经网络是数学构造。通过神经网络传递的数据被编码为浮点数。这意味着您想要处理的所有内容都必须被编码为浮点数向量。

人工神经元

神经网络的核心是人工神经元。人工神经元是神经网络中最小的单元,我们可以训练它来识别数据中的模式。神经网络内的每个人工神经元都有一个或多个输入。每个向量输入都有一个权重:

图像来源:https://commons.wikimedia.org/wiki/File:Artificial_neural_network.png

神经网络内部的人工神经元工作方式与此类似,但不使用化学信号。神经网络内的每个人工神经元都有一个或多个输入。每个向量输入都有一个权重。

为神经元的每个输入提供的数字被这个权重相乘。然后将此乘积的输出相加以产生神经元的总激活值。

然后,这个激活信号被传递给一个激活函数。激活函数对这个信号进行非线性变换。例如:它使用修正线性函数处理输入信号:

修正线性函数将负激活信号转换为零,但在正数时执行一个恒等(通过)变换。

另一个流行的激活函数是sigmoid函数。它与修正线性函数略有不同,它将负值转换为 0,正值转换为 1。然而,在-0.5 到+0.5 之间,信号以线性方式转换。

人工神经元中的激活函数在神经网络中起着重要作用。正是因为这些非线性变换函数,神经网络才能处理数据中的非线性关系。

使用神经网络预测输出

通过将神经元层组合在一起,我们创建一个堆叠函数,具有非线性变换和可训练权重,因此它可以学习识别复杂的关系。为了可视化这一点,让我们将前面章节中的神经网络转换为数学公式。首先,让我们看一下单层的公式:

X变量是一个向量,表示神经网络中某一层的输入。w参数表示输入向量X中每个元素的权重向量。在许多神经网络实现中,会添加一个额外的项b,这被称为偏置,它基本上用来增加或减少激活神经元所需的输入量。最后,还有一个函数f,它是该层的激活函数。

现在你已经看到了单层的公式,让我们组合更多层,构造出神经网络的公式:

注意观察公式是如何变化的。现在我们有了第一个层的公式,这个公式被包装在另一个layer函数中。当我们向神经网络添加更多层时,这种函数的包装或堆叠会继续进行。每一层都会引入更多需要优化的参数,以训练神经网络。它还使得神经网络能够从我们输入的数据中学习到更复杂的关系。

要使用神经网络进行预测,我们需要填充神经网络中的所有参数。假设我们知道这些参数,因为之前已经训练过它了。剩下的就是神经网络的输入值。

输入是一个浮动数值向量,它表示神经网络的输入。输出是一个向量,形成神经网络预测输出的表示。

优化神经网络

我们已经讨论了如何使用神经网络进行预测,但还没有讨论如何优化神经网络中的参数。接下来让我们逐一介绍神经网络中的各个组成部分,并探讨它们在训练过程中如何协同工作:

神经网络由多个相互连接的层组成。每一层都有一组我们希望优化的可训练参数。优化神经网络使用一种叫做反向传播(backpropagation)的方法。我们的目标是通过逐步优化前述图中w1w2w3参数的值,来最小化损失函数的输出。

神经网络的loss函数可以有多种形式。通常,我们选择一个表达期望输出Y与神经网络实际输出之间差异的函数。例如:我们可以使用以下loss函数:

首先,神经网络会被初始化。我们可以使用模型中所有参数的随机值来实现这一点。

在初始化神经网络后,我们将数据输入神经网络进行预测。接着,我们将预测结果与期望输出一起输入loss函数,以衡量模型与我们期望的结果有多接近。

loss函数的反馈被用来馈送给优化器。优化器使用一种叫做梯度下降的技术来找出如何优化每个参数。

梯度下降是神经网络优化的一个关键成分,它之所以有效,是因为loss函数的一个有趣特性。当你可视化神经网络中一组输入对应的loss函数输出,并使用不同的参数值时,你最终得到的图像类似于这个:

在反向传播过程的开始,我们从这座山地的某个坡道上的位置出发。我们的目标是沿着山坡走向一个点,在那里参数的值达到了最佳状态。这就是loss函数的输出被尽可能最小化的点。

为了找到下坡的路径,我们需要找到一个函数,它表示当前山坡上的坡度。我们通过从loss函数派生出一个导数函数来实现这一点。这个导数函数为模型中的参数提供了梯度。

当我们执行一次反向传播过程时,我们会使用参数的梯度沿着山坡向下走一步。我们可以通过将梯度加到参数上来实现这一点。但这种沿着坡道下山的方法是危险的。因为如果我们走得太快,可能会错过最优点。因此,所有神经网络优化器都有一个叫做学习率的设置。学习率控制了下降的速率。

由于我们只能在梯度下降算法中采取小步伐,我们需要多次重复这个过程,才能达到神经网络参数的最优值。

什么是 CNTK?

从头开始构建神经网络是一个巨大的工程——除非你在寻求编程挑战,否则我不建议任何人从这个开始。有一些很棒的库可以帮助你构建神经网络,而无需完全理解数学公式。

Microsoft Cognitive ToolkitCNTK)是一个开源库,包含构建神经网络所需的所有基本构件。

CNTK 是用 C++和 Python 实现的,但它也可以在 C#和 Java 中使用。训练只能在 C++或 Python 中进行,但你可以在训练神经网络之后,轻松地在 C#或 Java 中加载模型并进行预测。

还有一个 CNTK 的变体,使用一种叫做 BrainScript 的专有语言。但在本书中,我们将仅关注 Python 来介绍该框架的基本功能。稍后,在第七章《将模型部署到生产环境》中,我们将讨论如何使用 C#或 Java 加载和使用训练好的模型。

CNTK 的特点

CNTK 是一个同时具有低级和高级 API 的库,用于构建神经网络。低级 API 旨在为科学家提供构建下一代神经网络组件的工具,而高级 API 则是用于构建生产级神经网络。

在这些基本构建块之上,CNTK 提供了一组组件,使得将数据输入到神经网络中变得更加容易。它还包含了各种组件来监控和调试神经网络。

最后,CNTK 还提供 C#和 Java API。你可以使用这两种语言加载已训练的模型,并在你的 Web 应用程序、微服务,甚至是 Windows Store 应用中进行预测。此外,如果你愿意,也可以使用 C#来训练模型。

尽管可以从 Java 和 C#使用 CNTK,但重要的是要知道,目前 CNTK 的 C#和 Java API 并未完全支持 Python 版本中的所有功能。例如:在 Python 中为目标检测训练的模型,在 CNTK 2.6 版本的 C#中无法使用。

高速低级 API

在 CNTK 的核心,你会找到一个低级 API,它包含一组数学运算符,用于构建神经网络组件。低级 API 还包括自动求导功能,帮助优化神经网络中的参数。

微软在构建组件时考虑了高性能。例如:它包括了专门的代码来在图形处理单元(GPU)上训练神经网络。图形处理单元是专用处理器,能够以非常高的速度处理大量的向量和矩阵运算。你通常可以通过至少提高 10 倍的速度加速神经网络的训练过程。

用于快速创建神经网络的基本构建块

当你想要构建用于生产的神经网络时,通常使用高级 API。高级 API 包含构建神经网络所需的各种不同模块。

例如:有一个基本的密集层,可以用来构建最基本的神经网络。但你也会在高级 API 中找到更高级的层类型,例如处理图像或时间序列数据所需的层类型。

高级 API 还包含不同的优化器来训练神经网络,因此你不需要手动构建梯度下降优化器。在 CNTK 中,优化过程通过学习器和训练器实现,学习器定义使用哪种梯度下降算法,而训练器定义如何实现反向传播的基本过程。

在第二章,使用 CNTK 构建神经网络,我们将探索如何使用高级 API 来构建和训练神经网络。在第五章,处理图像,以及第六章,处理时间序列数据,你将学习如何使用一些更高级的层类型来处理图像和时间序列数据。

测量模型性能

一旦你构建了神经网络,你需要确保它能够正常工作。CNTK 提供了多个组件来衡量神经网络的性能。

你常常会寻找一些方法来监控模型训练过程的效果。CNTK 包括一些组件,这些组件可以从你的模型和相关的优化器中生成日志数据,供你监控训练过程。

加载和处理大规模数据集

使用深度学习时,你通常需要一个大型数据集来训练神经网络。使用数 GB 的数据来训练模型是很常见的。CNTK 包含了一套组件,允许你将数据馈送到神经网络中进行训练。

微软尽力构建了专用的读取器,这些读取器会将数据批量加载到内存中,这样你就不需要一 TB 的 RAM 来训练你的网络。我们将在第三章,将数据加载到你的神经网络中中更详细地讨论这些读取器。

从 C#和 Java 使用模型

主要的 CNTK 库是基于 Python 构建的,核心部分使用 C++实现。你可以使用 C++和 Python 来训练模型。当你想在生产环境中使用模型时,你有更多选择。你可以从 C++或 Python 使用训练好的模型,但大多数开发者会选择使用 Java 或 C#。在运行时性能上,Python 比这些语言要慢得多。此外,C#和 Java 在企业环境中使用更广泛。

你可以从 NuGet 或 Maven 中央库下载 C#和 Java 版本的 CNTK 作为独立库。在第七章,将模型部署到生产环境,我们将讨论如何从这些语言使用 CNTK 来在微服务环境中托管一个训练好的模型。

安装 CNTK

现在我们已经了解了神经网络是如何工作的,以及 CNTK 是什么,让我们来看一下如何在你的计算机上安装它。CNTK 支持 Windows 和 Linux 操作系统,因此我们将分别介绍这两种系统的安装方法。

在 Windows 上安装

我们将在 Windows 上使用 Anaconda 版的 Python 来运行 CNTK。Anaconda 是 Python 的再发行版,其中包括一些额外的包,比如SciPyscikit-learn,这些包被 CNTK 用来执行各种计算。

安装 Anaconda

你可以从 Anaconda 的官方网站下载 Anaconda:www.anaconda.com/download/

下载设置文件后,启动安装并按照指示安装 Anaconda。你可以在docs.anaconda.com/anaconda/install/找到安装说明。

Anaconda 将在你的计算机上安装多个工具。它将安装一个新的命令行提示符,该提示符会自动将所有 Anaconda 可执行文件添加到你的 PATH 变量中。你可以通过这个命令行快速管理 Python 环境、安装软件包,当然,还可以运行 Python 脚本。

可选地,你可以在安装 Anaconda 时一起安装 Visual Studio Code。Visual Studio Code 是一款类似于 Sublime 和 Atom 的代码编辑器,包含大量插件,可以帮助你在不同编程语言中编写程序代码,例如 Python。

CNTK 2.6 只支持 Python 3.6,这意味着并非所有版本的 Anaconda 都能正常工作。你可以通过 Anaconda 的存档网站 repo.continuum.io/archive/ 获取旧版本的 Anaconda。或者,如果你没有包含 Python 3.6 版本的 Anaconda,可以降级 Anaconda 中的 Python 版本。要在 Anaconda 环境中安装 Python 3.6,请打开新的 Anaconda 命令行并执行以下命令:

conda install python=3.6

升级 pip

Anaconda 附带的 Python 包管理器pip版本略显过时。这可能会导致我们在尝试安装CNTK包时出现问题。所以,在安装CNTK包之前,我们需要先升级pip可执行文件。

要升级pip可执行文件,打开 Anaconda 命令行并执行以下命令:

python -m pip install --upgrade pip

这将移除旧版的pip可执行文件,并安装一个新的版本来替代它。

安装 CNTK

有多种方式可以将CNTK包安装到你的计算机上。最常见的方式是通过pip可执行文件安装该包:

pip install cntk

这将从包管理器网站下载CNTK包并将其安装到你的机器上。pip会自动检查缺失的依赖项并一并安装。

有几种替代方法可以在你的计算机上安装 CNTK。官网上有一套详尽的文档,详细解释了其他安装方法:docs.microsoft.com/en-us/cognitive-toolkit/Setup-CNTK-on-your-machine

在 Linux 上安装

在 Linux 上安装 CNTK 与在 Windows 上有所不同。与 Windows 一样,我们将使用 Anaconda 来运行CNTK包。但在 Linux 上没有图形界面的 Anaconda 安装程序,而是一个基于终端的安装程序。该安装程序适用于大多数 Linux 发行版。我们将描述限制在 Ubuntu 这一广泛使用的 Linux 发行版上。

安装 Anaconda

在我们能够安装 Anaconda 之前,需要确保系统已完全更新。要检查这一点,请在终端中执行以下两个命令:

sudo apt update 
sudo apt upgrade

自动化工具APT)用于在 Ubuntu 中安装各种软件包。在代码示例中,我们首先让apt更新对各个软件包仓库的引用。然后我们让它安装最新的更新。

在计算机更新完成后,我们可以开始安装 Anaconda。首先,访问www.anaconda.com/download/以获取最新 Anaconda 安装文件的 URL。你可以右键点击下载链接并将 URL 复制到剪贴板。

现在打开一个终端窗口并执行以下命令:

wget -O anaconda-installer.sh url

确保将url占位符替换为你从 Anaconda 网站复制的 URL。按 Enter 执行命令。

一旦安装文件下载完成,你可以通过运行以下命令来安装 Anaconda:

sh ./anaconda-installer.sh

这将启动安装程序。按照屏幕上的说明将 Anaconda 安装到你的计算机上。默认情况下,Anaconda 会被安装在你主目录下名为anaconda3的文件夹中。

就像 CNTK 2.6 的 Windows 版本一样,它只支持 Python 3.6。你可以通过访问repo.continuum.io/archive/获取 Anaconda 的旧版本,或者通过在终端中执行以下命令来降级你的 Python 版本:

conda install python=3.6

升级 pip 到最新版本

一旦我们安装了 Anaconda,就需要将pip升级到最新版本。pip用于在 Python 中安装软件包。它是我们将用来安装 CNTK 的工具:

python -m pip install --upgrade pip

安装 CNTK 包

安装过程的最后一步是安装 CNTK。这是通过pip使用以下命令完成的:

pip install cntk

如果你愿意,也可以通过直接下载一个 wheel 文件或使用包含 Anaconda 的安装程序来安装 CNTK。你可以在docs.microsoft.com/en-us/cognitive-toolkit/Setup-CNTK-on-your-machine找到更多关于 CNTK 替代安装方法的信息。

使用你的 GPU 与 CNTK

我们已经讨论了如何安装适用于 CPU 的基本版本 CNTK。虽然 CNTK 包运行得很快,但在 GPU 上运行时会更快。不过,并非所有机器都支持这种配置,这就是为什么我把如何使用 GPU 的描述放在了单独的一节中。

在尝试安装 CNTK 以使用 GPU 之前,确保你拥有支持的显卡。目前,CNTK 支持至少支持 CUDA 3.0 的 NVIDIA 显卡。CUDA 是 NVIDIA 提供的编程 API,允许开发者在显卡上运行非图形程序。你可以在这个网站上检查你的显卡是否支持 CUDA:developer.nvidia.com/cuda-gpus

在 Windows 上启用 GPU 使用

要在 Windows 上使用显卡与 CNTK,你需要为你的显卡安装最新的 GeForce 或 Quadro 驱动程序(具体取决于你的显卡)。除了最新的驱动程序外,你还需要安装适用于 Windows 的 CUDA 工具包 9.0 版本。

你可以从 NVIDIA 网站下载 CUDA 工具包:developer.nvidia.com/cuda-90-download-archive?target_os=Windows&target_arch=x86_64。下载后,运行安装程序并按照屏幕上的说明操作。

CNTK 使用位于 CUDA 上方的一层,称为 cuDNN,用于神经网络特定的原语。你可以从 NVIDIA 网站下载 cuDNN 二进制文件,网址为 developer.nvidia.com/rdp/form/cudnn-download-survey。与 CUDA 工具包不同,下载 cuDNN 二进制文件之前,你需要在网站上注册一个账户。

并非所有的 cuDNN 二进制文件都与每个版本的 CUDA 兼容。网站上会注明每个版本的 cuDNN 与哪个版本的 CUDA 工具包兼容。对于 CUDA 9.0,你需要下载 cuDNN 7.4.1\。

下载 cuDNN 二进制文件后,将 zip 文件解压到你的 CUDA 工具包安装的根文件夹中。通常,CUDA 工具包位于 C:\program files\NVIDIA GPU Computing Toolkit\CUDA\v9.0

启用 CNTK 内 GPU 使用的最后一步是安装 CNTK-GPU 包。在 Windows 上打开 Anaconda 提示符并执行以下命令:

pip install cntk-gpu

在 Linux 上启用 GPU 使用

在 Linux 上使用显卡与 CNTK 需要运行 NVIDIA 的专有驱动程序。当你在 Linux 机器上安装 CUDA 工具包时,它会自动要求你安装适用于显卡的最新驱动程序。虽然你可以选择不通过 CUDA 工具包安装程序安装驱动程序,但我们强烈推荐你这样做,因为驱动程序将与 CUDA 工具包的二进制文件匹配,这可以减少安装失败或其他错误的风险。

你可以从 NVIDIA 网站下载 CUDA 工具包:developer.nvidia.com/cuda-90-download-archive?target_os=Linux&target_arch=x86_64&target_distro=Ubuntu&target_version=1604&target_type=runfilelocal

请确保选择合适的 Linux 发行版和版本。链接会自动选择 Ubuntu 16.04,并使用本地运行文件。

下载二进制文件到磁盘后,你可以通过打开终端并执行以下命令来运行安装程序:

sh cuda_9.0.176_384.81_linux-run

按照屏幕上的指示在你的机器上安装 CUDA 工具包。

安装了 CUDA 工具包后,你需要修改你的 Bash 配置文件。使用你喜欢的文本编辑器打开$HOME/.bashrc文件,并在脚本的末尾添加以下几行:

export PATH=/usr/local/cuda-9.0/bin${PATH:+:${PATH}}
export LD_LIBRARY_PATH=/usr/local/cuda-9.0/lib64\ 
 ${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}

第一行将 CUDA 二进制文件包含到 PATH 变量中,以便 CNTK 能够访问它们。脚本中的第二行将 CNTK 库包含到你的库路径中,以便 CNTK 在需要时能够加载它们。

保存文件的更改并关闭编辑器。请确保重新启动终端窗口,以确保新设置已生效。

最后一步是下载并安装 cuDNN 二进制文件。CNTK 使用一个位于 CUDA 之上的层,叫做 cuDNN,来处理神经网络特定的原语。你可以从 NVIDIA 网站下载 cuDNN 二进制文件,网址是:developer.nvidia.com/rdp/form/cudnn-download-survey。与 CUDA 工具包不同,你需要在该网站上注册一个账户,才能下载 cuDNN 二进制文件。

并非所有的 cuDNN 二进制文件都与每个版本的 CUDA 兼容。网站上会提到哪些版本的 cuDNN 与哪些版本的 CUDA 工具包兼容。对于 CUDA 9.0,你需要下载 cuDNN 7.4.1. 下载适用于 Linux 的版本,并使用以下命令将其提取到/usr/local/cuda-9.0文件夹中:

tar xvzf -C /usr/local/cuda-9.0/ cudnn-9.0-linux-x64-v7.4.1.5.tgz

文件名可能会有所不同;根据需要更改路径到相应的文件名。

总结

在本章中,我们学习了深度学习及其与机器学习和人工智能的关系。我们了解了深度学习背后的基本概念,以及如何使用梯度下降法训练神经网络。接着我们讨论了 CNTK,它是什么,以及该库提供了哪些功能来构建深度学习模型。最后,我们花了一些时间讨论了如何在 Windows 和 Linux 上安装 CNTK,并如何在需要时使用 GPU。

在下一章,我们将学习如何使用 CNTK 构建基本的神经网络,以便更好地理解本章中的概念如何在代码中实现。我们还将讨论如何在不同场景下使用深度学习模型中的各种组件。

第二章:使用 CNTK 构建神经网络

在上一章中,我们讨论了深度学习是什么,以及神经网络如何在概念层面上运作。最后,我们讨论了 CNTK 以及如何在你的机器上安装它。在本章中,我们将使用 CNTK 构建并训练我们的第一个神经网络。

我们将使用 CNTK 库中的不同函数和类来构建一个神经网络。我们将通过一个基本的分类问题来实现。

一旦我们为分类问题构建了一个神经网络,我们将使用从开放数据集中获取的样本数据对其进行训练。训练完神经网络后,我们将学习如何使用它来进行预测。

在本章的最后,我们将花一些时间讨论在训练完模型后,如何提高模型性能。

本章将覆盖以下主题:

  • CNTK 中的基本神经网络概念

  • 构建你的第一个神经网络

  • 训练神经网络

  • 使用神经网络进行预测

  • 改进模型

技术要求

在本章中,我们将处理一个使用 Python 在 Jupyter 笔记本中构建的示例模型。Jupyter 是一种开源技术,它允许你创建包含 Python 代码、Markdown 和 HTML 的交互式网页。这使得文档化代码以及记录在构建深度学习模型时所做的假设变得更加容易。

如果你按照第一章中定义的步骤安装了 Anaconda,开始使用 CNTK,那么你已经在你的机器上安装了 Jupyter。如果你还没有安装 Anaconda,你可以从以下网址下载:anacondacloud.com/download

你可以从以下网址获取本章的示例代码:github.com/PacktPublishing/Deep-Learning-with-Microsoft-Cognitive-Toolkit-Quick-Start-Guide/tree/master/ch2。要运行示例代码,请在下载代码的目录中通过终端运行以下命令:

cd ch2
jupyter notebook

找到Train your first model.ipynb笔记本,点击它以打开示例代码。你可以选择 Cell | Run All 一次性执行所有代码,这将执行笔记本中的所有步骤。

查看以下视频,了解代码的实际运行:

bit.ly/2YoyNKY

CNTK 中的基本神经网络概念

在上一章中,我们学习了神经网络的基本概念。现在我们将这些概念映射到 CNTK 库中的组件,并探索如何利用这些概念来构建你自己的模型。

使用层函数构建神经网络

神经网络是由多个神经元层组成的。在 CNTK 中,我们可以使用在 layers 模块中定义的层函数来建模神经网络的各个层。CNTK 中的layer函数看起来就像一个普通的函数。例如,你可以通过一行代码创建最基本的层类型Dense

from cntk.layers import Dense
from cntk import input_variable

features = input_variable(100)
layer = Dense(50)(features)

按照给定的步骤创建最基本的层类型:

  1. 首先,从 layers 包中导入Dense层函数

  2. 接下来,从cntk根包中导入input_variable函数

  3. 使用input_variable函数创建一个名为 features 的新输入变量,并将其大小设置为100

  4. 使用Dense函数创建一个新层,提供所需的神经元数量

  5. 调用配置好的Dense层函数,提供特征变量,以将Dense层与输入连接起来

在 CNTK 中处理层具有鲜明的函数式编程风格。当我们回顾前一章时,我们可以理解为什么 CNTK 选择了这种方式。归根结底,神经网络中的每一层都是一个数学函数。CNTK 中的所有层函数都会生成一个具有一组预定义参数的数学函数。再次调用该函数时,你将绑定最后一个缺失的参数——输入,来连接层。

当你想要创建具有复杂架构的神经网络时,通常会使用这种风格的编程。但是,对于大多数初学者来说,函数式风格可能会感到陌生。CNTK 提供了一个更简单的 API,当你想通过Sequential层函数构建基础神经网络时,可以使用它。

你可以使用Sequential层函数将多个层链接在一起,而无需使用函数式编程风格,方法如下:

from cntk.layers import Sequential, Dense
from cntk import input_variable

features = input_variable(7)

network = Sequential([
  Dense(64),
  Dense(32),
  Dense(3)
])(features)

按照给定的步骤操作:

  1. 首先,从layers包中导入你想使用的层函数

  2. 导入input_variable函数来创建一个输入变量,用于将数据输入到神经网络

  3. 创建一个新的输入变量,将数据输入到神经网络

  4. 通过调用Sequential函数创建一个新的顺序层块

  5. 将你想要链接在一起的层列表提供给Sequential函数

  6. 调用配置好的Sequential函数对象,提供特征输入变量以完成网络结构

通过将Sequential函数与其他层函数结合,你可以创建任何神经网络结构。在下一部分中,我们将看看如何通过设置自定义层,以配置诸如activation函数等内容。

自定义层设置

CNTK 为构建神经网络提供了一套非常好的默认设置。但你会发现自己经常需要对这些设置进行实验。神经网络的行为和性能会根据你选择的activation函数和其他设置有所不同。因此,了解你可以配置哪些选项是很有帮助的。

每个层都有自己独特的配置选项,其中一些你会经常使用,另一些则用得较少。当我们查看 Dense 层时,有几个重要的设置是你需要定义的:

  • shape:层的输出形状

  • activation:层的 activation 函数

  • init:层的 initialization 函数

层的输出形状决定了该层中神经元的数量。每个神经元需要定义一个 activation 函数,以便它能转换输入数据。最后,我们需要一个函数来初始化该层的参数,以便我们开始训练神经网络。输出形状是每个 layer 函数中的第一个参数。activationinit 参数作为关键字参数提供。这些参数有默认值,因此如果你不需要自定义设置,可以省略它们。以下示例演示了如何使用自定义 initializeractivation 函数配置 Dense 层:

from cntk.layers import Dense
from cntk.ops import sigmoid
from cntk.initializer import glorot_uniform

layer = Dense(128, activation=sigmoid, init=glorot_uniform)

配置 Dense 层的步骤如下:

  1. 首先,从 layers 包中导入 Dense

  2. 接下来,从 ops 包中导入 sigmoid 运算符,以便我们可以将其配置为 activation 函数

  3. 然后从 initializer 包中导入 glorot_uniform 初始化函数

  4. 最后,使用 Dense 层创建一个新层,将神经元数量作为第一个参数,并提供 sigmoid 运算符作为 activation 函数,glorot_uniform 函数作为层的 init 函数

有几种 activation 函数可供选择;例如,你可以使用 修正线性单元 (ReLU) 或 sigmoid 作为 activation 函数。所有的 activation 函数都可以在 cntk.ops 包中找到。

每个 activation 函数对神经网络的性能会产生不同的影响。当我们在本章稍后构建神经网络时,我们将更详细地讨论 activation 函数。

初始化函数决定了当我们开始训练神经网络时,层中参数的初始化方式。你可以从 CNTK 中选择多种初始化函数。Normaluniformglorot_uniformcntk.initializer 包中一些常用的初始化函数。当我们开始解决第一个深度学习问题时,我们会更详细地讨论选择哪个初始化函数。

无论你使用的是 CNTK 中的哪种初始化函数,都需要意识到它们使用随机数生成器来生成层中参数的初始值。这是一个重要的技术,因为它允许神经网络有效地学习正确的参数。CNTK 中的所有初始化函数都支持额外的种子设置。当你将此参数设置为固定值时,每次训练神经网络时都会得到相同的初始值。这在你尝试重现问题或实验不同设置时非常有用。

当你构建神经网络时,通常需要为网络中的多个层指定相同的设置。当你在实验你的模型时,这可能会变得很麻烦。为了解决这个问题,CNTK 提供了一个名为default_optionsutility函数:

from cntk import default_options
from cntk.layers import Dense, Sequential
from cntk.ops import sigmoid

with default_options(activation=sigmoid):
  network = Sequential([
    Dense(1024),
    Dense(512),
    Dense(256)
  ])

通过使用default_options函数,我们只需一行代码,就为所有三层配置了sigmoid激活函数。default_options函数接受一组标准设置,这些设置会应用到此函数作用范围内的所有层。使用default_options函数使得为一组层配置相同选项变得更加便捷。通过这种方式,你可以配置很多设置,例如,使用以下函数:

  • activation:要使用的activation函数

  • init:层的initialization函数

  • bias:是否应在层中包含bias

  • init_biasbias项的初始化函数

使用learnerstrainers优化神经网络中的参数

在前面的章节中,我们已经学习了如何创建神经网络的结构以及如何配置各种设置。现在,让我们看看如何使用learnerstrainers来优化神经网络的参数。在 CNTK 中,神经网络的训练是通过两部分组成的组合来完成的。第一部分是trainer组件,它实现了反向传播过程。第二部分是learner,它负责执行我们在第一章《CNTK 入门》中看到的梯度下降算法。

trainer将数据传递通过神经网络以获得预测。然后使用learner来获取神经网络中参数的新值。接着应用这些新值,并重复这个过程。这个过程一直进行,直到满足退出条件。当达到配置的迭代次数时,训练过程会停止。可以通过自定义回调来增强这一过程。

我们在第一章《CNTK 入门》中讨论了一个非常基础的梯度下降方法。但实际上,这个基本算法有许多变种。基本的梯度下降在复杂的情况下效果并不好。它经常会陷入局部最优解(可以理解为山坡上的一个小凸起),因此无法达到神经网络参数的全局最优值。其他算法,例如带动量的随机梯度下降SGD),考虑了局部最优,并使用动量等概念来跨越损失曲线的坡度中的“凸起”。

这里列出了一些在 CNTK 库中包含的有趣learners

  • SGD:基本的随机梯度下降,没有任何额外功能

  • MomentumSGD:应用动量来克服局部最优解

  • RMSProp:使用衰减学习率来控制下降速率

  • Adam:使用衰减的动量来减少随着时间推移的下降速率

  • Adagrad:为频繁和不频繁出现的特征使用不同的学习率

重要的是要知道,你可以根据你想要解决的问题选择不同的 learners。当我们开始使用神经网络解决第一个机器学习问题时,我们将进一步了解如何选择合适的优化器。

损失函数

为了使 trainerlearner 能够优化神经网络的参数,我们需要定义一个衡量神经网络损失的函数。loss 函数计算的是神经网络预测输出与我们预先知道的期望输出之间的差距。

CNTK 包含了多个 loss 函数,位于 cntk.losses 模块中。每个 loss 函数都有其特定的用途和特点。例如,当你想衡量一个预测连续值的模型的损失时,你需要使用 squared_error 损失函数。它衡量的是模型生成的预测值与在训练模型时提供的真实值之间的距离。

对于分类模型,你需要一组不同的 loss 函数。binary_cross_entropy 损失函数可以用来衡量用于二分类任务(如欺诈检测模型)模型的损失。cross_entropy_with_softmax 损失函数更适用于预测多个类别的分类模型。

模型度量

learnerloss 函数与 trainer 结合起来可以优化神经网络中的参数。这应该会生成一个好的模型,但为了确保这一点,我们需要使用度量来衡量模型的性能。度量是一个单一的数值,它告诉我们,例如,有多少百分比的样本被正确预测。

因为 loss 函数衡量的是实际值与预测值之间的差异,你可能会认为它是衡量我们模型表现的一个好指标。根据模型的不同,它可能提供一些有用的信息,但通常你需要使用单独的 metric 函数来以有意义的方式衡量模型的表现。

CNTK 提供了多个不同的 metric 函数,位于 cntk.metrics 包中。例如,如果你想衡量分类模型的性能,可以使用 classification_error 函数。它用于衡量正确预测的样本百分比。

classification_error 函数只是一个度量的例子。另一个重要的 metric 函数是 ndcg_at_1 度量。如果你正在使用排序模型,那么你会关心模型是如何根据预定义的排序来排列样本的。ndcg_at_1 度量就是用来评估这一点的。

构建你的第一个神经网络

现在我们已经了解了 CNTK 提供的概念来构建神经网络,我们可以开始将这些概念应用到一个实际的机器学习问题中。在这一部分,我们将探讨如何使用神经网络对鸢尾花的品种进行分类。

这不是一个典型的任务,通常你会选择使用神经网络。但正如你很快会发现的,数据集足够简单,可以帮助你很好地理解构建深度学习模型的过程,同时也包含足够的数据来确保模型的合理性能。

鸢尾花数据集描述了不同品种鸢尾花的物理属性:

  • 萼片长度(单位:厘米)

  • 萼片宽度(单位:厘米)

  • 花瓣长度(单位:厘米)

  • 花瓣宽度(单位:厘米)

  • 类别(鸢尾花 Setosa,鸢尾花 Versicolor,鸢尾花 Virginica)

本章的代码包括鸢尾花数据集,您需要在此数据集上训练深度学习模型。如果您感兴趣,可以在线查找原始文件:archive.ics.uci.edu/ml/datasets/Iris。这些文件也包括在本章的示例代码中。

我们将构建一个深度学习模型,该模型将基于萼片宽度和长度、花瓣宽度和长度来分类花卉。我们可以预测三种不同的类别作为模型的输出。

我们总共有 150 个不同的样本来进行训练,这应该足以在我们尝试使用模型来分类花卉时获得合理的性能。

构建网络结构

首先,我们需要确定要为神经网络使用什么架构。我们将构建一个常规的神经网络,通常被称为前馈神经网络。

我们首先需要定义输入层和输出层的神经元数量。接下来,我们需要定义神经网络中隐藏层的形状。由于我们解决的问题相对简单,所以我们不需要超过一层隐藏层。

当我们查看数据集时,我们可以看到它有四个特征和一个标签。由于我们有四个特征,我们需要确保神经网络的输入层有四个神经元。

接下来,我们需要为神经网络定义输出层。为此,我们需要查看需要模型能够预测的类别数量。在我们的例子中,我们有三种不同的花卉品种可以选择,因此我们需要在输出层中设置三个神经元。

首先,我们从 CNTK 库中导入必要的组件,这些组件包括层类型、activation 函数以及允许我们为网络定义输入变量的函数:

from cntk import default_options, input_variable
from cntk.layers import Dense, Sequential
from cntk.ops import log_softmax, relu

然后,我们使用 Sequential 函数创建模型,并将我们需要的层传递给它。我们在网络中创建了两个不同的层——首先是一个有四个神经元的层,然后是一个有三个神经元的层:

model = Sequential([
    Dense(4, activation=relu),
    Dense(3, activation=log_softmax)
])

最后,我们将网络与输入变量绑定,这将编译神经网络,使其具有一个包含四个神经元的输入层和一个包含三个神经元的输出层,如下所示:

features = input_variable(4)
z = model(features)

现在,让我们回到我们的层结构。注意到我们在调用Sequential层函数时没有建模输入层。这是因为我们在代码中创建的input_variable就是神经网络的输入层。

sequential调用的第一个层是网络中的隐藏层。通常的经验法则是,你希望隐藏层的大小不超过前一层神经元数量的两倍。

你需要通过实验来优化这个设置,以获得最佳结果。选择神经网络的层数和神经元数目需要一些经验和实验。没有硬性规定说明你应该包含多少个隐藏层。

选择激活函数

在前面的章节中,我们为神经网络选择了sigmoid激活函数。选择正确的激活函数对深度学习模型的性能有很大影响。

你会发现关于选择激活函数有很多不同的观点。这是因为有很多选择,而对于专家所做的选择,并没有足够的实质性证据。那么,如何为你的神经网络选择一个合适的激活函数呢?

选择输出层的激活函数

首先,我们需要定义我们正在解决的问题类型。这决定了你网络输出层的激活函数。对于回归问题,你需要在输出层使用线性激活函数。对于分类问题,二分类问题使用sigmoid函数,多分类问题使用softmax函数。

在我们构建的模型中,我们需要预测三类中的一种,这意味着我们需要在输出层使用softmax激活函数。

选择隐藏层的激活函数

现在,让我们来看一下隐藏层。为我们模型的隐藏层选择一个激活函数要难得多。我们需要进行一些实验并监控性能,以查看哪种激活函数效果最佳。

对于分类问题,像我们花卉分类模型那样,我们需要一些可以给出概率值的东西。我们需要这样做,因为我们需要预测一个样本属于某个特定类别的概率。sigmoid函数帮助我们实现这个目标。它的输出是一个概率,值介于 0 和 1 之间。

使用sigmoid激活函数时,我们需要解决一些问题。当你创建更大的网络时,你可能会遇到一个叫做梯度消失的问题。

sigmoid函数输入非常大的值时,输出会趋近于零或一,具体取决于输入值是负数还是正数。这意味着,当我们使用模型的大输入值时,sigmoid函数的输出变化不大。输入值的变化将只导致输出的非常小的变化。优化器在训练时计算的梯度也非常小。有时,它会小到计算机将其四舍五入为零,这意味着优化器无法检测到参数值的调整方向。当优化器由于 CPU 的四舍五入问题无法计算梯度时,我们就会遇到梯度消失问题。

为了解决这个问题,科学家们提出了一种新的激活函数,ReLU。这个激活函数将所有负值转化为零,并且对正值起到通过过滤器的作用。它有助于解决梯度消失问题,因为它不会限制输出值。

然而,ReLU函数有两个问题。首先,它将负输入值转换为零。在某些情况下,这可能导致优化器将某些参数的权重也设置为零。这会导致网络中出现“死亡神经元”,当然,这会限制网络的功能。

第二个问题是,ReLU函数存在梯度爆炸的问题。因为该函数输出的上限没有限制,它可能会放大信号,导致优化器计算出接近无穷大的梯度。当你将这个梯度应用到网络中的参数时,网络开始输出 NaN 值。

选择隐藏层的正确激活函数需要一定的实验。再次强调,并没有硬性规定说要使用哪种激活函数。在本章的示例代码中,我们在对模型进行一些实验后,选择了sigmoid函数。

选择损失函数

当我们有了模型的结构后,就该考虑如何优化它了。为此,我们需要一个需要最小化的loss函数。可以选择的loss函数有很多。

合适的loss函数取决于你要解决的问题。例如,在像我们这样的分类模型中,我们需要一个能够衡量预测类别与实际类别之间差异的loss函数。它需要针对三个类别进行计算。categorical cross entropy函数是一个不错的选择。在 CNTK 中,这个loss函数实现为cross_entropy_with_softmax

label = input_variable(3)
loss = cross_entropy_with_softmax(z, label)

我们需要先从cntk.losses包中导入cross_entropy_with_softmax函数。导入loss函数后,我们创建一个新的输入变量,以便将期望标签传递给loss函数。然后,我们创建一个新的loss变量,保存对loss函数的引用。CNTK 中的任何loss函数都需要模型的输出和期望标签的输入变量。

记录指标

在结构搭建好并拥有loss函数后,我们就拥有了优化深度学习模型所需的所有元素。但在我们开始训练模型之前,让我们先看一下指标。

为了让我们看到网络的表现,我们需要记录一些指标。由于我们正在构建一个分类模型,我们将使用classification_error指标。该指标会生成一个介于 0 和 1 之间的数值,表示正确预测的样本百分比:

error_rate = classification_error(z, label)

让我们从cntk.metrics包中导入classification_error。接着,我们创建一个新的error_rate变量,并将classification_error函数绑定到它。该函数需要网络的输出和期望标签作为输入。我们已经在定义模型和loss函数时准备好了这些。

训练神经网络

现在我们已经定义了深度学习的所有组件,接下来就可以开始训练了。你可以使用learnertrainer的组合在 CNTK 中训练模型。我们需要定义这些组件,然后通过trainer喂入数据来训练模型。让我们看看如何操作。

选择学习器并设置训练

有多种learners可供选择。对于我们的第一个模型,我们将使用stochastic gradient descent学习器。让我们配置learnertrainer来训练神经网络:

from cntk.learners import sgd
from cntk.train.trainer import Trainer

learner = sgd(z.parameters, 0.01)

trainer = Trainer(z, (loss, error_rate), [learner])

要配置learnertrainer以训练神经网络,请按照以下步骤进行:

  1. 首先,从learners包中导入sgd函数。

  2. 然后,从trainer包中导入Trainer,该包是train包的一部分。

  3. 现在通过调用sgd函数并提供模型的参数和学习率值来创建learner

  4. 最后,初始化trainer并提供网络、lossmetric的组合以及learner

我们提供给sgd函数的学习率控制优化速度,应该是一个小数字,通常在 0.1 到 0.001 之间。

请注意,每个learner都有自己的参数,因此请务必查看文档,了解在使用cntk.learners包中的特定learner时需要配置哪些参数。

向训练器输入数据以优化神经网络

我们花了很多时间来定义模型、配置lossmetrics,最后是learner。现在是时候在我们的数据集上进行训练了。然而,在我们训练模型之前,我们需要先加载数据集。

示例代码中的数据集存储为 CSV 文件。为了加载该数据集,我们需要使用类似pandas的数据处理包。这个包在你的 Anaconda 安装中默认包含。以下示例展示了如何使用pandas将数据集加载到内存中:

import pandas as pd

df_source = pd.read_csv('iris.csv', 
    names=['sepal_length', 'sepal_width','petal_length','petal_width', 'species'], 
    index_col=False)

使用pandas将数据集加载到内存中,请按照以下步骤进行操作:

  1. 首先,导入pandas包并为其起别名pd

  2. 然后,调用read_csv函数从磁盘加载iris.csv文件

由于 CSV 文件没有列头,我们需要自己定义它们。这样以后引用特定列时会更加方便。

通常,pandas会使用输入文件中的第一列作为数据集的索引。索引将作为识别记录的键。由于我们的数据集中没有索引,因此我们通过index_col关键字参数禁用它的使用。

加载完数据集后,让我们将其拆分为特征集和标签集:

X = df_source.iloc[:, :4].values
y = df_source['species'].values

要将数据集拆分为特征集和标签集,请按照以下步骤进行操作:

  1. 首先,使用iloc函数从数据集中选择所有行和前四列

  2. 接下来,从数据集中选择物种列,并使用 values 属性访问底层的numpy数组

我们的模型需要数值型输入值。但是,物种列是一个字符串值,表示花的类型。我们可以通过将物种列编码为数值向量表示来解决这个问题。我们正在创建的向量表示与神经网络的输出神经元数量匹配。向量中的每个元素代表一种花的物种,具体如下:

label_mapping = {
    'Iris-setosa': 0,
    'Iris-versicolor': 1,
    'Iris-virginica': 2
}

为了创建物种的 one-hot 向量表示,我们将使用一个小的utility函数:

def one_hot(index, length):
    result = np.zeros(length)
    result[index] = 1

    return result

one_hot函数执行以下步骤:

  1. 首先,初始化一个填充为零的新数组,长度为所需的length

  2. 接下来,选择指定的index处的元素并将其设置为1

现在我们已经有了一个将物种映射到索引的字典,并且有了创建 one-hot 向量的方法,我们可以通过添加一行代码将字符串值转换为其向量表示:

y = np.array([one_hot(label_mapping[v], 3) for v in y])

按照以下步骤进行操作:

  1. 首先,创建一个列表表达式遍历数组中的所有元素

  2. 对数组中的每个值,在label_mapping字典中查找对应的值

  3. 接下来,将这个转换后的数值应用one_hot函数转换为 one-hot 编码向量

  4. 最后,将转换后的列表转换为numpy数组

当你训练深度学习模型或任何机器学习模型时,你需要记住,计算机将尝试记住你用于训练模型的所有样本。与此同时,它将尝试学习一般规则。当模型记住样本但不能从训练样本中推断规则时,它就会在数据集上过拟合。

为了检测过拟合,你需要将数据集的一小部分与训练集分开。训练集用于训练模型,而测试集则用于衡量模型的性能。

我们可以使用scikit-learn包中的utility函数,将数据集分为训练集和测试集:

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)

按照以下步骤操作:

  1. 首先,从sklearn包中的model_selection模块导入train_test_split函数

  2. 然后,调用train_test_split函数,传入特征X和标签y

  3. 指定test_size0.2,将 20%的数据留作测试集

  4. 使用stratify关键字参数,传入标签数组y中的值,这样我们就能确保在训练集和测试集中,每个物种的样本数量是均等的。

如果你不使用stratify参数,最终可能会得到一个数据集,其中某个类别的样本完全没有,而另一个类别的样本却过多。这样,模型就无法学习如何分类缺失的类别,而会在另一个样本过多的类别上出现过拟合。

现在我们有了训练集和验证集,让我们看看如何将它们输入到模型中进行训练:

trainer.train_minibatch({ features: X_train, label: y_train })

要训练模型,调用trainer上的train_minibatch方法,并传入一个字典,将输入数据映射到你用来定义神经网络及其相关loss函数的输入变量。

我们使用train_minibatch方法作为将数据输入到trainer中的便捷方式。在下一章中,我们将讨论其他输入数据的方式,并详细介绍train_minibatch方法的作用。

请注意,你需要调用train_minibatch方法多次,才能让网络得到充分训练。所以我们需要写一个简短的循环来调用这个方法:

for _epoch in range(10):
    trainer.train_minibatch({ features: X_train, label: y_train })

    print('Loss: {}, Acc: {}'.format(
        trainer.previous_minibatch_loss_average,
        trainer.previous_minibatch_evaluation_average))

按照以下步骤操作:

  1. 首先,使用for语句创建一个新的循环,并设置范围为10

  2. 在循环中调用train_minibatch方法,并将输入变量与相关数据进行映射

  3. 最后,打印出previous_minibatch_loss_averageprevious_minibatch_evaluation_average,以监控训练进展。

当你调用train_minibatch方法时,trainer会更新loss函数的输出和我们提供给trainermetric函数的值,并将其存储在previous_minibatch_evaluation_average中。

每次循环完成,并且我们将整个数据集通过trainer运行一次后,就完成了一个训练周期。正如我们在前一章中看到的那样,通常需要运行多个周期,直到模型表现足够好。作为额外的奖励,我们还会在每个周期后打印出trainer的进度。

检查神经网络的性能

每次我们将数据传递给训练器以优化模型时,它会通过我们为训练器配置的度量标准来衡量模型的表现。训练期间衡量的模型性能是基于训练集的。衡量训练集上的准确率很有用,因为它能告诉你模型是否从数据中学到了什么。

要全面分析模型的性能,你需要使用测试集来衡量模型的表现。可以通过以下方式调用trainer上的test_minibatch方法:

trainer.test_minibatch( {features: X_test, label: y_test })

该方法接受一个字典,字典中包含输入变量与变量数据之间的映射关系。该方法的输出是你之前配置的metric函数的输出。在我们的例子中,它是基于我们输入的数据计算的模型准确率。

当测试集的准确率高于训练集的准确率时,我们会遇到欠拟合的情况。当测试集的准确率低于训练集的准确率时,我们会遇到过拟合的情况。

无论是欠拟合还是过拟合,过度都不好。当测试集和训练集的准确率几乎相同时,模型性能最佳。我们将在第四章中详细讨论模型性能,验证模型性能

使用神经网络进行预测

训练深度学习模型后,最令人满意的事情之一就是将它实际应用到应用程序中。目前,我们将仅限于使用从测试集中随机挑选的样本来使用模型。但稍后,在第七章,将模型部署到生产环境中,我们将探讨如何将模型保存到磁盘,并在 C#或.NET 中使用它来构建应用程序。

让我们编写代码,通过我们训练过的神经网络进行预测:

sample_index = np.random.choice(X_test.shape[0])
sample = X_test[sample_index]

inverted_mapping = {
    1: 'Iris-setosa',
    2: 'Iris-versicolor',
    3: 'Iris-virginica'
}

prediction = z(sample)
predicted_label = inverted_mapping[np.argmax(prediction)]

print(predicted_label)

按照以下步骤操作:

  1. 首先,使用np.random.choice函数从测试集中随机挑选一个项目

  2. 然后使用生成的sample_index从测试集中选择样本数据

  3. 接下来,创建一个反向映射,以便你可以将神经网络的数值输出转换为实际标签

  4. 现在,使用选定的sample数据,通过将神经网络z作为函数来进行预测

  5. 从预测输出中,使用numpy包中的np.argmax函数获取具有最大值的神经元索引作为预测值

  6. 使用inverted_mapping将索引值转换为真实标签

当你执行代码示例时,你会得到类似这样的输出:

Iris-versicolor

改进模型

你会很快意识到,构建和训练神经网络需要不止一次尝试。通常,模型的第一版效果并不会如你所愿。需要相当多的实验才能设计出一个优秀的模型。

一个好的神经网络始于一个优秀的数据集。在几乎所有情况下,使用合适的数据集会带来更好的性能。许多数据科学家会告诉你,他们大约花费 80%的时间在处理一个好的数据集上。就像所有计算机软件一样,如果你输入垃圾,输出的也会是垃圾。

即使有了良好的数据集,你仍然需要花费相当多的时间来构建和训练不同的模型,才能获得你想要的性能。那么,让我们看看在第一次构建模型后,你可以做些什么来改进模型。

在第一次训练模型后,你有几个选择来改进你的模型。

查看你训练集和验证集的准确率。训练集上的准确率较低吗?尝试训练模型更多的轮次。通常,这会有助于提升模型的表现。

即使你训练模型更长时间,训练准确率仍然没有提高?那么你的模型可能无法学习数据集中的复杂关系。尝试改变模型结构,然后再次训练模型,看看是否能提高准确率。

例如,尝试更改激活函数或隐藏层中神经元的数量。这通常有助于模型学习数据集中更复杂的关系。

另外,你还可以检查模型中层的数量。添加一层可能对模型从输入数据中学习规则的能力产生很大的影响。

最后,当这些方法无效时,看看你模型中层的初始化。在某些情况下,选择不同的初始化函数有助于模型在初始学习阶段。

实验过程的关键是一次只改变一个参数,并跟踪你的实验结果。使用像 Git 这样的版本控制工具可以帮助你跟踪不同版本的训练代码。

总结

在本章中,我们构建了第一个神经网络,并训练它来识别鸢尾花。虽然这个示例非常基础,但它展示了如何使用 CNTK 构建和训练神经网络。

我们已经看到如何利用 CNTK 中的层库来快速定义神经网络的结构。在本章中,我们讨论了一些基本的构建块,如Dense层和Sequential层,用于将多个其他层连接在一起。在接下来的章节中,我们将学习其他层函数,以构建其他类型的神经网络,如卷积神经网络。

在本章中,我们还讨论了如何使用learnertrainer构建一个基本算法来训练我们的神经网络。我们使用了train_minibatch方法,并配合基本的循环来构建自己的训练过程。这是一种非常简单而强大的训练模型的方式。在下一章中,我们将更详细地讨论其他训练方法以及train_minibatch方法。

在训练完模型后,我们利用了 CNTK 的函数特性,使用训练好的模型进行预测。模型作为函数的这一特性非常强大,使得在应用程序中使用训练好的模型变得直观且简便。

最后,我们已经了解了如何使用test_minibatch方法来衡量模型性能,以及如何使用性能指标检查模型是否发生了过拟合。之后我们讨论了如何使用指标来判断如何改进模型。

在下一章,我们将探讨不同的方式来访问和输入数据到 CNTK 模型中。我们还将深入探讨 CNTK 中每种数据访问方法,并了解在不同情况下哪些方法最为合适。

第三章:将数据输入神经网络

你可以使用许多技术将数据加载到神经网络中进行训练或预测。你使用什么技术取决于你的数据集有多大,以及你存储数据的格式是什么。在上一章中,我们已经看到了如何手动将数据传递给 CNTK 训练器。在本章中,我们将学习更多将数据输入神经网络的方法。

本章将覆盖以下主题:

  • 使用小批量高效训练神经网络

  • 处理小型内存数据集

  • 处理大型数据集

  • 控制小批量循环

技术要求

我们假设你已经在电脑上安装了最新版本的 Anaconda,并且已经按照第一章中的步骤,开始使用 CNTK,将 CNTK 安装到你的电脑上。本章的示例代码可以在我们的 GitHub 仓库中找到,地址是github.com/PacktPublishing/Deep-Learning-with-Microsoft-Cognitive-Toolkit-Quick-Start-Guide/tree/master/ch3

在本章中,我们将在存储在 Jupyter 笔记本中的几个示例上进行操作。要访问示例代码,请在 Anaconda 提示符中运行以下命令,命令路径为你下载代码的目录:

cd ch3
jupyter notebook

我们将在每个部分中提到相关的笔记本,以便你可以跟着做,并亲自尝试不同的技术。

查看以下视频,了解代码的实际应用:

bit.ly/2UczHuH

使用小批量高效训练神经网络

在上一章中,我们讨论了如何构建和训练神经网络。在本章中,我们将讨论如何将数据输入 CNTK 训练器的各种方法。在深入了解每种数据处理方法的细节之前,让我们先仔细看看训练神经网络时数据发生了什么。

训练神经网络需要一些东西。正如我们在上一章中讨论的,你需要有一个基本的模型结构和损失函数。trainerlearner是最后的拼图部分,负责控制训练过程。

trainer执行四个步骤:

  1. 它接受一批训练样本并将其输入到网络和loss函数中

  2. 接下来,它将loss函数的输出通过learner处理。

  3. 然后,它使用learner获取网络中参数的梯度集合

  4. 最后,它使用梯度来确定网络中每个参数的新值

这个过程会对数据集中的所有样本重复进行,以便训练网络完成一个完整的 epoch。通常,你需要训练网络多个 epoch 才能获得最佳结果。

我们之前只讨论了训练神经网络时的单个样本。但这并不是 CNTK 内部发生的情况。

CNTK 和许多其他框架都使用小批量来训练神经网络。小批量是从数据集中提取的一组样本。本质上,小批量就是一个非常小的样本表格。它包含输入特征的预定义数量的样本,以及与神经网络目标数量相等的样本。

小批量在训练过程中通过网络传递,用于计算损失函数的输出。loss函数的输出不再是一个单一的值,而是一个值的列表,列表中的值等于小批量中的行数。然后,这个值的列表会传递给learner,从而为神经网络的每个参数计算出一组梯度。

现在,处理小批量时有一个问题。我们希望每个参数都有一个梯度来优化其值。但实际上我们得到的是梯度的列表。我们可以通过对每个参数的梯度计算平均值来解决这个问题。然后,使用平均梯度来更新神经网络中的参数。

使用小批量加速了训练过程,但也带来了一些代价。因为我们现在必须处理平均值,所以在计算模型参数的梯度时,会失去一些分辨率。由于平均所有计算得到的梯度,单个小批量的梯度可能为零。当你使用小批量训练神经网络时,模型的质量可能较低。

在开始训练神经网络之前,你需要自己设置每个小批量的样本数量。选择较大的小批量大小将加速训练,但会以质量为代价。较小的小批量大小训练较慢,但能产生更好的模型。选择合适的小批量大小是一个实验性的问题。

选择小批量大小时还有一个内存方面的考量。小批量大小取决于你机器中可用的内存。你会发现,你的显卡内存能够容纳的样本比普通计算机内存少。

接下来章节中描述的所有方法都会自动使用小批量。在本章后面的章节,控制小批量循环部分,我们将讨论如何在需要时控制小批量循环。

使用小型内存数据集

有许多方法可以将数据提供给 CNTK 训练器。你应该使用哪种技术取决于数据集的大小和数据的格式。首先让我们来看一下如何处理较小的内存数据集。

当你在 Python 中处理内存数据时,你很可能会使用像 Pandas 或 NumPy 这样的框架。这些框架以浮动点或对象数据为核心,处理向量和矩阵,并在处理数据时提供不同级别的便利性。

让我们逐一了解这些库,并探讨如何使用存储在这些库中的数据来训练你的神经网络。

使用 numpy 数组

我们将首先探索的库是 numpy。Numpy 是 Python 中最基本的库,用于对 n 维数组执行数学操作。它提供了一种高效的方式来存储计算机内存中的矩阵和向量。numpy 库定义了大量操作符来操作这些 n 维数组。例如,它有内置的函数来计算整个矩阵或矩阵中的行/列的平均值。

你可以通过按照本章开始时描述的步骤,打开Training using numpy arrays.ipynb笔记本,在浏览器中跟随本节中的任何代码。

让我们来看一下如何在 CNTK 中使用基于 numpy 的数据集。作为示例,我们将使用一个随机生成的数据集。我们将模拟一个二元分类问题的数据。假设我们有一组包含四个特征的观察数据。我们希望用我们的模型预测两个可能的标签。首先,我们需要生成一组标签,其中包含我们想要预测的标签的热编码向量表示。接下来,我们还需要一组特征,这些特征将作为我们模型的输入特征:

import numpy as np

num_samples = 20000

label_mapping = np.eye(2)
y = label_mapping[np.random.choice(2,num_samples)].astype(np.float32)
X = np.random.random(size=(num_samples, 4)).astype(np.float32)

按照给定步骤操作:

  1. 首先,导入numpy包,并使用np作为别名。

  2. 然后,使用np.eye函数生成一个label mapping

  3. 然后,使用np.random.choice函数从生成的label mapping中收集20,000个随机样本。

  4. 最后,使用np.random.random函数生成一个随机浮点值数组。

生成的标签映射是我们支持的可能类别的一个热编码表示,看起来像这样:

[0, 1]
[1, 0]

生成的矩阵需要转换为 32 位浮点数,以匹配 CNTK 所期望的格式。没有这个步骤,你将看到一个错误,提示格式不是预期的类型。CNTK 要求你提供双精度或 float 32 的数据点。

让我们定义一个适配我们刚生成的数据集的基本模型:

from cntk.layers import Dense, Sequential
from cntk import input_variable, default_options
from cntk.ops import sigmoid
from cntk.losses import binary_cross_entropy

with default_options(activation=sigmoid):
   model = Sequential([
        Dense(6),
        Dense(2)
    ])

features = input_variable(4)

z = model(features)

按照给定步骤操作:

  1. 首先,从layers模块导入DenseSequential层函数。

  2. 然后,导入sigmoid作为网络中各层的激活函数。

  3. 然后,导入binary_cross_entropy函数作为loss函数来训练网络。

  4. 接下来,定义网络的默认选项,提供sigmoid激活函数作为默认设置。

  5. 现在,使用Sequential层函数创建模型。

  6. 使用两个Dense层,一个具有6个神经元,另一个具有2个神经元,后者将作为输出层。

  7. 初始化一个input_variable,它有4个输入特征,将作为网络的输入。

  8. 最后,将features变量连接到神经网络以完成它。

该模型将具有四个输入和两个输出,匹配我们随机生成的数据集的格式。为了演示目的,我们插入了一个额外的隐藏层,包含六个神经元。

现在我们有了神经网络,接下来用我们的内存数据集来训练它:

from cntk.learners import sgd
from cntk.logging import ProgressPrinter

progress_writer = ProgressPrinter(0)

labels = input_variable(2)
loss = binary_cross_entropy(z, labels)
learner = sgd(z.parameters, lr=0.1)

training_summary = loss.train((X,y), parameter_learners=[learner], callbacks=[progress_writer])

请按照以下步骤操作:

  1. 首先,从learners模块导入sgd学习器

  2. 接下来,从logging模块导入ProgressPrinter

  3. 为标签定义一个新的input_variable

  4. 为了训练模型,定义一个使用binary_cross_entropy函数的loss,并为其提供模型zlabels变量

  5. 接下来,初始化sgd学习器,并为其提供模型参数和labels变量

  6. 最后,调用train方法,并为其提供输入数据、sgd学习器以及progress_printer回调

你不必为train方法提供回调函数。但如果插入一个进度写入器来监控训练过程会很有用。没有这个,你无法真正看到训练过程中发生了什么。

当你运行示例代码时,它将产生类似于以下内容的输出:

 average      since    average      since      examples
 loss       last     metric       last 
 ------------------------------------------------------
Learning rate per minibatch: 0.5
 1.4        1.4          0          0           512
 1.4        1.4          0          0          1536
 1.39       1.39          0          0          3584
 1.39       1.39          0          0          7680
 1.39       1.39          0          0         15872

它列出了每个迷你批次的学习平均损失、上一个迷你批次以来的损失以及度量标准。由于我们没有提供度量标准,度量列中的值将保持为0。在最后一列中,列出了神经网络看到的示例数量。

在之前的示例中,我们使用默认批次大小执行了learner。你可以使用minibatch_size关键字参数来控制批次大小:

training_summary = loss.train((X,y), 
    parameter_learners=[learner], 
    callbacks=[progress_writer],
    minibatch_size=512)

minibatch_size设置为更大的值将提高训练速度,但代价是模型可能会稍微变差。

尝试在示例代码中使用不同的迷你批次大小,观察它对模型性能的影响。即使是使用随机数据训练的模型也是如此。

使用 pandas 数据框

Numpy 数组是存储数据的最基本方式。Numpy 数组在可以包含的内容上有很大限制。一个单一的 n 维数组只能包含单一数据类型的数据。在许多实际应用场景中,你需要一个能够处理单个数据集内多种数据类型的库。例如,你会发现许多在线数据集的标签列是字符串类型,而数据集中的其他列则包含浮动点数。

Pandas 库让处理这些类型的数据集变得更加容易,许多开发者和数据科学家都在使用它。它是一个可以将存储在不同格式中的数据集作为 DataFrame 加载的库。例如,你可以读取存储为 JSON、CSV 甚至 Excel 格式的 DataFrame。

Pandas 引入了数据框(DataFrame)的概念,并带来了大量可以在数据框上运行的数学和统计函数。让我们看看 pandas 数据框的结构,以了解这个库是如何工作的:

pandas 中的 DataFrame 是由定义各个列的系列(series)组成的集合。每个 DataFrame 还有一个索引,允许你通过存储在索引中的键值查找 DataFrame 中特定的行。

DataFrame 独特之处在于,它在系列和数据集本身上定义了大量方法。例如,你可以调用describe方法来获取整个 DataFrame 的摘要统计信息。

对单个系列调用describe方法将为该特定列提供相同的摘要统计信息。

Pandas 是数据科学家和开发人员在 Python 中处理数据的广泛使用的工具。因为它如此广泛使用,了解如何使用 CNTK 处理存储在 pandas 中的数据非常重要。

在上一章中,我们加载了一个包含鸢尾花样本的数据集,并使用该数据集训练了一个分类模型。之前,我们使用了一个训练器实例来训练神经网络。当你对loss函数调用train时,也会发生类似的情况。train方法将自动为你创建一个训练器和一个会话,因此你无需手动操作。

在第二章,使用 CNTK 构建神经网络中,我们讨论了如何根据四个特性对三种鸢尾花的物种进行分类。你可以通过本书附带的示例代码文件,或者从 UCI 数据集档案archive.ics.uci.edu/ml/datasets/Iris下载数据集来获取该文件。让我们看看如何在前一章中创建的网络上使用train方法来训练损失函数:

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)

我们之前用来分类花朵的模型包含一个隐藏层和一个输出层,输出层有三个神经元,以匹配我们可以预测的类别数。

为了训练模型,我们需要加载并预处理鸢尾花数据集,以便它与神经网络的预期布局和数据格式匹配:

import numpy as np
import pandas as pd

df_source = pd.read_csv('iris.csv', 
    names=['sepal_length', 'sepal_width','petal_length','petal_width', 'species'], 
    index_col=False)

label_mapping = {
    'Iris-setosa': 0,
    'Iris-versicolor': 1,
    'Iris-virginica': 2
}

X = df_source.iloc[:, :4].values

y = df_source['species'].values
y = np.array([one_hot(label_mapping[v], 3) for v in y])

X = X.astype(np.float32)
y = y.astype(np.float32)

按照给定的步骤操作:

  1. 首先,使用read_csv函数将数据集加载到内存中。

  2. 接下来,创建一个字典,将数据集中标签与其对应的数字表示进行映射。

  3. 使用iloc索引器选择DataFrame中的前四列。

  4. 选择物种列作为数据集的标签。

  5. 使用label_mapping映射数据集中的标签,并使用one_hot编码将它们转换为独热编码数组。

  6. 将特征和映射后的标签都转换为浮点数,以便可以与 CNTK 一起使用。

标签以字符串形式存储在数据集中,CNTK 无法处理这些字符串值,它需要使用表示标签的独热编码向量。为了对标签进行编码,我们需要使用映射表和one_hot函数,您可以使用以下代码创建它:

def one_hot(index, length):
    result = np.zeros(length)
    result[index] = index

    return result

按照给定的步骤操作:

  1. 使用np.zeros函数创建一个大小为length的新向量,并用零填充它。

  2. 选择提供的index处的元素,并将其值设置为1

  3. 返回result,以便在数据集中使用。

一旦我们获得了格式正确的 numpy 数组,我们可以像以前一样使用它们来训练我们的模型:

from cntk.losses import cross_entropy_with_softmax
from cntk.learners import sgd 
from cntk.logging import ProgressPrinter

progress_writer = ProgressPrinter(0)

labels = input_variable(3)
loss = cross_entropy_with_softmax(z, labels)
learner = sgd(z.parameters, 0.1)

train_summary = loss.train((X,y), 
    parameter_learners=[learner], 
    callbacks=[progress_writer], 
    minibatch_size=16, 
    max_epochs=5)

按照给定的步骤操作:

  1. 导入cross_entropy_with_softmax函数作为模型的损失函数。

  2. 然后,导入sgd学习器以优化参数。

  3. 之后,从logging模块导入ProgressPrinter以可视化训练进度。

  4. 接下来,创建一个新的ProgressPrinter实例,用于记录优化器的输出。

  5. 创建一个新的input_variable来存储训练标签。

  6. 初始化sgd学习器,并给它模型的参数和一个学习率为0.1

  7. 最后,调用train方法并将训练数据、learnerprogress_writer传递给它。此外,为train方法提供minibatch_size16,并将max_epochs关键字参数设置为5

train方法中loss函数的max_epochs关键字参数是可选的。如果你不提供该参数,trainer将训练模型一个 epoch。

我们使用ProgressWriter来生成训练过程的输出,以便我们监控训练过程的进展。你可以不使用它,但它对于理解训练过程中发生了什么非常有帮助。配置好进度写入器后,输出会类似于以下内容:

average      since    average      since      examples
 loss       last     metric       last 
 ------------------------------------------------------
Learning rate per minibatch: 0.1
 1.1        1.1          0          0            16
 0.835      0.704          0          0            48
 0.993       1.11          0          0           112
 1.14       1.14          0          0            16
 0.902      0.783          0          0            48
 1.03       1.13          0          0           112
 1.19       1.19          0          0            16
 0.94      0.817          0          0            48
 1.06       1.16          0          0           112
 1.14       1.14          0          0            16
 0.907       0.79          0          0            48
 1.05       1.15          0          0           112
 1.07       1.07          0          0            16
 0.852      0.744          0          0            48
 1.01       1.14          0          0           112

由于我们使用与常规 numpy 数组相同的方法来训练网络,因此我们也可以控制批量大小。我们留给你尝试不同的批量大小设置,并发现哪个设置能产生最佳的模型。

处理大数据集

我们已经看过了 NumPy 和 Pandas 作为将内存中的数据集传递给 CNTK 进行训练的方式。但并非每个数据集都足够小,能够完全载入内存。对于包含图像、视频样本或声音样本的数据集尤其如此。当你处理更大的数据集时,你只希望一次加载数据集的小部分到内存中。通常,你只会加载足够的样本到内存中,以运行一个单一的 minibatch 训练。

CNTK 支持通过使用MinibatchSource处理更大的数据集。现在,MinibatchSource是一个可以从磁盘分块加载数据的组件。它可以自动随机化从数据源读取的样本。这有助于防止神经网络由于训练数据集中的固定顺序而过拟合。

MinibatchSource具有内置的转换管道。你可以使用这个管道来增强数据。当你处理如图像之类的数据时,这是一个非常有用的功能。当你训练一个基于图像的模型时,你希望确保即使图像处于奇怪的角度,也能被识别。转换管道允许你通过旋转从磁盘读取的原始图像来生成额外的样本。

MinibatchSource的一个独特特点是,它会在与训练过程分开的后台线程中加载数据。通过在单独的线程中加载数据,它可以提前加载 minibatch,这样你的显卡就不会因为这一过程而卡住。

在本章中,我们将限制在MinibatchSource的基本使用上。在第五章,《与图像的工作》和第六章,《与时间序列数据的工作》中,我们将探讨如何使用MinibatchSource组件处理图像和时间序列数据。

让我们探讨如何使用 minibatch 源处理内存外数据,以便处理更大的数据集,并将其用于训练神经网络。

创建 MinibatchSource 实例

与 pandas DataFrame 的工作部分,我们处理了鸢尾花的例子。让我们回过头来,将使用来自 pandas DataFrame 的数据的代码替换为MinibatchSource。第一步是创建一个基本的MinibatchSource实例:

from cntk.io import StreamDef, StreamDefs, MinibatchSource, CTFDeserializer, INFINITELY_REPEAT

labels_stream = StreamDef(field='labels', shape=3, is_sparse=False)
features_stream = StreamDef(field='features', shape=4, is_sparse=False)

deserializer = CTFDeserializer('iris.ctf', StreamDefs(labels=labels_stream, features=features_stream))

minibatch_source = MinibatchSource(deserializer, randomize=True)

按照给定的步骤操作:

  1. 首先,从io模块导入用于 minibatch 源的组件。

  2. 接下来,使用StreamDef类为标签创建流定义。使用标签字段并设置为从流中读取3个特征。确保使用is_sparse关键字参数并将其设置为False

  3. 然后,创建另一个StreamDef实例,并从输入文件中读取特征字段。此流具有4个特征。使用is_sparse关键字参数指定数据以密集向量形式存储。

  4. 之后,初始化deserializer。提供iris.ctf文件作为输入,并通过将其包装在StreamDefs实例中来传递流定义。

  5. 最后,使用deserializer创建MinibatchSource实例。

创建 CTF 文件

我们正在使用的数据来自iris.ctf文件,并存储在一种叫做CNTK 文本格式CTF)的文件格式中。这是一种类似这样的文件格式:

|features 0.1 2.0 3.1 5.4 |labels 0 1 0
|features 2.3 4.1 5.1 5.2 |labels 1 0 1

每一行包含我们神经网络的一个样本。每一行可以包含多个输入的值。每个输入前面都有一个竖线。每个输入的值由一个空格分隔。

CTFDeserializer可以通过使用我们在代码示例中初始化的流定义来读取文件。

为了获取我们刚刚创建的MinibatchSource实例的数据,您需要为我们的数据集创建一个 CTF 文件。目前没有官方的转换器可以将诸如逗号分隔值CSV)这样的数据格式转换为 CTF 文件,因此您需要编写一些 Python 代码。您可以在本章的示例代码中的Creating a CTF file.ipynb笔记本中找到准备 CTF 文件以进行 minibatch 训练的代码。

让我们探讨如何使用 Python 创建 CTF 文件。第一步是将数据加载到内存中并转换为正确的格式:

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)

features = df_source.iloc[:,:4].values
labels = df_source['species'].values

label_mapping = {
    'Iris-setosa': 0,
    'Iris-versicolor': 1,
    'Iris-virginica': 2
}

labels = [one_hot(label_mapping[v], 3) for v in labels]

按照给定的步骤操作:

  1. 在我们开始处理数据之前,导入pandasnumpy包,以便访问数据处理函数。

  2. 首先,将iris.csv文件加载到内存中,并将其存储在df_source变量中。

  3. 然后,使用iloc索引器获取前四列的内容作为特征。

  4. 接下来,使用物种列中的数据作为我们数据集的标签。

  5. 现在,创建一个label_mapping字典,用于在标签名称和其数字表示之间建立映射。

  6. 最后,使用 Python 列表推导式和one_hot函数将标签转换为一组独热编码向量。

为了对标签进行编码,我们将使用一个名为one_hot的实用函数,你可以使用以下代码创建它:

def one_hot(index, length):
    result = np.zeros(length)
    result[index] = 1

    return result

按照给定步骤操作:

  1. 使用np.zeros函数生成一个指定length的空向量

  2. 接下来,取指定index处的元素并将其设置为1

  3. 最后,返回新生成的独热编码向量,以便你可以在代码的其余部分中使用它

一旦加载并预处理了数据,我们就可以将其存储到磁盘上的 CTF 文件格式中:

with open('iris.ctf', 'w') as output_file:
    for index in range(0, features.shape[0]):
        feature_values = ' '.join([str(x) for x in np.nditer(features[index])])
        label_values = ' '.join([str(x) for x in np.nditer(labels[index])])

        output_file.write('|features {} |labels {}\n'.format(feature_values, label_values))

按照给定步骤操作:

  1. 首先,我们打开iris.ctf文件进行写入

  2. 然后,遍历数据集中的所有记录

  3. 对于每条记录,创建一个新字符串,包含features向量的序列化值

  4. 接下来,使用 Python 列表推导式将labels序列化为字符串

  5. 最后,将featureslabels写入文件

featureslabels向量中的元素应由空格分隔。请注意,输出文件中的每个序列化数据片段都以管道字符和其名称为前缀。

将数据输入到训练会话中

要使用MinibatchSource进行训练,我们可以使用之前的相同训练逻辑。只不过这次,我们将使用MinibatchSource作为train方法在loss函数中的输入:

from cntk.logging import ProgressPrinter
from cntk.train import Trainer, training_session

minibatch_size = 16
samples_per_epoch = 150
num_epochs = 30

input_map = {
    features: minibatch_source.streams.features,
    labels: minibatch_source.streams.labels
}

progress_writer = ProgressPrinter(0)

train_history = loss.train(minibatch_source, 
           parameter_learners=[learner],
           model_inputs_to_streams=input_map,
           callbacks=[progress_writer],
           epoch_size=samples_per_epoch,
           max_epochs=num_epochs)

按照给定步骤操作:

  1. 首先,导入ProgressPrinter,这样我们就能记录训练会话的输出。

  2. 接下来,导入trainertraining_session,你将需要这些来设置训练会话。

  3. 然后,定义一组常量用于训练代码。minibatch_size用来控制每批次的样本数量,samples_per_epoch用来控制单个 epoch 中的样本数量,最后是num_epochs设置,用来控制训练的轮数。

  4. 为网络的输入变量和小批量源中的流定义映射,这样 CNTK 就知道如何在训练期间读取数据。

  5. 然后,用新的ProgressPrinter实例初始化progress_writer变量,用于记录训练过程中的输出。

  6. 最后,在loss上调用train方法,提供MinibatchSourceinput_map作为model_inputs_to_stream keyword参数。

当你从本章的示例代码中打开Training with a minibatch source.ipynb时,你可以运行此部分的代码。我们已经包括了一个progress printer实例,用于可视化训练会话的输出。运行代码后,你将看到类似以下的输出:

average since average since examples loss    last  metric  last ------------------------------------------------------ Learning rate per minibatch: 0.1
1.21    1.21  0       0     32
1.15    1.12  0       0     96
1.09    1.09  0       0     32
1.03    1.01  0       0     96
0.999   0.999 0       0     32
0.999   0.998 0       0     96
0.972   0.972 0       0     32
0.968   0.966 0       0     96
0.928   0.928 0       0     32
[...]

控制小批量循环

在上一节中,我们已经看到了如何使用MinibatchSource配合 CTF 格式将数据传递给 CNTK 训练器。但是大多数数据集并不是以这种格式提供的。因此,除非你创建自己的数据集或将原始数据集转换为 CTF 格式,否则你不能使用这种格式。

CNTK 当前仅支持有限的deserializers,用于图像、文本和语音。你目前不能扩展这些反序列化器,这限制了你使用标准MinibatchSource的能力。你可以创建自己的UserMinibatchSource,但这是一个复杂的过程。所以,在这里我们不会向你展示如何构建自定义的MinibatchSource,而是看一下如何手动将数据传递给 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)

模型与前几节保持一致;它是一个基本的分类模型,具有四个输入神经元和三个输出神经元。我们将使用类别交叉熵损失,因为这是一个多类别分类问题。

让我们使用手动小批量循环来训练模型:

import pandas as pd
import numpy as np
from cntk.losses import cross_entropy_with_softmax
from cntk.logging import ProgressPrinter
from cntk.learners import sgd
from cntk.train import Trainer

labels = input_variable(3)
loss = cross_entropy_with_softmax(z, labels)
learner = sgd(z. parameters, 0.1)

progress_writer = ProgressPrinter(0)
trainer = Trainer(z, (loss, None), learner, progress_writer)

input_data = pd.read_csv('iris.csv', 
    names=['sepal_length', 'sepal_width','petal_length','petal_width', 'species'], 
    index_col=False, chunksize=16)

for df_batch in input_data:
    feature_values = df_batch.iloc[:,:4].values
    feature_values = feature_values.astype(np.float32)

    label_values = df_batch.iloc[:,-1]
    label_values = label_values.map(lambda x: label_mapping[x])
    label_values = label_values.values

    encoded_labels = np.zeros((label_values.shape[0], 3))
    encoded_labels[np.arange(label_values.shape[0]), label_values] = 1.

    trainer.train_minibatch({features: feature_values, labels: encoded_labels})

按照给定的步骤进行:

  1. 首先,导入训练神经网络所需的组件。

  2. 接下来,定义一个input_variable来存储标签。

  3. 然后,使用cross_entropy_with_softmax函数定义loss函数,并将神经网络的输出和标签变量连接到它。

  4. 之后,使用神经网络的参数和学习率0.1初始化learner

  5. 创建一个新的ProgressWriter实例,用于记录训练过程的输出。

  6. 接下来,创建Trainer类的新实例,并使用网络、losslearnerprogress_writer对其进行初始化。

  7. 初始化网络后,从磁盘加载数据集,并使用chunksize关键字参数,使其分块读取,而不是一次性将整个数据集加载到内存中。

  8. 现在创建一个新的for循环,遍历数据集的各个数据块。

  9. 通过提取适当格式的labelsfeatures来处理每个数据块。使用前四列作为神经网络的输入特征,最后一列作为标签。

  10. 使用one_hot函数将标签值转换为独热编码向量,该函数来自于与 pandas DataFrame 一起使用章节。

  11. 最后,调用trainer上的train_minibatch方法,并将featureslabels传递给它。

请注意,我们没有编写任何代码来运行多轮训练。如果你需要,你可以通过在另一个for循环中包装读取和处理 CSV 文件中小批量数据的逻辑来实现。可以参考本章示例代码中的Training with a manual minibatch loop.ipynb笔记本来尝试一下。

你会发现,使用手动小批量循环准备单个小批量需要更多的工作。这主要是因为我们没有使用标准MinibatchSource逻辑中的自动分块功能。此外,由于我们没有事先对数据集进行预处理,我们需要在训练过程中对标签进行编码。

当你必须处理大型数据集且无法使用MinibatchSource时,手动小批量循环是最后的选择。然而,这种方法更强大,因为你可以更好地控制模型的训练过程。如果你希望对每个小批量执行复杂的操作或在训练过程中更改设置,使用手动小批量循环会非常有用。

总结

在本章中,我们探讨了如何使用小型和大型数据集来训练神经网络。对于较小的数据集,我们讨论了如何通过在loss函数上调用train方法来快速训练模型。对于较大的数据集,我们则探索了如何同时使用MinibatchSource和手动小批量循环来训练网络。

使用正确的训练方法可以显著影响训练模型所需的时间以及最终模型的效果。现在你可以在使用内存数据和分块读取数据之间做出明智的选择。确保你通过调整小批量大小设置来进行实验,看看哪个设置最适合你的模型。

直到本章为止,我们还没有研究如何监控你的模型。我们确实看过一些使用进度写入器的片段,帮助你可视化训练过程,但这些功能相对有限。

在下一章,我们将学习如何衡量神经网络的性能。我们还将探讨如何使用不同的可视化和监控工具来监控和调试 CNTK 模型。

第四章:验证模型性能

当你构建了一个深度学习模型并使用神经网络时,你面临的问题是,模型在面对新数据时的预测表现如何。模型做出的预测是否足够准确,以便在实际场景中使用?在本章中,我们将讨论如何衡量深度学习模型的性能。我们还将深入研究工具,帮助你监控和调试你的模型。

在本章结束时,你将对不同的验证技术有一个扎实的理解,这些技术可以用来衡量你的模型性能。你还将了解如何使用诸如 TensorBoard 之类的工具,深入探讨你的神经网络的细节。最后,你将知道如何应用不同的可视化方法来调试你的神经网络。

本章将涵盖以下主题:

  • 选择一个好的策略来验证模型性能

  • 验证分类模型的性能

  • 验证回归模型的性能

  • 衡量大数据集的性能

  • 监控你的模型

技术要求

我们假设你已经在电脑上安装了最新版本的 Anaconda,并且已经按照第一章的步骤,开始使用 CNTK,在电脑上安装了 CNTK。本章的示例代码可以在我们的 GitHub 仓库中找到,地址是github.com/PacktPublishing/Deep-Learning-with-Microsoft-Cognitive-Toolkit-Quick-Start-Guide/tree/master/ch4

在本章中,我们将在 Jupyter Notebooks 中进行一些示例操作。要访问示例代码,请在你下载代码的目录中,在 Anaconda 提示符下运行以下命令:

cd ch4
jupyter notebook

我们将在每个部分提到相关的 notebook,以便你可以跟着做并亲自尝试不同的技术。

查看以下视频,看看代码的实际运行:

bit.ly/2TVuoR3

选择一个好的策略来验证模型性能

在我们深入讨论不同模型的验证技术之前,先简单谈一下深度学习模型的验证方法。

当你构建一个机器学习模型时,你是用一组数据样本来训练它。机器学习模型从这些样本中学习并推导出一般规则。当你将相同的样本输入模型时,它将在这些样本上表现得很好。然而,当你输入一些新的、在训练时没有使用过的样本时,模型的表现会有所不同。它可能在这些样本上做得更差。这是因为你的模型总是倾向于偏向它之前见过的数据。

但我们不希望模型仅仅擅长预测它之前见过的样本的结果。它需要对模型之前没有见过的样本表现良好,因为在生产环境中,你将会得到不同的输入,需要预测结果。为了确保我们的模型表现良好,我们需要使用一组未用于训练的样本来验证它。

让我们来看看两种不同的创建数据集用于验证神经网络的方法。首先,我们将探讨如何使用保留数据集。之后,我们将重点介绍创建单独验证数据集的更复杂方法。

使用保留数据集进行验证

创建一个数据集来验证神经网络的第一个也是最简单的方法就是使用保留集。你将从训练中保留一组样本,并在训练完成后使用这些样本来衡量模型的性能:

训练样本与验证样本之间的比例通常是 80%的训练样本和 20%的测试样本。这确保了你有足够的数据来训练模型,同时也有足够的样本来获得准确的性能度量。

通常,你会从主数据集中随机选择样本来包含在训练集和测试集中。这确保了在各个集合之间能够均匀分配样本。

你可以使用train_test_split函数来自scikit-learn库来生成你自己的保留集。该函数接受任意数量的数据集,并根据train_sizetest_size关键词参数将其分成两个部分:

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)

每次运行训练会话时,随机划分数据集是一个好的做法。深度学习算法(例如在 CNTK 中使用的算法)高度依赖于随机数生成器,以及在训练过程中你为神经网络提供样本的顺序。因此,为了平衡样本顺序的影响,你需要每次训练模型时都对数据集的顺序进行随机化。

使用保留集在你想快速衡量模型性能时非常有效。当你有一个大的数据集或训练时间较长的模型时,这种方法也非常有用。但使用保留法也有一些缺点。

你的模型对训练过程中提供的样本顺序非常敏感。此外,每次你开始新的训练时,计算机中的随机数生成器会提供不同的值来初始化神经网络中的参数。这可能导致性能指标的波动。有时,你会得到非常好的结果,但有时也会得到很差的结果。最终,这种情况是不可取的,因为它不可靠。

在对包含应作为单个输入处理的样本序列的数据集进行随机化时要小心,例如在处理时间序列数据集时。像 scikit-learn 这样的库并不正确地处理这类数据集,你可能需要编写自己的随机化逻辑。

使用 k 折交叉验证

你可以通过使用一种叫做 k 折交叉验证的技术来提高模型性能度量的可靠性。交叉验证执行的技术与留出集相同。但它执行多次—通常是 5 到 10 次:

k 折交叉验证的过程如下:首先,将数据集划分为训练集和测试集。然后使用训练集训练模型。最后,使用测试集计算模型的性能度量。这个过程会重复进行所需的次数—通常是 5 到 10 次。在交叉验证过程结束时,会计算所有性能度量的平均值,得到最终的性能度量。大多数工具还会给出各个值,以便你看到不同训练运行之间的差异。

交叉验证给你提供了更稳定的性能衡量,因为你使用了更现实的训练和测试场景。在生产中,样本的顺序是未定义的,这可以通过多次运行相同的训练过程来模拟。此外,我们使用了单独的留出集来模拟未见过的数据。

使用 k 折交叉验证在验证深度学习模型时会消耗大量时间,所以要明智地使用它。如果你仍在进行模型设置的实验,最好使用基本的留出技术。稍后,当你完成实验后,可以使用 k 折交叉验证来确保模型在生产环境中的表现良好。

请注意,CNTK 不支持执行 k 折交叉验证。你需要编写自己的脚本来实现这一功能。

那么,什么是欠拟合和过拟合呢?

当你开始收集神经网络的度量时,无论是使用留出数据集还是应用 k 折交叉验证,你会发现训练数据集和验证数据集的输出度量是不同的。在本节中,我们将看看如何利用收集的度量信息来检测模型的过拟合和欠拟合问题。

当模型发生过拟合时,它在训练过程中见过的样本上表现得非常好,但在新样本上却表现不好。你可以通过查看度量来检测过拟合。当测试集上的度量低于训练集上的相同度量时,你的模型就发生了过拟合。

过拟合对业务有很大负面影响,因为你的模型无法理解如何处理新的样本。但在模型中有一些过拟合是合乎逻辑的;这是预期的,因为你希望最大化模型的学习效果。

过拟合的问题在模型训练时所用的数据集无法代表其应用的真实世界环境时会变得更严重。这样,你最终会得到一个过度拟合数据集的模型。它会在新样本上产生随机输出。遗憾的是,你无法检测到这种类型的过拟合。发现这个问题的唯一方法是将模型投入生产,使用适当的日志记录和用户反馈来衡量模型的表现。

与过拟合类似,你也可能遇到欠拟合的模型。这意味着模型没有从训练集学到足够的知识,无法预测有用的输出。你可以通过性能指标轻松检测到这一点。通常,性能指标会比你预期的要低。实际上,当你开始训练第一个周期时,模型会出现欠拟合,并随着训练的进行,欠拟合程度会逐渐减小。

一旦模型训练完成,它仍然可能会出现欠拟合。你可以通过查看训练集和测试集的指标来检测这一点。当测试集的指标高于训练集的指标时,你的模型就是欠拟合的。你可以通过仔细查看模型的设置并对其进行调整,以确保下次训练时模型表现更好。你也可以尝试训练更长时间,看看是否有帮助。

监控工具有助于检测模型的欠拟合和过拟合。因此,确保你使用这些工具。我们将在后面的监控你的模型一节中讨论如何在 CNTK 中使用它们。

验证分类模型的性能

在上一节中,选择合适的策略来验证模型性能,我们讨论了为你的神经网络选择一个好的验证策略。在接下来的几节中,我们将深入探讨为不同类型的模型选择度量标准。

当你构建分类模型时,你需要寻找能够表达正确分类样本数量的度量标准。你可能还会关心测量错误分类的样本数量。

你可以使用混淆矩阵——一个将预测输出与期望输出进行对比的表格——来详细了解模型的性能。这可能变得有些复杂,所以我们也将探讨一种使用 F-measure 来衡量模型性能的方法。

使用混淆矩阵验证你的分类模型

让我们仔细看看如何使用混淆矩阵来衡量分类模型的性能。为了理解混淆矩阵的工作原理,我们来为一个二分类模型创建一个混淆矩阵,该模型预测信用卡交易是正常的还是欺诈的:

实际欺诈 实际正常
预测欺诈 真正例 假正例
预测正常 假阴性 真阴性

样本的混淆矩阵包含两列和两行。我们有一列表示欺诈类别,另一列表示正常类别。我们还为欺诈和正常类别添加了行。表格中的单元格将包含数字,告诉我们有多少样本被标记为真正例、真阴性、假正例和假阴性。

当模型正确预测某笔交易为欺诈时,我们称之为真正例。当我们预测为欺诈,但该交易本应不被标记为欺诈时,我们称之为假正例。

您可以从混淆矩阵计算出许多不同的东西。首先,您可以根据混淆矩阵中的值计算精度:

精度告诉你我们预测的所有样本中有多少是正确预测的。高精度意味着你的模型很少出现假正例。

我们可以根据混淆矩阵计算的第二个指标是召回率指标:

召回率告诉你数据集中有多少欺诈案例实际上被模型检测到。高召回率意味着你的模型在发现欺诈案例方面表现良好。

最后,您可以计算模型的整体准确度:

整体准确度告诉你模型作为整体的表现如何。但在数据集不平衡时使用这个指标是有风险的。例如:如果你有 100 个样本,其中 5 个标记为欺诈,95 个标记为正常,那么如果对所有样本都预测为正常,得到的准确度为0.95。这看起来很高,但我们只是在自欺欺人。

计算平衡准确度要好得多。为此,我们需要知道模型的精度和特异性。我们已经知道如何计算模型的精度。我们可以使用以下公式计算特异性:

特异性告诉我们模型在检测样本是正常而非欺诈时的表现如何。它是精度的完全反向,精度告诉我们模型在检测欺诈时的表现如何。

一旦我们获得了特异性,我们可以将其与精度指标结合,计算平衡准确度:

平衡准确度告诉我们模型在将数据集分为欺诈和正常案例时的表现如何,这正是我们所期望的。让我们回到之前的准确度度量,并使用平衡版本的准确度指标重新尝试:

记住,我们有 100 个样本,其中 5 个应该标记为欺诈。当我们将所有样本都预测为正常时,我们的精确度为 0.0,因为我们没有正确预测任何欺诈案例。特异度为 0.95,因为在 100 个样本中,我们错误地将 5 个预测为正常。最终结果是一个平衡准确度 0.475,显然这个值并不高。

现在你对混淆矩阵的外观和工作原理有了基本了解,让我们谈谈更复杂的情况。当你有一个多类分类模型,且类别数超过两个时,你需要用更多的行和列来扩展矩阵。

例如:当我们为一个预测三种可能类别的模型创建混淆矩阵时,可能会得到以下结果:

实际 A 实际 B 实际 C
预测为 A 91 75 60
预测为 B 5 15 30
预测为 C 4 10 10

我们仍然可以计算该矩阵的精确度、召回率和特异度。但这样做更复杂,而且只能逐类进行。例如:当你想要计算 A 类的精确度时,你需要取 A 类的真正例率,91,然后将其除以实际为 A 但被预测为 B 和 C 的样本数,总共有 9 个。这就得到了以下计算:

计算召回率、特异度和准确率的过程也差不多。要获得指标的整体值,你需要对所有类进行平均计算。

有两种策略可以用来计算平均指标,比如精确度、召回率、特异度和准确率。你可以选择计算微平均或宏平均。我们首先来探讨使用精确度指标的宏平均:

要计算精确度指标的宏平均,我们首先将所有类别的精确度值相加,然后除以类别的数量,k。宏平均不考虑类别的不平衡。例如:A 类可能有 100 个样本,而 B 类只有 20 个样本。计算宏平均会导致结果失衡。

在处理多类分类模型时,最好为不同的指标—精确度、召回率、特异度和准确率—使用微平均。我们来看看如何计算微平均精确度:

首先,我们将每个类别的所有真正例相加,然后将其除以每个类别的真正例和假阴性的总和。这样我们就能更平衡地看待不同的指标。

使用 F-measure 作为混淆矩阵的替代

虽然使用精确度和召回率可以帮助你了解模型的性能,但它们不能同时最大化。这两个度量之间有着密切的关系:

让我们看看精确度和召回率之间的关系是如何体现的。假设你想使用深度学习模型将细胞样本分类为癌症或正常。理论上,要实现模型的最大精确度,你需要将预测数量减少到 1。这给你提供了最大机会达到 100%精确度,但召回率会变得非常低,因为你错过了很多可能的癌症病例。当你想要最大化召回率,尽可能多地检测到癌症病例时,你需要进行尽可能多的预测。但这会降低精确度,因为增加了出现假阳性的可能性。

实际上,你会发现自己在精确度和召回率之间进行平衡。你应该主要追求精确度还是召回率,取决于你希望模型预测的内容。通常,你需要与模型的用户进行沟通,以确定他们认为最重要的是什么:低假阳性数量,还是高概率发现那一个患有致命疾病的病人。

一旦你在精确度和召回率之间做出选择,你需要找到一种方法来用度量表达这一选择。F-度量允许你做到这一点。F-度量表示精确度和召回率之间的调和平均值:

F-度量的完整公式包括一个额外的参数 B,它被设置为 1,以使精确度和召回率的比例相等。这就是所谓的 F1-度量,也是你几乎在所有工具中会遇到的标准。它给召回率和精确度赋予相等的权重。当你想强调召回率时,可以将 B 值设置为 2。或者,当你想强调精确度时,可以将 B 值设置为 0.5。

在下一节中,我们将学习如何使用 CNTK 中的混淆矩阵和 F-度量来衡量分类模型的性能。

在 CNTK 中衡量分类性能

让我们看看如何使用 CNTK 的度量函数为我们在第二章《使用 CNTK 构建神经网络》中使用的花卉分类模型创建一个混淆矩阵。

你可以通过打开本章示例文件中的 Validating performance of classification models.ipynb 笔记本文件来跟随这一节的代码。我们将集中讨论这一节的验证代码。示例代码还包含了如何为模型预处理数据的更多细节。

在我们开始训练和验证模型之前,我们需要准备好训练数据集。我们将把数据集拆分为单独的训练集和测试集,以确保能够正确评估模型的性能:

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)

首先,我们将从sklearn.model_selection包中导入train_test_split函数。然后,我们将特征X和标签y传递给该函数进行拆分。我们将使用 20%的样本用于测试。

请注意,我们正在使用stratify关键字参数。因为我们正在验证一个分类模型,所以我们希望测试集和训练集中的各类别之间能够保持良好的平衡。理想情况下,每个类别在测试集和训练集中的分布应当相同。当你将类别或标签列表传递给stratify关键字时,scikit-learn会基于这些标签均匀地分配样本到训练集和测试集中。

现在我们已经有了训练集和测试集,让我们开始训练模型:

from cntk.losses import cross_entropy_with_softmax
from cntk.learners import sgd 
from cntk.logging import ProgressPrinter

progress_writer = ProgressPrinter(0)
loss = cross_entropy_with_softmax(z, labels)
learner = sgd(z.parameters, 0.1)

train_summary = loss.train((X_train,y_train), 
                           parameter_learners=[learner], 
                           callbacks=[progress_writer], 
                           minibatch_size=16, max_epochs=15)

我们将整个数据集通过训练函数运行,总共进行 15 个周期的训练。我们还包括了一个进度条,以便可视化训练过程。

此时,我们还不知道模型的表现如何。我们知道在 15 个训练周期内,损失值有所下降。但问题是,这个下降程度足够吗?让我们通过将验证样本输入模型并创建一个混淆矩阵来找出答案:

from sklearn.metrics import confusion_matrix

y_true = np.argmax(y_test, axis=1)
y_pred = np.argmax(z(X_test), axis=1)

matrix = confusion_matrix(y_true=y_true, y_pred=y_pred)

print(matrix)

我们使用scikit-learn中的confusion_matrix函数来创建混淆矩阵。这个函数需要真实标签和预测标签。这两个标签需要以 numpy 数组的形式存储,并且数组中的数值表示标签。我们没有这些数值,我们只有标签的二进制表示,因为模型要求的是这个格式。为了修正这个问题,我们需要将标签的二进制表示转换为数字表示。你可以通过调用numpy包中的argmax函数来实现。confusion_matrix函数的输出是一个 numpy 数组,类似于这样:

[[ 8 0 0]
 [ 0 4 6]
 [ 0 0 10]]

混淆矩阵有三行三列,因为我们模型可以预测三种可能的类别。输出结果本身不太容易阅读。你可以使用另一个叫做seaborn的包将这个表格转换为热力图:

import seaborn as sns
import matplotlib.pyplot as plt

g = sns.heatmap(matrix, 
                annot=True, 
                xticklabels=label_encoder.classes_.tolist(), 
                yticklabels=label_encoder.classes_.tolist(), 
                cmap='Blues')

g.set_yticklabels(g.get_yticklabels(), rotation=0)

g.set_xlabel('Predicted species')
g.set_ylabel('Actual species')
g.set_title('Confusion matrix for iris prediction model')

plt.show()

首先,我们基于混淆矩阵创建一个新的热力图。我们传入在数据集预处理时使用的label_encoder的类别,用于设置行和列的标签。

标准的热力图需要一些调整才能更易读。我们使用了一个自定义的热力图配色方案。我们还在 X 轴和 Y 轴上使用了自定义标签。最后,我们添加了标题并显示了图表:

标准热力图

从混淆矩阵来看,你可以迅速了解模型的表现。在这个例子中,模型漏掉了相当多的 Iris-versicolor 类别的样本。仅有 60%的该物种的花朵被正确分类。

虽然混淆矩阵提供了很多关于模型在不同类别上表现的详细信息,但有时获取一个单一的性能指标可能更有用,这样你就可以轻松地比较不同的实验。

获取单一性能指标的一种方法是使用 CNTK 中的classification_error度量。它计算被误分类样本的比例。

为了使用它,我们需要修改训练代码。我们不再只是拥有一个loss函数来优化模型,而是还要包含一个指标。之前我们只创建了一个loss函数实例,这一次我们需要编写一个criterion函数,生成一个组合的lossmetric函数,以便在训练过程中使用。以下代码演示了如何操作:

import cntk

@cntk.Function
def criterion_factory(output, target):
    loss = cntk.losses.cross_entropy_with_softmax(output, target)
    metric = cntk.metrics.classification_error(output, target)

    return loss, metric

按照给定的步骤操作:

  1. 首先,创建一个新的 Python 函数,将我们的模型作为output参数,并将我们想要优化的目标作为output参数。

  2. 在函数内,创建一个loss函数,并为其提供outputtarget

  3. 接下来,创建一个metric函数,并为其提供outputtarget

  4. 在函数的最后,返回一个元组,元组的第一个元素是loss函数,第二个元素是metric函数。

  5. @cntk.Function标记该函数。这将包装lossmetric,以便我们可以在其上调用train方法训练模型,并调用test方法来验证模型。

一旦我们拥有了组合的lossmetric函数工厂,就可以在训练过程中使用它:

from cntk.losses import cross_entropy_with_softmax
from cntk.learners import sgd 
from cntk.logging import ProgressPrinter

progress_writer = ProgressPrinter(0)
loss = criterion_factory(z, labels)
learner = sgd(z.parameters, 0.1)

train_summary = loss.train((X_train,y_train), 
                           parameter_learners=[learner], 
                           callbacks=[progress_writer], 
                           minibatch_size=16, max_epochs=15)

按照给定的步骤操作:

  1. 首先,从losses模块导入cross_entropy_with_softmax函数。

  2. 接下来,从learners模块导入sgd学习器。

  3. 同时,从logging模块导入ProgressPrinter,以便记录训练过程的输出。

  4. 然后,创建一个新的实例progress_writer,用于记录训练过程的输出。

  5. 之后,使用新创建的criterion_factory函数创建loss,并将模型变量zlabels变量作为输入。

  6. 接下来,使用sgd函数创建learner实例,并为其提供参数以及学习率0.1

  7. 最后,使用训练数据、learnerprogress_writer调用train方法。

当我们调用loss函数时,我们会得到略微不同的输出。除了损失值,我们还可以看到训练过程中metric函数的输出。在我们的例子中,metric的值应该随着时间的推移而增加:

 average      since    average      since      examples
 loss       last     metric       last 
 ------------------------------------------------------
Learning rate per minibatch: 0.1
 1.48       1.48       0.75       0.75            16
 1.18       1.03       0.75       0.75            48
 0.995      0.855      0.518      0.344           112
 1.03       1.03      0.375      0.375            16
 0.973      0.943      0.396      0.406            48
 0.848      0.753      0.357      0.328           112
 0.955      0.955      0.312      0.312            16
 0.904      0.878      0.375      0.406            48

最后,当我们完成训练时,可以使用test方法来计算分类错误,该方法基于我们之前创建的测试集,使用loss/metric组合函数:

loss.test((X_test, y_test))

当你在loss函数上执行test方法,并使用数据集时,CNTK 会将你提供的样本作为该函数的输入,并基于输入特征X_test进行预测。然后,它会将预测结果和存储在y_test中的值一起传入我们在criterion_factory函数中创建的metric函数。这会生成一个表示指标的单一标量值。

我们在这个示例中使用的classification_error函数衡量了真实标签和预测标签之间的差异。它返回一个表示被错误分类的样本百分比的值。

classification_error函数的输出应该与我们在创建混淆矩阵时看到的结果一致,类似于下面的样子:

{'metric': 0.36666666666666664, 'samples': 30}

结果可能会有所不同,因为初始化模型时使用的随机数生成器。你可以通过以下代码为随机数生成器设置固定的随机种子:

import cntk
import numpy 

cntk.cntk_py.set_fixed_random_seed(1337)
numpy.random.seed = 1337

这将修正一些输出的变异性,但并不能修正所有变异。CNTK 中有一些组件会忽略固定的随机种子,每次运行训练代码时仍然会生成不同的结果。

CNTK 2.6 包括了fmeasure函数,它实现了我们在“使用 F 值作为替代混淆矩阵”章节中讨论的 F 值。你可以通过在定义criterion factory函数时,将对cntk.metrics.classification_error的调用替换为对cntk.losses.fmeasure的调用,来在训练代码中使用fmeasure

import cntk

@cntk.Function
def criterion_factory(output, target):
    loss = cntk.losses.cross_entropy_with_softmax(output, target)
    metric = cntk.losses.fmeasure(output, target)

    return loss, metric

再次运行训练代码将生成不同的loss.test方法调用输出:

{'metric': 0.831014887491862, 'samples': 30}

与之前的示例一样,输出可能会有所不同,因为随机数生成器在初始化模型时的使用方式不同。

验证回归模型的性能

在前面的章节“验证分类模型性能”中,我们讨论了如何验证分类模型的性能。现在让我们来看一下如何验证回归模型的性能。

回归模型与分类模型不同,因为对于个体样本没有二进制的对错衡量标准。相反,你希望衡量预测值与实际值之间的接近程度。我们越接近期望的输出,模型的表现就越好。

在本节中,我们将讨论三种衡量用于回归的神经网络性能的方法。我们将首先讨论如何使用不同的错误率函数来衡量性能。然后,我们将讨论如何使用决定系数进一步验证回归模型。最后,我们将使用残差图来详细了解我们的模型表现。

衡量预测准确性

首先,我们来看看验证回归模型的基本概念。正如我们之前提到的,在验证回归模型时,你无法真正判断预测是对还是错。你希望预测值尽可能接近实际值,但一个小的误差范围是可以接受的。

你可以通过查看回归模型的预测值与实际值之间的距离来计算预测的误差范围。这个误差可以用一个简单的公式表示,像这样:

首先,我们计算预测值(带帽的y)与实际值(y)之间的距离并对其进行平方处理。为了得到模型的总体误差率,我们需要将这些平方的距离相加并计算平均值。

需要平方运算符将预测值(带帽的y)和实际值(y)之间的负距离转换为正距离。如果没有这个步骤,我们会遇到问题:例如,在一个样本中距离为+100,接下来的样本中距离为-100,那么最终的错误率将正好是 0。显然,这并不是我们想要的。平方运算符为我们解决了这个问题。

因为我们对预测值与实际值之间的距离进行了平方处理,所以我们对计算机的大错误进行了更多的惩罚。

均方误差函数可以作为验证的指标,也可以作为训练过程中的损失函数。在数学上,这两者没有区别。这样可以让我们在训练过程中更容易看到回归模型的性能。你只需要看损失值,就能大致了解模型的表现。

需要理解的是,使用均方误差函数你得到的是一个距离值。这并不是衡量模型表现好坏的绝对标准。你需要决定预测值与实际值之间的最大可接受距离。例如,你可以规定 90%的预测结果与实际值之间的最大差距应为 5%。这对模型的用户来说非常重要,因为他们通常希望得到某种形式的保证,确保模型的预测在某些限制范围内。

如果你正在寻找表达误差范围的性能数据,那么你可能不会发现均方误差函数有什么用处。相反,你需要一个表达绝对误差的公式。你可以使用平均绝对误差函数来做到这一点:

这个方法计算预测值与实际值之间的绝对距离,并将其求和,然后取平均值。这将给你一个更容易理解的数值。例如,当你讨论房价时,向用户展示一个 5000 美元的误差范围比展示一个 25000 美元的平方误差范围要容易理解得多。后者看起来似乎很大,但实际上并不是,因为它是一个平方值。

我们将专注于如何使用 CNTK 中的指标来验证回归模型。但也要记得与模型的用户进行沟通,了解什么样的性能表现才算足够好。

在 CNTK 中衡量回归模型性能

现在我们已经了解了如何从理论上验证回归模型,接下来让我们看看如何结合使用我们刚刚讨论的各种指标与 CNTK。在本节中,我们将使用以下代码处理一个预测汽车油耗(每加仑多少英里)的模型:

from cntk import default_options, input_variable
from cntk.layers import Dense, Sequential
from cntk.ops import relu

with default_options(activation=relu):
    model = Sequential([
        Dense(64),
        Dense(64),
        Dense(1,activation=None)
    ])

features = input_variable(X.shape[1])
target = input_variable(1)

z = model(features)

按照以下步骤操作:

  1. 首先,从cntk包中导入所需的组件。

  2. 接下来,使用default_options函数定义默认激活函数。在本例中,我们使用relu函数。

  3. 创建一个新的Sequential层集,并提供两个具有64个神经元的Dense层。

  4. Sequential层集添加额外的Dense层,并设置为1个神经元且不带激活函数。这一层将作为输出层。

  5. 创建网络后,在输入特征上创建一个输入变量,并确保其形状与我们将用于训练的特征相同。

  6. 创建另一个大小为 1 的input_variable,用于存储神经网络的预期值。

输出层没有分配激活函数,因为我们希望它是线性的。当你在层中留出激活函数时,CNTK 会使用一个恒等函数,该层不会对数据应用非线性变换。这对于回归场景很有用,因为我们不希望限制输出在特定值范围内。

要训练模型,我们需要拆分数据集并进行一些预处理:

from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split

X = df_cars.drop(columns=['mpg']).values.astype(np.float32)
y = df_cars.iloc[:,0].values.reshape(-1,1).astype(np.float32)

scaler = StandardScaler()
X = scaler.fit_transform(X)

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)

按照给定步骤操作:

  1. 首先,获取数据集并使用drop方法删除mpg列。这将从原始数据集中创建一个副本,我们可以从values属性获取 numpy 向量。

  2. 接下来,使用StandardScaler缩放数据,以便在神经网络中获得-1 到+1 之间的值。这样做有助于避免神经网络中的梯度爆炸问题。

  3. 最后,使用train_test_split函数将数据集拆分为训练集和验证集。

一旦我们拆分和预处理数据,就可以训练神经网络。要训练模型,我们将定义一个组合lossmetric函数:

import cntk 

def absolute_error(output, target):
    return cntk.ops.reduce_mean(cntk.ops.abs(output - target))

@cntk.Function
def criterion_factory(output, target):
    loss = squared_error(output, target)
    metric = absolute_error(output, target)

    return loss, metric

按照给定步骤操作:

  1. 定义一个名为absolute_error的新函数。

  2. absolute_error函数中计算输出和目标之间的平均绝对差。

  3. 返回结果。

  4. 下一步,创建另一个名为criterion_factory的函数。

  5. 使用@cntk.Function标记此函数,告诉 CNTK 在函数上包含traintest方法。

  6. 在函数内部,使用squared_loss函数创建loss

  7. 然后,使用absolute_error函数创建度量。

  8. lossmetric作为一个元组返回。

CNTK 会自动将这些组合成一个单独的可调用函数。当你调用train方法时,损失用于优化神经网络中的参数。当你调用test方法时,度量用于衡量先前训练的神经网络的性能。

如果我们想要为我们的模型测量绝对误差,我们需要编写自己的度量,因为框架中没有包含mean absolute error函数。这可以通过结合 CNTK 中包含的标准运算符来完成。

现在我们有了创建组合lossmetric函数的方法,让我们看看如何使用它来训练模型:

from cntk.logging import ProgressPrinter
from cntk.losses import squared_error
from cntk.learners import sgd

loss = criterion_factory(z, target)
learner = sgd(z.parameters, 0.001)

progress_printer = ProgressPrinter(0)

train_summary = loss.train((X_train,y_train), 
                           parameter_learners=[learner], 
                           callbacks=[progress_printer],
                           minibatch_size=16,
                           max_epochs=10)

我们将使用criterion_factory作为模型的lossmetric组合。当你训练模型时,你会看到损失值随着时间的推移不断下降。我们还可以看到平均绝对误差也在下降:

 average      since    average      since      examples
    loss       last     metric       last              
 ------------------------------------------------------
Learning rate per minibatch: 0.001
      690        690       24.9       24.9            16
      654        636       24.1       23.7            48
      602        563       23.1       22.3           112
      480        373       20.4         18           240
       62         62       6.19       6.19            16
     47.6       40.4       5.55       5.24            48
     44.1       41.5       5.16       4.87           112
     32.9       23.1        4.5       3.92           240
     15.5       15.5       3.12       3.12            16
     15.7       15.7       3.13       3.14            48
     15.8       15.9       3.16       3.18           112
[...]

现在,我们需要确保我们的模型能够像处理训练数据一样有效地处理新数据。为此,我们需要在loss/metric组合上调用test方法,并使用测试数据集。

loss.test((X_test,y_test))

这为我们提供了性能指标的值以及运行时的数据样本数。输出应该类似于以下内容。它较低,告诉我们模型的误差范围很小:

{'metric': 1.8967978561980814, 'samples': 79}

当我们在测试集上调用test方法时,它会将测试数据X_test传递给模型,通过模型获取每个样本的预测值。然后,它将这些预测值与期望的输出y_test一起传入metric函数。这将产生一个单一的标量值作为输出。

测量内存不足数据集的性能

我们已经讨论了很多不同的方法来验证神经网络的性能。到目前为止,我们只需要处理适合内存的数据集。但在生产场景中几乎从不这样,因为训练神经网络需要大量数据。在本节中,我们将讨论如何在内存不足的数据集上使用不同的指标。

测量使用小批量数据源时的性能

当使用小批量数据源时,你需要为损失函数和指标设置稍微不同的配置。让我们回过头来,回顾一下如何使用小批量数据源设置训练,并通过指标扩展它来验证模型。首先,我们需要设置一种方法,将数据提供给模型的训练器:

from cntk.io import StreamDef, StreamDefs, MinibatchSource, CTFDeserializer, INFINITELY_REPEAT

def create_datasource(filename, limit=INFINITELY_REPEAT):
    labels_stream = StreamDef(field='labels', shape=3, is_sparse=False)
    features_stream = StreamDef(field='features', shape=4, is_sparse=False)

    deserializer = CTFDeserializer(filename, StreamDefs(labels=labels_stream, features=features_stream))
    minibatch_source = MinibatchSource(deserializer, randomize=True, max_sweeps=limit)

    return minibatch_source

training_source = create_datasource('iris_train.ctf')
test_source = create_datasource('iris_test.ctf', limit=1)

按照以下步骤进行操作:

  1. 首先,导入创建小批量数据源所需的组件。

  2. 接下来,定义一个新的函数create_datasource,该函数有两个参数,filenamelimitlimit的默认值为INFINITELY_REPEAT

  3. 在函数内,为标签创建一个StreamDef,它从具有三个特征的标签字段读取数据。将is_sparse关键字参数设置为False

  4. 为特征创建另一个StreamDef,它从具有四个特征的特征字段中读取数据。将is_sparse关键字参数设置为False

  5. 接下来,初始化一个新的CTFDeserializer类实例,并指定要反序列化的文件名和数据流。

  6. 最后,使用deserializer创建一个小批量数据源,并配置它以随机打乱数据集,并指定max_sweeps关键字参数,设置为配置的遍历次数。

请记住,在第三章,将数据导入神经网络中提到,为了使用 MinibatchSource,您需要有兼容的文件格式。对于第三章,将数据导入神经网络中的分类模型,我们使用 CTF 文件格式作为 MinibatchSource 的输入。我们已将数据文件包含在本章的示例代码中。有关更多详细信息,请查看Validating with a minibatch source.ipynb文件。

一旦我们拥有数据源,就可以创建与前面章节中使用的模型相同的模型,验证分类模型性能,并为其初始化训练会话:

from cntk.logging import ProgressPrinter
from cntk.train import Trainer, training_session

minibatch_size = 16
samples_per_epoch = 150
num_epochs = 30
max_samples = samples_per_epoch * num_epochs

input_map = {
    features: training_source.streams.features,
    labels: training_source.streams.labels
}

progress_writer = ProgressPrinter(0)
trainer = Trainer(z, (loss, metric), learner, progress_writer)

session = training_session(trainer, 
                           mb_source=training_source,
                           mb_size=minibatch_size, 
                           model_inputs_to_streams=input_map, 
                           max_samples=max_samples,
                           test_config=test_config)

session.train()

按照给定的步骤操作:

  1. 首先,从ProgressPrinter导入,以记录有关训练过程的信息。

  2. 此外,从train模块导入Trainertraining_session组件。

  3. 接下来,定义模型的输入变量与 MinibatchSource 数据流之间的映射。

  4. 然后,创建一个新的ProgressWriter实例,以记录训练进度的输出。

  5. 然后,初始化trainer并为其提供模型、lossmetriclearnerprogress_writer

  6. 最后,调用training_session函数以启动训练过程。为该函数提供training_source、设置以及输入变量与 MinibatchSource 数据流之间的映射。

要在此设置中添加验证,您需要使用一个TestConfig对象,并将其分配给train_session函数的test_config关键字参数。TestConfig对象没有太多您需要配置的设置:

from cntk.train import TestConfig

test_config = TestConfig(test_source)

按照给定的步骤操作:

  1. 首先,从train模块导入TestConfig类。

  2. 然后,创建一个新的TestConfig实例,并将我们之前创建的test_source作为输入。

在训练过程中,您可以通过在train方法中指定test_config关键字参数来使用此测试配置。

当您运行训练会话时,您将得到类似于以下的输出:

 average      since    average      since      examples
    loss       last     metric       last              
 ------------------------------------------------------
Learning rate per minibatch: 0.1
     1.57       1.57      0.214      0.214            16
     1.38       1.28      0.264      0.289            48
     1.41       1.44      0.147     0.0589           112
     1.27       1.15     0.0988     0.0568           240
     1.17       1.08     0.0807     0.0638           496
      1.1       1.03     0.0949      0.109          1008
    0.973      0.845      0.206      0.315          2032
    0.781       0.59      0.409       0.61          4080
Finished Evaluation [1]: Minibatch[1-1]: metric = 70.72% * 30;

首先,您将看到模型使用来自训练 MinibatchSource 的数据进行训练。由于我们将进度打印机配置为训练会话的回调,因此我们可以看到损失的变化。此外,您还会看到一个指标值的增加。这个指标输出来自于我们为training_session函数提供了一个既配置了损失又配置了指标的训练器。

当训练完成时,将使用在TestConfig对象中配置的 MinibatchSource 提供的数据对模型进行测试。

很酷的是,不仅您的训练数据现在以小批次的方式加载到内存中,以避免内存问题,测试数据也以小批次加载。这在处理大型数据集的模型时特别有用,即使是测试时也是如此。

使用手动小批量循环时如何衡量性能。

使用 CNTK 中的常规 API 进行训练时,使用度量来衡量模型在训练过程中和训练后的性能是最简单的方式。但是,当你使用手动小批量循环时,事情就变得更加困难了。不过,这正是你获得最大控制力的地方。

让我们首先回顾一下如何使用手动小批量循环训练模型。我们将使用在验证分类模型性能一节中使用的分类模型。你可以在本章示例代码中的Validating with a manual minibatch loop.ipynb文件中找到它。

模型的损失定义为cross-entropy损失函数与我们在使用 F-measure 作为混淆矩阵的替代方法一节中看到的 F-measure 度量的组合。你可以使用我们在测量分类性能(在 CNTK 中)一节中使用过的函数对象组合,并使用手动训练过程,这是一个很好的补充:

import cntk
from cntk.losses import cross_entropy_with_softmax, fmeasure

@cntk.Function
def criterion_factory(outputs, targets):
    loss = cross_entropy_with_softmax(outputs, targets)
    metric = fmeasure(outputs, targets, beta=1)

    return loss, metric

一旦我们定义了损失,就可以在训练器中使用它来设置手动训练会话。正如你所预期的那样,这需要更多的工作来用 Python 代码编写:

import pandas as pd
import numpy as np
from cntk.logging import ProgressPrinter
from cntk.train import Trainer

progress_writer = ProgressPrinter(0)
trainer = Trainer(z, loss, learner, progress_writer)

for _ in range(0,30):
    input_data = pd.read_csv('iris.csv', 
        names=['sepal_length', 'sepal_width','petal_length','petal_width', 'species'], 
        index_col=False, chunksize=16)

    for df_batch in input_data:
        feature_values = df_batch.iloc[:,:4].values
        feature_values = feature_values.astype(np.float32)

        label_values = df_batch.iloc[:,-1]

        label_values = label_values.map(lambda x: label_mapping[x])
        label_values = label_values.values

        encoded_labels = np.zeros((label_values.shape[0], 3))
        encoded_labels[np.arange(label_values.shape[0]), label_values] = 1.

        trainer.train_minibatch({features: feature_values, labels: encoded_labels})

按照给定步骤操作:

  1. 首先,导入numpypandas包,以便加载和预处理数据。

  2. 接下来,导入ProgressPrinter类,以便在训练过程中记录信息。

  3. 然后,从train模块导入Trainer类。

  4. 导入所有必要的组件后,创建一个ProgressPrinter的实例。

  5. 然后,初始化trainer并提供模型、losslearnerprogress_writer

  6. 要训练模型,创建一个循环,迭代数据集三十次。这将是我们的外部训练循环。

  7. 接下来,使用pandas从磁盘加载数据,并将chunksize关键字参数设置为16,这样数据集就会以小批量的形式加载。

  8. 使用for循环迭代每个小批量,这将是我们的内部训练循环。

  9. for循环中,使用iloc索引器读取前四列作为训练特征,并将其转换为float32

  10. 接下来,读取最后一列作为训练标签。

  11. 标签以字符串形式存储,但我们需要使用 one-hot 向量,所以将标签字符串转换为其数字表示。

  12. 然后,将标签的数字表示转换为 numpy 数组,以便更容易处理。

  13. 之后,创建一个新的 numpy 数组,其行数与我们刚刚转换的标签值相同。但它有3列,表示模型可以预测的可能类别数量。

  14. 现在,基于数字标签值选择列,并将其设置为1,以创建 one-hot 编码标签。

  15. 最后,调用trainer上的train_minibatch方法,并将处理后的特征和标签作为小批量输入。

当你运行代码时,你将看到类似这样的输出:

average since average since  examples 
loss    last  metric  last 
------------------------------------------------------ 
Learning rate per minibatch: 0.1
1.45    1.45  -0.189  -0.189  16 
1.24    1.13  -0.0382  0.0371 48 
1.13    1.04  0.141    0.276  112 
1.21    1.3   0.0382  -0.0599 230 
1.2     1.18  0.037    0.0358 466

因为我们在函数对象中结合了度量和损失,并在训练器配置中使用了进度打印机,所以我们在训练过程中会得到损失和度量的输出。

为了评估模型性能,你需要执行与训练模型类似的任务。只不过这次我们需要使用Evaluator实例来测试模型:

from cntk import Evaluator

evaluator = Evaluator(loss.outputs[1], [progress_writer])

input_data = pd.read_csv('iris.csv', 
        names=['sepal_length', 'sepal_width','petal_length','petal_width', 'species'], 
        index_col=False, chunksize=16)

for df_batch in input_data:
    feature_values = df_batch.iloc[:,:4].values
    feature_values = feature_values.astype(np.float32)

    label_values = df_batch.iloc[:,-1]

    label_values = label_values.map(lambda x: label_mapping[x])
    label_values = label_values.values

    encoded_labels = np.zeros((label_values.shape[0], 3))
    encoded_labels[np.arange(label_values.shape[0]), label_values] = 1.

    evaluator.test_minibatch({ features: feature_values, labels: encoded_labels})

evaluator.summarize_test_progress()

按照给定的步骤进行操作:

  1. 首先,从cntk包中导入Evaluator

  2. 然后,创建Evaluator的一个新实例,并提供loss函数的第二个输出。

  3. 初始化Evaluator后,加载包含数据的 CSV 文件,并提供chunksize参数,以便我们按批次加载数据。

  4. 现在,遍历read_csv函数返回的批次,以处理数据集中的项。

  5. 在这个循环中,读取前四列作为features并将它们转换为float32

  6. 之后,读取标签列。

  7. 由于标签是以字符串形式存储的,我们需要首先将它们转换为数值表示。

  8. 之后,获取底层的 numpy 数组,方便处理。

  9. 接下来,使用np.zeros函数创建一个新数组。

  10. 将第 7 步中获取的标签索引处的元素设置为1,以创建标签的独热编码向量。

  11. 然后,在evaluator上调用test_minibatch方法,并提供特征和编码后的标签。

  12. 最后,使用evaluatorsummarize_test_progress方法来获取最终的性能指标。

当你运行evaluator时,你会得到类似这样的输出:

Finished Evaluation [1]: Minibatch[1-11]: metric = 65.71% * 166;

虽然手动的迷你批次循环在设置训练和评估时需要更多的工作,但它是最强大的之一。你可以改变一切,甚至在训练过程中以不同的间隔进行评估。如果你有一个需要长时间训练的模型,这尤其有用。通过定期测试,你可以监控模型何时开始过拟合,并在需要时停止训练。

监控你的模型。

现在我们已经对模型进行了验证,接下来是谈论在训练过程中如何监控模型。你之前在在 CNTK 中测量分类性能一节以及前面第二章《用 CNTK 构建神经网络》中,通过使用ProgressWriter类看到了一些,但还有更多监控模型的方法。例如:你可以使用TensorBoardProgressWriter。让我们更仔细地看看 CNTK 中的监控是如何工作的,以及如何利用它来检测模型中的问题。

在训练和验证过程中使用回调。

CNTK 允许你在 API 的多个位置指定回调。例如:当你在loss函数上调用 train 时,你可以通过回调参数指定一组回调:

train_summary = loss.train((X_train,y_train), 
                           parameter_learners=[learner], 
                           callbacks=[progress_writer], 
                           minibatch_size=16, max_epochs=15)

如果你正在使用迷你批次源或手动迷你批次循环,可以在创建Trainer时指定回调,用于监控目的:

from cntk.logging import ProgressPrinter

callbacks = [
    ProgressPrinter(0)
]

trainer = Trainer(z, (loss, metric), learner, [callbacks])

CNTK 将在特定的时刻调用这些回调函数:

  • 当一个小批次完成时

  • 当在训练过程中完成整个数据集的遍历时

  • 当一个小批次的测试完成时

  • 当在测试过程中完成整个数据集的遍历时

在 CNTK 中,回调函数可以是一个可调用函数或一个进度写入实例。进度写入器使用特定的 API,按四个不同的时间点写入数据日志。我们将留给你自己探索进度写入器的实现。相反,我们将重点看看如何在训练过程中使用不同的进度写入器来监控模型。

使用ProgressPrinter

你会经常使用的监控工具之一是ProgressPrinter。这个类实现了基本的基于控制台的日志记录,用于监控你的模型。如果需要,它也可以将日志写入磁盘。如果你在分布式训练场景下工作,或者无法登录控制台查看 Python 程序输出,这将特别有用。

你可以像这样创建一个ProgressPrinter实例:

ProgressPrinter(0, log_to_file='test_log.txt'),

你可以在ProgressPrinter中配置很多选项,但我们将仅限于最常用的参数。不过,如果你需要更多的信息,可以访问 CNTK 网站,了解更多关于ProgressPrinter的内容。

当你配置ProgressPrinter时,你可以指定频率作为第一个参数,配置数据多长时间输出一次。当你指定零作为值时,它将在每隔一个小批次(1,2,4,6,8,...)后输出状态信息。你可以将这个设置更改为大于零的值,创建一个自定义的调度。例如,当你输入 3 作为频率时,日志记录器将在每三个小批次后写入状态数据。

ProgressPrinter类还接受一个log_to_file参数。在这里你可以指定一个文件名,将日志数据写入该文件。当使用时,文件的输出将类似于这样:

test_log.txt
CNTKCommandTrainInfo: train : 300
CNTKCommandTrainInfo: CNTKNoMoreCommands_Total : 300
CNTKCommandTrainBegin: train
 average since average since examples
 loss  last  metric  last 
 ------------------------------------------------------
Learning rate per minibatch: 0.1
 8.91 8.91   0.296 0.296 16
 3.64 1      0.229 0.195 48
 2.14 1.02   0.215 0.204 112
 0.875 0.875  0.341 0.341 16
 0.88 0.883  0.331 0.326 48

这与我们在本章中使用ProgressPrinter类时看到的非常相似。

最后,你可以使用ProgressPrinter类的metric_is_pct设置指定如何显示度量。将其设置为False,以便打印原始值,而不是默认策略将度量值打印为百分比。

使用 TensorBoard

虽然ProgressPrinter在 Python 笔记本中监控训练进度时很有用,但它肯定还有很多不足之处。例如:使用ProgressPrinter很难清楚地看到损失和度量随着时间的变化。

在 CNTK 中,有一个很好的替代ProgressPrinter类的工具。你可以使用TensorBoardProgressWriter以原生的 TensorBoard 格式记录数据。

TensorBoard 是 Google 发明的一个工具,用于与 TensorFlow 配合使用。它可以在训练过程中和训练后可视化你模型的各种度量。你可以通过手动安装 PIP 来下载这个工具:

pip install tensorboard

要使用 TensorBoard,首先需要在你的训练代码中设置TensorBoardProgressWriter

import time
from cntk.logging import TensorBoardProgressWriter

tensorboard_writer = TensorBoardProgressWriter(log_dir='logs/{}'.format(time.time()), freq=1, model=z)
  1. 首先,导入time

  2. 接下来,导入TensorBoardProgressWriter

  3. 最后,创建一个新的TensorBoardProgressWriter并提供带时间戳的目录来进行日志记录。确保将模型作为关键字参数提供,以便在训练过程中将其发送到 TensorBoard。

我们选择为每次运行使用单独的日志dir,通过为日志目录设置一个时间戳来参数化。这确保了在同一模型上的多次运行被单独记录,并且可以进行查看和比较。最后,你可以指定在训练过程中使用 TensorBoard 进度写入器的模型。

一旦你训练完模型,确保调用TensorboardProgressWriter实例上的close方法,以确保日志文件完全写入。没有这个步骤,你可能会漏掉训练过程中收集的某些(如果不是全部)指标。

你可以通过在 Anaconda 提示符中使用命令启动 TensorBoard,来可视化 TensorBoard 的日志数据:

tensorboard --logdir logs

--logdir参数应该与所有运行的根dir匹配。在本例中,我们使用logs dir作为 TensorBoard 的输入源。现在,你可以在浏览器中通过控制台中显示的 URL 打开 TensorBoard。

TensorBoard 网页如下所示,默认页面为 SCALARS 标签页:

TensorBoard 网页,默认页面为 SCALARS 标签页

你可以通过选择屏幕左侧的多个运行来查看不同的运行。这使你能够比较不同的运行,看看在这些运行之间发生了哪些变化。在屏幕中间,你可以查看描绘损失和指标随时间变化的不同图表。CNTK 会按小批次和每个周期记录指标,二者都可以用来查看指标随时间的变化。

TensorBoard 还有更多方式来帮助你监控模型。当你进入 GRAPH 标签页时,你可以看到你的模型以精美的图形地图展示出来:

以图形地图的形式显示你的模型

这对于具有许多层的技术复杂型模型尤其有用。它有助于你理解各层之间的连接,并且通过这个标签页,许多开发人员能够避免头疼,因为他们可以发现哪些层是断开的。

TensorBoard 有更多监控模型的方式,但遗憾的是,CNTK 默认只使用 SCALARS 和 GRAPH 标签页。你也可以将图像记录到 TensorBoard 中,如果你在工作中使用它们的话。我们将在第五章《图像处理》中讨论这一点,届时我们将开始处理图像。

总结

在本章中,你学习了如何验证不同类型的深度学习模型,以及如何使用 CNTK 中的指标来实现模型的验证逻辑。我们还探讨了如何使用 TensorBoard 来可视化训练进度和模型结构,以便你可以轻松调试模型。

早期并频繁地监控和验证你的模型,将确保你最终得到的神经网络在生产环境中表现良好,并能按客户的期望工作。这是检测模型欠拟合和过拟合的唯一方法。

现在你已经知道如何构建和验证基本的神经网络,我们将深入探讨更有趣的深度学习场景。在下一章中,我们将探索如何使用图像与神经网络进行图像检测,另外在第六章,时间序列数据的处理,我们将探讨如何构建和验证适用于时间序列数据的深度学习模型,比如金融市场数据。你将需要本章以及前面章节中描述的所有技术,以便在接下来的章节中充分利用更高级的深度学习场景。

第五章:处理图像

在本章中,我们将探讨一些使用 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,并按照其中的说明获取本章的数据。

查看以下视频,看看代码的实际效果:

bit.ly/2Wm6U49

卷积神经网络架构

在前几章中,我们学习了如何使用常规的前馈网络架构来构建神经网络。在前馈神经网络中,我们假设不同输入特征之间存在相互作用,但我们并未对这些相互作用的性质做出假设。然而,这并不总是正确的做法。

当你处理复杂数据(如图像)时,前馈神经网络并不能很好地完成任务。这是因为我们假设网络输入之间存在相互作用,但我们没有考虑到它们是空间上组织的这一事实。当你观察图像中的像素时,它们之间存在水平和垂直关系。图像中的颜色与某些颜色像素的位置之间也有关系。

卷积网络是一种特殊的神经网络,它明确假设我们处理的数据具有空间关系。这使得它在图像识别方面非常有效。不过,其他具有空间组织的数据也同样适用。让我们来探索一种用于图像分类任务的卷积神经网络的架构。

用于图像分类的网络架构

用于图像分类的卷积网络通常包含一个或多个卷积层,后跟池化层,并且通常以常规的全连接层结束,以提供最终的输出,如下图所示:

此图片来源于:https://en.wikipedia.org/wiki/File:Typical_cnn.png

当你仔细观察卷积网络的结构时,你会发现它以一组卷积层和池化层开始。你可以将这一部分看作是一个复杂的、可训练的照片滤镜。卷积层过滤出分类图像所需的有趣细节,而池化层则总结这些特征,以便网络后端处理的数据点更少。

通常,在用于图像分类的神经网络中,你会发现多个卷积层和池化层的组合。这样做是为了能够从图像中提取更多复杂的细节。网络的第一层提取简单的细节,比如线条。接下来的层会将前一层的输出结合起来,学习更复杂的特征,比如角落或曲线。正如你所想的那样,后续的层会用来学习越来越复杂的特征。

通常,当你构建神经网络时,你希望对图像中的内容进行分类。这时,经典的全连接层就发挥了重要作用。通常,用于图像识别的模型会以一个输出层和一个或多个全连接层结束。

让我们来看看如何使用卷积层和池化层来创建一个卷积神经网络。

使用卷积层

现在,既然你已经了解了卷积网络的基本结构,让我们来看看卷积网络中使用的卷积层:

此图片来源于:https://en.wikipedia.org/wiki/File:Conv_layer.png

卷积层是卷积网络的核心构建块。你可以将卷积层看作一个可训练的滤镜,用来从输入中提取重要细节并去除被认为是噪声的数据。一个卷积层包含一组权重,这些权重覆盖一个小区域(宽度和高度),但涵盖了输入的所有通道。当你创建卷积层时,你需要指定其神经元的深度。你会发现,大多数框架,包括 CNTK,在讨论层的深度时,会提到滤镜。

当我们执行前向传播时,我们会将该层的卷积核滑动过输入数据,并在每个卷积核上执行输入数据与权重之间的点积运算。滑动的过程由步幅设置来控制。当你指定步幅为 1 时,最终得到的输出矩阵将具有与输入相同的宽度和高度,但深度与该层的卷积核数量相同。你可以设置不同的步幅,这会减少输出矩阵的宽度和高度。

除了输入大小和卷积核数量外,您还可以配置该层的填充。给卷积层添加填充会在处理过的输入数据周围添加零的边框。虽然这听起来像是一件奇怪的事,但在某些情况下,它是非常有用的。

查看卷积层的输出大小时,它将基于输入大小、卷积核数量、步幅和填充来决定。公式如下:

W 是输入大小,F 是卷积核数量或层深,P 是填充,S 是步幅。

不是所有输入大小、卷积核、步幅和填充的组合都是有效的。例如,当输入大小 W = 10,层深 F = 3,步幅 S = 2 时,最终得到的输出体积是 5.5。并不是所有输入都能完美地映射到这个输出大小,因此 CNTK 会抛出一个异常。这就是填充设置的作用。通过指定填充,我们可以确保所有输入都映射到输出神经元。

我们刚才讨论的输入大小和卷积核数量可能感觉有些抽象,但它们是有道理的。设置较大的输入大小会使得该层捕捉到输入数据中的较粗糙的模式。设置较小的输入大小则使得该层能够更好地检测更精细的模式。卷积核的深度或数量决定了能够检测到多少种不同的模式。在高级别上,可以说一个卷积核使用一个滤波器来检测一种模式;例如,水平线。而一个拥有两个滤波器的层则能够检测两种不同的模式:水平线和垂直线。

为卷积网络设定正确的参数可能需要不少工作。幸运的是,CNTK 提供了一些设置,帮助简化这个过程。

训练卷积层的方式与训练常规的密集层相同。这意味着我们将执行一次前向传播,计算梯度,并使用学习器在反向传播时更新参数的值。

卷积层后面通常会跟着一个池化层,用于压缩卷积层学习到的特征。接下来我们来看池化层。

处理池化层

在前一部分中,我们讨论了卷积层以及它们如何用于从像素数据中提取细节。池化层用于总结这些提取的细节。池化层有助于减少数据量,使得分类这些数据变得更加容易。

理解神经网络在面对具有大量不同输入特征的样本时,分类难度较大的问题非常重要。这就是为什么我们使用卷积层和池化层的组合来提取细节并进行总结的原因:

此图像来自: https://en.wikipedia.org/wiki/File:Max_pooling.png

池化层具有一个下采样算法,你可以通过输入大小和步幅来进行配置。我们将把前一卷积层的每个滤波器的输出传递到池化层。池化层会跨越数据片段,并提取与配置的输入大小相等的小窗口。它从这些小区域的值中选取最大的值作为该区域的输出。就像卷积层一样,它使用步幅来控制它在输入中移动的速度。例如,步幅为 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)

按照给定的步骤进行操作:

  1. 首先,导入神经网络所需的层。

  2. 然后,导入网络的激活函数。

  3. 接下来,导入glorot_uniform initializer函数,以便稍后初始化卷积层。

  4. 之后,导入input_variable函数来创建输入变量,以及default_options函数,使得神经网络的配置更加简便。

  5. 创建一个新的input_variable来存储输入图像,这些图像将包含3个通道(红、绿、蓝),尺寸为28 x 28 像素。

  6. 创建另一个input_variable来存储待预测的标签。

  7. 接下来,创建网络的default_options并使用glorot_uniform作为初始化函数。

  8. 然后,创建一个新的Sequential层集来构建神经网络的结构

  9. Sequential层集中,添加一个Convolutional2D层,filter_shape5strides设置为1,并将过滤器数量设置为8。启用padding,以便填充图像以保留原始尺寸。

  10. 添加一个MaxPooling层,filter_shape2strides设置为2,以将图像压缩一半。

  11. 添加另一个Convolution2D层,filter_shape为 5,strides设置为 1,使用 16 个过滤器。添加padding以保持由前一池化层产生的图像尺寸。

  12. 接下来,添加另一个MaxPooling层,filter_shape为 3,strides设置为 3,将图像尺寸缩小到原来的三分之一。

  13. 最后,添加一个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)

按照给定的步骤操作:

  1. 首先,导入os包,以便访问一些有用的文件系统功能。

  2. 接下来,导入必要的组件来创建一个新的MinibatchSource

  3. 创建一个新的函数create_datasource,该函数接收输入文件夹的路径和一个max_sweeps设置,用来控制我们遍历数据集的频率。

  4. create_datasource函数中,找到源文件夹中的mapping.bin文件。这个文件包含磁盘上的图像和其关联标签之间的映射。

  5. 然后创建一组流定义,用来从mapping.bin文件中读取数据。

  6. 为图像文件添加一个StreamDef。确保包括transforms关键字参数,并初始化为空数组。

  7. 为标签字段添加另一个StreamDef,该字段包含 10 个特征。

  8. 创建一个新的ImageDeserializer,并为其提供mapping_filestream_definitions变量。

  9. 最后,创建一个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)
  1. 首先,调用create_datasource函数,并传入mnist_train文件夹,以创建训练数据源。

  2. 调用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)

按照给定的步骤进行:

  1. 首先,从 cntk 包中导入 Function 装饰器。

  2. 接下来,从 losses 模块中导入cross_entropy_with_softmax函数。

  3. 然后,从 metrics 模块中导入classification_error函数。

  4. 在此之后,从 learners 模块导入sgd学习器。

  5. 创建一个新函数criterion_factory,带有两个参数:output 和 targets。

  6. 使用@Function装饰器标记该函数,将其转换为 CNTK 函数对象。

  7. 在函数内部,创建一个新的cross_entropy_with_softmax函数的实例。

  8. 接下来,创建一个新的classification_error指标的实例。

  9. 将损失和指标作为函数的结果返回。

  10. 在创建criterion_factory函数后,用它初始化一个新的损失。

  11. 最后,使用模型的参数和学习率 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])

按照给定的步骤进行:

  1. 首先从logging模块中导入ProgressPrinter类。

  2. 接下来,从train模块中导入TestConfig类。

  3. 创建一个新的ProgressPrinter实例,以便我们可以记录训练过程的输出。

  4. 然后,使用前面创建的test_datasource作为输入,为神经网络创建TestConfig

  5. 创建一个新的字典,将train_datasource的数据流映射到神经网络的输入变量。

  6. 最后,在loss上调用train方法,并提供train_datasource、训练器的设置、learnerinput_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 的机器,请注意运行本章的示例代码将需要较长时间。

请观看以下视频,查看代码的实际应用:

bit.ly/2TAdtyr

什么是循环神经网络?

循环神经网络是一种特殊类型的神经网络,能够进行时间推理。它们主要用于需要处理随时间变化的数值的场景。

在常规神经网络中,你只能提供一个输入,这样就只能得到一个预测结果。这限制了你使用常规神经网络的能力。例如,常规神经网络在文本翻译方面表现不佳,而循环神经网络在翻译任务中却取得了不少成功的实验。

在循环神经网络中,可以提供一系列样本,这将生成一个单一的预测结果。你还可以使用循环神经网络根据一个输入样本预测输出序列。最后,你可以根据输入序列预测输出序列。

和其他类型的神经网络一样,你也可以在分类任务和回归任务中使用循环神经网络,尽管根据网络输出的结果可能很难识别出循环神经网络执行的任务类型。

循环神经网络的变体

递归神经网络可以以多种方式使用。在本节中,我们将探讨递归神经网络的不同变体,以及它们如何用于解决特定类型的问题。具体来说,我们将关注以下几种变体:

  • 基于输入序列预测单个输出

  • 基于单个输入值预测序列

  • 基于其他序列预测序列

最后,我们还将探索如何将多个递归神经网络堆叠在一起,以及如何在处理文本等场景中提高性能。

让我们来看一下递归网络可以使用的场景,因为有多种方法可以利用递归神经网络的独特特性。

基于序列预测单个输出

递归神经网络包含一个反馈连接到输入。当我们输入一系列值时,它将按时间步处理序列中的每个元素。由于反馈连接,它可以将处理一个元素时生成的输出与下一个元素的输入结合起来。通过将前一时间步的输出与下一时间步的输入结合,它将构建一个跨整个序列的记忆,这可以用来进行预测。从示意图来看,基本的递归神经网络如下所示:

当我们将递归神经网络展开成其各个步骤时,这种递归行为变得更加清晰,下面的图示展示了这一点:

要使用这个递归神经网络进行预测,我们将执行以下步骤:

  1. 首先,我们将输入序列的第一个元素输入,创建一个初始的隐藏状态。

  2. 然后,我们将初始隐藏状态与输入序列中的第二个元素结合,生成更新后的隐藏状态。

  3. 最后,我们将输入序列中的第三个元素,生成最终的隐藏状态并预测递归神经网络的输出。

由于这个反馈连接,您可以训练递归神经网络识别随着时间发生的模式。例如,当你想预测明天的气温时,你需要查看过去几天的天气,以发现一个可以用来确定明天气温的模式。

基于单个样本预测序列

递归神经网络的基本模型也可以扩展到其他用例。例如,您可以使用相同的网络架构,基于单个输入预测一系列值,如下图所示:

在这种情况下,我们有三个时间步,每个时间步将根据我们提供的输入预测输出序列中的一个步骤。

  1. 首先,我们将输入样本送入神经网络,生成初始的隐藏状态,并预测输出序列中的第一个元素。

  2. 然后,我们将初始隐藏状态与相同的样本结合,生成更新后的隐藏状态和输出,预测输出序列中的第二个元素。

  3. 最后,我们再次输入样本,进一步更新隐藏状态,并预测输出序列中的最后一个元素。

从一个样本生成一个序列与我们之前的样本非常不同,之前我们收集了输入序列中所有时间步的信息以得到一个单一的预测。而在这种情况下,我们在每个时间步生成输出。

还有一种递归神经网络的变体,它结合了我们刚刚讨论的设置和前一节中讨论的设置,根据一系列值预测一系列值。

基于序列预测序列

现在我们已经了解了如何根据一个序列预测单个值,或根据一个单独的值预测一个序列,让我们看看如何进行序列到序列的预测。在这种情况下,你执行与前面情景中相同的步骤,其中我们是基于单个样本预测一个序列,如下图所示:

在这种情景下,我们有三个时间步,每个时间步接受输入序列中的元素,并预测一个我们想要预测的输出序列中的对应元素。让我们一步一步地回顾这个情景:

  1. 首先,我们取输入序列中的第一个元素,创建初始的隐藏状态,并预测输出序列中的第一个元素。

  2. 接下来,我们将初始隐藏状态与输入序列中的第二个元素结合,更新隐藏状态,并预测输出序列中的第二个元素。

  3. 最后,我们将更新后的隐藏状态和输入序列中的最后一个元素一起,预测输出序列中的最后一个元素。

所以,和我们在前一节中做的那样,我们不再在每一步重复相同的输入样本,而是一次输入序列中的一个元素,并将每一步生成的预测作为模型的输出序列。

堆叠多个递归层

递归神经网络可以拥有多个递归层。这使得递归网络的记忆容量增大,模型能够学习到更复杂的关系。

例如,当你想翻译文本时,你需要堆叠至少两个递归层,一个用于将输入文本编码为中间形式,另一个用于将其解码为你想翻译成的语言。谷歌有一篇有趣的论文,展示了如何使用这种技术进行语言间的翻译,论文地址是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)

按照给定步骤操作:

  1. 首先,创建一个新的输入变量来存储输入序列。

  2. 然后,初始化神经网络的 default_options,并将 initial_state 设置为 0.1。

  3. 接下来,创建一个神经网络的 Sequential 层集。

  4. 在 Sequential 层集里,提供一个带有 15 个神经元的 LSTM 递归层,并将其包装在一个 Fold 层中。

  5. 最后,添加一个包含一个神经元的 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)

请按照以下步骤操作:

  1. 首先,从cntk包中导入sequence模块、default_options函数和input_variable函数。

  2. 接下来,导入递归神经网络的相关层。

  3. 然后,创建一个新的具有 15 个神经元的LSTM层,并将其包装在Recurrence层中,以便该层返回一个序列,而不是单一的输出。

  4. 现在,创建第二个具有 15 个神经元的LSTM层,但这次将其包装在Fold层中,仅返回最后一个时间步的输出。

  5. 最后,使用特征变量调用创建的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)

按照给定的步骤:

  1. 首先,创建一个新函数create_datasource,它有两个参数:filenamesweeps,其中sweeps的默认值为 INFINITELY_REPEAT,以便我们可以多次迭代相同的数据集。

  2. create_datasource函数中,为小批量数据源定义两个流,一个用于输入特征,一个用于模型的期望输出。

  3. 然后使用CTFDeserializer来读取输入文件。

  4. 最后,返回一个新的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)

按照给定的步骤:

  1. 首先,初始化一个ProgressPrinter来记录训练过程的输出。

  2. 然后,创建一个新的测试配置,使用来自test_datasource的数据来验证神经网络。

  3. 接下来,创建一个映射,将神经网络的输入变量与训练数据源中的流进行关联。

  4. 最后,在损失函数上调用训练方法以启动训练过程。为它提供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

按照给定的步骤:

  1. 首先,导入 pickle 包。

  2. 接下来,定义设置以规范化数据。

  3. 之后,打开test_samples.pkl文件以进行读取。

  4. 文件打开后,使用 pickle.load 函数加载其内容。

  5. 最后,将样本通过网络运行,并用 NORMALIZE 常数乘以它们,以获得太阳能电池板的预测输出。

模型输出的值介于零和一之间,因为这正是我们在原始数据集中存储的值。这些值表示太阳能电池板功率输出的规范化版本。我们需要将它们乘以我们用来规范化原始测量值的规范化值,才能得到太阳能电池板的实际功率输出。

模型的最终反规范化输出如下所示:

array([[ 8161.595],
       [16710.596],
       [13220.489],
       ...,
       [10979.5  ],
       [15410.741],
       [16656.523]], dtype=float32)

使用递归神经网络进行预测与使用任何其他 CNTK 模型进行预测非常相似,区别在于您需要提供的是序列而不是单一样本。

总结

在本章中,我们探讨了如何使用递归神经网络根据时间序列数据进行预测。递归神经网络在处理财务数据、物联网数据或任何其他随时间收集的信息的场景中非常有用。

递归神经网络的一个重要构建模块是FoldRecurrence层类型,您可以将它们与任何递归层类型(如 RNNStep、GRU 或 LSTM)结合使用,以构建递归层集。根据您是要预测序列还是单一值,您可以使用RecurrenceFold层类型来包装递归层。

当您训练递归神经网络时,可以利用存储在 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

查看以下视频,查看代码的实际效果:

bit.ly/2U8YkZf

在 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)

按照以下步骤操作:

  1. 首先,创建一个新的CheckpointConfig,并为检查点模型文件提供文件名,设置在创建新检查点之前的小批量数量作为frequency,并将preserve_all设置为False

  2. 接下来,使用loss上的 train 方法,并在callbacks关键字参数中提供checkpoint_config以使用检查点功能。

当你在训练过程中使用检查点时,你会在磁盘上看到额外的文件,名为 solar.dnnsolar.dnn.ckpsolar.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)

按照以下步骤操作:

  1. 首先,从 cntk 包中导入 ModelFormat 枚举。

  2. 接下来,在训练好的模型上调用 save 方法,指定输出文件名,并将 ModelFormat.ONNX 作为 format 关键字参数。

在 C# 中使用 ONNX 模型

一旦模型存储在磁盘上,我们可以使用 C# 加载并使用它。CNTK 版本 2.6 包含了一个相当完整的 C# API,你可以用它来训练和评估模型。

要在 C# 中使用 CNTK 模型,你需要使用一个名为CNTK.GPUCNTK.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);

按照给定的步骤操作:

  1. 首先,创建一个设备描述符,以便模型在 CPU 上执行。

  2. 接下来,使用 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];
}

按照给定的步骤操作:

  1. 创建一个新的 Predict 方法,该方法接受模型的输入特征。

  2. Predict 方法中,将模型的输入和输出变量存储在两个独立的变量中,方便访问。

  3. 接下来,创建一个字典,将数据映射到模型的输入和输出变量。

  4. 然后,创建一个新的批次,包含一个样本,作为模型的输入特征。

  5. 向输入映射添加一个新条目,将批次映射到输入变量。

  6. 接下来,向输出映射添加一个新条目,映射到输出变量。

  7. 现在,使用输入、输出映射和设备描述符在加载的模型上调用Evaluate方法。

  8. 最后,从输出映射中提取输出变量并检索数据。

本章的示例代码包含一个基本的 .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)

按照给定的步骤操作:

  1. 首先,我们基于刚才创建的配置文件创建一个新的工作区。这将连接到 Azure 中的工作区。一旦连接成功,你可以创建一个新的实验,并为它选择一个名称,然后将其连接到工作区。

  2. 接下来,创建一个新的实验并将其连接到工作区。

在 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)

按照给定的步骤操作:

  1. 首先,导入default_optionsinput_variable函数。

  2. 接下来,从cntk.layers模块导入模型所需的层类型。

  3. 然后,从cntk.ops模块导入log_softmaxsigmoid激活函数。

  4. 创建一个新的Sequential层集。

  5. Sequential层集添加一个新的Dense层,包含 4 个神经元和sigmoid激活函数。

  6. 添加另一个具有 3 个输出的Dense层,并使用log_softmax激活函数。

  7. 创建一个新的input_variable,大小为 4。

  8. 使用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

按照给定的步骤操作:

  1. 导入pandasnumpy模块以加载包含训练样本的 CSV 文件。

  2. 使用 read_csv 函数加载包含训练数据的输入文件。

  3. 接下来,提取前 4 列作为输入特征。

  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]

按照以下步骤操作:

  1. 创建一个标签到其数值表示的映射。

  2. 接下来,定义一个新的工具函数one_hot,将数字值编码为独热向量。

  3. 最后,使用 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函数、learnertrainer,如下所示:

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])
  1. cntk.losses模块导入 cross_entropy_with_softmax 函数。

  2. 接下来,从cnkt.metrics模块导入 classification_error 函数。

  3. 然后,从cntk.learners模块导入sgd学习器。

  4. 创建一个新的input_variable,形状为 3,用于存储标签。

  5. 接下来,创建一个新的 cross_entropy_with_softmax 损失实例,并为其提供模型变量zlabel变量。

  6. 然后,使用 classification_error 函数创建一个新的指标,并为其提供网络和label变量。

  7. 现在,使用网络的参数初始化sgd学习器,并将其学习率设置为 0.001。

  8. 最后,使用网络、lossmetriclearner初始化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 })

按照以下步骤操作:

  1. 要开始一个新的训练,调用实验的start_logging方法。这将创建一个新的run。在该运行的范围内,我们可以执行训练逻辑。

  2. 创建一个新的 for 循环进行 10 个训练周期。

  3. 在 for 循环内,调用trainer上的train_minibatch方法进行模型训练。为其提供输入变量与用于训练的数据之间的映射。

  4. 在此之后,使用来自训练器对象的previous_minibatch_loss_average值记录average_loss指标。

  5. 除了平均损失外,还使用训练器对象的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() })

按照给定步骤操作:

  1. 首先,导入构建脚本所需的组件。

  2. 接着,定义一个全局模型变量,用于存储加载的模型。

  3. 之后,定义 init 函数来初始化脚本中的模型。

  4. 在 init 函数中,使用Model.get_model_path函数检索模型的路径。这将自动定位 Docker 镜像中的模型文件。

  5. 接下来,通过初始化onnxruntime.InferenceSession类的新实例来加载模型。

  6. 定义另一个函数run,该函数接受一个参数raw_data

  7. run函数中,将raw_data变量的内容从 JSON 转换为 Python 数组。

  8. 接着,将data数组转换为 Numpy 数组,这样我们就可以用它来进行预测。

  9. 然后,使用加载的模型的run方法,并将输入特征提供给它。包括一个字典,告诉 ONNX 运行时如何将输入数据映射到模型的输入变量。

  10. 模型返回一个包含 1 个元素的输出数组,用于模型的输出。该输出包含一行数据。从输出数组中选择第一个元素,再从选中的输出变量中选择第一行,并将其存储在prediction变量中。

  11. 最后,返回预测结果作为 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")

按照以下步骤操作:

  1. 首先,从azureml.core.image模块导入 ContainerImage 类。

  2. 接下来,使用ContainerImage.image_configuration方法创建一个新的镜像配置。为其提供 score.py 作为execution_script参数,Python runtime,并最终提供 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

按照以下步骤操作:

  1. 首先,为环境命名。这个步骤是可选的,但在你从此文件本地创建环境进行测试时会很有用。

  2. 接下来,为你的评分脚本提供 Python 版本 3.6.2。

  3. 最后,将一个包含azureml-defaultonnxruntime的子列表添加到 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)

按照以下步骤操作:

  1. 首先,从azureml.core.webservice模块导入 AciWebservice 和 Webservice 类。

  2. 然后,使用 AziWebservice 类上的deploy_configuration方法创建一个新的AciWebservice配置。为其提供一组资源限制,包括 1 个 CPU 和 1GB 内存。

  3. 当你为 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())

按照给定的步骤操作:

  1. 首先,导入 requests 和 json 包。

  2. 接下来,为 service_url 创建一个新变量,并用 Web 服务的 URL 填充它。

  3. 然后,创建另一个变量来存储您想要进行预测的数据。

  4. 之后,使用 requests.post 函数将数据发布到已部署的服务并存储响应。

  5. 最后,读取响应中返回的 JSON 数据以获取预测值。

可以通过以下步骤获取 service_url:

  1. 首先,导航到包含机器学习工作区的资源组。

  2. 然后,选择工作区并在详细信息面板的左侧选择部署部分。

  3. 选择您想查看详细信息的部署,并从详细信息页面复制 URL。

选择部署

当您运行刚创建的脚本时,您将收到包含输入样本预测类别的响应。输出将类似于以下内容:

{"scores": [-2.27234148979187, -2.486853837966919, -0.20609207451343536]}

总结

在本章中,我们了解了将深度学习和机器学习模型投入生产所需的条件。我们探讨了一些基本原理,这些原理将帮助您在持续交付环境中成功实施深度学习。

我们已经查看了将模型导出到 ONNX 的方法,借助 ONNX 格式的可移植性,使得将训练好的模型部署到生产环境并保持多年运行变得更加容易。接着,我们探讨了如何使用 CNTK API 在其他语言中,如 C#,来进行预测。

最后,我们探讨了如何使用 Azure 机器学习服务,通过实验管理、模型管理和部署工具提升您的 DevOps 体验。尽管您不需要像这样的工具就能入门,但在您计划将更大的项目投入生产时,拥有像 Azure 机器学习服务这样的工具,确实能够为您提供很大帮助。

本章标志着本书的结束。在第一章中,我们开始探索了 CNTK。接着,我们学习了如何构建模型、为模型提供数据并评估其性能。基础内容讲解完毕后,我们研究了两个有趣的应用案例,分别涉及图像和时间序列数据。最后,我们介绍了如何将模型投入生产。现在,你应该已经掌握了足够的信息,可以开始使用 CNTK 构建自己的模型了!

posted @ 2025-07-13 15:43  绝不原创的飞龙  阅读(31)  评论(0)    收藏  举报