编辑:Hao作者:Angulia

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

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 发生在解析之后,运行之前。

单句用:

julia>:(1+2)
        1+2
julia>(1+2)
      3

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

A = quote
      code1
      code2
    end

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

for (i,expr) in enumerate(A.args)
    println(n,expr)

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

julia> expr=:(x^y)
:(x ^ y)
julia> expr1=:($x^y)
:(-2 ^ y)
julia> expr2=:(x^$y)
:(x ^ 2)

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

julia> macro timeit(ex)
quote
local t0 = time()
local val = $(esc(ex))
local t1 = time()
print("elapsed time in seconds: ")
@printf "%.3f" t1 - t0
val
end
end

julia> using Printf

julia>  @timeit 10^5
elapsed time in seconds: 0.063100000

julia>  @timeit 10+1
elapsed time in seconds: 0.00011

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

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> using Sys
Julia>Sys.WORD_SIZE
64

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

julia> bitstring(5)
"0000000000000000000000000000000000000000000000000000000000000101"

julia> bitstring(-5)
"1111111111111111111111111111111111111111111111111111111111111011"

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

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

julia> sample_add(a,b)= a + b
sample_add (generic function with 1 method)

julia> @code_native sample_add(6,7)
        .text
; Function sample_add {
; Location: REPL[42]:1
        pushq   %rbp
        movq    %rsp, %rbp
; Function +; {
; Location: int.jl:53
        leaq    (%rcx,%rdx), %rax
;}
        popq    %rbp
        retq
        nopw    (%rax,%rax)
;}

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

2. 溢出(overflow)

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

julia>  typemax(Int64)
9223372036854775807

julia> typemin(Int64)
-9223372036854775808

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

julia> 9223372036854775806 + 1
9223372036854775807
julia> 9223372036854775806 + 1 + 1
-9223372036854775808
julia> 2^64
0
julia> 2^65
0

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

3. 超大整数(BigInt)

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

julia> big(9223372036854775806) + 1 + 1
9223372036854775808
julia> big(2)^64
18446744073709551616

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

4. 整型互换(Type Conversion)

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

julia> UInt64(UInt32(1))
0x0000000000000001
julia> UInt32(UInt64(1))
0x00000001
julia> UInt32(typemax(UInt64))
ERROR: InexactError: trunc(UInt32, 18446744073709551615)
Stacktrace:
 [1] throw_inexacterror(::Symbol, ::Any, ::UInt64) at .\boot.jl:567
 [2] checked_trunc_uint at .\boot.jl:597 [inlined]
 [3] toUInt32 at .\boot.jl:686 [inlined]
 [4] UInt32(::UInt64) at .\boot.jl:721
 [5] top-level scope at none:0

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 优化加减操作。

julia> function sample_diff(x)
       n = length(x); d = 1/(n-1)
       s = zero(eltype(x))
       s = s + (x[2] - x[1]) / d
       for i = 2:length(x)-1
       s = s + (x[i+1] - x[i+1]) / (2*d)
       end
       s = s + (x[n] - x[n-1])^2/d
       end
sample_diff (generic function with 1 method)


julia> function sample_diff_fast(x)
       n=length(x); d = 1/(n-1)
       s = zero(eltype(x))
       @fastmath s = s + (x[2] - x[1]) / d
       @fastmath for i = 2:n-1
       s = s + (x[i+1] - x[i+1]) / (2*d)
       end
       @fastmath s = s + (x[n] - x[n-1])^2/d
       end
sample_diff_fast (generic function with 1 method)

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

julia> t=rand(5000)

julia> sample_diff(t)
604.8332524539333

julia> sample_diff_fast(t)
604.8332524539333

julia> usingBenchmarkTools
julia> @benchmark sample_diff(t)
BenchmarkTools.Trial:
  memory estimate:  16 bytes
  allocs estimate:  1
  --------------
  minimum time:     7.146 μs (0.00% GC)
  median time:      7.253 μs (0.00% GC)
  mean time:        7.341 μs (0.00% GC)
  maximum time:     30.187 μs (0.00% GC)
  --------------
  samples:          10000
  evals/sample:     4

