MMM 0

2018/10/29

初探深度学习

几年前我就想碰深度学习,但一直拖延至今。本学期选修了语言学系的深度学习课程,也不为听课,只求一个外部动力。前两周,教授还在讲线性与逻辑回归,我便耐不住寂寞,用 Swift 写了一个 FFN(feed forward network),并在 MNIST 上达到了 95% 的准确率。

鉴于本片为第零篇 MMM,基调还需定一下。MMM 不是写教程,而是做日志。简单介绍主题可以,但字数要控制,不能洋洋洒洒千余字给高中生讲什么是 FFN,写到最后自己写不下去,弃坑。举个例子,介绍神经网络应该这么写:

所谓神经网络,即优化一个参数化的函数的参数,使其在测试数据上尽可能接近目标函数。这个参数化的函数就是神经网络,参数就是网络的参数。目标函数,则是具体要解决的问题的建模,其输入输出都被转化成实向量。

最简单的神经网络由多层线性变换组成,其对应的参数即为线性变换对应的矩阵。由于线性变换的组合依然是线性变换,表达能力有限,所以需要在每两层变换之间加入非线性的激活函数。激活函数可以很简单,比如 relu(x) = max(0, x)

刚开始这个项目时,我对深度学习一无所知,以为自己会学习到有用的、深度学习的知识。后来用 PyTorch 调了两个星期自己写的 BERT,发现手写反向传播毫无价值,倒是 Swift 编程略有收获。不管怎样,还是稍作记录。

由于本文写于 2018 年 12 月 28 日,而项目本身于十月中旬完成,因此部分细节可能有错误,敬请谅解。

Swift

一个开发舒适但生态不足的语言。

之所以选用 Swift 进行实现,有两个目的。其一,我想尽量避免借鉴所有现有代码,确保自己可以掌握 FFN 的全部细节。其二,我想体验一下 Swift 作为一个现代编程语言的好用程度。

实际开发过程中,Swift 的易用程度颇让我满意:

  • Swift Playground 被我用来可视化 MNIST 数据集,确保数据预处理没有问题。
  • Xcode 的代码补全、实时纠错支持很优秀,相比之下写 Python 无比痛苦。
  • Swift 的类型系统虽然简单,也足以帮助我思考数据结构,构造合适的抽象。

中肯地说,类型系统对使用 PyTorch 影响并不大,但是对于实现 PyTorch 却有很大益处。除此之外,类型系统主要的作用还是在于方便运行前纠错。

Swift 的生态环境倒很糟,比如说,一个好用的线性代数库都没有。虽然 GitHub 上有几个所谓的线性代数库,但抽象都无比简陋,连矩阵乘向量都很麻烦。最终我选择了 Apple 的 Accelerate 框架,一组 C 高性能计算库的合集。感谢 Apple 在 Swift 与 C 交互上的努力,Swift 数组可以自动转为 C 的指针,用起 Accelerate 倒也不烦:

  1. import Accelerate
  2. let a: [Float] = [1, 2, 3, 4]
  3. let b: [Float] = [0.5, 0.25, 0.125, 0.0625]
  4. var result: [Float] = [0, 0, 0, 0]
  5. vDSP_vadd(a, 1, b, 1, &result, 1, 4)
  6. // result now contains [1.5, 2.25, 3.125, 4.0625]

虽然没有酷炫的类型包装,但纯粹计算也够用。

API 设计

手动构图,半自动求导。

写这个项目之前,我完全没有见过任何主流深度学习框架的使用教程,并不了解它们的 API。我参考的接口是 Apple 的 MPSCNN,后者是基于 Metal Performance Shaders 的卷积神经网络库。思路很简单。库提供一些参数化的节点,比如全连接层、损失层、数据层。用户用 Swift 代码把这些层的连起来。构建好的运行图被解释执行。

这个接口最恶心的地方在于,运行图只支持前向求值,不能反向求导。Apple 在 WWDC 2018 给这套接口打补丁:每一层都对应一个梯度层。把前向网络每一层的 梯度层连起来,接在前向网络后面,便可以实现求导。此外,Apple 还加入了参数更新的支持。因此,MPSCNN 不止可以做推导,也可以训练,只是用者寥寥。

我照猫画虎的实现了类似的接口:用户手工构建完前向求值和反向求导图,我的库可以自动计算和更新参数。每一个基础层的运算和求导都是手工实现的。实现本身没有什么亮点,有编程基础的人琢磨琢磨都能做到。实现本身也没什么意义,毕竟大家都用可以自动求导的库了。

感兴趣的读者可以参考我的代码

这个项目结果很成功,在 MNIST 上达到了 95% 的正确率,算不错了。毕竟当时没怎么调参,没有 GPU 加速,用的是最简单的 SGD。过程也比较幸运,代码没有大错,调试阶段很短。相比之下,一个月后我调试两个星期 BERT 完全是一场灾难,且至今没有成功。

意义也不小。这可能是我很长一段时间来,唯一一个画上句号的项目。熟悉我的人都知道,我虎头蛇尾的编程项目数不尽数。坑的原因主要有两类:

  • 纠结技术细节的完美。
  • 项目野心太大。

事后想来,这个项目可以坑的地方还挺多的。比如说,我可能会琢磨怎么用 Swift 包装一个好的线性代数库,最后浪费无数时间在 Swift 元编程的研究上,可能还会换语言,折腾许久最终放弃。我也有可能会想着对标 PyTorch 等主流方案,或尝试构建更复杂的网络,无意义地浪费时间。能在一个学期中开展课外项目,精准的控制项目的细节与范围,实属幸运。