Adaptive Filter Theory and Applications in Digital Signal Processing

Covers digital filter fundamentals (FIR/IIR) through a complete derivation of the Wiener-Hopf equation as an MMSE problem, with numerically verified edge cases: ill-conditioned autocorrelation matrices, the relationship to LMS convergence, and the limits of the Wiener solution under non-stationarity.

Digital Filters

Digital filters process sampled time-series signals (e.g., \(x_0, x_1, \dots, x_N\) ). Unlike analog filters, they are implemented through digital signal processing. A representative type of digital filter is the linear filter.

Linear Filters

Linear filters generate output signals as a linear combination of input signals.

FIR (Finite Impulse Response) Filter

FIR filters compute output using only the current input and a finite number of past input samples. Since the impulse response has a finite length, FIR filters are always stable.

\[ y_k = w_0 x_k + w_1 x_{k-1} + \dots + w_N x_{k-N} \]

where \(w_i\) are the filter coefficients.

IIR (Infinite Impulse Response) Filter

IIR filters compute output using the current input, a finite number of past input samples, and a finite number of past output samples. Since the impulse response can continue indefinitely, IIR filters may become unstable depending on the design.

\[ y_k = w_0 x_k + w_1 x_{k-1} + \dots + w_N x_{k-N} - v_1 y_{k-1} - \dots - v_M y_{k-M} \]

where \(w_i, v_i\) are the filter coefficients.

Adaptive Filters

Adaptive filters are digital filters that automatically adjust (learn) their filter coefficients to adapt to changes in the environment, such as when the statistical properties of input signals change over time or when noise characteristics are unknown. The same approach to filter coefficient design can be applied to both FIR and IIR filter structures. This article is the derivation source (the theoretical starting point) that follow-up posts such as Adaptive Filters (LMS/RLS): Theory and Python Implementation refer to when they say “the derivation of Eq.(2) is covered in this article.” The concrete implementations, convergence-condition proofs, and noise-cancellation applications of LMS/NLMS/RLS are left to the companion articles ( LMS / NLMS Algorithms: Theory and Python Implementation , The RLS Algorithm in Python: Recursive Least Squares and Its Equivalence to the Kalman Filter ); this article focuses on the complete derivation of the Wiener-Hopf equation as an MMSE optimization problem, and the three fundamental issues that follow from it: ill-conditioning, the relationship to adaptive algorithms, and non-stationarity.

Input, Output, and Error of Adaptive Filters

  • Input: \(x_k = x(kT)\) (\(k=0, 1, 2, \dots\) )
  • Output: \(y_k = w_0 x_k + w_1 x_{k-1} + \dots + w_{N-1} x_{k-N+1}\) (This shows an FIR filter example, but the same concept applies to IIR filters)
  • Desired signal: \(d_k\) (ideal output signal)
  • Error: \(e_k = d_k - y_k\)

Define the filter coefficient vector \(W\) and input signal vector \(X_k\) as:

\[ W = [w_0, w_1, \dots, w_{N-1}]^T \] \[ X_k = [x_k, x_{k-1}, \dots, x_{k-N+1}]^T \]

Then the filter output \(y_k\) can be written as \(y_k = W^T X_k = X_k^T W\) , and the error \(e_k\) becomes:

\[ e_k = d_k - W^T X_k \]

Filter Coefficient Design: A Complete Derivation of the Wiener-Hopf Equation as an MMSE Problem

The goal of adaptive filtering is to find the optimal filter coefficients \(W^o\) that minimize the mean squared error (MSE). This can be formulated as the following MMSE (Minimum Mean Square Error) optimization problem.

\[ \min_{W} \; J(W) = \min_{W} \; \mathbb{E}\bigl[e_k^2\bigr] = \min_{W} \; \mathbb{E}\Bigl[\bigl(d_k - W^T X_k\bigr)^2\Bigr] \tag{1} \]

Expanding the Cost Function

Expanding the square in Eq.(1):

\[ J(W) = \mathbb{E}\bigl[d_k^2\bigr] - 2\, \mathbb{E}\bigl[d_k\, W^T X_k\bigr] + \mathbb{E}\bigl[(W^T X_k)^2\bigr] \]

Noting that \(W\) is not a random variable (it can be pulled out of the expectation), this simplifies to:

