Проблема с ленивой сверткой fn в Clojure

Я пишу некоторое программное обеспечение обработки сигналов, и я начинаюсь путем выписывания дискретной функции свертки.

Это хорошо работает для первых приблизительно десяти тысяч списков значений, но поскольку они становятся более крупными (скажите, 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: даже мой обязательный код Clojure с помощью массивов Java.

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))))

7
задан amalloy 26 March 2011 в 03:47
поделиться

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))

Риффинг на версию j-g-faustus'а, если бы я делал это в ветке Clojure equiv. Работает для меня. ~400 мс для 1,000,000 очков, ~25 мс для 100,000 на i7 Macbook Pro.

4
ответ дан 6 December 2019 в 10:47
поделиться

Вероятная причина ошибок переполнения стека заключается в том, что ленивые преобразователи становятся слишком глубокими. ( 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 создает ленивую последовательность, эта функция тоже ленивый.

4
ответ дан 6 December 2019 в 10:47
поделиться

Ничего не могу поделать с высокопроизводительной функциональной версией, но вы можете получить 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 и - это размер 2, ваш imp-convolve-1 занимает ~ 6000 мс на моей машине, когда он заключен в doall, в то время как этот занимает ~ 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 она составляет ~ 600 мс (варьируется от 450 до 750 мс) против ~ 6000 мс для imp-convolve-1 и ~ 35 мс для imp. -свернуть-2.

Так что он функциональный, ленивый и имеет приемлемую производительность. Тем не менее, это вдвое больше кода, чем императивная версия, и мне потребовалось еще 1-2 часа, чтобы найти, так что я не уверен, что вижу в этом смысл.

Я за чистые функции, когда они делают код короче или проще, или имеют какое-то другое преимущество по сравнению с императивной версией. Когда они этого не делают, я не возражаю перейти в императивный режим.

Это одна из причин, по которой я считаю, что Clojure великолепен, поскольку вы можете использовать любой подход по своему усмотрению.

Обновление 2:

Я исправлю свое «в чем смысл этого функционально», сказав, что мне нравится эта функциональная реализация (вторая, ниже) Дэвида Кабаны .

Он короткий, читаемый и время до ~ 140 мс при тех же размерах ввода, что и выше (100 000 и 2), что делает его, безусловно, наиболее эффективной функциональной реализацией из тех, что я пробовал.

Учитывая, что он функциональный (но не ленивый), не использует подсказок типов и работает со всеми числовыми типами (не только с двойными), это весьма впечатляет.

4
ответ дан 6 December 2019 в 10:47
поделиться
(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)))))))))

Можно кратко выразить ленивое функциональное решение. Увы, производительность при> 2к нецелесообразна. Мне интересно узнать, есть ли способы ускорить его, не жертвуя удобочитаемостью.

Изменить: После прочтения информативного сообщения drcabana по теме ( http://erl.nfshost.com/2010/07/17/discrete-convolution-of-finite-vectors/ ) я обновил свой код до принимать векторы разного размера. Его реализация лучше: для xs размера 3 это размер 1000000, ~ 2сек против ~ 3сек

Редактировать 2: Взяв за основу идеи drcabana о простом изменении xs и padding, я пришел к следующему:

(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 вычислений.

3
ответ дан 6 December 2019 в 10:47
поделиться
Другие вопросы по тегам:

Похожие вопросы: