首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

后Python时代,Julia告诉你速度和灵活性真的都可以有

机器之心原创

作者:Angulia

编辑:Hao

8 月份,

Julia 1.0 发布

,在社区内引发了极大的关注。之后不久,机器之心推荐了一篇简单的中文教程。在最新的这篇文章中,作者对 Julia 的众多特性进行了介绍,同时简略介绍了 Julia 在机器学习和深度学习方面的资源储备。

1. 简介

Julia 是 Jeff Bezanson、Stefan Karpinski、Viral Shah 和 Alan Edelman 几位科学家于 2009 年开始研发、2012 年首次发布的动态语言。设计它的最初目的是为了高性能的数值分析和计算机编程,科学家们发现目前的编程语言如 MatLab、C、Python、Ruby 在各自的优势领域发挥很好,但每种语言也有其自身不可避免的缺陷。所以科学家们野心勃勃地提出了对 Julia 的畅想:希望它可以有 C 一样的速度,对复杂公式处理跟 Matlab 一样友好,可视化或者粘合性跟 Python 一样方便,同时还兼具 Ruby 的动态性。而从最初版本发行至最新的 1.0 版(2018 年 8 月 8 日),Julia 也一直持续地提升高效性和易用性,并引入新功能。

Julia 设计的新颖性在于它有一个支持参数多态的类型(Type)架构,支持多重派发 (Multiple-Dispatch) 编程方式,同时允许并发、并行、分布式计算等。另在数值计算方面,Julia 使用元编程以及一些高效内嵌库函数,在易用性和高效性方面都做了很多改进。同时对于数据科学家和机器学习爱好者来说,必要的机器学习库和深度学习支持包也是一个重要的考察点,本文就从上述介绍的诸多 Julia 特性入手,同时简略介绍了 Julia 在机器学习和深度学习方面的资源储备。

2. 元编程

元编程(MetaProgramming)是 Julia 语言中一个非常核心的概念,它是代码优化和提升的基础,所以首先我们就从元编程的概念入手,通过比较小的代码实例来体会元编程的思想。(运行本文的代码实例或需要安装部分包,对 Julia 的包安装不熟悉的读者在运行之前可以先参考文章第三部分关于 Julia 依赖包添加管理的内容,同时注意不同版本的 Julia 在包名和函数所属包方面有较大变化,本文的代码实例都基于 Julia 1.0 运行。)

Julia 程序执行过程分两步,一是 AST 进行解析(parsing),之后由编译器运行(evaluation)。Metaprogramming 发生在解析之后,运行之前。

单句用:

单句的执行隔断可以使用冒号「:」。如果是较长的多行代码或者一个代码片段,使用 quote…...end 形式:

如果需要执行 A,使用 eval() 函数。如果需要查看 A 的结构,用 fieldnames()。如果想看整段代码的解析,那么就用 dump(),同时你还可以查看所有的 args list。

根据每行的子表达式 (sub-expr),可以看到程序是分步执行的,那么我们就可以在其中的某一步进行数值的修改:

在 Julia 中,宏(Macros)可以简单理解为函数一样的存在,接受输入然后返回我们需要的返回值。不同之处在于,宏接受的输入不是单纯的数值(value)而是表达式(expression)。我们在代码解析阶段对输入的表达式进行处理或者修改,返回扩展后的表达式,与上一部分元编程结合,那么在代码编写过程中,我们对希望处理的流程代码嵌入宏的表达处理以及中断执行的嵌套组合句式 (quote-end),从而在代码解析时就会跳入宏的处理流程,得到修改后的代码表达式并得以执行,通过以下的代码示例应用这部分思想:

通过宏来计算任意函数的执行时间,节省了代码量,也相应做到了效率的提高。

3. 高性能计算

Julia 问世之初就以『性能媲美 C++』闻名,所以高性能计算(high performance computing)是这个语言的一大亮点,无论是之前的元编程还是宏,都体现了该语言本身效率至上的特点。从 built-in 的数据结构、程序设计思路,以及重构函数、简化循环等等方面,Julia 对于性能优化做了很多工作,铺陈展开一文难以尽叙,所以本文侧重介绍 Julia 在数值类型与计算方面为高性能做出的改进和特色性质。

上图引用自维基百科

https://commons.wikimedia.org/wiki/File:Type-hierarchy-for-julia-numbers.png

参考如上的 Julia 数值设计

A. 整型 (Integers)

1. 编码

为了最大程度上利用用户设备的计算性能,Julia 的数值类型(Number Type)设计尽可能贴近硬件运算力。首先对于整型(Integers),默认精度大小取决于用户使用的操作系统(OS)或者 CPU,常见的就是 Int32 与 Int64 两种整数类型,而对于 Int 类型来说,它代表的类型也默认成你的系统支持位宽 (bit width),用如下代码可以检查当前 Julia 环境整型的默认位宽:

