Ruby [踩坑 / 分享] Ruby 机器学习尝试 — 训练 Atari (雅达利) 打弹球游戏 AI

happybai · 2018年04月18日 · 最后由 staticor 回复于 2018年05月11日 · 2521 次阅读
本帖已被设为精华帖!

众所周知 Ruby 的机器学习一直是相对弱势的区域,那么它的表现究竟如何呢?本文通过尝试实现一个相对复杂的机器学习算法(DQN)来踩一遍在 Ruby 环境下使用机器学习遇到的坑。由于各式各样的坑,最终 Ruby 版本只完成了模型的 80% 且部分核心依旧借助了 Python。而且因为效率问题,最后也无法真正训练出效果,不过即便如此还是分享出来让大家了解一下吧。

Python 版源码:https://github.com/happybai/atari-agents

Ruby 实验源码:https://github.com/happybai/atari-agents-rb

Atari 游戏环境 Ruby 接口:https://github.com/happybai/ale_ruby_interface

(下图为 Python 版本算法用 CPU 训练1个小时的结果)

一、Atari(雅达利) 训练环境准备

坑一:缺乏相应环境或相应环境的 Ruby 实现。

很多机器学习的用例都会提供一个学习环境来方便大家来做研究比如,atari 的环境 Arcade-Learning-Environment,增强学习算法的环境 gym, 星际争霸2的学习环境 pysc2。这些环境基本都是 C++ 加上 Python 的接口,或者只有 Python 的版本。好在很多真正有效率的机器学习的库其实都是 C++ 为核心加上 Python 做为接口。本例子中也是以 Arcade-Learning-Environment 为核心,制作了 Ruby 的调用接口 ale_ruby_interface,来实现了训练的环境。是的,这样就可以用 Ruby 玩 Atari 游戏了!

二、相关 Ruby 机器学习库的准备

坑二:主流 Ruby 机器学习库活跃低,更新缓慢,文档不全,不研究一下连装都装不上。

这个例子里用到了两个机器学习的库 NMatrix 和 Tensorflow 的 Ruby 实现 tensorflow.rb

其中 NMatrix 的安装就遇到了问题 https://github.com/SciRuby/nmatrix/issues/591 且不支持高版本的 gcc 编译。

tensorflow.rb 的原理也仅仅是 Ruby 调用 Tensorflow 的 C++ 文件。需要 Bazel 来编译,通过 swig 来调用,安装起来也相对复杂。

三、构建 Tensorflow 算法图

Tensorflow 的基本思想是构建一个算法图,然后加载到 Session 里运行。那么图的构建是 Tensorflow 中非常重要的一步,tensorflow.rb 自然也提供了这部分功能。但是(坑三:tensorflow.rb 提供 api 太少,且文档和错误提示不足)仅仅实现了最核心的部分,所有操作仅通过一个方法 AddOperation 来进行的。具体使用方法,只能去看 Tensorflow 的文档和源码来猜。最关键的是 — 没有错误提示!。参数错了只会报内存溢出。最终我还是放弃了在 Ruby 里进行图的构建,不过好在 tensorflow.rb 里提供了加载已经构建完毕图的方法(实际上 tensorflow.rb 自己的例子也是加载现成的图文件)。所以最终我还是选择从 Python 版本把算法图导出成文件(protobuf 格式),再通过 Ruby 导入,来完成运算图的构建。

四、数据预处理

除了核心图的构建,很多数据的预处理也是构成算法的重要的组成部分,比如本例子中的 History、Memory 类、图片的转换,各种矩阵的 reshape,将训练的结果录成视频。这方面 Ruby 差距就不是很明显了,相反比之 Python 还更顺手一些,只是(坑四:NMatrix 的效率应该不是很好) NMatrix 处理大矩阵效率好像有些差。最终运行的时候效率还不到 Python 版本的 1%,不过关于效率的问题,也有部分原因是有些库我没找到对应的 Ruby 版本就自己随便实现了下,忽略这部分的差距,初步估计应该至少能达到 python 的10%。

五、开始训练

除了上面的效率问题,我刚开始训练模型的时候,跑了10分钟后突然就内存溢出了(坑五:由于 Ruby 会经常和 C++ 的库进行交互,不成熟的库动不动就内存溢出,且原因很难查找)。最后原因也许是 tensorflow.rb 的一个 bug 。总之修改了 tensorflow.rb 中的一行源码,重新编译安装后。就解决了这个问题。但这种错误真心及其难调试。最终这个模型终于可以稳定训练了,不过由于效率问题以及其他因素,所以并不会有效果。即便如此,也算是跑了一个相对完整的算法了。

六、使用训练后的成熟模型

虽然上面的模型最终没有训练成功,不过假如我们只是使用训练后的程序模型来玩一下,那 Ruby 应该还是可以的。也就是说,如果不考虑做科学研究,仅仅在应用层面上,也许 Ruby 并没有太多的短板。

七、总结

