OCaml 科学计算教程

返回
目录

介绍

本章简要介绍整本书的概要、目标受众、如何使用本书,以及 Owl 的安装。与 Owl 交互的方式有很多种,包括 utop、notebook 和 Owl-Jupyter。随时选择一种方式,与我们一同探索 Owl 的世界。

什么是科学计算

科学计算是一个迅速发展的跨学科领域,利用先进的计算能力来理解和解决复杂问题。科学计算中使用的算法通常可以分为两类:数值分析和计算机代数(或符号计算)。前者使用数值逼近来解决数学问题,而后者需要对计算进行精确的闭合形式表示,并操纵未分配特定值的符号。

这两种方法广泛应用于各种应用领域,如工程学、物理学、生物学、金融等。尽管这些先进应用非常复杂,但它们都建立在科学库中的基本数值运算之上,而其中大部分已由 Owl 提供。例如,您可以使用 Owl 用几行代码编写一个深度神经网络:

open Owl
open Neural.S
open Neural.S.Graph
open Neural.S.Algodiff

let make_network input_shape =
  input input_shape
  |> lambda (fun x -> Maths.(x / F 256.))
  |> conv2d [|5;5;1;32|] [|1;1|] ~act_typ:Activation.Relu
  |> max_pool2d [|2;2|] [|2;2|]
  |> dropout 0.1
  |> fully_connected 1024 ~act_typ:Activation.Relu
  |> linear 10 ~act_typ:Activation.(Softmax 1)
  |> get_network

它实际上包括基本操作,如 adddiv卷积dot 等。如果您对这段代码的作用一无所知,完全没关系,我们将在本书后面进行介绍。关键是如何将复杂的应用程序分解成数值库中的基本构建块,这正是我们试图在整本书中传达的内容。

什么是函数式编程

大多数现有的数值或科学计算软件都基于命令式编程范 paradigm,这使用更改程序状态的语句。命令式程序通常通过从一个或多个过程或函数构建而成。这种模块化样式被广泛采用。后来在 1980 年左右,面向对象的编程思想迅速发展。它将模块化编程样式扩展到包括“对象”的概念。对象既可以包含数据又可以包含过程代码。命令式编程之所以在数值计算中广泛采用并非没有原因。几乎所有计算机的硬件实现都遵循命令式设计。实际上,FORTRAN,第一个跨平台编程语言和一种命令式语言,在 1950 年代初开发后,仍然广泛用于各个领域的数值和科学计算。有很大的机会,即使您使用现代流行的数值库,如 SciPy、Julia 或 Matlab,它们仍然在某个核心部分依赖 FORTRAN。

相比之下,函数式编程 似乎是为执行高级任务而生的。当 John McCarthy 设计了第一种函数式编程语言 LISP 时,他打算在人工智能领域使用它。它使用的 S 表达式原本是一种中间表示,但后来被证明足够强大和富有表现力。在 LISP 中,您可以清楚地看到函数式和命令式编程之间的明显区别。虽然后者使用一系列语句来更改程序的状态,但前者通过使用和组合函数来构建构建一个表达式树的程序。

这两种编程范式之间的根本区别在于底层的计算模型。命令式的基础是 Alan Turing 模型。在他们的书 Alan Turing: His Work and Impact 中,S. Barry Cooper 和 J. Van Leeuwen 表示“通过图灵机的可计算性引起了命令式编程的出现”。另一方面,函数式编程起源于 lambda 演算,这是一种构建于函数应用之上的形式系统。Lambda 演算由 Alonzo Church 在 1930 年代发明,它原本是一种形式上的数学逻辑系统,而不是编程语言。实际上,直到编程语言被发明后,这两者之间的关系才被揭示出来。图灵本人证明了 lambda 演算是图灵完备的。 (有趣的事实:图灵是 Church 的学生。)我们可以说 lambda 演算是所有函数式编程语言的基础。

与命令式编程相比,函数式编程具有不可变数据、一级函数和对尾递归进行优化的特点。通过使用高阶函数、柯里化、map 和 reduce 等技术,函数式编程通常可以实现线程的并行化、惰性求值和程序执行的确定性。但除了这些优点之外,我们现在正在讨论需要良好性能的数值计算。问题是,我们是否希望使用函数式编程语言进行科学计算?我们希望通过介绍构建在函数式编程语言 OCaml 上的 Owl,能够给您一个满意的答案。

