The RLS Algorithm in Python: Recursive Least Squares, Its Equivalence to the Kalman Filter, and the Forgetting Factor

Implement the RLS adaptive filter from scratch via the matrix inversion lemma (Sherman-Morrison formula) using numpy.outer and numpy.eye. A 200-trial Monte Carlo study compares convergence speed against LMS, shows that RLS with forgetting factor lambda=1 is numerically identical to a Kalman filter, and quantifies the forgetting-factor trade-off between steady-state accuracy and tracking speed under non-stationary conditions.

Introduction

https://yuhi-sa.github.io/en/posts/20260310_adaptive_filter/1/ and https://yuhi-sa.github.io/en/posts/20260501_lms_nlms/1/ covered LMS and NLMS adaptive filters in depth. Both articles ended by noting that “RLS converges faster than LMS but at higher computational cost” and that “RLS is mathematically equivalent to a Kalman filter,” without deriving either claim. This article follows up: we derive RLS (Recursive Least Squares) from the matrix inversion lemma, implement it from scratch in Python, numerically confirm its exact equivalence to a Kalman filter under specific conditions, and quantify how the forgetting factor \(\lambda\) trades off steady-state accuracy against tracking speed.

Problem Setup: Exponentially Weighted Least Squares

Unlike LMS, which stochastically approximates the instantaneous gradient, RLS exactly minimizes the weighted sum of squared errors over all past observations. For an FIR filter of length \(M\) with coefficients \(\mathbf{w}\) , define the cost function at time \(n\) as

\[ J(\mathbf{w}, n) = \sum_{i=1}^{n} \lambda^{n-i} \bigl[d(i) - \mathbf{x}(i)^\top \mathbf{w}\bigr]^2 \tag{1} \]

where \(\lambda \in (0, 1]\) is the forgetting factor, exponentially discounting the contribution of older samples. \(\lambda = 1\) weights all samples equally (ordinary least squares); \(\lambda < 1\) emphasizes recent samples, giving an adaptive estimate.

Setting the derivative of \(J(\mathbf{w}, n)\) with respect to \(\mathbf{w}\) to zero yields the normal equations

\[ \mathbf{R}(n) \mathbf{w}(n) = \mathbf{r}(n), \qquad \mathbf{R}(n) = \sum_{i=1}^n \lambda^{n-i} \mathbf{x}(i)\mathbf{x}(i)^\top, \quad \mathbf{r}(n) = \sum_{i=1}^n \lambda^{n-i} \mathbf{x}(i) d(i) \tag{2} \]

Inverting \(\mathbf{R}(n)\) from scratch every step costs \(O(M^3)\) . The matrix inversion lemma below reduces this to an \(O(M^2)\) recursive update.

O(M²) Recursion via the Matrix Inversion Lemma

\(\mathbf{R}(n)\) updates recursively as

\[ \mathbf{R}(n) = \lambda \mathbf{R}(n-1) + \mathbf{x}(n)\mathbf{x}(n)^\top \tag{3} \]

Letting \(P(n) = \mathbf{R}(n)^{-1}\) and applying the Sherman-Morrison formula (matrix inversion lemma)

\[ (A + \mathbf{u}\mathbf{v}^\top)^{-1} = A^{-1} - \frac{A^{-1}\mathbf{u}\mathbf{v}^\top A^{-1}}{1 + \mathbf{v}^\top A^{-1}\mathbf{u}} \tag{4} \]

with \(A = \lambda \mathbf{R}(n-1)\) and \(\mathbf{u} = \mathbf{v} = \mathbf{x}(n)\) gives

\[ P(n) = \frac{1}{\lambda}\left[P(n-1) - \frac{P(n-1)\mathbf{x}(n)\mathbf{x}(n)^\top P(n-1)}{\lambda + \mathbf{x}(n)^\top P(n-1)\mathbf{x}(n)}\right] \tag{5} \]

The key point: \(P(n)\) is computed from \(P(n-1)\) using only matrix products and a scalar division, never a direct matrix inversion. Defining a Kalman-gain-like quantity