同时在不进行特别指定的情况下,Julia 默认使用带符整型 (signed integer),上述提到的这么多数值类型均以二进制数字的形式存储,用 bits() 函数可以帮助我们查看数值的二进制表示:

测试时本机用的是 64 位操作系统,所以正整数 5 用 64 位的二进制数进行存储,-5 则利用 64 位相应的二进制补码储存。

对于整型(Int),我们知道 Python 中的整型在 C 语言里的长整型(Long)上来实现,浮点数 (Float) 则由双精度浮点数(Double)来实现。但是在 Julia 中,因为直接采用二进制数值存储的缘故,整型或者浮点型的数值都可以被称为 bits 类型,而这样的数值类型可以支持一种称之为封装(box)的操作,即将数值在内存中存储的同时,加上一个表示它类型的前缀,可以简单理解为类似于在 C 程序中指定一个整型变量,然而 Julia 的 JIT 编译器在编译代码时却能够做到很好地去除不必要的封装/解封操作(box/unbox),从而不必产生冗余的汇编代码。下面是一个简单的整数加法的实例代码,我们调用一个宏来看它产生的编译代码(类似汇编程序),可以看到生成的代码简单地对两个整数进行了加和,而没有多余的封装后的解封代码了。

那么通过对比我们就知道,少了类型判别和转换,数值操作的效率自然也就得到了提高。更进一步,在这样的数值类型基础上建立数组,所占的存储空间是整片连续的存储空间,类型的标签前缀也会加载在数组的起始位置,类似的存放方式不仅解除了指针引用(pointer dereference)的麻烦,同时也能方便进一步的数组优化操作。

2. 溢出(overflow)

相信有过编程经验的读者对溢出这个概念都不陌生,它体现在我们使用的数值超出了该类型所能表示的最大范围或最小范围,由此衍生出一系列代码异常。而在 Julia 中大家还记得上一小节我们提到过整型的默认长度表示会根据你的操作系统来给出,那么这就潜在地帮我们规避了一部分溢出风险。譬如,你在使用 Int 类型,而你的 Julia 环境根据你的 64 位系统默认判断使用 Int64,那么 64 位整型能表示的整数上界与下界就可以通过如下代码获知:

那么如果你的程序中生成的整数还是不幸超过了这个范围呢?

如上代码做出了很好的说明,针对你的溢出值根据机器默认机器二进制值表示的上限,不再予以任何增长,而其二进制表示在溢出后也变为 0。和 Python 和 Ruby 这样的动态语言横向对比呢?Python 采用了一种自动扩容的方式,首先加入对每种数值类型的溢出检测,当检测到溢出行为时,Python 自动将你的数字进行更高范围表示类型的升级(如超过 Int16 的表示范围,自动升值你的数据为 Int32 类型),一定程度来说他赋予了程序灵活性,帮助用户省事,但是代价就是严格动态检查的时间损耗以及仍然隐藏的 bug 风险。而 Julia 就选择在这方面跟随 C 的思想,释放效率,严格根据机器二进制码表示的上限对你的数值表示进行界定,但是也不需要你一直手动制定类型(默认跟随你的操作系统类型),一定程度上解放了用户可操作的范围。

3. 超大整数(BigInt)

根据上一节我们讲到的 Julia 对数值溢出的严格控制,那么如果真的需要用到『越界』整数怎么办呢?Julia 给出了一种称之为 BigInt() 的数值类型,你可以自由地使用超大数值来为你的程序服务了:

诚然超大整型的使用会比使用默认整型在速度上有明显劣势。不过针对用户的具体情况,它的存在既使得你正常使用的整型与溢出绝缘,同时又为你的特殊需要打开了方便的使用接口。

4. 整型互换(Type Conversion)

之前提过 Julia 默认使用的都是带符号整型的表示(Signed Integer),如果你在实际应用中不需要,那么可以使用无符整型的构造函数 UInt32() 或 UInt64(),如下:

32 位二进制表示的无符整型数可以用同样的构造函数自由转换,而一旦尝试用 UInt32 表示范围之外的大数字,就会报同样的越界错误。相同的规则也同样适用于 16 位、8 位无符整型。

B. 浮点数(Floating Number)

Julia 的浮点数构造函数为 Float64(),它的特性以及标准制定都遵从被广泛接受的 IEEE 754(Python、C++等语言的浮点数均遵从此标准),其默认位宽都是 64 位而不取决于你本身的机器或者系统位宽。

浮点数类型的基本常用操作与其他语言没有差别,然而考虑浮点数计算时,精度和效率是不得不考虑的一个方面。Julia 在设计普通的运算操作时,同时照顾了大部分计算的效率和精确度,例如 sum()、+/- 等操作。然而当我们面对一些非常规数 (denormal number) 或者时间敏感的计算操作时,Julia 也提供了二者权衡(tradeoff)的函数。

