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

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

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

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

EDIT: Просто чтобы показать, как легко выразить на императивном языке, и дать людям алгоритм, который хорошо работает и легко читается, вот версия 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))))

person Isaac    schedule 15.07.2010    source источник
comment
Это вовсе не ответ на ваш вопрос, но... реализация свертки с использованием быстрых преобразований Фурье значительно сокращает количество операций (см. stackoverflow.com/questions/3084987/, например).   -  person mtrw    schedule 16.07.2010
comment
Я недостаточно знаю функциональные языки, чтобы знать, поможет ли это, но вам может быть интересно stackoverflow.com/questions/803055/how-do-i-do-convolution-in-f.   -  person mtrw    schedule 16.07.2010
comment
@mtrw В производстве это обязательно будет сделано с использованием БПФ; на данный момент, тем не менее, я хотел бы иметь красивое, функциональное решение. Но я не уверен, насколько это возможно (в Clojure).   -  person Isaac    schedule 16.07.2010
comment
У вас есть ссылка на оригинальный алгоритм? Хотя попытка сделать ленивую функциональную версию является интересным упражнением, выполнение довольно буквального перевода с использованием массивов Java, вероятно, будет более эффективным.   -  person dnolen    schedule 16.07.2010
comment
dspguide.com/ch6/3.htm Имеет исходный алгоритм (другая версия на другой странице - это по сути то же самое.Так что нет никакого способа сделать это эффективно в функциональном порядке?   -  person Isaac    schedule 16.07.2010
comment
@Isaac С предстоящей работой 1.3 это, вероятно, в конечном итоге можно будет сделать эффективно и функционально. Но требуется дополнительная поддержка для работы с массивами примитивных данных Java в более высоком порядке.   -  person dnolen    schedule 16.07.2010
comment
@dnolen (Кстати, спасибо за руководство по Enlive. Это то, что в первую очередь привело меня к Clojure.) Так что, вероятно, нет способа красиво выразить это в Clojure 1.2? Даже используя into-array и ему подобные?   -  person Isaac    schedule 16.07.2010
comment
@Isaac: Не расстраивайся. Что касается производительности, императивная версия в Clojure уничтожит любую версию, написанную на Python. Однако я думаю, что то, что вы хотите, все еще немного ошибочно. Лень — это хорошо, но только не в том случае, если вам нужна грубая производительность. Даже в Haskell избегают лени, если вы стремитесь к производительности. Будьте терпеливы, с изменениями в 1.3 есть все шансы, что такие вещи могут быть - красивыми, функциональными, быстрыми. Однако помните, будьте практичны — в этом отношении Clojure и Python похожи.   -  person dnolen    schedule 17.07.2010


Ответы (5)


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

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

person dnolen    schedule 17.07.2010
comment
Вызов (double-array xs) для последовательности чисел перед передачей в качестве аргументов - person Petrus Theron; 06.09.2018

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

person Brian    schedule 16.07.2010
comment
К сожалению, обертывание моих maps и concats в doalls приводит к тому, что требуется неприемлемо много времени (я не уверен, что это закончится!) для вычисления свертки с одним массивом, имеющим 100 000 точек, и другим, имеющим 2... Ваша круговая свертка не t возвращает списки правильной длины или правильных значений (я не верю!) Спасибо за помощь! - person Isaac; 16.07.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))

С xs размером 100 КБ и is размером 2 ваш imp-convolve-1 занимает на моей машине ~ 6000 мс, когда он завернут в куклу, а этот - ~ 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), что делает его на сегодняшний день наиболее эффективной функциональной реализацией из тех, что я пробовал.

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

person j-g-faustus    schedule 16.07.2010
comment
Я ценю это — у меня практически нет опыта работы с императивным Clojure! Возможно, мне следует научиться... Спасибо! (Не беру, так как еще ищу функциональную версию, но есть мой +1) - person Isaac; 17.07.2010
comment
Хотя, как ни странно, когда я пробую это с 1 миллионом точек, я получаю ошибку в фильтре процесса: Неверное количество аргументов: nil, 0. Хотя ошибок Java я не вижу... Хотя для 100k работает нормально. Моя версия Python работает на 1 миллион — больше не пробовал. - person Isaac; 17.07.2010
comment
Он должен работать до Integer/MAX_VALUE (около 2 миллиардов) точек, это максимальный размер массива Java. Я не вижу ничего другого за 1 миллион баллов. Уверен, что это не происходит в другом месте? Ошибка в фильтре процесса не выглядит так, как будто она исходит от функции свертки. - person j-g-faustus; 17.07.2010
comment
Я посмотрю еще немного: я также столкнулся с ошибкой Java: OutOfMemory или что-то в этом роде. Я полагаю, двух миллиардов было бы достаточно, но было бы неплохо иметь возможность использовать лонг и практически не иметь ограничений: есть ли способ сделать это? - person Isaac; 17.07.2010
comment
С двумя миллиардами двойников по 8 байтов каждый вам потребуется 16 ГБ ОЗУ только для хранения двойников. В этом масштабе вы, вероятно, захотите изучить фрагментацию (обработка N чисел из xs за раз, после чего склеивать результаты вместе на границах) или лень. Я не знаю простого, высокопроизводительного и функционального способа, думаю, мне придется оставить это экспертам. Что касается меня, я склонен думать, что высокопроизводительная обработка чисел — это одна из тех вещей, которые лучше всего работают с изменчивостью, хотя бы потому, что вам не нужно заново изобретать каждый алгоритм, который вы найдете в литературе. - person j-g-faustus; 17.07.2010
comment
Я забыл свое эмпирическое правило: подумай, прежде чем говорить. Двух миллиардов удвоений, конечно, достаточно. Даже если бы это было не так, я бы использовал БПФ для свертки сигналов в реальном приложении. Я все еще обеспокоен тем, что нет достойного функционального решения. - person Isaac; 17.07.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)))))))))

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

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

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

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

person Joseph Gay    schedule 17.07.2010
comment
Для вашего второго решения не должно быть заполнения: (dec (/ (count xs) 2))? - person rafd; 28.04.2018

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

  1. Вероятно, вам не следует использовать nth для векторов. Да, это O(1), но поскольку nth работает с другими последовательностями за O(n), это (а) не дает понять, что вы ожидаете, что ввод будет вектором, и (б) означает, что если вы сделаете ошибка, ваш код таинственным образом станет очень медленным, а не сразу.

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

person amalloy    schedule 26.03.2011