- PVSM.RU - https://www.pvsm.ru -
Развивая тему конспектов по магистерской специальности "Communication and Signal Processing" (TU Ilmenau), продолжить хотелось бы одной из основных тем курса "Adaptive and Array Signal Processing" [1]. А именно основами адаптивной фильтрации.
Для кого в первую очередь была написана эта статья:
1) для студенческой братии родной специальности [2];
2) для преподавателей, которые готовят практические семинары, но ещё не определились с инструментарием — ниже будут примеры на python и Matlab/Octave;
3) для всех, кто интересуется темой фильтрации.
Что можно найти под катом:
1) сведения из теории, которые я постарался оформить максимально сжато, но, как мне кажется, информативно;
2) примеры применения фильтров: в частности, в рамках эквалайзера для антенной решетки;
3) ссылки на базисную литературу и открытые библиотеки (на python), которые могут быть полезны для исследований.
В общем, добро пожаловать и давайте разбирать всё по пунктам.

Задумчивый человек на фото — знакомый многим, я думаю, Норберт Винер. Фильтр его имени мы, по большей части, и будем изучать. Однако, нельзя не упомянуть и о нашем соотечественнике — Андрее Николаевиче Колмогорове, чья статья 1941 года [3] также внесла значительный вклад в развитие теории оптимальной фильтрации, которая даже в англоязычных источниках так и называется Kolmogorov-Wiener filtering theory [4].
Сегодня мы рассматриваем классический фильтр с конечной импульсной характеристикой (КИХ, FIR — finite impulse response), описать который можно следующей простой схемой (рис. 1).

Рис.1. Схема КИХ-фильтра для изучения фильтра Винера.[1. c.117]
Запишем в матричном виде, что именно будет на выходе данного стенда:
Расшифруем обозначения:
Наверное вы уже догадались, что стремиться мы будем к наименьшей разнице между заданным и отфильтрованным сигналом, то есть к наименьшей ошибке. А значит перед нами вырисовывается оптимизационная задача.
Оптимизировать, а точнее минимизировать мы будем не просто ошибку, среднюю квадратичную ошибку (MSE — Mean Sqared Error [5]):
где обозначает функцию издержек (cost function) от вектора коэффициентов фильтра, а
обозначает мат. ожидание.
Квадрат в данном случае очень приятен, так как он означает, что перед нами задача выпуклого программирования (я нагуглил только такой аналог английскому convex optimization), что, в свою очередь, подразумевает единственный экстремум (в нашем случае минимум).

Рис.2. Поверхность средней квадратичной ошибки [6].
У нашей функции ошибки есть каноническая форма [1, c.121]:
где — это дисперсия ожидаемого сигнала,
— это вектор кросс-корреляции между входным вектором и ожидаемым сигналом, а
— это вектор автокорреляции входного сигнала.

Как мы уже отметили выше, если мы ведем речь о выпуклом программировании, то и экстремум (минимум) у нас будет один. А значит, чтобы найти минимальное значение функции издержек (минимальную среднюю квадратичную ошибку), достаточно найти тангенс угла наклона касательной или, иначе говоря, частную производную по нашей исследуемой переменной:
В оптимальном случае (), ошибка должна быть, конечно же, минимальной, а значит приравниваем производную к нулю:
Собственно, вот она, наша печка, от которой мы будем плясать дальше: перед нами система линейных уравнений.
Нужно отметить сразу, что оба решения, которые мы рассмотрим ниже, в данном случае являются теоретическими и учебными, так как и
заранее известны (то есть у нас была предполагаемая возможность собрать достаточную статистику для вычисления оных). Однако, разбор таких вот упрощенных примеров — это лучшее, что можно придумать для понимания основных подходов.
Решать эту задачу можно, так сказать, в лоб — с помощью обратных матриц:
Такое выражение называется уравнением Винера-Хопфа (Wiener–Hopf equation) — оно нам ещё пригодится в качестве некого эталона.
Конечно, если быть совсем дотошным, то, наверное, правильнее было бы записать это дело в общем виде, т.е. не с
, а с
(псевдо-инвертирование [7]):
![]()
Однако, автокорреляционная матрица не может быть не-квадратной или сингулярной, поэтому вполне справедливо считаем, что никакого противоречия нет.
Из данного уравнения аналитически можно вывести, чему будет равняться минимальное значение функции издержек (т.е. в нашем случае MMSE [8] — minimum mean square error):