\[ J(W) = \mathbb{E}\bigl[d_k^2\bigr] - 2\, W^T \mathbb{E}\bigl[d_k X_k\bigr] + W^T \, \mathbb{E}\bigl[X_k X_k^T\bigr] \, W \]

We define three statistics:

  • \(\sigma_d^2 = \mathbb{E}[d_k^2]\) : the variance (power) of the desired signal
  • \(R = \mathbb{E}[X_k X_k^T]\) : the autocorrelation matrix of the input signal (\(N \times N\) )
  • \(p = \mathbb{E}[d_k X_k]\) : the cross-correlation vector between the desired signal and the input signal (\(N \times 1\) )

Using these, the cost function becomes a quadratic form in \(W\) :

\[ J(W) = \sigma_d^2 - 2 W^T p + W^T R W \tag{2} \]

By definition \(R\) is always positive semi-definite (\(a^T R a = \mathbb{E}[(a^T X_k)^2] \ge 0\) for any \(a\) ), so \(J(W)\) is a convex function of \(W\) , and any point where the gradient vanishes is a global minimum (if \(R\) is positive definite, that minimum is unique).

Setting the Gradient to Zero

Differentiating Eq.(2) with respect to \(W\) , using \(\nabla_W (W^T p) = p\) and \(\nabla_W (W^T R W) = 2RW\) (since \(R\) is symmetric):

\[ \nabla_W J(W) = -2p + 2RW \]

Setting this to zero yields the equation the optimal filter coefficients \(W^o\) must satisfy:

\[ R W^o = p \tag{3} \]

This relation is called the Wiener-Hopf equation (the normal equation). If \(R\) is nonsingular (has an inverse), \(W^o\) can be computed analytically:

\[ W^o = R^{-1} p \tag{4} \]

The Minimum MSE \(J_{\min}\)

Substituting Eq.(4) into Eq.(2), and using \(W^{oT} R W^o = W^{oT} p\) (obtained by left-multiplying both sides of Eq.(3) by \(W^{oT}\) ), we get the minimum MSE at the optimum:

\[ J_{\min} = J(W^o) = \sigma_d^2 - 2 W^{oT} p + W^{oT} R W^o = \sigma_d^2 - 2 W^{oT} p + W^{oT} p = \sigma_d^2 - W^{oT} p \]

Substituting \(W^o = R^{-1}p\) :

\[ J_{\min} = \sigma_d^2 - p^T R^{-1} p \tag{5} \]

This means the minimum achievable error power is the desired-signal power \(\sigma_d^2\) minus the portion “explained” by the input signal (\(p^T R^{-1} p\) ). If \(R\) is positive definite, then \(p^T R^{-1} p \ge 0\) and \(J_{\min} \ge 0\) always hold.

Edge Case 1: When \(R\) Is Singular or Ill-Conditioned

Eq.(4)’s \(W^o = R^{-1}p\) assumes \(R\) is nonsingular. In practice, however, when filter taps (inputs) are strongly correlated or redundant, \(R\) can become singular or ill-conditioned, making the Wiener solution non-unique or numerically unstable. We verify this numerically for a typical situation: 8 input taps, several of which are “nearly identical signals” (redundant sensors).

import numpy as np

np.random.seed(0)
N = 3000

# Two "genuinely independent" sources
s1 = np.random.randn(N)
s2 = np.random.randn(N)

# 8 taps where taps 2,3 are near-duplicates of tap 0 (s1) and taps 5,6 are
# near-duplicates of tap 1 (s2) -- a redundant setup (e.g. oversampled sensors)
eps = 1e-3
X_full = np.stack([
    s1,
    s2,
    s1 + eps * np.random.randn(N),
    s1 + eps * np.random.randn(N),
    0.5 * s1 + 0.5 * s2,
    s2 + eps * np.random.randn(N),
    s2 + eps * np.random.randn(N),
    0.3 * s1 + 0.7 * s2 + eps * np.random.randn(N),
], axis=1)  # shape (N, 8)

# The true system uses only the two independent sources
h_true = np.array([0.8, -0.5])
v = np.random.randn(N) * 0.05
d = h_true[0] * s1 + h_true[1] * s2 + v

M = X_full.shape[1]
R = (X_full.T @ X_full) / N
p = (X_full.T @ d) / N

