作者 | Zahar Chikishev
译者 | 弯月
出品 | CSDN(ID:CSDNnews)

使用 Pytorch 已经好几年了,我很喜欢这个框架。它清晰、直观、灵活,而且速度很快。后来一次偶然的机会,我决定使用 TensorFlow 构建一个新的计算机视觉项目。本文讲述的这个故事就始于此。
在这里插入图片描述
TensorFlow是一个很完善,且使用很广泛的框架。因此,当时我对自己说,不可能太糟糕。鉴于 Google 强大的工程师团队以及机器学习方面的专业知识,我以为相信他们准没错。然而,去年使用了一阵子 TensorFlow 之后,如今的我可以说这个框架的组织非常混乱、维护不善、方向不正确,而且代码中 bug 累累。以下是我遇到的一系列难题。

首先声明,我不是 TensorFlow 专家,文本的观点也许不够全面,有些方面我肯定没有注意到。

安装

这个故事首先从安装开始。TensorFlow 的安装有一个先决条件:需要安装 NVidia CUDA 和 cuDNN 库。

出于某种原因,要下载 cuDNN,必须先注册和登录 NVidia 门户网站,这一点让我觉得很烦。接下来,实际安装的 CUDA 与 cuDNN 的版本必须与 TensorFlow 版本相匹配,否则就无法运行。而且即使你知道版本匹配表在什么地方表(https://www.tensorflow.org/install/source#gpu),也很难找到。这些依赖性会持续引发一系列的问题。实验升级和降级很难,我经常缺少一些库。

除了 tensorflow 本身,还有一个经常使用的 python 包 tensorflow-addons。其中应该是包含一些不太常用的代码,但实际上开发人员总是需要安装两者,因此说到底将代码分割成两部分根本没必要。当然,两个包的版本必须完全匹配,而且也有一个对照表。

最后,还有 tensorflow-gpu。尽管我尽了最大努力,但仍然没有搞清楚为什么这个包至今仍在pip 上, 而且一直在与 tensorflow 同步更新,尽管它在 2 年前就退休了。

安装非常艰难,但面对接下来的工作,我们必须坚强起来。

Eagerexecution 就是一个骗局

Eagerexecution 是 TensorFlow 官方文档引入的一个概念(https://www.tensorflow.org/guide/effective_tf2)。
Eagerexecution 的主要问题在于,它的运行速度通常比图模式慢。对于我的 CNN 模型,Eager execution 的执行速度慢了 5 倍。就冲着这一点,它就是一个非常小众的调试工具,在默认情况下谁会使用这样的工具?
TensorFlow文档中有明确的说明(https://www.tensorflow.org/guide/eager#benchmarks):
对于计算量繁重的模型(如在 GPU 上训练的 ResNet50),EagerExecution 性能与 tf.function 执行相当。但是对于计算量较小的模型来说,这种性能差距会越来越大,并且在为有大量小运算的模型优化热代码路径方面,其性能还有待提升。

尽管他们承诺 Eager Execution 的性能与 tf.function 执行相当,但我使用 ResNet50 进行了测试,结果还是慢了 5 倍。但我也使用了自定义头部和自定义损失函数,对于希望急速处理的人来说,这些处理显然太麻烦了。Keras 团队意识到了这些问题,他们默认的执行模式是图模式。他们甚至强烈建议不要使用eager:
在这里插入图片描述
请注意,Keras 是 TensorFlow 的主要推荐 API。然而,TensorFlow 2.0 却宣布默认情况下会运行 Eager Execution。

tf.data.DataSetAPI 更是一团糟

在 TensorFlow v2 中,构建数据流水线的推荐方法是调用tf.data.DataSet API。从表面上看,“直观”的模块化结构与承诺的性能改进确实不错。但是在试用了很多天之后,我感到很失望。

我的机器学习任务是一个基本的图像分类问题。使用旧的 Sequence API 处理同一个图像加载和增强流水线,能够达到相似的 GPU 利用率,甚至比 tf.data.DataSet 略快。

但更糟糕的是,众所周知,tf.data.DataSet API 非常难以使用,而且调试也非常困难。我们必须明确指定参数类型,这一点似乎完全没必要。如果使用 tf.py_function,还需要在输出中指定张量形状,而文档中却没有很好的解释,你需要大费周折才能找到。

对于处理函数,你需要在 tf.function、tf.py_function 和 tf.numpy_function 之间做出选择。tf.function 是编译版本,完全不支持 Eager Execution。但是 tf.py_function 会很慢,因为每次调用都会锁定 python GIL。

API重复

猜猜看我们一共有多少个二维卷积层的实现?根据 TensorFlow 开发人员的说法,一共有 7 个之多:
1、tf.nn.conv2d
2、tf.keras.layers.Conv2D
3、tf.compat.v1.layers.Conv2D
4、tf.compat.v1.layers.conv2d
5、tf.layers.Conv2D
6、tf.raw_ops.Conv2D
7、tf.contrib.slim.conv2d

其中一些是底层 API,而有些则是高层 API;一些由第三方开发,一些已弃用。但请注意,相应的文档页面上并没有提到这些。
这种重复在 TensorFlow 中随处可见。因此,我们很难知道使用哪些功能才是正确的,有什么区别,以及某些功能之间是否互相兼容但与其他功能不兼容。这是一个非常现实的问题,开发人员每天都必须浪费宝贵的时间和精力从 7 个函数中挑选一个正确的。

开发的各种不严谨

以下是我遇到的其他几个问题。

ImageDataGenerator是 TensorFlow 的增强库,与所有现代库一样,不允许局部生成随机种子。相反,它使用全局 numpy.random.seed()。使用全局种子会导致可复现实验的开发变得异常复杂,尤其是在多线程的情况下。

Sequence 数据 API 方法 on_epoch_end() 没有使用 epoch 作为参数。在几个版本中,它甚至没有被调用,因此相比之下,缺少的参数似乎只是一个小问题。通过这个例子,我们也可以看出 TensorFlow 的测试覆盖率(远低于应有水平)。

TF 中的 GPU 内存管理很糟糕。首先,无论实际模型大小如何,默认情况下都会在模型初始化时占用所有内存。幸运的是,有一个配置选项允许分配的内存按需递增。但接下来还有一个大问题:分配的 GPU 内存在使用后无法释放,释放内存的唯一方法是杀死进程。

TF 训练日志总是包含几个模糊的警告消息。根据版本不同,警告也会不同。我个人不喜欢日志中出现警告消息,我会想办法解决掉。但我无法解决掉 TF 训练日志中的警告,因为通常是一个 TF 函数抱怨另一个 TF 函数。这些函数真的应该选择其他的交流方式。

最后,model.predict() 函数会泄漏 RAM 内存。这不是生产推理需要使用的主要API调用吗?难道 TF不是应该至少能投入生产吗?

结论

我还是换回 Pytorch吧。
本文以 TensorFlow 2.3.0 ~ 2.5.0 为基础。

参考链接:

  • https://medium.com/geekculture/tensorflow-sad-story-cf8e062d84ba
Logo

20年前,《新程序员》创刊时,我们的心愿是全面关注程序员成长,中国将拥有新一代世界级的程序员。20年后的今天,我们有了新的使命:助力中国IT技术人成长,成就一亿技术人!

更多推荐