摘要
我们研究了机器学习背景下函数式编程的特点。Python和R是该领域最广泛使用的语言。我们可以使用NumPy或TensorFlow这样的库来实现几乎所有的模型。 在这篇文章中,我们讨论了Haskell与Python和R相比的优势。线性代数是机器学习的主要数学工具之一,还有概率论和统计。因此,我们的研究旨在调查和改进Haskell工具。
一般背景
Haskell是一种多态的函数式编程语言,具有懒惰评估功能。Haskell有一系列令人印象深刻的特点,如参数化的多态性、强大的类型系统、更容易重构和调试。Haskell已经证明了它在编程语言设计、区块链系统、并发和并行计算等方面的效率。在我们看来,在机器学习中使用Haskell的潜力还没有得到足够深入的研究。我们想强调几个与函数式编程和机器学习相关的先前工作的例子。
此外,我们推荐阅读相当有趣的博文系列,名为Haskell中的实用依赖类型。Justin Le的《类型安全的神经网络》。看一下haskell-ml库也是很有用的,那里的机器学习例子是基于Le的想法的,在这篇求助文章中,对Haskell生态系统在机器学习和数据科学方面的投射也描述得很好。
问题
我们要讨论的是技术方面的实现,而不是机器学习本身。在机器学习和相关领域,人们经常需要处理多维数据,我们通常用矩阵来表示。就Haskell而言,纯洁性、不变性和翻译器开发的简易性提供了使计算无与伦比的方法,并将代码转化为在其他架构(如NVidia微架构)上执行。
处理多维数据需要使用线性代数。我们认为数据集是一个二维矩阵,其中的每一列都是对外部世界某些现象的单一观察。这种数据集的维度相当大,这使得数据处理和存储的成本太高。另一方面,观察结果之间可能存在潜在的相关性。这些相关性的存在有利于用较低的维度来表示给定的数据集。也就是说,寻求这种依赖关系可以优化输入数据。机器学习提供了一套庞大的程序,用于从我们的数据集中提取主要信息,并表示给定的观察值,直到它们之间的相关性。
当人们写有大量的矩阵操作的代码时,例如矩阵转置,会出现很多令人遗憾的错误。这些错误与类型没有关系,所以类型检查器无法检测到它们。最好的情况是出现运行时错误。有时,一个评估过程在没有任何运行时错误的情况下成功完成,但其结果可能与预期的不同。例如,一个输出矩阵可能有错误的尺寸。有时没有办法立即识别这种错误。最糟糕的是,可能会有一个所谓的 "浮动 "错误,在个案的基础上周期性地再现。到目前为止,浮动错误是调试中最困难的,因为要确定这种故障的原因并不直接。此外,也不清楚为什么正好有这些维度,而不是其他维度。
这些错误经常影响矩阵维度。由于这个原因,我们把它们放在最优先的位置。在应用一些矩阵操作时,人们应该始终注意维度的一致性,例如,矩阵乘法或线性求解器,其中输入维度之间的关系很重要。我们试图解决类型级维度的问题,为广泛使用的常见机器学习程序提供类型安全的实现。在我们的调查中,我们正在研究诸如(概率)主成分分析(简单地说,PPCA)和高斯过程潜变量模型(GPLVM)等降维程序的Haskell实现。
在我们看来,处理类型级矩阵维度是值得的。这种处理阵列维度的方法允许人们抓住上面描述的整类错误,并处理一些相当简单的证明,只要我们要求维度之间的关系,并且这种关系是可以证明的。
下面我们讨论并回顾相关的矩阵和数字库,以了解哪些数组库对Haskell中的机器学习有用,以及哪些类型级自然数的方法可能被应用于安全矩阵维度。
HMatrix
hmatrix 是Haskell中最流行的矩阵库之一。 ,在著名的库hmatrix BLAS和LAPACK上提供了一个接口。来自Tweag.io的同事在博文中对 库进行了相当精细的描述,hmatrix 这里也有。我们将相当简要地讨论这个库,并看一下几个例子,以了解 是如何工作的。hmatrix
例如,让我们考虑Cholesky分解。Cholesky分解是对矩阵的一种操作,它将给定的矩阵(应该是赫米特的和正定义的)分解为下三角矩阵和其共轭转置的乘积。这种操作被广泛用于解决线性系统和类似的东西。在我们的工作中,我们也多次使用Cholesky分解。在hmatrix ,Cholesky分解是一个具有以下特征的函数。
chol :: Field t => Herm t -> Matrix t
其中Field 是一个类型类,表示t 是一个字段。Matrix 是一个由严格的维度和可存储的向量组成的矩阵的数据类型,它是矩阵本身的内容。Herm 只是一个newtype overMatrix 。在我们的研究中,我们隐含地使用了hmatrix 函数。正如我们将在后面描述的那样,我们决定使用repa 库作为主库,而repa 中的矩阵和相关操作都是从HMatrix 中获得的。更多信息请参见以下库。
注意,在hmatrix 中有一个叫做Numeric.LinearAlgebra.Static的模块,具有相同的功能,但是具有静态类型级的维度。例如,在hmatrix 中有一个具有静态维度的线性求解器。
(<>) :: forall m k n. (KnownNat m, KnownNat k, KnownNat n) => L m k -> L k n -> L m n
其中L 是一个二维矩阵的数据类型,通过其行数和列数进行参数化。m,n, 和k 是自然数(事实上,GHC.Types中的类型Nat );KnownNat是一个连接整数和类型级自然数的类型类。因此,hmatrix 中的类型安全问题得到了部分解决,但来自上述模块的Nat 只是类型级自然数的一个表示,而不是自然数本身。原因是这些自然数上的算术运算是作为开放类型族实现的,但我们希望这些运算能明确地实现。在这种情况下,我们应该声称,hmatrix 中的这种类型级维度的版本对于证明和基本验证来说是不方便的。
加速
accelerateaccelerate 允许编写代码,这些代码将在运行时被编译成特殊领域的语言,然后将通过LLVM编译,在多线程CPU或NVidia GPU上执行。不幸的是,没有像SVD或Cholesky分解这样有用的函数,所以accelerate ,不像数字库那样丰富。另一方面,这些操作可以作为CUDA-solver的低级绑定,尽管还没有实现明确的功能。
例子:
map :: (Shape sh, Elt a, Elt b) => (Exp a -> Exp b) -> Acc (Array sh a) -> Acc (Array sh b)
fold :: (Shape sh, Elt a) => (Exp a -> Exp a -> Exp a)
-> Exp a -> Acc (Array (sh :. Int) a) -> Acc (Array sh a)
Exp a 表示一个用于计算单个值的表达式。在这样的表达式中,所有的操作都是按顺序执行的。 表示一个计算类型为 的数组的表达式,由许多类型为 的表达式组成。所有表达式的执行都是平行进行的。这种分离有助于避免现在不支持的嵌套并行。 表示结果数组的维度( 表示向量, 表示矩阵,等等)。 是数组元素的类型。 是表示数组维度的类型。Acc (Array sh a) a Exp a``sh``DIM1 DIM2``Elt``Shape
尽管人们可以像处理数组和标量一样处理 "Acc (..) "和 "Exp a",但它们在运行时不是真正的数组或值,而是计算它们的代码。这段代码可能被编译,并通过 功能在CPU或GPU上执行。run
如果你对accelerate 感兴趣,可以看一下这些例子,以了解它的工作细节。
Repa
repa是一个带有常规多维数组的包,就像accelerate 。但是repa 有一个重要的与众不同的特点。在repa ,人们可以处理大量表示的数组。repa 中的数组数据类型是一个名为Source的类型类中的关联数据类型,其定义如下。
class Source r e where
data Array r sh e
extent :: Shape sh => Array r sh e -> sh
linearIndex :: Shape sh => Array r sh e -> Int -> e
deepSeqArray :: Shape sh => Array r sh e -> b -> b
其中r 是一种表示类型,e 是一种元素类型。除了这个关联的数组类型外,这个类还有三个方法(事实上,不止三个,但这些方法构成了最小的完整定义)。
与accelerate 相比,在repa 中有几种数组表示法。D 是一个数据类型,它对应于通过从索引到元素的函数来延迟表示数组。C 是通过所谓的游标函数来延迟表示。延迟数组和游标数组与其说是真正的数组,不如说是 "伪数组"。真正的数组还有另一种表示方法:U 和V 分别是非盒式数组和盒式数组。B 是一个严格的字节串数组。F 表示一个外来的内存缓冲区。在我们的工作中,为了方便和简单,我们决定默认使用延迟的数组。
不幸的是,它的发展已经停滞。
Massiv
这个库的主要思想来自于repa ,但它正由我们的俄罗斯同事Alexey Kuleshevich积极开发。与repa 和accelerate 相比,massiv 提供了一套广泛的工具来处理可变数组。massiv 比repa 更有成效。另一方面,massiv 的生态系统缺乏数值功能:诸如Cholesky分解等功能还没有实现。但是与repa 不同,这个库和它的生态系统正在积极发展。
维度
Dimensions是一个为多维数据提供安全的类型级维度的库,可以不受特定数据结构的影响而使用。这个库是EasyTensor包的一部分。这个库的目的是为张量算法中的矩阵提供类型安全。但是这种效率的实现是以牺牲刚性为代价的。例如,在dimensions ,在类型之间有很多不安全的coerce用于证明创建。此外,未知维度和已知维度之间的区别增加了代码的复杂性,并使模板成倍增加。另外,作者使用了来自GHC.TypeLits的类型级自然数。这种方法有一些缺点,我们在下面描述:他们不依赖于dimensions 功能。
GHC.TypeLits和type-natural
类型级自然数已经在base 库中的GHC.TypeLits模块中实现。这个版本的类型级自然数工作得相当好,并且被广泛地支持在边库中,但是这些数字不是归纳的数据类型,所以要实现与自然数相关的琐碎事实的最基本证明是相当困难的。换句话说,字词 "2 "和 "134 "之间没有任何联系。这个问题可能通过一些特殊的工具得到部分解决,但这并不能完全解决这个问题。
从我们的角度来看,名为type-natural的库更适合这个目的。与GHC.TypeLits 相比,这里的自然数是按照Peano的方式归纳引入的,可以归纳地证明一些简单的属性。
总结
在这一部分,我们介绍了Haskell中类型安全的尺寸问题,并回顾了相关的库和工具。我们简要地描述了矩阵和维度库,并讨论了它们的优点和局限性。
在我们看来,我们所解决的大多数任务都有一个通过依赖类型的更优雅的解决方案。在第二部分,我们还将讨论允许在Haskell中模拟依赖类型编程的有用的库以及相关的困难。我们将概述我们自己的方法,即通过其维度参数化的矩阵数据类型。