这本书适合谁

我们真的希望这本书能够覆盖尽可能广泛的受众。科学计算和函数式编程都是大领域,因此编写一本能够满足所有人的书是一项相当大的挑战。如果您正在阅读这本书,我们假设您已经对分析任务感兴趣,并热衷于通过使用函数式编程语言获得一些实际经验。我们还假设您知道如何使用 OCaml 编程,并熟悉函数式编程的核心概念。

我们希望这本书相对通用,因此涵盖了科学计算的许多主题。但是,这意味着我们无法深入研究每个主题,而每个主题本身可能值得一本书。在设计章节时,我们选择了那些经典的(例如统计学、线性代数)或在行业中流行且被证明有效的主题(例如深度神经网络、概率编程等)。我们努力在广度和深度之间取得良好的平衡。对于每个主题,我们将尽力列出足够的参考文献,以指导读者深入研究。

与其他数据科学书籍不同,这本书还可以作为构建现代数值软件系统的其他软件架构师的参考。本书的重要部分之一是解释 Owl 的底层详细信息。我们不仅会给您一个鸟瞰全貌的 Owl 系统,还会教您如何逐步构建和优化系统的每个组件。如果您使用 Owl 构建应用程序,本书也可以作为有用的参考手册。

书的结构

本书分为三个部分,每个部分关注不同的领域。

第一部分首先介绍了 Owl 系统的基础知识和重要的约定,以帮助您学习如何使用 Owl 进行编程。然后,它探讨了科学计算的各种主题,从经典数学、统计学、线性代数、算法微分、优化、回归到流行的深度神经网络、自然语言处理、概率编程等。这些章节松散地根据它们的依赖性组织,例如在学习回归和深度神经网络之前,您需要了解优化。

第二部分致力于介绍 Owl 系统的架构。我们将深入研究每个核心组件,并展示我们如何构建和优化软件。通过这样做,您将全面了解现代数值系统如何结构化和开发,以及在这样一个复杂系统中需要哪些关键组件。请注意,尽管 Owl 是用 OCaml 开发的,但您在本部分学到的知识可以推广到其他语言。

第三部分是案例研究的集合。对于数据科学家和从业者来说,这部分可能是最有趣的部分。我们将演示如何使用 Owl 从头开始快速构建完整的数值应用程序。案例包括计算机视觉、推荐系统、金融技术等。

本书不强制要求按照顺序阅读,您可以直接跳转到最感兴趣的主题。如果您对 Owl 系统完全不了解,我们仍然强烈建议您从本书的前两章开始,以便了解如何设置工作环境并开始编程。本书中包含的所有代码片段都可以使用最新的 Owl 主分支进行编译,我们的工具确保书中的材料始终与软件保持最新。

安装

话虽如此,从简单的数学计算到那些大型用例还有很长的路要走。现在让我们从第一步开始:安装 Owl。Owl 需要 OCaml 版本 >=4.10.0。请确保在开始安装 Owl 之前已经有了可用的 OCaml 环境。您可以阅读关于如何 安装 OCaml 的指南。

Owl 的安装相当简单。有四种可能的方式,如下所示,从最直接的方式到最不直接的方式。

选项 1:从 OPAM 安装

由于 OCaml Labs 的同行们,OPAM 使得 OCaml 中的软件包管理比以前更加容易。您只需输入以下命令行即可进行安装。


                  opam install owl
                

ubuntu 类型的发行版上安装 Owl 时存在已知问题。原因是 BLAS 和 LAPACK 的二进制分发版本已过时,未能提供 Owl 需要的所有接口。您需要手动编译 openblas,并使用适当的环境变量指向您新编译的库。您可以使用 Owl 的 Docker 文件 作为解决此问题的参考。

这种安装方式拉取了 OPAM 上最新发布的 Owl。Owl 没有固定的发布计划。我们通常在积累足够的更改或实现重要功能时发布新版本。如果您想尝试最新的开发功能,我们建议使用下面的其他安装方式安装 Owl。

选项 2:从 Docker Hub 拉取

Owl 的 Docker 镜像 与主分支同步。每当有新的提交时,图像就会自动构建。您可以在 Docker Hub 上查看构建历史记录。

您只需拉取图像,然后启动容器。


                  docker pull owlbarn/owl
                  docker run -t -i owlbarn/owl
                