1. @fastmath 宏

@fastmath 是 Julia 提供的一个宏函数,它通过一定程度上放宽浮点数的 754 标准来实现计算速度的大幅提升,比如它用自己改进的计算操作变体替代普通的计算操作,或者在执行(evaluation)阶段对部分代码的执行顺序进行重整,又或者采用另外的机制跳过 NAN 或者 INF 值的检测等。这些变化都可以一定程度上提高整体代码的执行速度。然而由于计算过程中的近似取整或者约等于,结果会存在一定的精度损失。

我们通过两个实现同样功能的函数对比来初步体会一下 @fastmath 的用法,两个函数都实现了对一个一维数组的元素两两求差再加和,区别在于在进行加减操作时,原始函数用正常的加减,而 fast 版本增添使用了 @fastmath 优化加减操作。

对比运行结果如下,由于加减操作并不复杂二者精度几乎没有相差,而计算效率上使用 fastmath 的版本比原始函数快了很多。如果涉及密集的乘除次幂运算,二者之间的区别会更加明显:

2. KBN 求和(KBN summation)

我们知道在求和操作中或多或少会有近似约等于的计算误差存在,当被求和数处在相近的量级,误差就可以近似被忽略,然而如果数字之间量级相差很大(如百万加百万分之一),误差难免时,Julia 提供了另一种精确计算的函数 sum_kbn(),参考如下示例代码,sum_kbn() 保存了求和结果的精确度,但同时付出更多一点的时间代价。

3. 多重派发(Multiple-Dispatch)

正如之前提过的,在函数定义和调用的时候,Python 之类的动态语言弱化了类型判断,提供了更加灵活的接口,但是每次执行时都需要进行一次类型判断也一定程度上影响了代码性能。Julia 在类似的情况下提出了自己的折中方法——多重派发。

Julia 语言里函数的定义都是泛化的(Generic),即同一个函数可以接受多个类型的参数。函数里具体的一种参数组合可以被称为函数的一种方法(method),我们定义这样一种新方法的过程就被称为函数重载(Overloading),即同样的函数名称但是接受了不同的参数组合。

在 Julia 里如果我们为自己的函数定义了一系列这样的方法,则它们会被存储在一个虚方法列表(virtual method table, vtable),函数不属于任意一个类型,也就是类似于一块全局区域,调用函数运行时,Julia 根据你指定的参数组合会搜寻匹配的方法选择执行。上述过程就被称为多重派发,多重派发机制是 Julia 区别于 C++、Python 等语言所独有的。

虽然以上语言均支持函数重载,然而 C++或 Python 等语言的重载均建立在类上,并不具备泛化的性质,具体来说就是每一个方法的调用都是类似于 obj.method() 的形式,函数特属于某个类,并不支持多个类型,其所有的虚方法存储在各自对应的类或类型之中,而 Julia 的虚方法列表 (vtable) 存储在函数本身内部,因此多重派发是一种更加泛化简单的用法。我们也继续通过简单的代码实例来体会多重派发的用法:

以上五行代码会返回一个存在五个方法的函数 f(),在执行时也会寻找和你传入的参数类型适合的方法。

4. Julia 依赖包添加、更新和管理