\[ \mathbf{k}(n) = \frac{P(n-1)\mathbf{x}(n)}{\lambda + \mathbf{x}(n)^\top P(n-1)\mathbf{x}(n)} \tag{6} \]

lets us write equation (5) compactly as \(P(n) = \frac{1}{\lambda}\bigl[P(n-1) - \mathbf{k}(n)\mathbf{x}(n)^\top P(n-1)\bigr]\) . The weight update is

\[ e(n) = d(n) - \mathbf{x}(n)^\top \mathbf{w}(n-1), \qquad \mathbf{w}(n) = \mathbf{w}(n-1) + \mathbf{k}(n) e(n) \tag{7} \]

Per-step cost is dominated by computing \(\mathbf{k}(n)\) , giving \(O(M^2)\) — heavier than LMS/NLMS’s \(O(M)\) .

Python Implementation

import numpy as np

def rls_filter(xs, d, M, lam=1.0, delta=1.0):
    """Adaptive filtering via the RLS algorithm

    Parameters
    ----------
    xs : ndarray, shape (N, M)
        Input vector at each time step
    d : ndarray, shape (N,)
        Desired signal
    M : int
        Filter length
    lam : float
        Forgetting factor (0 < lam <= 1)
    delta : float
        Regularization for the initial covariance (P(0) = I / delta)

    Returns
    -------
    W : ndarray, shape (N, M)
        Filter coefficient trajectory over time
    """
    w = np.zeros(M)
    P = np.eye(M) / delta
    W = np.zeros((len(d), M))
    for n in range(len(d)):
        x = xs[n]
        Px = P @ x
        k = Px / (lam + x @ Px)
        e = d[n] - x @ w
        w = w + k * e
        P = (P - np.outer(k, x @ P)) / lam
        W[n] = w
    return W

np.outer(k, x @ P) implements the rank-1 update term in equation (5); Px / (lam + x @ Px) implements the gain in equation (6).

Experiment 1: Convergence Speed vs. LMS

For an \(M=8\) -tap FIR system identification problem (SNR 20 dB), we compared LMS (step size \(\mu=0.05\) ) and RLS (\(\lambda=1\) ) learning curves averaged over 200 Monte Carlo trials.

Sample \(n\)LMS MSERLS MSE
200.29160.0279
500.03100.0112
1000.01450.0120

Samples needed to reach twice the noise floor (MSE ≈ 0.0207): LMS took 57 samples, RLS took 27 — roughly half as many. This numerically confirms the textbook claim that LMS converges linearly (rate depends on the input’s eigenvalue spread) while RLS converges quadratically (reaches the normal-equation solution in the shortest possible time, independent of input statistics). The trade-off is RLS’s \(O(M^2)\) per-step cost versus LMS’s \(O(M)\) , which drives the choice between them in practice.

RLS’s Equivalence to the Kalman Filter

Recall the Kalman filter update equations (prediction, innovation, gain, update) derived in https://yuhi-sa.github.io/en/posts/20260224_kalman_filter/1/. RLS can be formulated as a special case of the Kalman filter:

RLS as a state-space model:

Kalman filter symbolRLS correspondenceMeaning
State \(\mathbf{x}_k\)Filter coefficients \(\mathbf{w}\)The parameter being estimated
Transition \(A\)\(I\) (identity)Coefficients assumed constant over time
Process noise \(Q\)\(0\)The true coefficients never change
Observation matrix \(H\)\(\mathbf{x}(n)^\top\)The time-varying input vector
Observation noise \(R\)\(\lambda\) (constant)The forgetting factor plays the role of observation variance
Initial covariance \(P_0\)\(I / \delta\)Matches RLS’s regularization parameter

Writing out the Kalman filter update equations (\(K_k = P_{k|k-1}H^\top S_k^{-1}\) , etc.) under this mapping reproduces the RLS update equations (6)-(7) exactly. We implemented both and applied them to the same data to compare the coefficient trajectories directly.

def kalman_param_filter(xs, d, M, R=1.0, delta=1.0):
    w = np.zeros(M)
    P = np.eye(M) / delta
    W = np.zeros((len(d), M))
    for n in range(len(d)):
        x = xs[n]
        Px = P @ x
        S = x @ Px + R          # innovation covariance
        K = Px / S               # Kalman gain
        e = d[n] - x @ w
        w = w + K * e
        P = P - np.outer(K, x @ P)
        W[n] = w
    return W

