导读
做研究打比赛和真正的做一个机器学习和深度项目是不一样的,如果你有这方面的困惑的话,可以看看这篇文章。
我们处理Kaggle竞赛、黑客马拉松、业余数据科学任务甚至是论文的方式,与在专业工作环境中所期望的是不一样的。在快速原型开发中是没什么问题的,但是你还必须知道如何从单个机器学习代码过渡到结构化的代码版本控制,而这不会损害团队中的软件工程师。不幸的是,要遵循这些样板并不容易。幸运的是,有一套软件工程和机器学习的最佳实践和指南来帮助我们改进方法,并成功地完成这项任务。
这篇文章是一系列文章中的第一篇,将致力于形成一条以整体方式引导深度学习项目的路径。我们将从讨论制定一个好的策略来构建深度学习项目的重要性开始。然后,我们将分解负责在生产上开发深度学习项目的单元,并研究第一个单元。
以下是本系列的文章:
这可能是全球所有软件开发项目的核心。在生产级别上开发软件项目时,你最有可能是在一个团队中工作。该团队将由许多成员组成,他们的职责各不相同——其中一些人将是后端开发人员,一些人将负责文档,而另一些人将负责单元测试。如果你是一个人,事情会变得更糟——作为一个承包商来开发一个PoC,在那里,只有你独自负责PoC的每一个小部件。同样的理论也适用于深度学习项目,因为归根结底,我们不仅想推动该领域的进步,而且对使用深度学习开发应用感兴趣。当涉及到以适当的规模开发具有深度学习的应用时,它只不过是一个软件项目。
软件工程已经存在很久了。已经存在一组与领域无关的通用最佳实践(软件工程中的设计模式),然后还有一些非常特定于领域的实践,12因素应用程序方法论。深度学习实验是由许多模块组成的,即使是在非常初级的阶段。例如数据集构建、模型构建、模型训练、模型调优、模型选择等等。现在,考虑一下将其扩展到生产环境时可能出现的复杂性。这就是软件工程的重要性发挥作用的地方。
深度学习模型的性能可以通过许多不同的方式得到改善。例如,如果缺少数据,可以收集更多的数据,可以训练更长的时间的网络,可以调整深度学习模型的超参数,等等。但是,当涉及到使用深度学习设计整个系统时,经常会看到这些想法失败。即使在提高了训练数据的质量之后,你的模型也可能无法像预期的那样工作,可能是新的训练数据不能很好地代表边缘情况,也可能是训练数据中仍然存在大量的标签噪声,原因有很多。现在,这完全没问题,但是当这个失败发生时,花在收集更多数据、标记它、清理它上的时间就被浪费了。我们希望这个尽可能地减少。拥有一套良好的策略将有助于此。
这对于深度学习非常重要,因为神经网络本质上是随机的。这就是为什么它们能很好的近似函数的原因之一。由于神经网络固有的随机性,在同一个实验中得到不同的结果是完全可能的。例如,今天,你可能在使用网络架构的猫与狗数据集上获得了90%的精确度,而第二天,在使用相同架构和实验pipeline的情况下,你将观察到精确度的±1%的变化。这在生产系统中是不可取的。我们希望结果有一致性,并希望实验尽可能重现。
当你的实验结果传达给你的团队时,请考虑另一个场景。但是,当其他成员试图复制你的实验以便在项目中进一步开发时,他们却无法这样做。更糟糕的是,即使固定了随机数生成器的种子,也不一定能保证重现性。由于依赖项版本、环境设置、硬件配置等原因,结果可能会有所不同。例如,你使用的是 TensorFlow
1.13运行了所有的实验,但是团队的其他成员使用的是最新的版本(2.0.0-beta0)。这可能会在实验执行过程中造成大量不必要的问题。在最坏的情况下,整个代码库可能会因为这种依赖关系不匹配而崩溃。另一个例子是,假设你使用GPU的环境中完成了所有实验,而没有使用任何线程机制加载数据。但是这个环境可能并不总是相同的——你的队友可能没有启用GPU系统。因此,他们可能会得到不同的结果。因此,除了固定随机种子之外,在整个团队中维护一个固定的、一致的开发环境也是很重要的。
软件代码库很大,而且是经常变化的。你正在进行的软件项目很可能像大多数软件一样有多个版本。因此,很自然地,这些版本更改的代码库也会彼此不同,这就产生了对版本控制的需求。代码库的版本控制可能比我们想象的要重要得多。例如,你的软件的最新版本是2.0.1。在执行了严格的用户反馈之后,在你看来,该软件早期版本中的某个特性要优于当前版本。如果开发人员团队不能有效地维护这些不同版本的代码库,你将永远无法轻松地在当前版本的软件中进行更改,或者回滚到以前的工作版本。
说到深度学习,我们不仅有不同版本的代码库,还可能有不同版本的训练数据。你可能已经开始了包含多个图像的训练集的实验,随着项目的进展,你的团队决定向训练集添加更多的图像。你可能会想为什么要单独的版本数据?它不能和代码版本放在一起完成吗?
事实上,这一点经常被忽视。如果有的话,你有可能需要比你的数据更频繁地更改软件代码库。当将软件代码库部署到生产服务器时,如果数据没有被更改,那么将数据与代码库一起推送就没有任何意义。这一点对于项目可能处理的任何卷数据都是实用的。
如果你正确地设置了模型检查点,你可以在深度学习实验中节省大量的时间。通常,模型检查点是指在训练过程中保存你的网络模型。使用不同的策略,有可能不一样。例如,一个非常常见的模型检查点策略是记录在模型训练过程中验证损失在哪里随着训练损失的减少而停止,并保存相应模型的权重。这就是所谓的模型检查点。当你在为深度学习实验做原型实现时,你能够重用这些检查点模型,并且这将绝对避免你从头开始重复可能花费数小时/天的训练。
当你在团队中工作或作为一个单独的机器学习实践者时,将你的深度学习实验的所有部分放在一个良好的结构中非常重要,这样其他团队成员就可以尽可能轻松地引用它们。考虑下面的例子:
在上面的例子中,如果团队成员将大部分时间花在为项目开发寻找正确的文件上,那么它将影响项目开发的有效性。维护一个良好的目录结构可以在很大程度上防止类似的问题。另外,请注意,即使团队同意的项目结构,这也不能代替记录项目存储库/代码版本管理。
如前所述,深度学习是一个高度实验性的领域。在他们的深度学习实验中,总是有很多的可能性和想法可以尝试。事实上,在实验阶段,我们发现自己正在探索改进性能的可能性领域。当然,可能性越多,复杂性就越大。随着代码基的发展,要跟踪执行流并记住哪一部分导致了什么就变得非常困难。这就产生了“技术债务”。这极大地损害了项目的整体开发,因为假设项目团队中的所有成员都理解您在实验中尝试的所有内容是不切实际的。然而,如果您以适当的方式记录您在试验阶段尝试的所有内容,那么跨团队的沟通就会变得容易得多。提供良好的文档,采用高质量的代码重构实践通常被认为是“不那么酷”的开发人员的特征,因为这些事情需要时间,而且被认为很无聊。在项目环境中,有时最好慢一些进行,这样就可以尽可能地减少由技术债引起的通信间隔和较慢的实验迭代周期。
在下一节中,我们将讨论我们可以采取的措施,以检查上述因素,从而给我们的深度学习项目一个良好的结构。我们将模拟一个试点项目场景,并将在此基础上进行构建。
为了帮助我们开发关于构建深度学习项目的坚实知识,可以模拟一个示例项目场景。就本文而言,假设你正在为一家专注于时尚的电子商务公司工作。该公司的目标是建立一个服装分类系统。最终的产品将是一个web应用程序,该应用程序将能够获取服装的图像,并将其类别和评分作为输出。
经理已经向你提供了有关公司感兴趣的性能指标的指导方针,以及其他细节,如推断时间、深度学习模型的最大大小等等。现在,你需要与由前端开发人员、后端开发人员、数据工程师等组成的其他团队一起执行。数据工程师已经收集了一些初始数据,你可以使用这些数据开始实验。但是,你决定与团队的其他成员开会,就项目的合适的目录结构达成一致,而不是直接写代码。到目前为止,你应该已经知道为什么为项目维护目录结构很重要。
假设基础设施团队已经为团队配置了要使用的机器。如果你独立工作,请想象你已经为自己配置好了基础设施。
在这一点上,如果团队中有一组将要进行短期目标的实验,那么将更容易得出什么目录结构可能会运行得很好。理想情况下,在这个阶段,团队应该——
当项目的业务价值仍然不清楚,并且你需要用更有针对性的问题来理解你的客户数据时,这就更加有用了。
随着项目的进展,团队将开发更多的资产
我们必须记住,在这个阶段,将涉及到快速构建原型。作为一个深度学习的实践者,你很可能会尝试不同的网络拓扑结构、不同的损失函数、不同的训练策略等等。机器学习中不存在“免费午餐定理”,这一点在深度学习中更适用。因此,为了确保使用可用的数据训练出一个良好的模型,你可以探索各种可能性和想法。这将导致一些技术债。将有许多文件以无序的方式驻留在项目目录中——重复和冗余的代码、检查点和匿名文件夹中的日志等等。为了解决这个问题,可以遵循一个相对成熟的目录结构。
这本质上是为了将最终模型暴露为REST API的端点。它可以包含基本测试,以测试预测是否按照预期的方式进行,即将模型包装到API端点的所需web服务器逻辑。与这些脚本一起,与开发环境的Docker映像相关的所有规范都将驻留在这里。Docker只是我举的一个例子。它可以是任何有助于将软件打包为容器的东西。你会拥有一个生产服务器(如AppEngine、Lambda), API将部署到该服务器。相应的部署脚本也将位于此目录中。
这就是项目中所有的脚本放的地方。这包括检查代码质量的测试、模型预测的测试、部署任务、训练任务等等。
你可以看到结构中有两个单独的目录—一个包含构建模型所需的所有部分,另一个实际上根据一组模型配置来训练模型。注意模型和网络之间的区别。这是因为神经网络在这里被认为是一个dummy函数,它只有神经元节点,但它不知道如何操作——如何加载数据,如何向前传递,如何产生预测等等。驻留在models中的脚本负责这一点。
注意,这是一个参考结构,它可能因团队和项目而异。在本文和后续文章中,我们将构建它。
在团队就一个相互目录结构达成一致之后,下一个理想的步骤是建立开发工作区——使用环境管理器工具,比如pip
和pipenv
或Docker。
在前面几节中,我们已经讨论了在整个项目中维护公共开发工作区的重要性。我们不希望看到项目因为依赖关系问题或依赖关系更新而中断。假设项目将基于Python作为中央编程语言,我们可以利用“pip”和“pipenv”等工具来委托环境管理任务。因此,有必要讨论这些工具,以了解它们在这个阶段可能有什么用处。
团队成员可能已经在他们的机器上安装了一些依赖项,但是他们可能在版本上有所不同。因此,好的第一步是为项目创建一个虚拟环境或容器,并在整个项目中维护它。项目所需的所有依赖项都应该驻留在这个环境/容器中。
有很多工具可以做到这一点,比如conda
、pipenv
、pip
+virtualenv
等等。他们都很受欢迎。根据我的经验,应该首选pipenv
而不是conda
和pip
+virtualenv
。这是因为有时可能会发生这样的情况:Python包(依赖项)已经在Python包索引(PyPI)中发布了,但没有作为conda
包发布。在这种情况下,你可能必须自己构建一个单独的conda
包。
conda
和pipenv
都能够创建独立的Python环境,但它们解决依赖关系的方式不同。值得注意的是,conda
本质上可以安装任何conda
包,而且它可能并不总是Python包。
在设置工作区时可以使用的另一个解决方案是使用Docker。在下一节中,我们将看到如何实现。
Docker使你能够在容器环境中开发、部署和执行应用程序。我们可以在Docker映像中以独立的方式封装开发环境。然后,我们可以将Docker映像作为Docker容器分发。使用Docker是有利的,因为它提供了将整个开发环境打包为一个容器的能力,该容器可以在其他机器上运行。如果我们将我们的项目作为容器运送到其他方,那么它将特别有用。
深度学习实验包括从数据准备到建模的一系列不同步骤。每一步都建立在另一步之上。可以有一些步骤是集体迭代的。例如,构建一个模型,然后训练它,调优它的超参数,评估它和实验,然后再次回到模型构建步骤。
通常情况下,为了尝试不同的模型体系结构,我们将以不同的方式预处理数据,使其和相应的模型适应。假设你正在使用一个预先训练好的VGG16网络开始你的深度学习实验。为了使数据适合于预训练的网络,你使用网络所训练的数据集的统计数据(平均值和标准偏差)对其进行归一化。然而,这个网络并没有得到满意的结果,因此你决定尝试另一个预训练网络——这次是针对CIFAR10 dataset训练的Inception网络。但是在将原始数据提供给Inception网络之前,你忘记使用CIFAR10 dataset的统计数据对其进行归一化。这确实会导致意想不到的后果,而且你可能无法找出原因。
这就是为什么在开始执行实验之前,准备一个关于整个实验执行流程的心理模型是很重要的。你可以参考它,反复检查你的实验中是否有出乎意料的错误。下面是FloydHub团队在他们的一个内部黑客马拉松项目中构建的这种心理模型的一个例子。
观察这个模型是如何精心策划的。它优雅地概述了团队在做实验时要执行的步骤。它仔细地登记了这些操作,然后按阶段的方式对这些操作进行隔离。这些操作已经相互关联,以便团队能够跟踪执行流程。
关于项目结构要考虑的最后一个指针是数据和代码库的版本控制,我们将在接下来讨论这个问题。
数据的版本控制仍然没有代码库的版本控制那么完善。我们已经阐明了为什么将数据与代码库分离很重要。随着时间的推移,用于训练深度学习系统的数据常常会发生变化。考虑以下场景:
我们现在有一个关于如何构建深度学习项目的软指南。这些对我的经验非常有帮助,我真诚地希望它们对你也有帮助。我们重复了很多关于深度学习的实验。但重要的是要知道,在深入研究之前,有一些基本的工作是需要做的。
数据是机器学习系统的燃料。因此,与数据融为一体非常重要,我真的不能过分强调这一点。在下一节中,我们将深入探讨这个问题。我们将尝试制定一个清单,可以很容易地在你自己的工作中引用。
在本文中,我们讨论了构建深度学习项目时应该考虑的要点。但这段旅程并没有就此结束。在下一篇文章中,我们将继续今天的内容。我们将学习一些在深度学习实验开始之前应该遵循的一般准则。这将包括与数据融为一体、数据转换、探索性数据分析、人工基线等主题。
—END—