除去语言本身包括的基本 built-in 函数,Julia 也像 Python 一样提供开发包(package)的安装与管理。Julia 语言中的 Pkg 模块就提供了自动管理和增删包的功能,调用 Pkg.status() 可以很轻易地查看已经安装的包及其对应版本。Julia 的包分为官方注册(registered)的包(均列在 https://pkg.julialang.org/ 中)与第三方(unofficial)的包。注册包以可查找的列表的方式存储在 METADATA.jl 文件中(https://github.com/JuliaLang/METADATA.jl),你可以很容易地根据你查找到的包名称进行安装,执行命令 Pkg.add(『packageName』)。除此之外,针对第三方开发的包的安装,你依然可以用相同的 Pkg.add() 命令,在参数中提供这个第三方包所在的 URL(例如 github repo 地址)进行包添加。

添加官方注册包:

从第三方网址安装:

除了支持多个来源的包添加方式以外,Julia 的包管理器还可以根据你的需求用 Pkg.update() 进行包的版本更新。Julia 的 Pkg 管理器支持的基本操作可参考以下示例:

目前看来在对开发包管理的基本操作上,Julia 做的还是比较完善。而包管理工具的另一重要模块就是多个包依赖关系以及版本控制。Julia1.0 后的 Pkg 模块可以自动进行相关包依赖的检测而无需手动配置,同时为了应对不同项目之间包的版本互不兼容的问题,Julia 提出了独立环境(Enviroment)的解决方式,用户可以为自己的不同项目创建独立的环境,在各自环境下选择对应版本的包进行添加和控制。

由于 Pkg() 是 Julia 自带的包管理工具,所以无论对于 Linux、Windows,或者 MacOS 系统均有良好的支持(本文涉及实验均在 Windows 10 下完成,同时从 Julia 最初版本到目前的 1.0 版本,很多包名或者函数所属包都发生改动,本文默认都是在最近发布的 Julia1.0 上进行的包添加和函数调用,与文章版本不同的用户需要注意更改引入的包名,才能成功运行函数)。

5. 机器学习/深度学习

对于广大从事算法和数据科学的人士,机器学习与深度学习的资源和第三方库便利性也是非常重要的权衡标准。Julia 问世时间并不长所以对比当前的 Python 来说,它没有类似 Python 中 Scikit-Learn 这样比较全面的算法库,不过常见的监督学习模型(如决策树、SVM、Bayes)、无监督学习 KMeans 等算法模型也均有第三方包实现,另一方面 Julia 也提供 Scikit-Learn 的接口方便用户使用。初步测试对比结果:

基于同样的随机生成的数据集,Julia 训练决策树需要 31ms,而 Python Scikit-Learn 需要 1993ms。

1. 决策树

为了帮助读者体会 Julia 中机器学习相关包的调用,我们使用决策树作为一个实例,体会构建模型、喂入数据以及进行测试的一系列过程。在此省去关于决策树的理论介绍,如需了解决策树背后的理论知识,可以参考我们之前的文章:

首先我们加载所需要的决策树库 ScikitLearn 库:

ScikitLearn 提供了很多常见算法实现的接口,也可以提供给 Julia 调用,我们声明将要用到的所有包。

加载了必要的包之后,我们随机生成训练数据与对应标签:

接着使用决策树的构造函数分别建立几个参数不同的回归器模型作为对比,同时拟合我们生成的数据集。

训练好模型之后,我们可以继续用 predict 函数进行测试:

为了让读者对 Python 和 Julia 在基础的机器模型运行上有一个比较直观的对比,我们也同时用 Python 的 Scikit-Learn 机器学习库构建一个最简单的决策树模型,从运行时间上做基础对比,如下:

我们将两种语言下的决策树模型从训练到测试都封装为一个函数,然后测试它们分别的耗时:

在 Julia 的 DecisionTree 模块中,运行结果执行时间为 31ms 左右:

在 Python 中运行结果执行时间为 1993ms:

虽然上述例子并不算十分严谨,不过相对来说,通常对于初学者或者算法使用者,不涉及自己 DIY 的优化设计,直接调用模型训练以及预测,Julia 下实现确实是性能更高的选择。同时从以上代码流程可以看出,Julia 机器学习的算法从训练到测试模型的整体过程与 Python Scikit-Learn 的整体流程是相近的,也易于学习和迁移。如果你需要可视化,一样可以用 Pyplot 提供的接口进行数据的呈现。随着 Julia 社区的成长,相信以后也会出现更多、优化更好的学习算法。

2. 深度学习

在原生 Julia 上,MIT 的 PhD 学生 Chiyuan Zhang 开发实现了 Julia 自己的深度学习框架 Mocha,支持基本的网络结构、损失函数的定义与 GPU 上的模型训练测试,同时平台也提供了一些基本网络的 pre-train 模型。

另一个开源工作是 FluxML,它提供图像、文本与强化学习等多项任务的模型与调用,目前很多工作也正在开发中。

如果是从主流框架迁移,Julia 也提供了 TensorFlow 的接口,让用户能够方便地迁移代码,或者灵活使用 TensorFlow 已实现的模型与方法。

参考资料:

https://github.com/JuliaStats/MLBase.jl

http://julialang.org

https://github.com/bensadeghi/DecisionTree.jl

http://scikit-learn.org/stable/

https://docs.julialang.org/en/v1/

https://github.com/pluskid/Mocha.jl http://www.deeplearningbook.org/contents/intro.html

https://mochajl.readthedocs.io/en/latest/tutorial/ijulia-imagenet.html

https://github.com/FluxML/model-zoo/

https://github.com/malmaud/TensorFlow.jl

http://scikit-learn.org/stable/auto_examples/tree/plot_tree_regression.html

https://github.com/cstjean/ScikitLearn.jl/blob/master/examples/Decision_Tree_Regression_Julia.ipynb

Julia: High Performance Programming

Learning Julia-Build high-performance applications for scientific computing

Julia-CookBook

Getting Started with Julia Programming

本文为机器之心原创,转载请联系本公众号获得授权。

------------------------------------------------

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20181006A0LHSD00?refer=cp_1026
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券