除了完整的 Owl 系统,Docker 镜像还包含一个增强的 OCaml toplevel - utop。您可以在容器中启动 utop 并尝试一些示例。Owl 的源代码存储在 /root/owl 目录中。您可以在启动的容器中直接修改源代码并重新构建系统。Owl Docker 镜像适用于各种 Linux 发行版,可以使用标签进一步指定,例如 docker pull owlbarn/owl:alpine

选项 3:固定到开发仓库

opam pin 允许您将本地代码固定到 Owl 在 Github 上的开发仓库。第一条命令 opam depext 安装 Owl 需要的所有依赖项。


                  opam depext owl
                  opam pin add owl --dev-repo
                

选项 4:从源代码编译

直接从源代码编译是一种老派但推荐的选项。首先,您需要克隆存储库。


                  git clone git@github.com:owlbarn/owl.git
                

其次,您需要找出缺失的依赖项并安装它们。


                  dune external-lib-deps --missing @install @runtest
                

最后,这可能是最经典的一步。


                  make && make install
                

如果您的 OPAM 旧于 V2 beta4,您需要额外的一步。这是因为 OPAM 中存在一个错误,它将编译的库复制到 /.opam/4.06.0/lib/stubslibs 而不是 /.opam/4.06.0/lib/stublibs。如果您不想升级 OPAM,那么您需要手动将 dllowl_stubs.so 文件从 stubslib 移动到 stublib 文件夹,然后一切都应该正常工作。但是,如果您安装了最新版本的 OPAM,这将不是您的问题。

CBLAS/LAPACKE 依赖关系

Owl 最重要的依赖关系是 OpenBLAS 库,它高效地实现了 BLAS 和 LAPACK 线性代数例程。链接到正确的 OpenBLAS 是实现最佳性能的关键。根据具体平台,您可以使用 yumapt-getbrew 安装二进制格式。例如,在我的 Mac OSX 上,安装如下:


                  brew install homebrew/science/openblas
                

然而,从 OpenBLAS 源代码安装给我们额外的好处。首先,它实现了与本地软件包管理工具提供的过时二进制分发版本相比最新的接口。其次,它导致了更好的性能,因为 OpenBLAS 根据您的系统配置和架构调整了许多参数,以生成最优化的二进制代码。

OpenBLAS 已经包含了 LAPACKE 的实现,只要您的计算机上安装了 Fortran

CBLAS/LAPACKE 依赖

Owl 最重要的依赖是 OpenBLAS 库,它高效地实现了 BLAS 和 LAPACK 线性代数例程。链接到正确的 OpenBLAS 是实现最佳性能的关键。根据具体的平台,您可以使用 yumapt-getbrew 安装二进制格式。例如,在我的 Mac OSX 上,安装过程如下:


                      brew install homebrew/science/openblas
                    

然而,从 OpenBLAS 源代码安装会带来额外的好处。首先,它实现了与本机包管理工具提供的过时二进制分发相比最新的接口。其次,它会带来更好的性能,因为 OpenBLAS 会根据您的系统配置和架构调整许多参数,生成最优化的二进制代码。

OpenBLAS 已经包含 LAPACKE 的实现,只要您的计算机上安装了 Fortran 编译器,LAPACKE 将自动编译并包含在安装中。

与 Owl 交互

与 Owl 系统交互有几种方式。传统的方式是编写 OCaml 应用程序,编译代码,链接到 Owl 系统,然后在计算机上本地运行它。您还可以跳过编译和链接步骤,使用 Owl 中的 Zoo 系统将代码作为脚本运行。

然而,对于初学者来说,尝试 Owl 最简单的方式是使用 REPL(Read–Eval–Print Loop),即 Python 等交互式 toplevel。toplevel 提供了一种方便的方式来玩耍小的代码片段。在 toplevel 中运行的代码会被编译成字节码而不是本机代码。字节码通常比本机代码运行速度慢得多。然而,这对 Owl 的性能影响很小,因为它的所有性能关键函数都是用 C 语言实现的。

OCaml 代码可以编译为字节码或本机代码。字节码在 OCaml 虚拟机上执行,它的性能不如平台优化的本机代码。Toplevel 在字节码模式下运行用户代码,但这对 Owl 的性能几乎没有影响,因为它的核心函数是用 C 语言实现的。如果您在脚本中运行 Owl,几乎不会注意到任何性能下降。接下来,我们将介绍两种为 Owl 设置交互环境的选项。

