- 内容提要
- 前言
- 作者简介
- 封面简介
- 第1章 理解高性能 Python
- 第2章 通过性能分析找到瓶颈
- 2.1 高效地分析性能
- 2.2 Julia 集合的介绍
- 2.3 计算完整的 Julia 集合
- 2.4 计时的简单方法——打印和修饰
- 2.5 用 UNIX 的 time 命令进行简单的计时
- 2.6 使用 cProfile 模块
- 2.7 用 runsnakerun 对 cProfile 的输出进行可视化
- 2.8 用 line_profiler 进行逐行分析
- 2.9 用 memory_profiler 诊断内存的用量
- 2.10 用 heapy 调查堆上的对象
- 2.11 用 dowser 实时画出变量的实例
- 2.12 用 dis 模块检查 CPython 字节码
- 2.13 在优化期间进行单元测试保持代码的正确性
- 2.14 确保性能分析成功的策略
- 2.15 小结
- 第3章 列表和元组
- 第4章 字典和集合
- 第5章 迭代器和生成器
- 第6章 矩阵和矢量计算
- 第7章 编译成 C
- 第8章 并发
- 第9章 multiprocessing 模块
- 第10章 集群和工作队列
- 第11章 使用更少的 RAM
- 第12章 现场教训
12.2 使用 RadimRehurek.com 让深度学习飞翔
Radim Řehůřek (radimrehurek.com)
当Ian请我来为本书写写我在Python和优化方面的“来自现场的教训”时,我立即思考,“告诉他们你怎样制作了一个比Google的C语言原始版本要更快的Python移植版!”。Google的深度学习的典型代表比原始的Python实现快了12000倍,这是一个创造机器学习算法的鼓舞人心的故事。任何人可以写出糟糕的代码,接着再鼓吹巨大的速度提升。但是有点令人吃惊的是,优化过的Python移植版也运行得比Google团队所写的原始代码几乎快4倍!那就是,比晦涩的、配置紧凑的优化过的C代码快4倍。
但是在吸取“机器层面”的优化教训之前,有一些通用的关于“人的层面”的优化建议。
12.2.1 最佳时机
我运转着一个小型的专注于机器学习的咨询业务,我的同事和我帮助公司让数据分析的混乱世界变得有意义,目的是赚钱或节省成本(或两者兼得)。我们帮助客户为数据处理设计和构建令人惊叹的系统,尤其是在文本数据方面。
客户范围从大型的跨国企业到新生的初创公司,尽管每个项目各不相同并且需要不同的技术栈来插入客户已经存在的数据流和管道中,Python是明显的优先选择。不是要白费口舌,Python的简单直接的开发哲学、可塑性,以及丰富的库生态系统让它成为了一个理想的选择。
首先,一些“来自现场”的关于是什么发挥着作用的思考:
沟通,沟通,沟通。这很明显,但值得重复提及。在决定一种方法前,在更高的层面(业务)上理解客户的问题。坐下来谈谈他们认为所需要的东西(基于在联系你之前他们对什么是有可能做的以及/或者他们从谷歌上搜索到的片面的知识),直到对他们真正所需的东西一清二楚,而免于晦涩和偏见。要事先对验证解决方案的方式达成一致。我想要把这个过程可视化成构筑一条绵长而曲折的道路:得到正确的开端(问题定义,可利用的数据资源)和正确的终点(评估,解决方案的优先级),以及属于这两者之间的路径。
寻找有前途的技术。一个得到良好理解的、健壮的、正在得到关注的,然而在工业领域还是相对模糊的新兴技术能够给客户(或你自己)带来巨大的价值。例如,几年前,Elasticsearch是一个罕为人知的有一些粗糙的开源项目。但是我评估了它的方法,认为它是扎实的(构建于Apache Lucene之上,提供了副本、集群分片等)并把它的用途推荐给了客户。结果我们使用Elasticsearch作为核心构建了一个搜索系统,相比可观的替代方案(大型的商业数据库),帮客户省下了在版权、开发和维护方面的巨额资金。甚至更重要的就是,使用一个崭新的、灵活又强大的技术给产品带来了巨大的竞争优势。现在,Elasticsearch已经进入了企业市场并传递出无与伦比的竞争优势——每个人都知道和使用它。掌握正确的时机就是我所能声称的“最佳时机”,让价值/成本之比达到最大化。
KISS(让它简单,愚蠢!)这是另一种不必花脑筋的事。最好的代码就是你不必编写和维护的代码。从简单开始,并且在必须的地方改进和迭代。我倾向于遵循UNIX的“做一件事情,并把它做好”哲学的工具。宏伟的编程框架可能是吸引人的,让每样可设想到的事情处于同一屋檐下并且干净地适配在一起。但是你迟早总是需要一些巨大的框架所设想不到的东西,然后甚至看起来简单(从概念上)的改动串联起了一场噩梦(从编程角度而言)。宏伟的项目以及它们所包含的全部APIs在它们自身的重量下趋向于崩溃。要使用模块化、专注的、尽可能短小而简单的工具。要倾向于对简单的可视化检查开放的文本格式,不然除非是性能上的强行规定。
在数据管道中使用手动的完整性检查。当优化数据处理系统时,容易停留在“二进制的思维”模式中,使用紧凑的管道、高效的二进制数据格式和压缩过的I/O。当数据以不可见和未经检查的(可能除了它的类型)方式通过系统时,它保持着不可见性,直到某些情况彻底爆发。然后调试开始。我建议把一些简单的日志消息撒遍整个代码,把在各种不同的内部处理点上展示数据的形态看作一个良好的实践——没有什么花哨的,只是模拟UNIX的head命令,挑选并对一些数据点做可视化。这样不仅有助于前面提到过的调试,而且以人类可读的格式看到数据经常会令人惊讶地产生“啊!”的时刻,甚至于在一切看起来都良好的情况下。奇怪的符号化!它们承诺的输出总是以latin1来编码!这种语言的文档怎么会出现在那里?图像文件泄漏到了期待和解析文本文件的管道中!这些常常超越了由自动类型检查或者固定的单元测试所提供的认识力,暗示着跨越组件边界的问题。真实世界的数据是混乱的。早期捕获到事件甚至不一定会产生异常或者明显的错误。宁可失之于过度冗长。
小心地在潮流中导航。只是因为一个客户一直听说X并说出他们必须也要X并不意味着他们真正需要它。它可能是一个市场问题而不是一个技术问题,所以要仔细分辨并相应地传达。X随时间变化,因为炒作的浪潮来来去去,一个最近的值将会是 X = 大数据。
总之,谈论业务足够了——这就是我如何用Python获得了比C运行更快的word2vec。
12.2.2 优化方面的教训
word2vec是一个允许检测相似单词和短语的机器学习算法。随着文本分析和搜索引擎优化(SEO)方面的有趣应用以及附于其上的谷歌的光辉品牌,初创公司和业务蜂拥而上地利用起这个新工具。
不幸的是,唯一可利用的代码就是由谷歌自己所生产的,一个用C语言编写的开源Linux命令行工具。这个工具得到了良好的优化,但是相当难以用来实现。我决定要把word2vec移植到Python上的主要理由就是我能够把word2vec扩展到其他的平台,让它更易于为客户集成和扩展。
在这里不关乎细节,但是word2vec需要一个具有很多输入数据的训练阶段来产生有用的相似模型。例如,谷歌的工程师们在他们的GoogleNews数据集上运行word2vec,训练了大约1000亿个单词。这种尺度的数据集明显不适合放入RAM中,所以必须采用一个节约内存的方法。
我已创造了一个机器学习库gensim,目标准确定位于这种内存优化的问题:数据集不再是小规模(“小规模”就是任何能完全放入RAM中的事物),而又没足够大到有必要使用PB字节规模的MapReduce计算机集群的地步。令人惊奇的是,这个“千兆”范围的问题适合于真实世界的一大部分情况,包括word2vec。
细节在我的博客上有描述,但是这里有一些外带的优化:
流化你的数据,观察你的内存。让你的输入按照一次一个数据点的方式被访问和处理,目的是得到一个小而固定的内存占用空间。流化的数据点(在word2vec的情况下是句子)可能为了性能在内部被组织成更大的批量(例如同一时刻处理100条句子),但是高级的、流化的API证实了一种强大而又灵活的抽象。Python语言使用它内置的产生器,很自然和优雅地支持这种模式—— 一场真正优美的问题和技术的竞赛。避免致力于那些把一切都装载进RAM中的算法和工具,除非你知道数据总是保持小规模,或者你不介意以后自己去重新实现一个生产版本。
利用Python的丰富生态系统。我从一个可读的、使用numpy的干净的word2vec的移植版开始。Numpy在本书的第6章中被深度地涉猎到了,但是作为一个短小的提醒,这是一个美妙的库,是Python科学社区的基石,是用Python做数字爆破的事实标准。挖掘numpy的强大数组接口、内存访问模式以及为超速的通用矢量操作所包装的BLAS例程产生了简洁、干净和快速的代码——比原生的Python代码要快好几百倍。通常我在这点上就此打住,但是“几百倍更快”还是比谷歌优化过的C版本要慢20倍,所以我要强调一下。
配置和编译热点。word2vect是一个典型的高性能计算应用,因为在一个内循环中的少数几行代码占用了90%的整体训练运行时间。在这里,我用C重写了一个单独的核心例程(大约20行代码),使用一个外部的Python库,把Cython作为胶水。尽管技术上光彩夺目,我却不认为Cython从概念上是一个特别方便的工具——它基本上就像在学习一门其他语言,一种在Python、numpy和C之间的非直觉的混合物,而有它自己的说明和特质。但是直到Python的JIT(即时编译器)技术成熟前,Cython可能就是我们的最佳赌注。使用一个Cython编译成的热点,word2vec的Python移植版本的性能现在与原来的C代码不相上下。从一个干净的numpy版本开始的另一个优势就是通过与更慢但是正确的版本做对比,我们就得到了免费的正确性测试。
知道你的BLAS。numpy的一个干净特性就是它内部在可利用的地方包装了BLAS(基础线性代数子例程)。这些是低级的例程集合,直接通过处理器供应商(英特尔、AMD等)使用汇编、Fortran或者C来做优化,被设计用于从一种特定的处理器架构中挤榨出最佳的性能。例如,调用一个axpy的BLAS例程来计算vector_y += scalar * vector_x,这样比通用的编译器为一个等价的显式的循环所产生的代码要更快。把word2vec的训练表示成BLAS操作导致了额外的4倍速度提升,胜过了C版本的word2vec的性能。获胜了!公平来说,C代码也能够链接BLAS,所以这不是Python与生俱来的优势。numpy只是让诸如此类的事物突显出来并让它们变得容易利用。
并行化和多核。gensim包含了一些算法的分布式集群实现。对于word2vec来说,我选取了在一台机器上的多线程方式,因为它的训练算法具有细粒度的本质。使用多线程也允许我们避免Python多进程所带来的fork-without-exec的POSIX问题,尤其是在与某些BLAS库混用的时候。因为我们的核心例程已经使用了Cython,我们能够担负起释放Python的GIL(全局解释器锁,请看7.8节中的“在一台机器上使用OpenMP来做并行解决方案”),这通常使得多线程对于CPU密集型任务无效。速度提升:在一台机器上使用4核另外又提高了3倍。
静态内存分配。在这点上,我们每秒处理好几万条句子。训练是如此快速,甚至于没有什么类似于创建一个新的numpy数组(对每一个句子流调用malloc)那样拖慢我们的速度。解决方案:预分配静态的“工作”内存并以良好的老Fortran的风格来周转。让我流泪了。在这里的教训就是尽量在干净的Python代码中保持记账和应用逻辑,并且要让优化过的热点保持精简。
具体问题的优化。原来的C语言实现包含了具体的微观优化,例如在特定的内存边界对齐数组或者在内存的查找表中预先计算某些函数。一阵来自过去的令人怀念的风气,随着如今复杂的CPU指令流水线、内存缓存层级以及协处理器,这种优化已不再是确定的赢家。细心的剖析暗示着一定百分比的提高,可能不值得为之付出额外的代码复杂性。另外,使用注解和剖析工具来高亮出优化不够的点。使用你的领域知识来引入以准确度换取性能(或反之)的渐进算法。但是从不要把它当作信条,剖析倾向于使用真实的生产数据。
12.2.3 总结
在合适的地方优化。以我的经验来看,从来没有充分的沟通来完全确认问题范围、优先级以及和客户业务目标的关系——即“人的层面”上的优化。确认你交付了相关的问题,而不是为它迷失在“极客工具”中。当你卷起袖子准备干活时,让它值得去做!
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论