Хорошо, одно решение есть.
Однако, да, решать систему линейных уравнений можно и без инвертирования автокорреляционной матрицы — итеративно (to save computations [9]). Для этой цели рассмотрим родной и понятный метод градиентного спуска (method of steepest/gradient descent [10]).
Суть алгоритма можно свести к следующему:
Отсюда и название: gradient — градиентный или steepest — пошаговый descent — спуск.
Градиент в нашем случае уже известен: по сути, мы нашли его, когда дифференцировали функцию издержек (поверхность же вогнутая, сравните с [1, с. 220]). Запишем как будет выглядеть формула для итеративного обновления искомой переменной (коэффициентов фильтра) [1, с. 220]:
где — это номер итерации.
Теперь давайте поговорим о выборе величины шага.
Перечислим очевидные предпосылки:
Относительно фильтра Винера ограничения, конечно, уже были давно найдены [1, с.222-226]:
где — это наибольшее собственное число автокорреляционной матрицы
.
Кстати говоря, собственные числа и вектора — это отдельная интересная тема в контексте линейной фильтрации. Под это дело есть даже целый Eigen filter (см. Приложение 1).
Но и это, к счастью, не все. Есть и оптимальное, адаптивное решение:
где — это отрицательный градиент. Как видно из формулы, шаг пересчитывается в каждую итерацию, то есть адаптируется.

Окей, под второе решение мы тоже подготовили почву.
Наглядности ради проведем небольшое моделирование. Использовать будем Python 3.6.4.
Скажу сразу, данные примеры — это часть одного из домашних заданий, каждое из которых предлагается студентам для решения в течении двух недель. Часть я переписал под python (в целях популяризации языка среди радиоинженеров). Возможно, вы встретите в Сети ещё какие-то варианты от других бывших студентов.
import numpy as np
import matplotlib.pyplot as plt
from scipy.linalg import toeplitz
def convmtx(h,n):
return toeplitz(np.hstack([h, np.zeros(n-1)]),
np.hstack([h[0], np.zeros(n-1)]))
def MSE_calc(sigmaS, R, p, w):
w = w.reshape(w.shape[0], 1)
wH = np.conj(w).reshape(1, w.shape[0])
p = p.reshape(p.shape[0], 1)
pH = np.conj(p).reshape(1, p.shape[0])
MSE = sigmaS - np.dot(wH, p) - np.dot(pH, w) + np.dot(np.dot(wH, R), w)
return MSE[0, 0]
def mu_opt_calc(gamma, R):
gamma = gamma.reshape(gamma.shape[0], 1)
gammaH = np.conj(gamma).reshape(1, gamma.shape[0])
mu_opt = np.dot(gammaH, gamma) / np.dot(np.dot(gammaH, R), gamma)
return mu_opt[0, 0]
Наш линейный фильтр мы применим для задачи выравнивания канала (channel equalization), основной целью которого является нивелировать различные воздействия этого самого канала на полезный сигнал.
Исходники одним файлом можно скачать здесь [11] или здесь [12] (да, было у меня такое хобби — Википедию править).
Допустим, есть антенная решетка (её мы уже рассматривали в статье про MUSIC [13]).

Рис. 3. Ненаправленная линейная антенная решетка (ULAA — uniform linear antenna array) [2, с. 32].
Определим исходные параметры решетки:
M = 5 # количество элементов решетки (number of sensors)
В данной работе мы будем рассматривать что-то вроде широкополосного канала с замираниями, характерной чертой которого является многолучевое распространение. Для таких случаев обычно применяют подход, при котором каждый луч моделируется с помощью задержки определенной величины (рис. 4).

Рис. 4. Модель широкополосного канала при n фиксированных задержках.[3, c. 29]. Как вы понимаете, конкретные обозначения роли не играют — далее мы будем использовать немного другие.
Модель принимаемого сигнала для одного сенсора выразим следующим образом:
В данном случае обозначает номер отсчета,
— это отклик канала по l-ому лучу, L — количество регистров задержки, s — передаваемый (полезный) сигнал,
— аддитивный шум.
Для нескольких сенсоров формула примет вид:
где и
— имеют размерность
, размерность
равна
, а размерность
равняется
.
Предположим, что каждый сенсор принимает сигнал тоже с какой-то задержкой, в силу падения волны под каким-то углом. Матрица в нашем случае будет сверточной матрицей для вектора откликов по каждому лучу. Думаю, в коде будет более понятно:
h = np.array([0.722-1j*0.779, -0.257-1j*0.722, -0.789-1j*1.862])
L = len(h)-1 # number of signal sources
H = convmtx(h,M-L)
print(H.shape)
print(H)
Выводом будет:
>>> (5, 3)
>>> array([[ 0.722-0.779j, 0. +0.j , 0. +0.j ],
[-0.257-0.722j, 0.722-0.779j, 0. +0.j ],
[-0.789-1.862j, -0.257-0.722j, 0.722-0.779j],
[ 0. +0.j , -0.789-1.862j, -0.257-0.722j],
[ 0. +0.j , 0. +0.j , -0.789-1.862j]])
Далее зададим исходные данные для полезного сигнала и шума:
sigmaS = 1 # мощность полезного сигнала (the signal's s(n) power)
sigmaN = 0.01 # мощность шума (the noise's n(n) power)
Теперь переходим к корреляциям.
Rxx = (sigmaS)*(np.dot(H,np.matrix(H).H))+(sigmaN)*np.identity(M)
p = (sigmaS)*H[:,0]
p = p.reshape((len(p), 1))

