From 509dfe2c97aaec221574d4f73406cc8ff53179d2 Mon Sep 17 00:00:00 2001 From: ascoders <576625322@qq.com> Date: Mon, 21 Oct 2019 08:55:05 +0800 Subject: [PATCH] 125 --- ...17\344\271\213\347\276\216\343\200\213.md" | 136 ++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 "125.\347\262\276\350\257\273\343\200\212\346\267\261\345\272\246\345\255\246\344\271\240 - \345\207\275\346\225\260\345\274\217\344\271\213\347\276\216\343\200\213.md" diff --git "a/125.\347\262\276\350\257\273\343\200\212\346\267\261\345\272\246\345\255\246\344\271\240 - \345\207\275\346\225\260\345\274\217\344\271\213\347\276\216\343\200\213.md" "b/125.\347\262\276\350\257\273\343\200\212\346\267\261\345\272\246\345\255\246\344\271\240 - \345\207\275\346\225\260\345\274\217\344\271\213\347\276\216\343\200\213.md" new file mode 100644 index 00000000..7779e02d --- /dev/null +++ "b/125.\347\262\276\350\257\273\343\200\212\346\267\261\345\272\246\345\255\246\344\271\240 - \345\207\275\346\225\260\345\274\217\344\271\213\347\276\216\343\200\213.md" @@ -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),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。** + +> 关注 **前端精读微信公众号** + + + +> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))