使用 Toplevel

OCaml 语言已经捆绑了一个简单的 toplevel,但我建议使用 utop 作为一个更高级的替代品。使用 OPAM 安装 utop 非常简单,只需在系统 shell 中运行以下命令。


                        opam install utop
                      

安装后,您可以使用以下命令在 utop 中加载 Owl。 owl-top 是 Owl 的 toplevel 库,它将自动加载几个相关库(包括 owl-zooowl-baseowl 核心库),以建立一个完整的数值环境。

 #require "owl-top"
                 open Owl
                

如果您不想在每次启动 toplevel 时键入这些命令,可以将它们添加到 .ocamlinit 文件中。 toplevel 在启动时读取 .ocamlinit 文件以初始化环境。这个文件通常存储在计算机上的主目录中。

使用 Notebook

Jupyter Notebook 是将演示与交互式代码执行混合在一起的一种流行方式。它源自 Python 世界,并得到各种语言的广泛支持。Notebook 的一个吸引人之处在于它使用客户端/服务器架构,并在浏览器中运行。

如果您想知道如何使用笔记本及其技术细节,请阅读 Jupyter 文档。在这里,让我逐步向您展示如何设置笔记本以逐步运行 Owl。

在 shell 中运行以下命令将为您安装所有依赖项。这包括 Jupyter Notebook 及其 OCaml 语言扩展


                        pip install jupyter
                        opam install jupyter
                        jupyter kernelspec install --name ocaml-jupyter "$(opam config var share)/jupyter"
                      

要启动 Jupyter 笔记本,您可以运行此命令。该命令启动在 http://127.0.0.1:8888/ 上运行的本地服务器,然后在浏览器中打开一个标签作为客户端。


                        jupyter notebook
                      

如果您希望远程运行笔记本服务器,请参阅 “运行笔记本服务器” 获取更多信息。要为多个用户设置服务器,这对于教育目的尤其有用,请参阅 JupyterHub 系统。

当一切准备就绪时,您可以在 Web 界面中启动一个新的笔记本。在新的笔记本中,您必须在第一个输入字段中运行以下 OCaml 代码以加载 Owl 环境。

#use "topfind"
                #require "owl-top, jupyter.notebook"
                

此时,在 Jupyter Notebook 中已经设置了完整的 Owl 环境,您可以随意进行任何实验。例如,您可以简单地复制并粘贴整个 lazy_mnist.ml 以在笔记本中训练卷积神经网络。但在这里,让我们只使用以下代码。

#use "topfind"
#require "owl-top, jupyter.notebook"
open Owl
open Neural.S
open Neural.S.Graph
open Neural.S.Algodiff
let make_network input_shape =
  input input_shape
  |> lambda (fun x -> Maths.(x / F 256.))
  |> conv2d [|5;5;1;32|] [|1;1|] ~act_typ:Activation.Relu
  |> max_pool2d [|2;2|] [|2;2|]
  |> dropout 0.1
  |> fully_connected 1024 ~act_typ:Activation.Relu
  |> linear 10 ~act_typ:Activation.(Softmax 1)
  |> get_network
>val make_network : int array -> network = <fun>

make_network 函数定义了卷积神经网络的结构。通过传递输入数据的形状,Owl 自动推断整个网络的形状,并在屏幕上漂亮地打印出网络结构的摘要。