julia> @benchmark sample_diff_fast(t)
BenchmarkTools.Trial:
  memory estimate:  16 bytes
  allocs estimate:  1
  --------------
  minimum time:     1.835 μs (0.00% GC)
  median time:      1.920 μs (0.00% GC)
  mean time:        1.948 μs (0.00% GC)
  maximum time:     13.056 μs (0.00% GC)
  --------------
  samples:          10000
  evals/sample:     10

2. KBN 求和(KBN summation)

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

julia> sum([1 1e-200 -1])
0.0

julia> using KahanSummation
julia> sum_kbn([1 1e-200 -1])
1.0e-200

julia> @benchmark sum([1 1e-200 -1])
BenchmarkTools.Trial:
  memory estimate:  224 bytes
  allocs estimate:  5
  --------------
  minimum time:     112.085 ns (0.00% GC)
  median time:      113.931 ns (0.00% GC)
  mean time:        147.602 ns (19.42% GC)
  maximum time:     45.530 μs (99.56% GC)
  --------------
  samples:          10000
  evals/sample:     925


julia> @benchmark sum_kbn([1 1e-200 -1])
BenchmarkTools.Trial:
  memory estimate:  384 bytes
  allocs estimate:  11
  --------------
  minimum time:     227.825 ns (0.00% GC)
  median time:      231.045 ns (0.00% GC)
  mean time:        289.537 ns (17.16% GC)
  maximum time:     79.454 μs (99.52% GC)
  --------------
  samples:          10000
  evals/sample:     530

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(n, m) = "base case"
f(n::Number, m::Number) = "n and m are both numbers"
f(n::Number, m) = "n is a number"
f(n, m::Number) = "m is a number"
f(n::Integer, m::Integer) = "n and m are both integers"

以上五行代码会返回一个存在五个方法的函数 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 地址)进行包添加。

添加官方注册包:

(v1.0) pkg> add AccurateArithmetic
   Cloning default registries into C:\Users\XXX\.julia\registries
   Cloning registry General from "https://github.com/JuliaRegistries/General.git"
  Updating registry at `C:\Users\XXX\.julia\registries\General`
  Updating git-repo `https://github.com/JuliaRegistries/General.git`
 Resolving package versions...
 Installed AccurateArithmetic ─ v0.1.3
  Updating `C:\Users\XXX\.julia\environments\v1.0\Project.toml`
  [22286c92] + AccurateArithmetic v0.1.3
  Updating `C:\Users\XXX\.julia\environments\v1.0\Manifest.toml`
  [22286c92] + AccurateArithmetic v0.1.3
  [2a0f44e3] + Base64
  [8ba89e20] + Distributed
  [b77e0a4c] + InteractiveUtils
  [8f399da3] + Libdl
  [37e2e46d] + LinearAlgebra
  [56ddb016] + Logging
  [d6f4376e] + Markdown
  [9a3f8284] + Random
  [9e88b42a] + Serialization
  [6462fe0b] + Sockets
  [8dfed614] + Test

从第三方网址安装:

(v1.0) pkg> add https://github.com/bensadeghi/DecisionTree.jl
   Cloning git-repo `https://github.com/bensadeghi/DecisionTree.jl`
  Updating git-repo `https://github.com/bensadeghi/DecisionTree.jl`
 Resolving package versions...
 Installed ScikitLearnBase ─ v0.4.1
 Installed Compat ────────── v1.2.0
  Updating `C:\Users\XXX\.julia\environments\v1.0\Project.toml`
  [7806a523] + DecisionTree v0.8.1+ #master (https://github.com/bensadeghi/DecisionTree.jl)
  Updating `C:\Users\XXX\.julia\environments\v1.0\Manifest.toml`
  [34da2185] + Compat v1.2.0
  [7806a523] + DecisionTree v0.8.1+ #master (https://github.com/bensadeghi/DecisionTree.jl)
  [6e75b9c4] + ScikitLearnBase v0.4.1
  [ade2ca70] + Dates
  [8bb1440f] + DelimitedFiles
  [76f85450] + LibGit2
  [a63ad114] + Mmap
  [44cfe95a] + Pkg
  [de0858da] + Printf
  [3fa0cd96] + REPL
  [ea8e919c] + SHA
  [1a1011a3] + SharedArrays
  [2f01184e] + SparseArrays
  [10745b16] + Statistics
  [cf7118a7] + UUIDs
  [4ec0a83e] + Unicode

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

