Engee documentation
Notebook

Comparison of LMS and NLMS algorithms for adaptive signal filtering

Adaptive filtering is a key technology in digital signal processing, with applications in noise reduction, echo cancellation, system identification, and many other fields. In this article, we will look at two classic adaptive filtering algorithms - LMS (Least Mean Squares) and its modification NLMS (Normalized LMS), implemented in the Engee language using the EngeeDSP package.
LMS (Least Mean Squares) is a stochastic gradient algorithm that minimizes the RMS error between the desired signal and the output of the adaptive filter. The basic equation of updating weights:

w(n+1) = w(n) + μ·e(n)·x(n)

where:

  • w(n) is the vector of filter weights at step n
  • μ is the adaptation step
  • e(n) is the error (the difference between the desired and actual output)
  • x(n) is the input vector

NLMS Algorithm

NLMS (Normalized LMS) is a modification of the LMS, where the adaptation step is normalized by the input signal power:

w(n+1) = w(n) + (μ / (xᵀ(n)x(n) + ε))·e(n)·x(n)

where ε is a small constant to prevent division by zero.

Analysis of the first example

The first code compares the LMS and NLMS on a signal with varying amplitude:

x_window = 0.05 * randn(1024*10) .* repeat(0.5 .+ 0.5sin.(2π*(0:1024*10-1)/10240), inner=1)
d_window = filt(FIRFilter([0.5, -0.3, 0.2, 0.1, -0.05]), x_window)

It is being created here:

  1. A noise signal with an amplitude modulated by a sine wave
  2. The "desired" signal obtained by filtering through a FIR filter

The analysis is performed by calculating the frequency response of the adaptive filter on each frame and visualizing using a surface-plot.

In [ ]:
using EngeeDSP, DSP, FFTW, Plots

frameSize = 500
Fs = 8192
x_window = 0.05 * randn(1024*10) .* repeat(0.5 .+ 0.5sin.(2π*(0:1024*10-1)/10240), inner=1)
d_window = filt(FIRFilter([0.5, -0.3, 0.2, 0.1, -0.05]), x_window)

HA = EngeeDSP.LMSFilter(
    Algorithm = "LMS",
    FilterLength = 32,
    StepSize = 1.0,
    LeakageFactor = 1.0,
    InitialValueOfFilterWeights = 0,
    AdaptPort = false, 
    ResetPort = "None",
    OutputFilterWeights = true
)

numFrames = length(d_window) ÷ frameSize
response = zeros(512, numFrames)
t_frame = (0:frameSize:length(d_window)-1)/Fs

i = 1
for start_idx in 1:frameSize:length(x_window)-frameSize+1
    end_idx = start_idx + frameSize - 1
    x_frame = x_window[start_idx:end_idx]
    d_frame = d_window[start_idx:end_idx]
    
    if i == 1
        setup!(HA, x_frame, d_frame)
    end
    
    y, e, w = step!(HA, x_frame, d_frame)
    response[:,i] = 20*log10.(abs.(fft([w; zeros(512-length(w))])))
    i += 1
end

p1 = surface(t_frame, Fs*(0:511)/512, response,
    title="LMS Algorithm Response",
    xlabel="Time (s)", ylabel="Frequency (Hz)", zlabel="Power (dB)",
    camera=(-70, 18), zlims=(-80, 2))

HA = EngeeDSP.LMSFilter(
    Algorithm = "Normalized LMS",
    FilterLength = 32,
    StepSize = 0.3,
    LeakageFactor = 1.0,
    InitialValueOfFilterWeights = 0,
    AdaptPort = false,  
    ResetPort = "None",
    OutputFilterWeights = true
)

response = zeros(512, numFrames)

i = 1
for start_idx in 1:frameSize:length(x_window)-frameSize+1
    end_idx = start_idx + frameSize - 1
    x_frame = x_window[start_idx:end_idx]
    d_frame = d_window[start_idx:end_idx]
    
    if i == 1
        setup!(HA, x_frame, d_frame)
    end
    
    y, e, w = step!(HA, x_frame, d_frame)
    response[:,i] = 20*log10.(abs.(fft([w; zeros(512-length(w))])))
    i += 1
end

p2 = surface(t_frame, Fs*(0:511)/512, response,
    title="NLMS Algorithm Response",
    xlabel="Time (s)", ylabel="Frequency (Hz)", zlabel="Power (dB)",
    camera=(-70, 18), zlims=(-80, 2))

