【问题标题】:In clojure, what's the efficient way to convolve a vector by a kernel?在clojure中,将向量与内核卷积的有效方法是什么?
【发布时间】:2011-11-24 00:48:13
【问题描述】:

我想出了这个:

(def kernel [0 1 1 2 3 3 0 0 0 0 0 0])
(def data [1 5 7 4 8 3 9 5 6 3 2 1 1 7 4 9 3 2 1 8 6 4])

(defn capped+ [a b c] (let [s (+ a b)] (if (> s c) c s)))

(defn *+ [a b]
  (if (> (count a) (count b))  
    (reduce + (map-indexed (fn _ [i x] (* (a i) (b i))) b))
    (reduce + (map-indexed (fn _ [i x] (* (a i) (b i))) a))))

(defn slide*i [d k] 
 (let [ki (into [] (reverse k)) kl (count k) dl (count d)]
   (map-indexed 
      (fn [idx item] 
        (/ (*+ ki (subvec d idx (capped+ idx kl dl))) 
           (reduce + ki))) 
      d)))

(def s (slide*i data kernel))

这不是最优雅的代码,但它运行良好。 我其实想用它来平滑一些非常尖的!数据。

欢迎提出任何使这个更漂亮、更高效或更准确的建议(我个人并不关心尾巴不准确,因为在我的情况下我从不使用它)。

【问题讨论】:

    标签: vector clojure linear-algebra convolution


    【解决方案1】:

    您当然可以显着提高此操作的性能。好消息是,您无需为此投入 Java:如果您正确优化 Clojure,它的速度非常快,并且在大多数情况下可以产生与纯 Java 相同的速度。

    为了在 Clojure 中实现数字代码的最大性能,您需要使用:

    • 数组,因为您需要具有非常快速写入和查找的可变存储。 Clojure 序列和向量很漂亮,但它们会带来开销,对于真正的性能关键代码,您可能希望避免这些开销
    • double 原语,因为它们提供更快的数学运算。
    • aset / aget / areduce - 这些是专为数组设计的极其快速的操作,基本上为您提供与纯 Java 等价物相同的字节码。
    • 命令式风格 - 尽管它在 Clojure 中是单调的,但它获得了最快的结果(主要是因为您可以避免内存分配、装箱和函数调用的开销)。例如,使用 dotimes 进行快速命令式循环。
    • (set!*warn-on-reflection* true) - 并消除代码产生的任何警告,因为反射是一个很大的性能杀手。

    以下内容应该是正确的,并且可能会让您获得与 Java 大致相当的性能:

    (def kernel (double-array [0 1 1 2 3 3 0 0 0 0 0 0]))
    (def data (double-array [1 5 7 4 8 3 9 5 6 3 2 1 1 7 4 9 3 2 1 8 6 4]))
    
    (defn convolve [^doubles kernel-array ^doubles data-array]
      (let [ks (count kernel-array)
            ds (count data-array)
            output (double-array (+ ks ds))
            factor (/ 1.0 (areduce kernel-array i ret 0.0 (+ ret (aget kernel-array i))))]    
        (dotimes [i (int ds)]
          (dotimes [j (int ks)]
            (let [offset (int (+ i j))]
              (aset output offset (+ (aget output offset) (* factor (* (aget data-array i) (aget kernel-array j))))))))
        output))
    
    (seq (convolve kernel data))
    => (0.0 0.1 0.6 1.4 2.4 4.4 5.5 6.1000000000000005 5.600000000000001 6.200000000000001 5.499999999999999 5.9 4.199999999999999 3.3000000000000003 2.5 2.2 3.3 4.4 5.6000000000000005 4.8 4.8999999999999995 3.1 3.5 4.300000000000001 5.0 3.0 1.2000000000000002 0.0 0.0 0.0 0.0 0.0 0.0 0.0)
    

    我没有修剪输出数组或做任何边界,所以你可能需要稍微修改一下这个解决方案才能得到你想要的输出,但希望你明白......

    一些非常粗略的基准测试:

    (time (dotimes [i 1000] (seq (convolve kernel data))))
    => "Elapsed time: 8.174109 msecs"
    

    即每个内核/数据对组合大约需要 30ns - 我预计这几乎达到了缓存内存访问的界限。

    【讨论】:

    • 很棒的答案,非常全面的 cmets。 defn 中的 ^doubles 是什么?为了让我明白,你的意思是我最好尽可能接近我在java中实现它的方式,对吧?我的意思是基本上嵌套了for 循环。
    • ^doubles 是一种类型提示,用于指示原始双精度数组。它帮助编译器优化 get 方法。是的,嵌套 for 循环和数组变异基本上是解决这个特定问题的最快方法。不是很 Clojure-y,但它可以工作!
    • 令我惊讶的是,这并不比下面简单的 HOF 方法快多少,尤其是在启用 *unchecked-math*(1.3 中的新功能)时。
    • 有趣 - 你是什么时候得到 Alex 的?我还注意到,我上面的算法比它需要的写入密集度更高,因此可能还有另一个可能的加速。
    【解决方案2】:
    ;未使用,但留作演示 (定义上限+ [a b c] (分钟 (+ a b) c)) (定义 *+ [a b] (减少+(地图* a b))) (定义幻灯片*i [d k] (让[ki(反向k) kl(计数 k) ks (减少 + k)] (map #(/ (*+ %1 ki) ks) (partition kl 1 d))))

    使用partition,结果为:

    (59/10 21/5 33/10 5/2 11/5 33/10 22/5 28/5 24/5 49/10 31/10)

    但是使用partition-all,您将得到您的解决方案的确切结果:

    (59/10 21/5 33/10 5/2 11/5 33/10 22/5 28/5 24/5 49/10 31/10 7/2 43/10 5 3 6/5 0 0 0 0 0 0)

    【讨论】:

    • 通常(至少对于我所做的事情的类型)内核比数据向量短得多(例如具有 100 个点的内核和具有 2000 个油漆的数据)。并且是卷积的定义,你需要在滑动之前反转(clojure)内核(尽管在大多数情况下内核是对称的,所以你不需要这样做,或者你可以让你的内核反转开始节省时间和资源)。
    • 您认为我的解决方案有什么不足吗?
    • 看起来它工作得很好,而且肯定比我的更优雅。
    • 如果k 是一个向量,你也可以使用rseq 而不是reverse。但是如果k 只有 100 个元素,它应该是一个改进。 rseq 在向量上是 O(1),而 reverse 是 O(n)。
    【解决方案3】:

    执行此操作的有效方法是创建执行卷积的 java 类并从 clojure 调用它,如果可能,将其传递给 java 数组。如果效率是一个问题,Clojure 实现也应该在 java 数组上运行。

    【讨论】:

    • 谢谢@Looka,虽然我不想听!我不喜欢也不太了解 java,如果想使用 java,我最好使用 c# 或 Objective-C 或 C++ 或者......也许我太依赖于 clojure 方式思考(我正在学习)。
    • 如果您查看shootout.alioth.debian.org/u64q/…,您会注意到大多数 clojure 解决方案使用 java 数组(并且在 CPU 或内存效率方面仍然不匹配 java。如果您使用普通的 clojure 代码,您将产生大量和大量的小分配只是遍历一个序列-从分配中添加大量执行时间,GC触发。所有分配clojure也会导致大量内存占用,这将导致大量缓存未命中-缓存未命中花费20-100正常内存访问。在大型数据集上进行卷积时很痛苦。
    猜你喜欢
    • 2013-09-29
    • 2020-10-10
    • 2012-01-02
    • 2016-11-11
    • 1970-01-01
    • 1970-01-01
    • 2011-05-04
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多