Беда с ленивой сверткой fn в Clojure - PullRequest
6 голосов
/ 16 июля 2010

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

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

Я немного удивлен, насколько сложно выразить алгоритм, который довольно легко выразить на императивном языке в Лиспе.Но, возможно, я делаю это неправильно!

РЕДАКТИРОВАТЬ: Просто чтобы показать, как легко выразить на императивном языке, и дать людям алгоритм, который работает хорошо и легкочитайте, вот версия 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))))

Ответы [ 5 ]

4 голосов
/ 17 июля 2010

Не могу помочь с высокопроизводительной функциональной версией, но вы можете получить 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))

С размером xs100k и is size 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-convolve-2.

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

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

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

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

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

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

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

4 голосов
/ 17 июля 2010
(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. Работает для меня. ~ 400 мс для 1000000 точек, ~ 25 мс для 100 000 на i7 Mackbook Pro.

4 голосов
/ 16 июля 2010

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

3 голосов
/ 26 марта 2011

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

  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-add достаточно вызвать vec только когда вы сделаете окончательное возвращаемое значение: в (vec (reduce foo bar)) нет никаких оснований для foo превращать свои промежуточные результаты в векторы, если он никогда не использует их для произвольного доступа.

3 голосов
/ 17 июля 2010
(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к нецелесообразна. Мне интересно посмотреть, есть ли способы ускорить его, не жертвуя удобочитаемостью.

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

Редактировать 2: Приняв идеи дркабаны о простом изменении x и отступов, я пришел к:

(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), ему понадобятся только первые 10 тыс. Вычислений для получения результата.

...