print(f"cond(R) = {np.linalg.cond(R):.4e}")
print("eigenvalues =", np.array2string(np.linalg.eigvalsh(R), precision=6))

Output:

cond(R) = 1.3337e+16
eigenvalues = [4.957129e-16 3.013686e-07 3.861934e-07 9.736620e-07 9.878075e-07
 1.008506e-06 2.936495e+00 3.935144e+00]

The condition number is \(1.33 \times 10^{16}\) — essentially at the limit of double-precision floating point (about \(10^{16}\) ). 6 of the 8 eigenvalues collapse to nearly zero (\(10^{-7}\) to \(10^{-16}\) ), confirming that the effective rank of \(R\) is close to 2 (the true number of independent sources).

try:
    w_opt = np.linalg.solve(R, p)
    print("solve() succeeded:", np.array2string(w_opt, precision=4))
except np.linalg.LinAlgError as e:
    print("solve() failed:", e)

w_lstsq, *_ = np.linalg.lstsq(R, p, rcond=None)
print("lstsq() minimum-norm solution:", np.array2string(w_lstsq, precision=4))
solve() succeeded: [-3.9873 -5.4822 -0.4334  0.6601  9.2412  1.1976 -0.695  -0.2011]
lstsq() minimum-norm solution: [ 0.6714 -0.8236 -0.4334  0.6601 -0.0761  1.1976 -0.695  -0.2011]

This is the most dangerous point in practice. np.linalg.solve “succeeds” without raising an error, but the resulting coefficients (e.g. \(w_4 \approx 9.24\) ) are meaningless — far from the true relationship (only two independent components). A perfectly singular (rank-deficient) \(R\) would fail loudly with LinAlgError: Singular matrix, but an ill-conditioned matrix (numerically nonsingular but extremely close to singular) returns a wrong answer with no error at all, so you won’t notice unless you explicitly check the condition number.

Next, we measure the sensitivity of the solution by adding a tiny perturbation (standard deviation \(10^{-6}\) ) to the observed data, 200 times.

rng = np.random.default_rng(1)
deltas = []
for _ in range(200):
    Xp = X_full + rng.normal(0, 1e-6, size=X_full.shape)
    Rp = (Xp.T @ Xp) / N
    pp = (Xp.T @ d) / N
    wp, *_ = np.linalg.lstsq(Rp, pp, rcond=None)
    deltas.append(np.linalg.norm(wp - w_lstsq))
deltas = np.array(deltas)
print(f"perturbation 1e-6 -> ||delta w|| range: {deltas.min():.4e} - {deltas.max():.4e} (mean {deltas.mean():.4e})")
perturbation 1e-6 -> ||delta w|| range: 1.8197e+01 - 2.5618e+03 (mean 6.9458e+02)

A \(10^{-6}\) -scale perturbation to the observed data moves the solution vector by as much as \(2{,}562\) — more than \(10^9\) times the input perturbation. This is a direct consequence of the general linear-algebra fact that the relative error of the solution can be amplified by up to a factor of \(\text{cond}(R)\) .

The fix is ridge regularization: adding a small positive constant \(\lambda\) to the diagonal of \(R\) (\(R + \lambda I\) ).

for lam in [0, 1e-8, 1e-4, 1e-2, 1e-1]:
    Rreg = R + lam * np.eye(M)
    w_reg = np.linalg.solve(Rreg, p)
    print(f"lambda={lam}: cond={np.linalg.cond(Rreg):.4e}  ||w||={np.linalg.norm(w_reg):.4f}")
lambda=0: cond=1.3337e+16  ||w||=11.5730
lambda=1e-08: cond=3.9351e+08  ||w||=1.8992
lambda=0.0001: cond=3.9352e+04  ||w||=0.5412
lambda=0.01: cond=3.9451e+02  ||w||=0.5392
lambda=0.1: cond=4.0351e+01  ||w||=0.5232

Regularizing with \(\lambda=10^{-2}\) improves the condition number to \(394\) . Re-running the sensitivity test at \(\lambda=10^{-2}\) , \(\|\Delta w\|\) drops to \(6.3\times10^{-8}\) – \(4.1\times10^{-7}\) — an improvement of nearly 10 orders of magnitude — restoring practically usable stability. However, if \(\lambda\) is made too large the solution itself becomes biased away from the true value (note \(\|w\|\) shrinking monotonically), so \(\lambda\) must be chosen while balancing the condition number against estimation bias.

