Я работаю над сверткой фрагментов сигнала с помощью операции сглаживания типа скользящего среднего, но у меня возникают проблемы с ошибками заполнения, которые влияют на последующие вычисления.
Если это отображено на графике, это проблема, возникающая на границах фрагментов.
Чтобы это исправить, я добавляю N образцов из предыдущего фрагмента в начало массива.
Вопрос в том, удаляет ли приведенная ниже операция свертки образцы из начала или конца исходного массива?
arr = np.array([0.9, 0.9, 0.9, 0.1, 0.1, 0.1, 1.0, 0.1, 0.1, 0.1, 0.9, 0.9, 0.9, ])
print(f"length input array {len(arr)}")
result = np.convolve(arr, [1]*3, 'valid')/3
print(f"length result array {len(result)}")
print(result)
plt.plot(result)
Я настроил приведенный выше пример, чтобы попытаться подтвердить, что добавление образцов в начало массива является правильным местом, но это не помогло. Входная длина — 13, выходная длина — 11.
С перекрытием свертка выглядит так:
result = np.convolve(np.concatenate(previous_samples[-2:], arr), [1]*3, 'valid')/3
В любом случае это похоже на проблему XY. Почему бы вам не объяснить свою настоящую конечную цель?
Я могу попробовать переписать более широкую проблему, если это не противоречит правилам, или перенести ее в чат?
@Julien, никакие данные не могут быть «отброшены», но факт в том, что выходной массив не такой же длины, как входной массив.
измените valid на same, и вы получите вывод, размер которого такой же, как и вход. Но края могут быть не такими, как вы хотите.
Затем вам нужно объяснить, как вы хотите, чтобы выходной массив соответствовал размеру, как вы говорите, используя какое-то дополнение для обращения к краю ваших данных, но это ваше решение, нет внутреннего «хорошего» или «плохого» способ сделать это.
@Julien Итак, вопрос в том, должно ли отступы быть в начале или в конце?
Стоит ли носить синюю или красную рубашку?
@julien, как исправить проблему с границами, показанную на картинке.