make_network [|28;28;1|]
>- : network =
>18839
>
>[ Node input_0 ]:
>    Input : in/out:[*,28,28,1]
>    prev:[] next:[lambda_1]
>
>[ Node lambda_1 ]:
>    Lambda       : in:[*,28,28,1] out:[*,28,28,1]
>    customised f : t -> t
>    prev:[input_0] next:[conv2d_2]
>
>[ Node conv2d_2 ]:
>    Conv2D : tensor in:[*;28,28,1] out:[*,28,28,32]
>    init   : tanh
>    params : 832
>    kernel : 5 x 5 x 1 x 32
>    b      : 32
>    stride : [1; 1]
>    prev:[lambda_1] next:[activation_3]
>
>[ Node activation_3 ]:
>    Activation : relu in/out:[*,28,28,32]
>    prev:[conv2d_2] next:[maxpool2d_4]
>
>[ Node maxpool2d_4 ]:
>    MaxPool2D : tensor in:[*,28,28,32] out:[*,14,14,32]
>    padding   : SAME
>    kernel    : [2; 2]
>    stride    : [2; 2]
>    prev:[activation_3] next:[dropout_5]
>
>[ Node dropout_5 ]:
>    Dropout : in:[*,14,14,32] out:[*,14,14,32]
>    rate    : 0.1
>    prev:[maxpool2d_4] next:[fullyconnected_6]
>
>[ Node fullyconnected_6 ]:
>    FullyConnected : tensor in:[*,14,14,32] matrix out:(*,1024)
>    init           : standard
>    params         : 6423552
>    w              : 6272 x 1024
>    b              : 1 x 1024
>    prev:[dropout_5] next:[activation_7]
>
>[ Node activation_7 ]:
>    Activation : relu in/out:[*,1024]
>    prev:[fullyconnected_6] next:[linear_8]
>
>[ Node linear_8 ]:
>    Linear : matrix in:(*,1024) out:(*,10)
>    init   : standard
>    params : 10250
>    w      : 1024 x 10
>    b      : 1 x 10
>    prev:[activation_7] next:[activation_9]
>
>[ Node activation_9 ]:
>    Activation : softmax 1 in/out:[*,10]
>    prev:[linear_8] next:[]
>

第二个示例演示了在笔记本中绘制图表的方法。由于 Owl 的 Plot 模块不支持内存绘图,图表需要先写入文件,然后再传递给 Jupyter_notebook.display_file 进行渲染。

#use "topfind"
#require "owl-top, owl-plplot, jupyter.notebook"
open Owl
open Owl_plplot
let f x = Maths.sin x /. x in
let h = Plot.create "plot_00.png" in
Plot.set_title h "Function: f(x) = sine x / x";
Plot.set_xlabel h "x-axis";
Plot.set_ylabel h "y-axis";
Plot.set_font_size h 8.;
Plot.set_pen_size h 3.;
Plot.plot_fun ~h f 1. 15.;
Plot.output h
>- : unit = ()

为了将图像加载到浏览器中,我们需要调用 Jupyter_notebook.display_file 函数。然后,我们可以在运行在浏览器中的笔记本中正确渲染图表 [@fig:introduction:example00]。绘图功能极大丰富了交互式演示的内容。

Jupyter_notebook.display_file ~base64:true "image/png" "plot_00.png"
使用 Owl Notebook 的绘图示例
使用 Owl Notebook 的绘图示例

尽管对 display_file 的额外调用并不理想,但显然 OCaml 生态系统中的工具正在迅速发展。我相信我们很快就会有更好、更方便的工具用于交互式数据分析应用。

使用 Owl-Jupyter

目前,如果你想要省略显示在 Jupyter 中显示图像的额外一行代码,有一个方便的模块叫做 owl-jupyter。Owl-jupyter 模块重载了原始的 Plot.output 函数,以便可以直接在页面上显示绘制的图表。


# #use "topfind"
# #require "owl-jupyter"
# open Owl_jupyter

# let f x = Maths.sin x /. x in
  let h = Plot.create "plot_01.png" in
  Plot.set_title h "Function: f(x) = sine x / x";
  Plot.set_xlabel h "x-axis";
  Plot.set_ylabel h "y-axis";
  Plot.set_font_size h 8.;
  Plot.set_pen_size h 3.;
  Plot.plot_fun ~h f 1. 15.;
  Plot.output h
- : unit = ()
使用 Owl-Jupyter 的绘图示例
使用 Owl-Jupyter 的绘图示例

从上面的示例中,你可以看到使用笔记本可以显著提高 Owl 用户的体验。

总结

在本章中,我们简要介绍了 Owl 的背景,包括科学计算、函数式编程和目标受众,以及本书的布局。然后,我们开始介绍了如何安装和使用 Owl,作为开始这个旅程的第一步。您可以随意浏览本书的任何部分。

此时,您已在计算机上安装了 Owl 的工作环境,您应该为此感到非常自豪。老实说,对于新用户来说,这可能是最具挑战性的部分,即使 Owl 团队在改进其编译和安装方面花费了大量时间。现在,让我们继续探索更有趣的主题。

下一章: 第02章约定