Edge Case 2: The Relationship Between the Wiener Filter and LMS — What Is an Adaptive Filter Converging To?

Eq.(4)’s \(W^o = R^{-1}p\) is a closed-form (batch) solution that assumes \(R\) and \(p\) are known. In practice, however, signal statistics are unknown or costly to collect, making it hard to compute this closed-form solution directly. This is where gradient-descent-based iterative algorithms come in.

\[ W_{k+1} = W_k - \eta \, \nabla_W J(W_k) = W_k - \eta\,(-2p + 2RW_k) = W_k + 2\eta\,(p - RW_k) \tag{6} \]

This is called the steepest descent method, and it converges exponentially to \(W^o\) in Eq.(4) if \(R\) and \(p\) are known. But when \(R, p\) are unknown, replacing the expectation in the gradient of Eq.(6), \(\nabla_W J(W_k) = -2\mathbb{E}[e_k X_k]\) , with the instantaneous sample \(-2 e_k X_k\) gives the LMS (Least Mean Squares) algorithm:

\[ W_{k+1} = W_k + \eta\, e_k X_k \tag{7} \]

LMS never computes \(W^o\) from Eq.(4) directly — it is a stochastic gradient descent algorithm that uses only the observed data \(\{X_k, d_k\}\) and is expected to converge to the same solution. We verify this “converges to the same solution” claim numerically on a stationary synthetic system-identification problem.

import numpy as np

np.random.seed(42)
N = 4000
M = 4

x = np.random.randn(N)  # white-noise input (stationary)
h_true = np.array([1.0, -0.6, 0.3, -0.1])
v = np.random.randn(N) * 0.1  # measurement noise
d = np.zeros(N)
for n in range(M - 1, N):
    d[n] = h_true @ x[n - M + 1 : n + 1][::-1] + v[n]

# Closed-form Wiener solution (Eq.(4): batch, computed from all data)
X = np.zeros((N - M + 1, M))
for i in range(M):
    X[:, i] = x[M - 1 - i : N - i]
dd = d[M - 1 :]
R = (X.T @ X) / X.shape[0]
p = (X.T @ dd) / X.shape[0]
w_wiener = np.linalg.solve(R, p)
Jmin = np.mean(dd**2) - w_wiener @ p

print("Wiener solution w* =", np.array2string(w_wiener, precision=4))
print("true h_true =", h_true)
print(f"Jmin = {Jmin:.6f} (measurement noise variance sigma_v^2 = {0.1**2})")

# LMS (sequential: never uses R, p, updates only from the observed data)
mu = 0.01
w = np.zeros(M)
w_hist = np.zeros((N, M))
for n in range(M - 1, N):
    xv = x[n - M + 1 : n + 1][::-1]
    e = d[n] - w @ xv
    w = w + mu * e * xv
    w_hist[n] = w

print("LMS final w(N) =", np.array2string(w, precision=4))
print(f"||w_LMS(N) - w_wiener|| = {np.linalg.norm(w - w_wiener):.6e}")

Output:

Wiener solution w* = [ 0.9989 -0.5984  0.3015 -0.1022]
true h_true = [ 1.  -0.6  0.3 -0.1]
Jmin = 0.010337 (measurement noise variance sigma_v^2 = 0.010000000000000002)

LMS final w(N) = [ 1.0006 -0.5947  0.3051 -0.1077]
||w_LMS(N) - w_wiener|| = 7.692268e-03

First, the closed-form Wiener solution \(w^*=[0.9989, -0.5984, 0.3015, -0.1022]\) computed via Eq.(4) is nearly identical to the true value \(h_{\text{true}}=[1.0, -0.6, 0.3, -0.1]\) , and \(J_{\min}=0.010337\) is almost exactly the measurement-noise variance \(\sigma_v^2=0.01\) (confirming, as predicted by Eq.(5), that the achievable minimum error equals the noise floor). And LMS — which never explicitly computed \(R\) or \(p\) — converges to a final weight vector \([1.0006, -0.5947, 0.3051, -0.1077]\) whose Euclidean distance from the Wiener solution is only \(7.7\times10^{-3}\) .