Найдем решение по Винеру:
# Solution of the Wiener-Hopf equation:
wopt = np.dot(np.linalg.inv(Rxx), p)
MSEopt = MSE_calc(sigmaS, Rxx, p, wopt)
Теперь перейдем к методу градиентного спуска.
Найдем наибольшее собственное число, чтобы из неё потом вывести верхнюю границу шага (см. формулу (9)):
lamda_max = max(np.linalg.eigvals(Rxx))
Теперь зададим некоторый массив шагов, которые будут составлять определенную долю от максимального:
coeff = np.array([1, 0.9, 0.5, 0.2, 0.1])
mus = 2/lamda_max*coeff # different step sizes
Определим максимальное количество итераций:
N_steps = 100
Запускаем алгоритм:
MSE = np.empty((len(mus), N_steps), dtype=complex)
for mu_idx, mu in enumerate(mus):
w = np.zeros((M,1), dtype=complex)
for N_i in range(N_steps):
w = w - mu*(np.dot(Rxx, w) - p)
MSE[mu_idx, N_i] = MSE_calc(sigmaS, Rxx, p, w)
Теперь сделаем то же самое, но уже для адаптивного шага (формула (10)):
MSEoptmu = np.empty((1, N_steps), dtype=complex)
w = np.zeros((M,1), dtype=complex)
for N_i in range(N_steps):
gamma = p - np.dot(Rxx,w)
mu_opt = mu_opt_calc(gamma, Rxx)
w = w - mu_opt*(np.dot(Rxx,w) - p)
MSEoptmu[:, N_i] = MSE_calc(sigmaS, Rxx, p, w)
Получить должны нечто такое:
x = [i for i in range(1, N_steps+1)]
plt.figure(figsize=(5, 4), dpi=300)
for idx, item in enumerate(coeff):
if item == 1:
item = ''
plt.loglog(x, np.abs(MSE[idx, :]),
label='$mu = '+str(item)+'mu_{max}$')
plt.loglog(x, np.abs(MSEoptmu[0, :]),
label='$mu = mu_{opt}$')
plt.loglog(x, np.abs(MSEopt*np.ones((len(x), 1), dtype=complex)),
label = 'Wiener solution')
plt.grid(True)
plt.xlabel('Number of steps')
plt.ylabel('Mean-Square Error')
plt.title('Steepest descent')
plt.legend(loc='best')
plt.minorticks_on()
plt.grid(which='major')
plt.grid(which='minor', linestyle=':')
plt.show()

Рис. 5. Кривые обучения (learning curves) для шагов разных размеров.
Закрепления ради проговорим основные моменты по градиентному спуску:
Вот мы и нашли оптимальный вектор коэффициентов фильтра, который будет наилучшим образом нивелировать влияния канала — обучили эквалайзер.
Конечно! Мы уже несколько раз проговорили, что сбор статистики (т.е. вычисление корреляционных матриц и векторов) в real-time системах — это далеко не всегда доступная роскошь. Однако, человечество приспособилось и к этим сложностям: вместо подхода детерминированного на практике используются подходы адаптивные. Разделить их можно на две большие группы [1, c. 246]:
Тема адаптивных фильтров неплохо представлена в рамках open-source сообщества (примеры для python):
Во втором примере мне особенно нравится документация. Однако, будьте внимательны! Когда я тестировал пакет padasip, я столкнулся со сложностями в обработке комплексных чисел (по дефолту там подразумеваются float64). Возможно, такие же проблемы могут возникнуть и при работе с какими-то другими реализациями.
Алгоритмы, конечно же, имеют свои достоинства и недостатки, сумма которых и определяет область применения алгоритма.
Коротко взглянем на примеры: рассматривать будем три уже упомянутых нами алгоритма SG, LMS и RLS (моделировать будем на языке MATLAB — каюсь, заготовки уже были, а переписывать всё на питон единообразия ради… ну...).
Описание алгоритмов LMS и RLS можно подсмотреть, например, в доке по padasip [18].
Основное отличие от градиентного спуска состоит в изменяемом градиенте:
при
1) Случай, аналогичный рассмотренному выше.
Исходники можно скачать здесь [20].