Running both on the same \(M=4\) -tap, 500-sample system identification problem with matched \(\lambda = 1\) , \(R = 1\) , and \(\delta\) :

max |RLS - KF| over all n, all taps (lambda=1, R=1): 0.0
final RLS weights: [ 0.99811908 -0.49835497  0.29976788  0.19937108]
final KF  weights: [ 0.99811908 -0.49835497  0.29976788  0.19937108]
true weights     : [ 1.  -0.5  0.3  0.2]

The two trajectories are bit-for-bit identical at every time step and every tap, numerically confirming that RLS is a special case of the Kalman filter. This equivalence is practically useful: instead of tuning the forgetting factor, one can switch to the more general Kalman-filter framework and model non-stationarity explicitly through the process noise \(Q\) .

The Forgetting Factor λ: Steady State vs. Tracking Speed

The intuition behind \(\lambda < 1\) is that it approximates a Kalman filter with \(Q > 0\) . To test this, we constructed a non-stationary system whose true coefficients jump abruptly partway through (at sample 1000) and ran RLS with several values of \(\lambda\) .

\(\lambda\)Steady-state error before the jumpError 100 samples after the jump
1.0000.00132.0197
0.9950.00061.3605
0.9800.00100.2947
0.9000.00300.0053

\(\lambda = 1\) gives the smallest steady-state error (statistically most efficient, since it uses all past data equally) but tracks the change most slowly — it has barely converged to the new value even 100 samples after the jump. Conversely, \(\lambda = 0.9\) has a slightly larger steady-state error (effectively using only the last ~10 samples) but tracks changes very quickly, nearly converging within 100 samples. This experiment quantitatively confirms that \(\lambda\) directly trades off steady-state accuracy against the ability to track a non-stationary environment.

Computational Cost vs. LMS/NLMS

Reproducing the comparison table from https://yuhi-sa.github.io/en/posts/20260310_adaptive_filter/1/:

PropertyLMSNLMSRLS
Cost per step\(O(M)\)\(O(M)\)\(O(M^2)\)
Memory\(O(M)\)\(O(M)\)\(O(M^2)\)
Convergence speedSlow (linear)ModerateFast (quadratic)
Steady-state errorDepends on step sizeDepends on step sizeSmall (depends on \(\lambda\) )
Numerical stabilityHighHigh\(P\) can diverge — watch for it

Because RLS iteratively updates the matrix \(P\) , finite-precision arithmetic can accumulate rounding errors that make \(P\) asymmetric or non-positive-definite, eventually causing divergence. In practice this is mitigated by symmetrizing \(P \leftarrow (P + P^\top)/2\) each step, or by using QR-decomposition-based RLS variants. The implementation in this article is a straightforward, pedagogical one; embedded applications running continuously for long periods need these numerical-stability safeguards.

Which to Use

ScenarioRecommendationReason
Small tap count (\(M \lesssim 20\) ), fast convergence requiredRLSQuadratic convergence reaches the normal-equation solution fastest
Large tap count / embedded, low-resource targetsLMS / NLMS\(O(M)\) keeps compute and memory bounded
Mildly non-stationary environmentRLS (\(\lambda < 1\) )Forgetting factor tunes the steady-state/tracking trade-off
Large, abrupt environmental changesKalman filterProcess noise \(Q\) can be modeled explicitly
Long-running continuous operation (stability matters)NLMSNo risk of \(P\) divergence, and the implementation is simpler

References

  • Haykin, S. (2014). Adaptive Filter Theory (5th ed.). Pearson. Chapters 9-13.
  • Sayed, A. H. (2008). Adaptive Filters. Wiley-IEEE Press. Chapter 5 (RLS).
  • Kalman, R. E. (1960). A new approach to linear filtering and prediction problems. Journal of Basic Engineering, 82(1), 35-45.
  • Haykin, S. (2001). Kalman Filtering and Neural Networks. Wiley. Chapter 1 (RLS-Kalman equivalence).