Tracking the weight error \(\|W_k - W^o\|\) over iterations makes the convergence process even clearer.

Iteration \(k\)\(\|W_k - W^o\|\)
500.81228
1000.51381
2000.19565
5000.01374
10000.01389
20000.01747
40000.00769

Starting from \(W_0=0\) , LMS rapidly approaches the neighborhood of the Wiener solution within a few hundred iterations, then continues to fluctuate around a residual error of order \(10^{-2}\) (excess MSE / misadjustment, caused by the noise in the instantaneous gradient) rather than converging exactly to zero. This residual fluctuation is an inherent property of LMS — a smaller step size \(\eta\) reduces it but slows convergence, a tradeoff whose quantitative theory (together with the convergence condition on the step size) is covered in LMS / NLMS Algorithms: Theory and Python Implementation .

Each LMS weight component (solid line) converging over iterations to the corresponding Wiener-solution component (dashed line). All four weights approach their dashed target within a few hundred iterations and then fluctuate slightly around it

Edge Case 3: The Limits of the Wiener Solution Under Non-Stationarity, and Tracking with LMS

The Wiener solution \(W^o=R^{-1}p\) in Eq.(4) implicitly assumes the input/desired-signal statistics (\(R, p\) ) are time-invariant (stationary). But many real-world systems (acoustic echo paths, fading communication channels, etc.) change over time (non-stationary), in which case a “fixed Wiener solution computed once” is no longer optimal after the statistics change. Adaptive algorithms like LMS, by contrast, keep updating their coefficients and can potentially track such changes. We verify this on a system-identification problem in which the unknown system abruptly changes partway through.

import numpy as np

np.random.seed(7)
N = 6000
M = 4
change_point = N // 2  # abrupt system change at n=3000

x = np.random.randn(N)
h1 = np.array([1.0, -0.6, 0.3, -0.1])
h2 = np.array([-0.5, 0.8, -0.4, 0.2])  # different system after the change
v = np.random.randn(N) * 0.1

d = np.zeros(N)
for n in range(M - 1, N):
    xv = x[n - M + 1 : n + 1][::-1]
    h = h1 if n < change_point else h2
    d[n] = h @ xv + v[n]

def build_R_p(x, d, M, n0, n1):
    X = np.zeros((n1 - n0, M))
    dd = np.zeros(n1 - n0)
    for k, n in enumerate(range(n0, n1)):
        X[k] = x[n - M + 1 : n + 1][::-1]
        dd[k] = d[n]
    R = (X.T @ X) / X.shape[0]
    p = (X.T @ dd) / X.shape[0]
    return R, p

# "fixed Wiener solution" computed only from pre-change data
R1, p1 = build_R_p(x, d, M, M - 1, change_point)
w_fixed = np.linalg.solve(R1, p1)
print("Fixed Wiener solution (pre-change data only) w_fixed =", np.array2string(w_fixed, precision=4))
print("h1 =", h1, " h2 =", h2)

# apply the fixed solution over the whole signal (both halves)
e_fixed = np.zeros(N)
for n in range(M - 1, N):
    xv = x[n - M + 1 : n + 1][::-1]
    e_fixed[n] = d[n] - w_fixed @ xv

# adaptive LMS run over the whole signal
mu = 0.02
w = np.zeros(M)
e_lms = np.zeros(N)
for n in range(M - 1, N):
    xv = x[n - M + 1 : n + 1][::-1]
    e_lms[n] = d[n] - w @ xv
    w = w + mu * e_lms[n] * xv

seg_pre = slice(M - 1, change_point)
seg_post_early = slice(change_point, change_point + 500)
seg_post_late = slice(N - 500, N)

for name, e in [("fixed-Wiener", e_fixed), ("LMS", e_lms)]:
    mse_pre = np.mean(e[seg_pre] ** 2)
    mse_early = np.mean(e[seg_post_early] ** 2)
    mse_late = np.mean(e[seg_post_late] ** 2)
    print(f"{name:12s}: MSE pre-change={mse_pre:.5f}  MSE post-change(early,500)={mse_early:.5f}  "
          f"MSE post-change(late,500)={mse_late:.5f}")

