由于使用C实现的自适应滤波器往往较为复杂,代码量较大,而python更加适合科学计算,使用python实现仅需几行代码就可以搞定,更加容易理解,本文使用简单的几行python代码实现8个自适应滤波器,包括lms,nlms,rls,kalman,
frequency domain adaptive filter,partition-block-based frequency adaptive filter,frequency domain kalman filter,partition-block-based frequency domain kalman filter),由简到难,方便学习自适应滤波器原理。
lms滤波器算法步骤如下:
假设真实的回声路径 h \boldsymbol h h长度为 L L L
h = [ h 0 h 1 h 2 h 3 . . . h L − 1 ] T \boldsymbol h = [h_0 h_1 h_2 h_3...h_{L-1}]^T h=[h0h1h2h3...hL−1]T
在n时刻输入的远端参考信号为:
x ( n ) = [ x ( n ) x ( n − 1 ) x ( n − 2 ) . . . x ( n − L + 1 ) ] T \boldsymbol x(n) = [x(n) x(n-1) x(n-2)... x(n-L+1)]^T x(n)=[x(n)x(n−1)x(n−2)...x(n−L+1)]T
估计的回声为:
y ^ ( n ) = x T ( n ) . h ^ \hat y(n) = \boldsymbol x^T(n).\hat\boldsymbol h y^(n)=xT(n).h^
估计的回声和近端信号间的误差为:
e ( n ) = d ( n ) − y ^ ( n ) e(n) = d(n) - \hat y(n) e(n)=d(n)−y^(n)
系数更新公式:
h ^ n + 1 = h ^ n + 2 μ e ∗ ( n ) x ( n ) \hat\boldsymbol h_{n+1} = \hat\boldsymbol h_n + 2 \mu e^* (n) \boldsymbol x(n) h^n+1=h^n+2μe∗(n)x(n)
python代码如下:
import numpy as np
def lms(x, d, N = 4, mu = 0.05):
L = min(len(x),len(d))
h = np.zeros(N)
e = np.zeros(L-N)
for n in range(L-N):
x_n = x[n:n+N][::-1]
d_n = d[n]
y_n = np.dot(h, x_n.T)
e_n = d_n - y_n
h = h + mu * e_n * x_n
e[n] = e_n
return e
nlms相对于lms对输入信号做了归一化,系数更新公式变为:
h ^ n + 1 = h ^ n + μ e ∗ ( n ) x ( n ) x H ( n ) x ( n ) \hat\boldsymbol h_{n+1} = \hat\boldsymbol h_n + \frac{\mu e^* (n) \boldsymbol x(n)}{ \boldsymbol x^H(n) \boldsymbol x(n)} h^n+1=h^n+xH(n)x(n)μe∗(n)x(n)
python代码如下:
import numpy as np
def nlms(x, d, N = 4, mu = 0.05):
L = min(len(x),len(d))
h = np.zeros(N)
e = np.zeros(L-N)
for n in range(L-N):
x_n = x[n:n+N][::-1]
d_n = d[n]
y_n = np.dot(h, x_n.T)
e_n = d_n - y_n
h = h + mu * e_n * x_n / (np.dot(x_n,x_n)+1e-8)
e[n] = e_n
return e
python代码如下:
import numpy as np
def rls(x, d, N = 4, lmbd = 0.999, delta = 0.0002):
L = min(len(x),len(d))
lmbd_inv = 1/lmbd
h = np.zeros((N, 1))
P = np.eye(N)/delta
e = np.zeros(L-N)
for n in range(L-N):
x_n = np.array(x[n:n+N][::-1]).reshape(N, 1)
d_n = d[n]
y_n = np.dot(x_n.T, h)
e_n = d_n - y_n
g = np.dot(P, x_n)
g = g / (lmbd + np.dot(x_n.T, g))
h = h + e_n * g
P = lmbd_inv*(P - np.dot(g, np.dot(x_n.T, P)))
e[n] = e_n
return e
卡尔曼滤波器回声消除公式如下::
E n = D n − X n T h n − 1 E_n = D_n - X_n^Th_{n-1} En=Dn−XnThn−1
P n − = P n − 1 + Q P_n^- = P_{n-1} + Q Pn−=Pn−1+Q
K n = P n − X n ( X n P n − X n T + R ) − 1 K_n = P_n^-X_n(X_nP_n^-X_n^T+R)^{-1} Kn=Pn−Xn(XnPn−XnT+R)−1
x ^ n = x ^ n − 1 + K n E n \hat x_n = \hat x_{n-1} + K_nE_n x^n=x^n−1+KnEn
P n = ( I − K n X n T ) P n − P_n = (I-K_nX_n^T)P_n^- Pn=(I−KnXnT)Pn−
对应的代码:
import numpy as np
def kalman(x, d, N = 64, beta=0.9,sgm2u=1e-2,sgm2v=1e-6):
L = min(len(x),len(d))
Q = np.eye(N)*sgm2v
R = np.array([sgm2u]).reshape(1, 1)
H = np.zeros((N, 1))
P = np.eye(N)*sgm2v
I = np.eye(N)
e = np.zeros(L-N)
for n in range(L-N):
x_n = np.array(x[n:n+N][::-1]).reshape(1, N)
d_n = d[n]
y_n = np.dot(x_n, H)
e_n = d_n - y_n
R = beta*R + (1-beta)*(e_n**2)
Pn = P + Q
K = np.dot(Pn, x_n.T) / (np.dot(x_n, np.dot(Pn, x_n.T)) + R)
H = H + np.dot(K, e_n)
P = np.dot(I - np.dot(K, x_n), Pn)
e[n] = e_n
return e
以上都是时域的自适应滤波器,我们可以通过OLS的方法进行分段然后使用FFT快速计算卷积,这样我们就可以在频域实现自适应滤波器
我们将NLMS算法基于这种思想进行实现,就得到了频域实现的NLMS自适应滤波器
公式如下:
对应代码:
import numpy as np
from numpy.fft import rfft as fft
from numpy.fft import irfft as ifft
def fdaf(x, d, M, mu=0.05, beta=0.9):
H = np.zeros(M+1,dtype=np.complex)
norm = np.full(M+1,1e-8)
window = np.hanning(M)
x_old = np.zeros(M)
num_block = len(x) // M
e = np.zeros(num_block*M)
for n in range(num_block):
x_n = np.concatenate([x_old,x[n*M:(n+1)*M]])
d_n = d[n*M:(n+1)*M]
x_old = x[n*M:(n+1)*M]
X_n = np.fft.rfft(x_n)
y_n = ifft(H*X_n)[M:]
e_n = d_n-y_n
e_fft = np.concatenate([np.zeros(M),e_n*window])
E_n = fft(e_fft)
norm = beta*norm + (1-beta)*np.abs(X_n)**2
G = mu*E_n/norm
H = H + X_n.conj()*G
h = ifft(H)
h[M:] = 0
H = fft(h)
e[n*M:(n+1)*M] = e_n
return e
类似的,我们也可以得到频域卡尔曼自适应滤波器
import numpy as np
from numpy.fft import rfft as fft
from numpy.fft import irfft as ifft
def fdkf(x, d, M, beta=0.95, sgm2u=1e-2, sgm2v=1e-6):
Q = sgm2u
R = np.full(M+1,sgm2v)
H = np.zeros(M+1,dtype=np.complex)
P = np.full(M+1,sgm2u)
window = np.hanning(M)
x_old = np.zeros(M)
num_block = len(x) // M
e = np.zeros(num_block*M)
for n in range(num_block):
x_n = np.concatenate([x_old,x[n*M:(n+1)*M]])
d_n = d[n*M:(n+1)*M]
x_old = x[n*M:(n+1)*M]
X_n = np.fft.rfft(x_n)
y_n = ifft(H*X_n)[M:]
e_n = d_n-y_n
e_fft = np.concatenate([np.zeros(M),e_n*window])
E_n = fft(e_fft)
R = beta*R + (1.0 - beta)*(np.abs(E_n)**2)
P_n = P + Q*(np.abs(H))
K = P_n*X_n.conj()/(X_n*P_n*X_n.conj()+R)
P = (1.0 - K*X_n)*P_n
H = H + K*E_n
h = ifft(H)
h[M:] = 0
H = fft(h)
e[n*M:(n+1)*M] = e_n
return e
频域自适应滤波器每M点计算一次输出和更新滤波器,当抽头数量很大时,会造成比较大的延时,因此可以将滤波器进行分割,将长度为M的自适应滤波器分割为几个小块,这样每次计算滤波器的输入和输出只需要一个小块的长度,大大降低了延时。
通过将频域块NLMS自适应滤波器进行分块,就得到了也就是众所周知的webrtc AEC和speex中所使用的滤波器,代码相比较上面的滤波器比较复杂,这里就不贴了,感兴趣的同学可以去我的repository中查看:https://github.com/ewan-xu/pyaec
同样的我们也可以得到分块的频域卡尔曼滤波器,代码也比较复杂,感兴趣的同学可以去我的repository中查看: https://github.com/ewan-xu/pyaec
另外贴上分区块卡尔曼滤波器实现参考论文,感兴趣的同学可以参考参考:
F. Kuech, E. Mabande, and G. Enzner, “State-space architecture of the partitioned-block-based acoustic echo controller,”
in 2014 IEEE International Conference on Acoustics, Speech and Signal Processing (ICASSP), 2014, pp. 1295-1299: IEEE
有任何疑问,欢迎加微信交流:xu19315519614