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