Рис. 6. Кривые обучения (learning curves) для LMS, RLS и SG.
Сразу можно отметить, что при своей относительной простоте алгоритм LMS может в принципе и не прийти к оптимальному решению при относительно большом шаге. Самый быстрый результат даёт RLS, но и он может засбоить при относительно небольшом факторе забывания (forgetting factor). Пока молодцом держится SG, но давайте посмотрим на другой пример.
2) Случай, когда канал меняется во времени.
Исходники можно скачать здесь [21].

Рис. 7. Кривые обучения (learning curves) для LMS, RLS и SG (канал изменяется во времени).
А вот тут картина уже гораздо интересней: при резком изменении отклика канала самым надежным решением уже кажется LMS. Кто бы мог подумать. Хотя и RLS при правильном forgetting factor тоже обеспечивает приемлемый результат.
Да, конечно каждый алгоритм имеет свою определенную вычислительную сложность, однако по моим замерам моя старенькая машина справляется с одним ансамблем примерно за 120 мкс в итерацию в случае с LMS и SG и примерно за 250 мкс в итерацию в случае RLS. То есть разница, в общем-то, сравнимая.
А на этом у меня сегодня всё. Спасибо всем, кто заглянул!
Haykin S. S. Adaptive filter theory. – Pearson Education India, 2005.
Haykin, Simon, and KJ Ray Liu. Handbook on array processing and sensor networks. Vol. 63. John Wiley & Sons, 2010. pp. 102-107
Arndt, D. (2015). On Channel Modelling for Land Mobile Satellite Reception (Doctoral dissertation).
Основная цель такого фильтра — это максимизировать отношение сигнал-шум (SNR — Signal-to-Noise Ratio).

Но судя по наличию в расчетах корреляций, это тоже больше теоретический конструкт, нежели практическое решение.
Автор: ritchie_kyoto
Источник [22]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/python/324516
Ссылки в тексте:
[1] "Adaptive and Array Signal Processing": https://www5.tu-ilmenau.de/nt/de/teachings/vorlesungen/anwendungen_der_systemtheorie/index.html
[2] родной специальности: https://griat.kai.ru/communications-and-signal-processing
[3] статья 1941 года : http://www.mathnet.ru/links/441fec07e07f42e7bc02c73edaff85fc/im3775.pdf
[4] Kolmogorov-Wiener filtering theory: http://depts.washington.edu/sce2003/Papers/109.pdf
[5] MSE — Mean Sqared Error: https://en.wikipedia.org/wiki/Mean_squared_error
[6] Поверхность средней квадратичной ошибки: https://yadi.sk/i/pf6qmAItGk9QDw
[7] псевдо-инвертирование: https://en.wikipedia.org/wiki/Moore%E2%80%93Penrose_inverse#Definition
[8] MMSE: https://en.wikipedia.org/wiki/Minimum_mean_square_error
[9] to save computations: https://www.eit.lth.se/fileadmin/eit/courses/ett042/LEC/notes7.pdf
[10] method of steepest/gradient descent: https://en.wikipedia.org/wiki/Gradient_descent#Solution_of_a_linear_system
[11] здесь: https://gist.github.com/kirlf/8e77cc17b7b1be4e35dbf651ff82f759#file-steepest_descent_linear_filter-py
[12] здесь: https://commons.wikimedia.org/wiki/File:Steepest_descent.png
[13] MUSIC: https://habr.com/ru/post/446674/
[14] SG: https://scikit-learn.org/0.20/modules/sgd.html
[15] LMS: https://en.wikipedia.org/wiki/Least_mean_squares_filter
[16] RLS: https://en.wikipedia.org/wiki/Recursive_least_squares_filter
[17] pyroomacoustics: https://pyroomacoustics.readthedocs.io/en/pypi-release/pyroomacoustics.adaptive.html
[18] padasip: http://matousc89.github.io/padasip/index.html
[19] adaptfilt: https://pypi.org/project/adaptfilt/
[20] здесь: https://gist.github.com/kirlf/8e77cc17b7b1be4e35dbf651ff82f759#file-adaptive_filters_1-m
[21] здесь: https://gist.github.com/kirlf/8e77cc17b7b1be4e35dbf651ff82f759#file-adaptive_filters_2-m
[22] Источник: https://habr.com/ru/post/455497/?utm_campaign=455497&utm_source=habrahabr&utm_medium=rss
Нажмите здесь для печати.