PyTorch + NumPy这么做会降低模型准确率,这是bug还是预期功能?

2021-04-14 21:10 897 阅读 ID:326
机器之心
机器之心
    近日,有用户在自己的项目中发现了一个微小的 bug,在 PyTorch 同时使用 NumPy 的随机数生成器和多进程数据加载会导致相同的扩充数据,只有专门设置 seed 才可以解决这个 bug,否则会降低模型的准确率。不过,有人认为这并不是一个 bug,而是预期功能,是「按预期工作的」。

    行内人都知道,机器学习(ML)代码中的 bug 很难修复,并且它们不会造成编译错误,而是悄悄地降低准确率。这些 bug 简直防不胜防。最近,一位专注于机器学习的用户遇到了一个非常熟悉的 bug,修复了之后性能有了大幅度提升。这是一个什么样的 bug 呢?

    根据用户的描述,bug 是这样的:除非你在 DataLoader 中使用 worker_init_fn 选项专门设置 seed,否则在 PyTorch 同时使用 NumPy 的随机数生成器和多进程数据加载会导致相同的扩充数据。用户没有这样做,因而这个 bug 悄悄地降低了模型的准确率。

    该 bug 非常小并且很容易出现。所以,这位用户很好奇会不会也对其他项目造成损害呢?ta 从 GitHub 上下载了 10 万个导入 PyTorch 的库,并分析了这些库的源代码。之后,ta 保留了那些具有自定义数据集、同时使用 NumPy 的随机数生成器和多进程数据加载以及或多或少使用抽象语法树进行分析的项目。

    结果显示,95% 以上的库存在着这个 bug,如 PyTorch 的官方教程、OpenAI 的代码以及 NVIDIA 的项目。甚至特斯拉 AI 负责人 Andrej Karpathy 也曾遭受过该 bug 的困扰。

    OpenAI 的 ebm_code_release 项目。

    这个 bug 究竟怎样影响模型的准确率?这位用户从以下两个示例中进行了简要描述。

    bug 描述

    在 PyTorch 中加载、预处理和扩充数据的标准方法是子类化 torch.utils.data.Dataset 并重写 __getitem__方法。要应用扩充方法(如随机裁剪、图像翻转),__getitem__方法经常使用 NumPy 来生成随机数,然后将 map-styled 数据集传递给 DataLoader 来创建 batch。这种训练 pipeline 可能会受到数据预处理的阻碍,因此并行加载数据是有意义的。可以通过增加 DataLoader 对象中的 num_workers 参数来实现。

    问题是,这个工作流导致了相同的数据扩充。

    PyTorch 使用多进程并行加载数据,worker 进程是使用 fork start 方法创建的。这意味着每个工作进程继承父进程的所有资源,包括 NumPy 的随机数生成器的状态。

    示例 1

    为了更加形象地描述问题,用户从以下两个示例中进行了简要概述。

    示例 1 为一个示例数据集,它返回三个元素的随机向量。示例使用两个和四个工作进程的 batch 大小。

    代码返回如下结果:每个进程返回的随机数都是相同的。

    示例 2

    示例 2 演示了如何在 face-landmarks 数据集上使用 Dataset 和 DataLoader 类。此外,还提到了数据扩充的重要性,并提供了一个随机裁剪扩充的例子。这是使用 NumPy 的随机数生成器实现的。

    通过增加 num_workers 来加速数据加载,可以得到相同的裁剪结果:

    batch 大小为 8, num_workers 为 2,random crop augmentation(随机裁剪扩充)

    这个 bug 很容易产生。在某些情况下,它对最终性能的影响很小。在另一些情况下,相同的扩充会导致严重的退化。

    基于对开放源码 PyTorch 项目的分析,发现 bug 的这位用户担心这个问题在许多支持真实产品的代码库中都存在。

    究竟是 bug,还是预期功能或特征?

    这位用户描述的 bug 也引起了众多网友的热议,其中一些人并不认为这是 bug。

    用户「amasterblaster」认为,这不是一个 bug,而是所有种子随机函数的预期功能。这是因为即使在随机实验中,有时你想要对比静态参数的变化,并得到相同的随机数。只有当你被读为真随机(true random)时,才会根据 OS time 设置 seed。

    用户「xicor7017」表示自己也遇到了相同的问题,也认为它并不是一个 bug,而是一个可能不为人所知的特征。如果忽略它的话,调试问题时会很麻烦。

    与此同时,另一些人表达出了不同的观点,认为既然「如果事情朝着人们不希望的方向发展,那么它就不应该这样,也就构成了 bug。」

    用户「IntelArtiGen」称自己意识到了这个 bug,认为它是不正常的,并且对自己的项目造成了一些小问题。用户「gwern」赞同这种观点,认为如果 95% 以上的用户使用时出现错误,则代码就是错的。

    用户「synonymous1964」进一步解读了这个 bug。ta 认为,人们可能误解了这个问题,问题不在于设置特定的随机种子会导致每次训练过程中生成相同序列的随机数,这显然是按预期工作的。相反,问题在于多个数据下载进程中(由 PyTorch 中的 num_workers 设置)的每个进程都会在某个特定的训练过程中输出相同序列的随机数。毫无疑问,这当然会对项目造成影响,具体取决于你如何进行数据加载和扩充。所以,即使这个 bug 是「按预期工作的」,但向更多其他用户指出来也挺好的。

    不知道机器之心的读者,有没有遇到过类似的 bug 呢?如果有,可以在评论中发表自己对该 bug 的观点。

    免责声明:作者保留权利,不代表本站立场。如想了解更多和作者有关的信息可以查看页面右侧作者信息卡片。
    反馈
    to-top--btn