-
Notifications
You must be signed in to change notification settings - Fork 3.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
1 changed file
with
136 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
## 1 引言 | ||
|
||
函数式语言在深度学习领域应用很广泛,因为函数式与深度学习模型的契合度很高,[The Beauty of Functional Languages in Deep Learning — Clojure and Haskell](https://www.welcometothejungle.co/fr/articles/btc-deep-learning-clojure-haskell) 就很好的诠释了这个道理。 | ||
|
||
通过这篇文章可以加深我们对深度学习与函数式编程的理解。 | ||
|
||
## 2 概述与精读 | ||
|
||
深度学习是机器学习中基于人工神经网络模型的一个分支,通过模拟多层神经元的自编码神经网络,将特征逐步抽象化,这需要多维度、大数据量的输入。[TensorFlow](https://www.tensorflow.org/) 和 [PyTorch](https://pytorch.org/) 是比较著名的 Python 深度学习框架,同样 [Keras](https://blog.rstudio.com/2017/09/05/keras-for-r/) 在 R 语言中也很著名。然而在生产环境中,基于 **性能和安全性** 的考虑,一般会使用函数式语言 [Clojure](https://www.clojure.org/) 或 [Haskell](https://www.haskell.org/)。 | ||
|
||
在生产环境中,可能要并发出里几百万个参数,因此面临的挑战是:如何高效、安全的执行这些运算。 | ||
|
||
**所以为什么函数式编程语言可以胜任深度学习的计算要求呢?** 深度学习的计算模型本质上是数学模型,而数学模型本质上和函数式编程思路是一致的:数据不可变且函数间可以任意组合。这意味着使用函数式编程语言可以更好的表达深度学习的计算过程,因此更容易理解与维护,同时函数式语言内置的 Immutable 数据结构也保障了并发的安全性。 | ||
|
||
另外函数式语言的函数之间都是相互隔离的,即便在多线程环境下也不会发生竞争和死锁的情况,函数式编程语言会自动处理这些情况。 | ||
|
||
比如说 [Clojure](https://www.clojure.org/),**它甚至可在两个同时修改同一引用的程序并发运行时,自动重试其中之一,而不需要手动加锁**: | ||
|
||
```clojure | ||
(import ‘(java.util.concurrent Executors)) | ||
(defn test-stm [nitems nthreads niters] | ||
(let [refs (map ref (repeat nitems 0)) | ||
pool (Executors/newFixedThreadPool nthreads) | ||
tasks (map (fn [t] | ||
(fn [] | ||
(dotimes [n niters] | ||
(dosync | ||
(doseq [r refs] | ||
(alter r + 1 t)))))) | ||
(range nthreads))] | ||
(doseq [future (.invokeAll pool tasks)] | ||
(.get future)) | ||
(.shutdown pool) | ||
(map deref refs))) | ||
(test-stm 10 10 10000) -> (550000 550000 550000 550000 550000 550000 550000 550000 550000 550000) | ||
``` | ||
|
||
上面的代码创建了引用(refs),同时创建了多个线程自增这个引用对象,按理说每个线程都修改这个引用会导致竞争状态出现,但从结果来看是正常的,说明 Clojure 引擎在执行时会自动解决这个问题。实际上当两个线程出现竞争而失败时,Clojure 会自动重试其中之一。 | ||
|
||
> [原文介绍](https://clojure.org/about/concurrent_programming) | ||
**Clojure 的另一个优势是并行效率高:** | ||
|
||
```clojure | ||
(defn calculate-pixels-2 [] | ||
(let [n (* *width* *height*) | ||
work (partition (/ n 16) (range 0 n)) | ||
result (pmap (fn [x] | ||
(doall (map | ||
(fn [p] | ||
(let [row (rem p *width*) col (int (/ p *height*))] | ||
(get-color (process-pixel (/ row (double *width*)) (/ col (double *height*)))))) | ||
x))) | ||
work)] | ||
(doall (apply concat result)))) | ||
``` | ||
|
||
使用 `partition` 结合 `pmap` 可以使并发效率达到最大化,也就是 CPU 几乎都消耗在实际计算上,而不是并行的任务管理与上下文切换。Clojure 凭借 `partition` 对计算进行分区,采取分而治之并对分区计算结果进行合并的思路优化了并发性能。 | ||
|
||
> [原文介绍](http://www.fatvat.co.uk/2009/05/jvisualvm-and-clojure.html) | ||
Clojure 另一个特性是函数链式调用: | ||
|
||
```clojure | ||
;; pipe arg to function | ||
(-> "x" f1) ; "x1" | ||
|
||
;; pipe. function chaining | ||
(-> "x" f1 f2) ; "x12" | ||
``` | ||
|
||
其中 `(-> "x" f1 f2)` 等价于 `f2(f1("x"))`,这种描述不仅更简洁清晰,也更接近于实际数学模型。 | ||
|
||
> [原文介绍](http://xahlee.info/clojure/clojure_function_chaining.html) | ||
最后,Clojure 还具备计算安全性,计算过程不会修改已有的数据,因此在神经网络的任何一层的原始值都会保留,每层计算都可以独立运行且函数永远幂等。 | ||
|
||
[Haskell](https://www.haskell.org/) 也有独特的优势,**它具有类型推断、惰性求值等特性**,被认为更适合用于机器学习。 | ||
|
||
类型推断即 Haskell 类型都是静态的,如果试图赋予错误的类型会报错。 | ||
|
||
Haskell 的另一个优势是可以非常清晰的描述数学模型。 | ||
|
||
想想一般数学模型是怎么描述函数的: | ||
|
||
```text | ||
fn => | ||
f1 = 1 | ||
f2 = 9 | ||
f3 = 16 | ||
n > 2, fn = 3fn-3 + 2fn-2 + fn-1 | ||
``` | ||
|
||
一般语言用 `if-else` 描述等价关系,但 Haskell 可以几乎原汁原味的还原函数定义过程: | ||
|
||
```haskell | ||
solve :: Int -> Interger | ||
solve 1 = 1 | ||
solve 2 = 9 | ||
solve 3 = 16 | ||
solve n = 3 * solve (n - 3) + 2 * solve (n - 2) + solve (n - 1) | ||
``` | ||
|
||
这使得阅读 Haskell 代码和阅读数学公式一样轻松。 | ||
|
||
> [原文](https://blog.jle.im/entry/purely-functional-typed-models-1.html) | ||
Haskell 另一个优势是惰性求值,即计算会在真正用到时才进行,而不会在计算前提前消费掉,比如: | ||
|
||
```haskell | ||
let x = [1..] | ||
let y = [2,4 ..] | ||
head (tail tail( (zip x y))) | ||
``` | ||
|
||
可以看到,`x` 与 `y` 分别是 `1,2,3,4,5,6...` 与 `2,4,6,8...` 的无限数组,而 `zip` 函数将其整合为一个新数组 `(1,2),(2,4),(3,6),(4,8)...` 这也是无限数组,如果将 `zip` 函数执行完那么程序就会永远执行下去。但 Haskell 却不会陷入死循环,而是直接输出第一位数字 `1`。这就是惰性计算的特性,无论数组有多长,只有真正用到某项时才对其进行计算,所以哪怕初始数据量或计算量很大,实际消耗的运算资源只取决于这次计算实际用到的部分。 | ||
|
||
由于深度学习数据量巨大,惰性求值可以忽略海量数据输入,大大提升计算性能。 | ||
|
||
## 3 总结 | ||
|
||
本文介绍了为什么深度学习更适合使用函数式语言,以及介绍了 Clojure 与 Haskell 语言的共性:安全性、高性能,以及各自独有的特性,证明了为何这两种语言更适合用在深度学习中。 | ||
|
||
在前端领域说到函数式或函数之美,大部分时候想到的是 Class Component 与 Function Component 的关系,这个理解是较为片面的。通过本文我们可以了解到,函数式的思想与数学表达式思想如出一辙,以写数学公式的思维方式写代码,就是一种较好的函数式编程思路。 | ||
|
||
函数式应该只有表达式,没有语句,这是因为函数式是为了处理运算而诞生的,因此很适合用在深度学习领域。 | ||
|
||
> 讨论地址是:[精读《深度学习 - 函数式之美》 · Issue #212 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/212) | ||
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。** | ||
|
||
> 关注 **前端精读微信公众号** | ||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg"> | ||
|
||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh)) |