7

私はいくつかの信号処理ソフトウェアを書いています、そして私は離散畳み込み関数を書き出すことから始めています。

これは最初の1万程度の値のリストでは問題なく機能しますが、値が大きくなると(たとえば、100k)、もちろんStackOverflowエラーが発生し始めます。

残念ながら、私は命令型畳み込みアルゴリズムを、実際に使用するのに十分高速な再帰的で怠惰なバージョンに変換するのに多くの問題を抱えています(少なくとも少しの優雅さもあればいいでしょう)。

また、この機能が完全に正しいかどうかも100%確信していませんが、何かが足りない、または何か間違ったことをしている場合はお知らせください。正しいと思います。

(defn convolve
  "
    Convolves xs with is.

    This is a discrete convolution.

    'xs  :: list of numbers
    'is  :: list of numbers
  "
  [xs is]
  (loop [xs xs finalacc () acc ()]
    (if (empty? xs)
      (concat finalacc acc)
      (recur (rest xs)
             (if (empty? acc)
               ()
               (concat finalacc [(first acc)]))
             (if (empty? acc)
               (map #(* (first xs) %) is)
               (vec-add
                (map #(* (first xs) %) is)
                (rest acc)))))))

私はどんな種類の助けも必要です。私はまだClojureで自分の方向性を理解しており、これをエレガントで怠惰なものにしたり、再帰的にしたりするのは素晴らしいことです。

Lispで命令型言語で表現するのが非常に簡単なアルゴリズムを表現するのがどれほど難しいか少し驚いています。しかし、おそらく私はそれを間違っています!

編集: 命令型言語で表現するのがいかに簡単であるかを示し、人々にうまく機能し、読みやすいアルゴリズムを提供するために、ここにPythonバージョンがあります。短く、簡潔で、推論がはるかに簡単であることに加えて、Clojureコードよりも桁違いに高速に実行されます。Java配列を使用する私の必須のClojureコードですら。

from itertools import repeat

def convolve(ns, ms):
    y = [i for i in repeat(0, len(ns)+len(ms)-1)]
    for n in range(len(ns)):
        for m in range(len(ms)):
            y[n+m] = y[n+m] + ns[n]*ms[m]
    return y

一方、ここに命令型のClojureコードがあります。また、畳み込みから最後の、完全に没頭していない値を削除します。したがって、遅くて醜いことを除けば、100%機能するわけではありません。機能的でもありません。

(defn imp-convolve-1
  [xs is]
  (let [ys (into-array Double (repeat (dec (+ (count xs) (count is))) 0.0))
        xs (vec xs)
        is (vec is)]
     (map #(first %)
          (for [i (range (count xs))]
            (for [j (range (count is))]
              (aset ys (+ i j)
                    (+ (* (nth xs i) (nth is j))
                       (nth ys (+ i j)))))))))

これはとてもがっかりします。誰かが私に何か明らかなことを見逃したことを見せてください。

編集3:

これが昨日私が考えた別のバージョンで、それをどのように表現できるようにしたいかを示しています(他のソリューションは非常にエレガントですが、私は別のソリューションを公開しています!)

(defn convolve-2
  [xs is]
  (reduce #(vec-add %1 (pad-l %2 (inc (count %1))))
         (for [x xs]
           (for [i is]
             (* x i)))))

このユーティリティ関数を使用しますvec-add

(defn vec-add
  ([xs] (vec-add xs []))
  ([xs ys]
     (let [lxs (count xs)
           lys (count ys)
           xs (pad-r xs lys)
           ys (pad-r ys lxs)]
       (vec (map #(+ %1 %2) xs ys))))
  ([xs ys & more]
     (vec (reduce vec-add (vec-add xs ys) more))))
     (vec (reduce vec-add (vec-add xs ys) more))))
4

5 に答える 5

4

スタックオーバーフローエラーの考えられる原因は、怠惰なサンクが深くなりすぎていることです。(concatそしてmap怠惰です)。それらの呼び出しをラップしdoallて、戻り値の評価を強制してみてください。

より機能的な解決策については、次のようなものを試してください。

(defn circular-convolve
  "Perform a circular convolution of vectors f and g"
  [f g]
  (letfn [(point-mul [m n]
        (* (f m) (g (mod (- n m) (count g)))))
      (value-at [n]
        (reduce + (map #(point-mul % n) (range (count g)))))]
    (map value-at (range (count g)))))

を使用するreduceと、簡単に合計を実行できますmap。また、レイジーシーケンスを生成するため、この関数もレイジーです。

于 2010-07-16T01:20:47.583 に答える
4

高性能の機能バージョンでは役に立ちませんが、怠惰を先取りし、タイプのヒントを追加することで、命令バージョンの100倍のスピードアップを得ることができます。

(defn imp-convolve-2 [xs is]
  (let [^doubles xs (into-array Double/TYPE xs)
        ^doubles is (into-array Double/TYPE is)
        ys (double-array (dec (+ (count xs) (count is)))) ]
    (dotimes [i (alength xs)]
      (dotimes [j (alength is)]
        (aset ys (+ i j)
          (+ (* (aget xs i) (aget is j))
            (aget ys (+ i j))))))
    ys))

xsサイズ100kとisサイズ2の場合、imp-convolve-1は、ドールに包まれたときに私のマシンで約6,000ミリ秒かかりますが、これは約35ミリ秒かかります。

アップデート

これが怠惰な機能バージョンです:

(defn convolve 
  ([xs is] (convolve xs is []))
  ([xs is parts]
    (cond
      (and (empty? xs) (empty? parts)) nil
      (empty? xs) (cons
                    (reduce + (map first parts))
                    (convolve xs is
                      (remove empty? (map rest parts))))
      :else (cons
              (+ (* (first xs) (first is))
                (reduce + (map first parts)))
              (lazy-seq
                (convolve (rest xs) is
                  (cons 
                    (map (partial * (first xs)) (rest is))
                    (remove empty? (map rest parts)))))))))

サイズ100kおよび2では、〜600ms(450〜750msで変化)でクロックインしますが、imp-convolve-1では〜6,000ms、imp-convolve-2では〜35msです。

そのため、機能的で怠惰で、許容できるパフォーマンスを備えています。それでも、それは命令型バージョンの2倍のコードであり、見つけるのにさらに1〜2時間かかったので、要点がわかるかどうかはわかりません。

コードを短くしたり単純にしたり、命令型バージョンに比べて他の利点がある場合は、純粋関数が必要です。そうでない場合は、命令モードに切り替えることに異論はありません。

これが、Clojureが優れていると思う理由の1つです。どちらのアプローチも、適切と思われる方法で使用できるからです。

アップデート2:

David Cabanaによるこの機能的な実装(2番目の実装(ページのさらに下))が好きだと言って、「これを機能的に行うことのポイント」を修正します。

簡潔で読みやすく、上記と同じ入力サイズ(100,000および2)で約140ミリ秒の時間がかかるため、私が試したものの中で群を抜いて最高のパフォーマンスを発揮する機能実装になっています。

それが機能的であり(しかし怠惰ではない)、型のヒントを使用せず、すべての数値型(doubleだけでなく)で機能することを考えると、それは非常に印象的です。

于 2010-07-16T20:30:30.283 に答える
4
(defn ^{:static true} convolve ^doubles [^doubles xs ^doubles is]
  (let [xlen (count xs)
        ilen (count is)
        ys   (double-array (dec (+ xlen ilen)))]
    (dotimes [p xlen]
      (dotimes [q ilen]
        (let [n (+ p q), x (aget xs p), i (aget is q), y (aget ys n)]
          (aset ys n (+ (* x i) y)))))
    ys))

Clojureのequivブランチでこれを行っていた場合は、jg-faustusのバージョンをリフします。私のために働きます。i7 Mackbook Proでは、1,000,000ポイントで約400ミリ秒、100,000ポイントで約25ミリ秒。

于 2010-07-17T16:38:07.207 に答える
3
(defn convolve [xs is]
  (if (> (count xs) (count is))
    (convolve is xs)
    (let [os (dec (+ (count xs) (count is)))
          lxs (count xs)
          lis (count is)]
      (for [i (range os)]
        (let [[start-x end-x] [(- lxs (min lxs (- os i))) (min i (dec lxs))]
              [start-i end-i] [(- lis (min lis (- os i))) (min i (dec lis))]]
          (reduce + (map *
                         (rseq (subvec xs start-x (inc end-x)))
                         (subvec is start-i (inc end-i)))))))))

怠惰で機能的な解決策を簡潔に表現することが可能です。残念ながら、2kを超えるパフォーマンスは実用的ではありません。読みやすさを犠牲にすることなくスピードアップする方法があるかどうか興味があります。

編集: トピックに関するdrcabanaの有益な投稿(http://erl.nfshost.com/2010/07/17/discrete-convolution-of-finite-vectors/)を読んだ後、さまざまなサイズのベクトルを受け入れるようにコードを更新しました。彼の実装のパフォーマンスは優れています。xsサイズ3の場合、サイズは1000000、〜2s vs〜3sです。

編集2: 単純にxsとパディングを逆にするというdrcabanaのアイデアを取り入れて、私は次のことに到達しました。

(defn convolve [xs is]
  (if (> (count xs) (count is))
    (convolve is xs)
    (let [is (concat (repeat (dec (count xs)) 0) is)]
      (for [s (take-while not-empty (iterate rest is))]
         (reduce + (map * (rseq xs) s))))))

これはおそらく取得するのと同じくらい簡潔ですが、おそらく時間がかかるため、全体的にはまだ遅くなります。よく考えられたアプローチに対するブログの作者への称賛。ここでの唯一の利点は、上記が本当に怠惰であるということです。私が尋ねると(nth res 10000)、結果に到達するために最初の10kの計算だけが必要になります。

于 2010-07-17T19:02:24.510 に答える
3

あなたが尋ねた多くの質問のどれにも実際には答えではありませんが、私はあなたが尋ねなかったものについていくつかのコメントがあります。

  1. おそらくnthベクトルには使用しないでください。はい、それはO(1)ですがnth、O(n)の他のシーケンスで機能するため、(a)入力がベクトルであることを期待していることを明確にせず、(b)間違いを犯した場合を意味します、コードはすぐに失敗するのではなく、不思議なことに本当に遅くなります。

  2. for両方ともmap怠惰であり、aset副作用のみです。この組み合わせは災害のレシピです。副作用のforような動作には、を使用しますdoseq

  3. for複数のバインディングをdoseq許可するので、Pythonで(明らかに)行うようにそれらの負荷を積み上げる必要はありません。

    (doseq [i (range (count cs))
            j (range (count is))] 
      ...)
    

    あなたが望むことをします。

  4. #(first %)より簡潔に次のように記述されfirstます; 同様#(+ %1 %2)にです+

  5. ベクトルである必要vecのない一連の中間結果を呼び出すと、速度が低下します。具体的には、最終的な戻り値を作成するときにのみ呼び出すだけで十分です。ランダムアクセスに中間結果を使用しない場合は、中間結果をベクトルに変換する理由はありません。vec-addvec(vec (reduce foo bar))foo

于 2011-03-26T03:42:28.213 に答える