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

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 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 在数值类型与计算方面为高性能做出的改进和特色性质。


微信图片_20211130111852.jpg

上图引用自维基百科

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 已实现的模型与方法。微信图片_20211130092842.png


参考资料:



相关文章
|
6月前
|
缓存 程序员 开发者
Python中的装饰器:提升代码灵活性与可维护性
在Python编程中,装饰器是一种强大的工具,它能够动态地修改或扩展函数或类的行为,从而提升代码的灵活性和可维护性。本文将深入探讨装饰器的工作原理、常见用法以及如何在项目中合理地应用装饰器,帮助开发者更好地理解和利用这一特性。
|
6月前
|
测试技术 开发者 Python
Python中的装饰器:提升代码灵活性与可维护性
在Python编程中,装饰器是一种强大的工具,可以在不修改函数源代码的情况下,动态地修改其行为或添加额外功能。本文将介绍装饰器的基本概念和用法,并通过实际示例演示如何利用装饰器提升代码的灵活性和可维护性,从而更高效地开发Python应用程序。
|
6月前
|
监控 Python
Python中的装饰器:提升代码灵活性与可读性
在Python编程中,装饰器是一种强大的工具,能够提升代码的灵活性和可读性。本文将介绍装饰器的基本概念、使用方法以及实际应用场景,帮助读者更好地理解和利用这一功能。
|
5月前
|
测试技术 开发者 Python
Python中的装饰器:提升函数的灵活性和可重用性
在Python编程中,装饰器是一种强大的工具,它可以在不修改函数本身的情况下,动态地扩展函数的功能。本文将介绍装饰器的工作原理及其在实际开发中的应用,帮助读者更好地理解和利用这一特性。
|
5月前
|
人工智能 程序员 Shell
程序员必知:对比python学julia(第二章)
程序员必知:对比python学julia(第二章)
30 2
|
5月前
|
缓存 Python
Python中的装饰器:提升代码可读性和灵活性
在Python编程中,装饰器是一种强大的工具,它允许我们动态地修改函数或类的行为。本文将深入探讨Python中装饰器的使用方法以及如何利用装饰器提升代码的可读性和灵活性。通过实际示例,我们将展示装饰器在不同场景下的应用,帮助读者更好地理解和运用这一重要的编程技术。
|
6月前
|
缓存 开发者 Python
Python中的装饰器:提升代码灵活性与可复用性
众所周知,Python作为一门流行的编程语言,其装饰器(Decorator)机制为代码的优化和重用提供了强大支持。本文将深入探讨Python中装饰器的概念、用法和实际应用,帮助读者更好地理解并应用这一技术,从而提升代码的灵活性和可复用性。
|
6月前
|
缓存 开发者 Python
Python中的装饰器:提升代码灵活性和可维护性
Python中的装饰器是一种强大的工具,它可以帮助开发者提升代码的可维护性和灵活性。本文将深入探讨Python装饰器的原理、用法以及实际应用场景,帮助读者更好地理解并运用装饰器来优化自己的代码。
|
6月前
|
测试技术 Python
解密Python中的装饰器:提升代码可读性与灵活性
Python中的装饰器是一种强大的工具,能够在不改变原有函数结构的情况下,为函数添加额外功能。本文将深入探讨装饰器的原理及应用,介绍装饰器的基本语法和常见用法,并结合实例演示如何利用装饰器提升代码的可读性和灵活性,使代码更加简洁、模块化和易于维护。
|
6月前
|
监控 Python
Python中的装饰器:提升代码灵活性和可维护性
在Python编程中,装饰器是一种强大的工具,可以提高代码的灵活性和可维护性。本文将深入探讨装饰器的概念、用法和实际应用,帮助读者更好地理解并运用装饰器来优化自己的Python代码。
下一篇
无影云桌面