plot(p1, p2, layout=(1,2), size=(1000,400))
Out[0]:
  1. LMS algorithm:

    • Uses a fixed adaptation step (µ = 1.0)
  • Can be sensitive to changes in the amplitude of the input signal
    • Adapts faster, but may be less stable
  1. NLMS algorithm:

    • Uses a normalized adaptation step (µ = 0.3)
  • Automatically adjusts to the input signal level
    • More stable operation when the amplitude changes

Analysis of the second example

The second code examines the behavior of the LMS filter on stationary and non-stationary signals:

x = 0.05 * randn(1024*10)
d_stat = filt(FIRFilter([0.5, -0.3, 0.2, 0.1, -0.05]), x)
d_nonstat = filt(FIRFilter([0.8, -0.2, 0.0, 0.1, -0.1]), x)

Here:

  1. Stationary case: immutable system (one FIR filter)
  2. Non-stationary case: the system is changing (another FIR filter)
In [ ]:
using EngeeDSP, DSP, FFTW

frameSize = 500
Fs = 44100
x = 0.05 * randn(1024*10)
d_stat = filt(FIRFilter([0.5, -0.3, 0.2, 0.1, -0.05]), x)
d_nonstat = filt(FIRFilter([0.8, -0.2, 0.0, 0.1, -0.1]), x)

HA = EngeeDSP.LMSFilter(
    Algorithm = "LMS",
    FilterLength = 32,
    StepSize = 1.0,
    LeakageFactor = 1.0,
    InitialValueOfFilterWeights = 0,
    AdaptPort = false,
    ResetPort = "None",
    OutputFilterWeights = true
)

t_frame = (0:frameSize:length(x)-1)/Fs
response = zeros(512, length(t_frame))
i = 1
for start_idx in 1:frameSize:length(x)-frameSize+1
    end_idx = start_idx + frameSize - 1
    x_frame = x[start_idx:end_idx]
    d_frame = d_stat[start_idx:end_idx]
    
    if i == 1
        setup!(HA, x_frame, d_frame)
    end
    
    y, e, w = step!(HA, x_frame, d_frame)
    response[:,i] = 20*log10.(abs.(fft([w; zeros(512-length(w))])))
    i += 1
end

p1 = surface(t_frame, Fs*(0:511)/512, response,
    title="Stationary Signal Response",
    xlabel="Time (s)", ylabel="Frequency (Hz)", zlabel="Power (dB)",
    camera=(-70, 18), zlims=(-80, 2))

    response = zeros(512, length(t_frame))
i = 1
for start_idx in 1:frameSize:length(x)-frameSize+1
    end_idx = start_idx + frameSize - 1
    x_frame = x[start_idx:end_idx]
    d_frame = d_nonstat[start_idx:end_idx]
    
    y, e, w = step!(HA, x_frame, d_frame)
    response[:,i] = 20*log10.(abs.(fft([w; zeros(512-length(w))])))
    i += 1
end

p2 = surface(t_frame, Fs*(0:511)/512, response,
    title="Non-Stationary Signal Response",
    xlabel="Time (s)", ylabel="Frequency (Hz)", zlabel="Power (dB)",
    camera=(-70, 18), zlims=(-80, 2))

plot(p1, p2, layout=(1,2), size=(1000,400))
Out[0]:
  1. Stationary signal:

    • The filter successfully converges to the optimal solution
    • The frequency response stabilizes over time
  2. Non-stationary signal:

    • The filter is trying to adapt to a changing system
  • The characteristic continues to change, reflecting non-stationarity

Conclusion

The presented examples demonstrate the key features of adaptive filters. The choice between LMS and NLMS depends on the signal characteristics and system requirements. NLMS provides more stable operation when the signal strength changes, while classic LMS may be preferable in conditions of limited computing resources. The analysis of nonstationary systems requires a special approach and can serve as a topic for further research.
Also, based on their experiences, the following conclusions can be drawn:

  1. NLMS is preferable for signals with varying amplitude.

  2. When working with non-stationary systems, it may be necessary:

    • Reduction of the adaptation step
    • Using algorithms with variable pitch
    • Regular filter reset
  3. Visualization of the frequency response in dynamics is a powerful analysis tool