客观的说,Ruby 的科学计算还是远远落后于 Python,如果我们真想要涉足这个领域,学习一下 Python 的代价可能还不如看两遍线性代数来得大。不过如果在未来,很多算法会变得更加成熟,仅考虑在应用层面,相信 Ruby 也未尝不是一种优秀的选择。

DQN 算法相关:

https://ai.intel.com/demystifying-deep-reinforcement-learning/

https://medium.com/@awjuliani/simple-reinforcement-learning-with-tensorflow-part-4-deep-q-networks-and-beyond-8438a3e2b8df

http://mnemstudio.org/path-finding-q-learning-tutorial.htm

共收到 7 条回复
jasl 将本帖设为了精华贴 04月18日 15:52

做Python的人在重复造轮子,做Ruby的没人造轮子,Matz 经常说的一句话:“Ruby 旨在让程序员开心。” 我想看看Ruby程序员能开心多久。

@zhaoyshine 在 AI 方面 ruby 确实不强,可你这样无脑喷不太好吧

4楼 已删除

我觉得很多人夸大了 Python 对机器学习的“占领”,除非是 PyTorch,Python 的代码又不真正运行,不过是接口罢了(甚至连接口都不是,只是 Wrapper),高性能的机器学习框架不可能用 Python 开发,同样用 Ruby 也完全不可能。

以前做车牌识别的时候,为了用一个现有的开源的车牌识别库(C++),尝试过包括swig、ffi在内的三四种方法实现,也参考过很多开源的gem实现,最后放弃了,直接编译了以后用Ruby调用程序了。

在Windows下,调用一个DLL中的函数,是非常方便的。在*nx下,同样编译成动态链接库,却要把很多数据结构都声明一遍。。。。

Ruby扩展C很方便,扩展C++特别麻烦,在Twitter上问过Martz,有什么好的方法,他说没有,然后竟然反问我....

对于机器学习和深度学习来说,我觉得Ruby没有相应的库只会对想尝试的人有些影响。一旦真正的去学习深度学习,机器学习之类的东西,你会发现数学才是最重要的。最终的算法,你既不可能用Ruby实现,也不可能用Python来实现,因为这些都不够快。

谷歌的Tensorflow,不单单在算法本身上大做文章,它的某些算法已经优化到输入变成了瓶颈了。。。

总之,如果希望能真正从事机器学习、深度学习之类的研究应用,建议踏踏实实学习一下课程,如Andrew Ng 的DeepLearning课程,而不是跟着微信圈里的教程来个hello, word式的应用。

不考虑性能, ruby 写数学方面的东西,也不见得有优势。机器学习的语言,需要的是数学语言,而不是自然语言。R 也慢,但用的人还是很多,一部分原因是它的语法适合。

ruby 不单单是没有优化的问题,面向对象本身就快不起来。

yfractal 回复

哈哈哈,“面向对象本身就快不起来”,嗯,怎么说吧,跟“面向对象”可能关系吧,个人觉得有点不太大(个人而已),Py Rb 的对象都是一块块有点体积的内存,相对于纯 C / C++ / Go writing 来说哈,我在 C / Cpp programming 的时候,ref 或 指针引用是高效的,其中 ref 是最高效的。 就拿字符串拷贝讨论,你从文件 read 一波数据,你的变量的内存得 copy 来 copy 去,C++ 的 vector 可以:

auto data = new std::vector<char>();
std::FILE* f = std::fopen("test.txt", "r");
char * data_piece = new data_piece[4096];

while (fin.eof()) {
    length = std::fread(& data_piece[0], sizeof(char), 1024, f);
    data.insert(data_piece, data_piece + length); // 重点在这一句
}

...

请不要吐槽这段代码,纯粹是随即编写的,就当作伪代码好了。

C / C++ / Go 可以很好地重复利用一块内存其中的一部分,而且很好地自由地去利用它。因为反复内存分配(系统调用)是在 C++ 开发界普遍认为是很低效的。

但是脚本语言让人遗忘了这些,反正 GC 会管理,写得痛快。

所以之前就有人说,你要是用 JS 去处理 pack unpack 二进制,还得转 Array,总之就是不方便。

另外,这也是当初我比较喜欢 ObjC 的原因,因为 ObjC 是面向对象的可半手动管理内存的语言,虽然 ObjC 有 ARC,但是很多场景你是可以高效利用内存的。

要想在 AI 高效反复利用这些变量去进行快速而且高效的操作,那肯定还是这样的语言(C / C++)比较擅长。但是这还并不算最高效的,C / C++ 编译之后出来的机器码具体运算算法并不是最优的,有些大神喜欢用自己手工优化的汇编来将无法优化或编译器优化得不够极致但是需要效率的地方。

我觉得没有必要涉及到语言之争. 更好的态度是将以前Rubist没有人啃的东西将之拿出来, 第一步迈出来固然很难, 但只要出来, 即使是相反方向的180度, 让第二人上来改进, 也会引到正轨.

也不要想"开发出来 反正没人会用Ruby搞" 这样的有点功利的想法.

我们能不能简单一点, Just for fun.

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册