(v1.0) pkg> help
  Welcome to the Pkg REPL-mode. To return to the julia> prompt, either press backspace when the input line is empty or press Ctrl+C.

  Synopsis

  pkg> [--env=...] cmd [opts] [args]

  Multiple commands can be given on the same line by interleaving a ; between the commands.
  ...
  build: run the build script for packages
  pin: pins the version of packages
  free: undoes a pin, develop, or stops tracking a repo.
  ...
  instantiate: downloads all the dependencies for the project
  resolve: resolves to update the manifest from changes in dependencies of developed packages
  generate: generate files for a new project
  preview: previews a subsequent command without affecting the current state
  precompile: precompile all the project dependencies
  gc: garbage collect packages not used for a significant time
  activate: set the primary environment the package manager manipulates

目前看来在对开发包管理的基本操作上,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 库:

julia> Pkg.update()
julia> Pkg.add("DecisionTree")
julia> Pkg.add("ScikitLearn")

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

julia> using ScikitLearn
julia> using DecisionTree

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

# Create a random dataset
Random.seed!(42)
X = sort(5 * rand(80))
XX = reshape(X, 80, 1)
y = sin.(X)
y[1:5:end] += 3 * (0.5 .- rand(16))

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

# Fit regression model
regr_1 = DecisionTreeRegressor()
regr_2 = DecisionTreeRegressor(pruning_purity_threshold=0.05)
regr_3 = RandomForestRegressor(n_trees=20)
fit!(regr_1, XX, y)
fit!(regr_2, XX, y)
fit!(regr_3, XX, y)

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

# Predict
X_test = 0:0.01:5.0
y_1 = predict(regr_1, hcat(X_test))
y_2 = predict(regr_2, hcat(X_test))
y_3 = predict(regr_3, hcat(X_test))

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

# Import the necessary modules and libraries
import numpy as np
from sklearn.tree import DecisionTreeRegressor

#run in Python IDE
import matplotlib.pyplot as plt

# Create a random dataset
rng = np.random.RandomState(1)
X = np.sort(5 * rng.rand(80, 1), axis=0)
y = np.sin(X).ravel()
y[::5] += 3 * (0.5 - rng.rand(16))

# Fit regression model
regr_1 = DecisionTreeRegressor()
regr_2 = DecisionTreeRegressor(max_depth=5)
regr_1.fit(X, y)
regr_2.fit(X, y)

# Predict
X_test = np.arange(0.0, 5.0, 0.01)[:, np.newaxis]
y_1 = regr_1.predict(X_test)
y_2 = regr_2.predict(X_test)

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

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

julia> @timeit sample_dt()
elapsed time in seconds: 0.030501-element Array{Float64,1}:

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

t0 = time.time()
# Import the necessary modules and libraries
rng = np.random.RandomState(1)
X = np.sort(5 * rng.rand(80, 1), axis=0)
y = np.sin(X).ravel()
y[::5] += 3 * (0.5 - rng.rand(16))
regr_1 = DecisionTreeRegressor()
#regr_2 = DecisionTreeRegressor(max_depth=5)
regr_1.fit(X, y)
#regr_2.fit(X, y)
t1 = time.time()-t0
print(t1*1000)
#1993.1974411010742

虽然上述例子并不算十分严谨,不过相对来说,通常对于初学者或者算法使用者,不涉及自己 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

工程编程语言Julia
2
相关数据
决策树学习技术
Decision tree learning

决策树/决策规则学习是一种决策支持工具,使用了树状图来模拟决策和对应结果。它的现实应用包括业务管理、客户关系管理和欺诈检测。最流行的决策树算法包括 ID3、CHAID、CART、QUEST 和 C4.5。

分布式计算技术
Distributed computing

在计算机科学中,分布式计算,又译为分散式運算。这个研究领域,主要研究分布式系统如何进行计算。分布式系统是一组电脑,通过网络相互链接传递消息与通信后并协调它们的行为而形成的系统。组件之间彼此进行交互以实现一个共同的目标。

机器学习技术
Machine Learning