print(f"\nLMS final w(N) = {np.array2string(w, precision=4)}")
print(f"||w_LMS(N) - h2|| = {np.linalg.norm(w - h2):.5f}")
print(f"||w_fixed - h2||  = {np.linalg.norm(w_fixed - h2):.5f}")

Output:

Fixed Wiener solution (pre-change data only) w_fixed = [ 1.0011 -0.5988  0.3002 -0.0979]
h1 = [ 1.  -0.6  0.3 -0.1]  h2 = [-0.5  0.8 -0.4  0.2]
fixed-Wiener: MSE pre-change=0.00966  MSE post-change(early,500)=4.34709  MSE post-change(late,500)=5.09337
LMS         : MSE pre-change=0.02295  MSE post-change(early,500)=0.25829  MSE post-change(late,500)=0.01016

LMS final w(N) = [-0.5012  0.8011 -0.3913  0.1942]
||w_LMS(N) - h2|| = 0.01060
||w_fixed - h2||  = 2.18833

The fixed Wiener solution \(w_{\text{fixed}}=[1.0011, -0.5988, 0.3002, -0.0979]\) , computed from pre-change data, is nearly identical to \(h_1\) and achieves an MSE of \(0.00966\) (essentially the noise floor \(0.01\) , the theoretical optimum) in the pre-change segment. But once the system abruptly changes to \(h_2\) , \(w_{\text{fixed}}\) remains \(\|w_{\text{fixed}}-h_2\|=2.188\) away from \(h_2\) , and its MSE stays elevated at \(4.347\) immediately after the change and \(5.093\) even long after — the fixed solution never improves on its own.

Adaptive LMS (\(\mu=0.02\) ), by contrast, has a slightly worse pre-change MSE of \(0.02295\) (due to its inherent residual error), but although its MSE spikes temporarily to \(0.258\) right after the change, continuous coefficient updates let it re-learn \(h_2\) , and by the last 500 samples its MSE has recovered to \(0.01016\) — essentially the noise floor again. Its final weights, \(\|w_{\text{LMS}}(N)-h_2\|=0.0106\) , also match \(h_2\) almost exactly.

This result quantitatively demonstrates the essential difference, in a non-stationary environment, between “a Wiener solution computed once” and “an adaptive filter that keeps updating.” The Wiener solution is only optimal for the statistics at one instant, and does not automatically track an environment whose statistics keep changing. The real reason adaptive filters are chosen in practice is not merely that they can “learn to approximate” the Wiener solution — it is that they can chase the Wiener solution when it itself moves over time.

Smoothed squared error before and after the abrupt system change (log scale; dotted line marks the change point). The fixed Wiener solution (red) jumps up right after the change and stays elevated, while adaptive LMS (blue) spikes temporarily and then re-converges to the pre-change level

Recent Research

The theory of the Wiener-Hopf equation and MMSE linear filtering was established over half a century ago, but how to handle its ill-conditioning and its modern applications remain active research areas.

  • Automatic determination of regularization: Zanco, Szczecinski, & Benesty (2023/2024, arXiv:2312.06560) propose a Bayesian statistical framework that automatically determines the regularization parameter \(\lambda\) from the observed signal itself, for the ridge regularization of an ill-conditioned \(R\) that we used in Edge Case 1 above, achieving near-optimal performance in system-identification and beamforming applications. Determining \(\lambda\) in a data-driven way, instead of a manual grid search, is a direct extension of the experiment in this article.
  • A unified view of the Wiener solution, Kalman filtering, and LMS/NLMS: Szczecinski, Benesty, & Kuhn (2025, arXiv:2502.18325) re-derive adaptive filters from the framework of Bayesian sequential inference, showing that LMS, NLMS, and the Kalman filter emerge naturally under a Gaussian noise assumption, while robust variants such as the sign-error algorithm emerge under non-Gaussian (e.g. Laplacian) noise. This offers a broader statistical lens on Edge Case 2 above (what LMS is converging toward, and why).
  • Re-evaluating the classical Wiener filter: Bled & Pitié (2023, arXiv:2303.16640) show that a carefully optimized implementation of the Wiener filter for image denoising can match the performance of deep-learning-based methods such as DnCNN — evidence that linear MMSE theory over half a century old remains practically relevant even in the era of deep learning.

References