如果你从事软件开发,你就会知道 Bug 是生活的一部分。当你开始你的项目时,Bug 就可能存在,当你把你的产品交付给客户时,Bug 也可能存在。在过去的几十年中,软件开发社区已经开发了许多的技术工具、IDE、代码库等来帮助开发者尽早地发现 Bug,以避免在产品交付的时候仍旧存在 Bug。
不幸的是,机器学习开发人员和数据科学家并没有享受到传统软件所提供的强大的调试工具。这就是为什么我们中的许多人在训练脚本中经常性使用 “print” 语句。这一问题在分布式训练和在集群上开展大规模实验时尤其突出,虽然你可以保存工作日志,但是通过这些工作日志来定位 Bug 简直无异于大海捞针。
在这篇文章中,将讨论调试机器学习代码与传统软件的不同之处,以及为什么调试机器学习代码要困难得多。然后,将展示如何使用更好的机制来捕获调试信息、在训练期间实时监控常见问题、发现问题后及时干预以防止发生进一步的错误及浪费计算机资源。
具体地,主要通过 Amazon SageMaker Debugger(一个用于机器学习模型调试的开源库)实现上述目的。
机器学习调试与传统软件开发调试有何不同?
如果机器学习以软件的形式呈现,那么将能够找到许多调试工具来解决 Bug 的问题,比如:
- 使用集成开发环境(IDE),设置断点并检查中间变量;
- 使用开发所使用的编程语言进行异常处理和类型检查;
- 使用静态代码分析工具查找错误并检查是否符合标准;
- 使用诸如 gdb 的调试库;
- 使用日志和“print”语句。
但是现阶段的机器学习调试仍然是一项十分困难的工作,主要原因如下:
机器学习不仅仅是简单的代码
首先,让我们考察一个典型的数据科学问题——面对一个数据集和一个对应的问题描述,需要建立一个基于数据的模型来实现预测,并且评价该模型的准确性,然后在模型达到要求后,进行部署、集成、销售等。
相较于传统软件,机器学习代码涉及到更多的非固定的组分。如:数据集、模型结构、微调过后的模型权重、优化算法及其参数、训练后的梯度等。
在某种意义上,机器学习代码在训练阶段是“动态的”。因为模型本身是随着模型训练而改变或发展的。在训练过程中,模型中的数百万个参数或权重每一步都在变化。一旦训练完成,它就会停止改变,此时,在训练过程中没有发现的错误现在已经成为模型的一部分。而传统软件代码中,有严格的逻辑和规则,不会在每次运行时改变,即使有条件分支,但代码仍然是“静态的”。
调试这个动态的、不断演化的代码需要不同于传统软件开发调试的工具。需要的是通过分析数百万个不断变化的变量来监测训练进度,并在满足某些条件时采取动作。主要通过监视模型参数、优化参数和指标,及时发现诸如梯度消失、activation saturation 等问题。
而调试工具的缺乏,导致大部分机器学习开发人员通过 “print” 语句分析模型训练的过程。
- 难以在机器学习训练过程中实施监测和干预
考虑到效率和经济因素,很多机器学习训练代码运行在集群上,或者至少在各大云平台中,大部分都不是在个人计算机上运行。而在集群上训练模型时设置断点几乎是不可能的。
当你的编程范式改变时,你的调试工具和方法也应该随之改变。在集群上进行分布式训练时,监视进度的主要方法是插入代码以生成日志以供分析。但这是不够的,相反,需要的是一种更简单的方法来实时监控进度,并在满足特定条件时发出提醒或采取一些行动。而这就给我们带来了下一个挑战。
- 调试机器学习代码可能需要大量重写或改变框架
机器学习代码的核心依赖于一系列高度优化的线性代数子程序,这些语言通常用C语言、C++语言和CUDA语言编写。更高层次的框架,如TensorFlow、PyTorch、MXNet和其他框架,对底层程序代码进行封装,并提供一种设计和训练模型的简便方法。当减少代码复杂度时,一定程度上提升了调试的困难度。
机器学习框架的实现方式有以下两种:(1)声明式方法,将模型体系结构定义为一个计算图,然后进行编译、优化和执行(例如TensorFlow)(2)命令式方法,将模型体系结构定义为一个计算图,然后按定义执行(例如Pythorch,TensorFlow eager mode)。在声明式方法中,无法访问优化的计算图,因此调试可能会更困难。在命令式方法中,调试更容易,但需要在较低的级别上测试代码以获取调试数据,在某些情况下,还需要权衡性能。
为了更好地进行调试,必须编写额外的代码加入到训练脚本中,或者重写代码以支持不同的框架。或者更糟的是,在多个框架上维护相同的模型。而这些操作可能会引入更多的 bug。
- Bug 会让开发者在硬件、时间上付出更多的成本
大多数机器学习 Bug 可以在训练过程的早期发现,如一些常见的问题:初始化不好、梯度消失、activation saturation 等。而其他问题则是随着时间的推移而显现的,如过拟合等。而无论是训练早期还是训练后期发现的问题,都将导致资源的浪费。
在上图中可以看到,当模型开始超过20k步时,应该停止。当训练持续到40k步左右,计算成本是原来的两倍。这样的问题很常见,因为普遍存在着指定了固定数量的 epochs 来执行训练,然后出去吃午饭的情况。