Я предполагаю, что (1) читатель имеет базовое представление о том, что такое свертка, и (2) используется ядро с нечетным числом элементов > 1.
Короткий ответ:
Используйте np.convolve(…, mode = "valid").
Прежде чем сворачивать, дополните куски
(len(kernel) - 1) / 2 последними элементами предыдущего фрагмента,(len(kernel) - 1) / 2 из следующего фрагмента,где kernel относится к вашему скользящему ядру ([1]*3 в вашем вопросе).
Длинный ответ:
Поскольку свертку можно рассматривать как операцию со скользящим ядром, вопрос, как вы поняли, в том, что происходит на границах.
Функция numpy.convolve() имеет 3 режима обработки границ, как описано здесь:
mode = "valid" гарантирует, что скользящее ядро никогда не покинет сигнал. Однако это означает, что результат будет содержать меньше элементов, чем сигнал; а именно len(kernel) - 1 меньше элементов. Итак, если у вас есть сигнал из 100 элементов и ядро из 5 элементов, ваш результат будет содержать 100-(5-1)=96 элементов. Вопрос о том, где операция «удаляет» недостающие элементы, может немного ввести в заблуждение; однако один из способов представить результат - записать его значения в центры скользящего ядра (что имеет смысл для симметричного ядра с нечетным числом элементов, как в случае с вашим вопросом), поэтому с этой интерпретацией можно было бы симметрично удалить элементы: (len(kernel) - 1) / 2 элементы в начале, (len(kernel) - 1) / 2 элементы в конце сигнала. Мы увидим, что это содержательная интерпретация в примере кода ниже.mode = "same" гарантирует, что результирующий сигнал будет иметь то же количество значений, что и входной сигнал. Однако это подразумевает, что нам придется «придумать» значения, чтобы компенсировать эффект меньшего количества элементов, который мы видели с помощью mode = "valid". Как следствие, в этом режиме сигнал дополняется нулями – (len(kernel) - 1) / 2 как в начале, так и в конце – перед операцией свертки.mode = "full", наконец, гарантирует, что будут созданы все потенциальные комбинации перекрытия между сигналом и скользящим ядром. Это означает, что (а) нам придется «составить» еще больше значений и (б) что результат будет больше, чем входной сигнал. «Придуманные» значения здесь снова равны нулям, а результат будет содержать элементы len(signal) + len(kernel) - 1, то есть 100+5-1=104 элемента для предыдущего примера.Если вы обрабатываете свой сигнал частями, лучше всего использовать mode = "valid": вы можете гарантировать, что полученные свернутые фрагменты будут иметь ту же длину, что и входные фрагменты, без необходимости «составлять» значения (т. е. без необходимости заполнения нулями) , путем заполнения конечными значениями из предыдущего фрагмента и ведущими значениями из следующего фрагмента. Это справедливо для всех фрагментов, кроме первого и последнего, и в этих случаях вам придется самостоятельно решать, как следует обрабатывать граничную ситуацию. Во всех остальных случаях дополняйте значениями предыдущего и последующего фрагментов на половину единицы меньше, чем длина ядра.
Код ниже демонстрирует, что описанное заполнение фрагментов дает точно такие же результаты, как если бы сигнал вообще не дробился на фрагменты. Обратите внимание, что я сместил полученные свернутые сигналы на графиках на значения (len(kernel) - 1) / 2, чтобы учесть описанные недостающие значения с mode = "valid" в самом начале сигнала.
import matplotlib.pyplot as plt
import numpy as np
len_signal, len_kernel = 100, 5
num_chunks = 10
rand = np.random.default_rng(seed=42)
signal = np.cumsum(rand.normal(size=len_signal))
kernel = np.ones(len_kernel) / len_kernel
# Produce convolution result without chunking for reference
signal_convolved = np.convolve(signal, kernel, mode = "valid")
# Split into chunks, then pad them: (1) start with `(len_kernel - 1) / 2`
# last values from preceding chunk (except first chunk), (2) end with
# `(len_kernel - 1) / 2` first values from following chunk (except last chunk)
len_arm = (len_kernel - 1) // 2 # Length of a "kernel arm"
chunks_convolved = []
for i, chunk in enumerate(chunks := np.split(signal, num_chunks)):
p1 = [] if i == 0 else chunks[i - 1][-len_arm:] # Start padding
p2 = [] if i == len(chunks) - 1 else chunks[i + 1][:len_arm] # End padding
chunk_conv = np.convolve(np.r_[p1, chunk, p2], kernel, mode = "valid")
chunks_convolved = np.r_[chunks_convolved, chunk_conv] # Append
assert np.allclose(signal_convolved, chunks_convolved) # Check: same result?
# Show: convolved w/o chunking (left) vs chunked, padded, and convolved (right)
x_c = np.arange(len(signal_convolved)) + len_arm # Right-shift conv. result
plt.subplot(121)
plt.plot(signal, "--"); plt.plot(x_c, signal_convolved); plt.title("no chunks")
plt.subplot(122)
plt.plot(signal, "--"); plt.plot(x_c, chunks_convolved); plt.title("chunks")
plt.show()
Как вы можете видеть (и как мы проверили с помощью assert), оба результата одинаковы:
И последнее примечание: convolve1d() scipy предоставляет больше возможностей, кроме заполнения нулями, для обработки оставшейся граничной ситуации в начале и конце вашего общего сигнала и, таким образом, может быть альтернативой, в зависимости от варианта использования.
Это отличный ответ. Спасибо. Единственный вопрос, который у меня остается, это как подойти к заполнению «внешней части» обрабатываемого фрагмента? Первый вариант, который приходит на ум, — не обрабатывать фрагмент до тех пор, пока следующий фрагмент не будет поставлен в очередь — и, следовательно, следующий фрагмент не будет доступен для захвата образцов заполнения?
@AlGrant Это хороший момент. Обычно я вижу 3 проблемы: (1) При использовании самого первого фрагмента у вас никогда не будет предшествующих данных. Таким образом, вам нужно подумать, приемлемо ли удаление нескольких образцов или вы хотите использовать какой-либо режим заполнения (либо нули, либо любой из convolve1d(), упомянутых выше, я бы сказал, что все это является обычной практикой). (2) Для каждого фрагмента вы можете дополнить его конец только после того, как станет доступен преемник. Я бы подумал, приемлемо ли «бесконечное ожидание данных» или через определенное время вам снова захочется найти другой режим работы с недостающими данными…
@AlGrant… например, удаление или дополнение другими значениями. (3) Для самого последнего фрагмента есть два вопроса: (3a) знаете ли вы, что это последний фрагмент, и (3b) как вы будете обрабатывать недостающие последующие данные? Re 3a: Если вы не знаете, что это последний фрагмент, проблема, по сути, такая же, как и в случае, если вам придется слишком долго ждать преемника (см. 2). В любом случае (в отношении 3b) вам снова придется решить, как обрабатывать недостающие данные заполнения в конце. Я бы сказал, что нет правильных или неправильных вариантов, а есть лучшие и менее идеальные варианты для вашего конкретного случая использования. Надеюсь, это поможет.
Вы, кажется, не понимаете, что такое свертка. Никакие данные не «отбрасываются». Может быть, еще раз прочитать документацию? При использовании аргумента «действительный» размер вывода должен быть равен input_size — kernel_size + 1 по определению.