机器学习是人工智能的一个分支,是一门多领域交叉学科,涉及概率论、统计学、逼近论、凸分析、计算复杂性理论等多门学科。机器学习理论主要是设计和分析一些让计算机可以自动“学习”的算法。因为学习算法中涉及了大量的统计学理论,机器学习与推断统计学联系尤为密切,也被称为统计学习理论。算法设计方面,机器学习理论关注可以实现的,行之有效的学习算法。

损失函数技术
Loss function

在数学优化,统计学,计量经济学,决策理论,机器学习和计算神经科学等领域,损失函数或成本函数是将一或多个变量的一个事件或值映射为可以直观地表示某种与之相关“成本”的实数的函数。

参数技术
parameter

在数学和统计学裡,参数(英语:parameter)是使用通用变量来建立函数和变量之间关系(当这种关系很难用方程来阐述时)的一个数量。

随机森林技术
Random Forest

在机器学习中,随机森林是一个包含多个决策树的分类器,并且其输出的类别是由个别树输出的类别的众数而定。 Leo Breiman和Adele Cutler发展出推论出随机森林的算法。而"Random Forests"是他们的商标。这个术语是1995年由贝尔实验室的Tin Kam Ho所提出的随机决策森林(random decision forests)而来的。这个方法则是结合Breimans的"Bootstrap aggregating"想法和Ho的"random subspace method" 以建造决策树的集合。

强化学习技术
Reinforcement learning

强化学习是一种试错方法,其目标是让软件智能体在特定环境中能够采取回报最大化的行为。强化学习在马尔可夫决策过程环境中主要使用的技术是动态规划(Dynamic Programming)。流行的强化学习方法包括自适应动态规划(ADP)、时间差分(TD)学习、状态-动作-回报-状态-动作(SARSA)算法、Q 学习、深度强化学习(DQN);其应用包括下棋类游戏、机器人控制和工作调度等。

监督学习技术
Supervised learning

监督式学习(Supervised learning),是机器学习中的一个方法,可以由标记好的训练集中学到或建立一个模式(函数 / learning model),并依此模式推测新的实例。训练集是由一系列的训练范例组成,每个训练范例则由输入对象(通常是向量)和预期输出所组成。函数的输出可以是一个连续的值(称为回归分析),或是预测一个分类标签(称作分类)。

深度学习技术
Deep learning

深度学习(deep learning)是机器学习的分支,是一种试图使用包含复杂结构或由多重非线性变换构成的多个处理层对数据进行高层抽象的算法。 深度学习是机器学习中一种基于对数据进行表征学习的算法,至今已有数种深度学习框架,如卷积神经网络和深度置信网络和递归神经网络等已被应用在计算机视觉、语音识别、自然语言处理、音频识别与生物信息学等领域并获取了极好的效果。

张量技术
Tensor

张量是一个可用来表示在一些矢量、标量和其他张量之间的线性关系的多线性函数,这些线性关系的基本例子有内积、外积、线性映射以及笛卡儿积。其坐标在 维空间内,有 个分量的一种量,其中每个分量都是坐标的函数,而在坐标变换时,这些分量也依照某些规则作线性变换。称为该张量的秩或阶(与矩阵的秩和阶均无关系)。 在数学里,张量是一种几何实体,或者说广义上的“数量”。张量概念包括标量、矢量和线性算子。张量可以用坐标系统来表达,记作标量的数组,但它是定义为“不依赖于参照系的选择的”。张量在物理和工程学中很重要。例如在扩散张量成像中,表达器官对于水的在各个方向的微分透性的张量可以用来产生大脑的扫描图。工程上最重要的例子可能就是应力张量和应变张量了,它们都是二阶张量,对于一般线性材料他们之间的关系由一个四阶弹性张量来决定。

重构技术
Refactoring

代码重构(英语:Code refactoring)指对软件代码做任何更动以增加可读性或者简化结构而不影响输出结果。 软件重构需要借助工具完成,重构工具能够修改代码同时修改所有引用该代码的地方。在极限编程的方法学中,重构需要单元测试来支持。

TensorFlow技术
TensorFlow

TensorFlow是一个开源软件库,用于各种感知和语言理解任务的机器学习。目前被50个团队用于研究和生产许多Google商业产品,如语音识别、Gmail、Google 相册和搜索,其中许多产品曾使用过其前任软件DistBelief。