Adaptive ASR for changing recording statistics#

Can adaptive calibration follow a change in the clean recording statistics while continuing to suppress transient artifacts? This controlled example contains a baseline calibration chunk, a changed-regime adaptation chunk, and an independent changed-regime test chunk. Frozen ASR is compared with AdaptiveASR after one adaptation chunk.

The quiet changed-regime test data are the preservation endpoint. Error in the independent test burst windows is the artifact endpoint. The comparison illustrates the streaming fit / partial_fit / transform lifecycle on one controlled substrate; it is not a benchmark of adaptive variants.

The use case is motivated by adaptive ASR research [1] and the standard ASR evaluation [2].

References#

Create independent baseline, adaptation, and test chunks#

import numpy as np

from mne_denoise.asr import ASR, AdaptiveASR
from mne_denoise.qa import rms_change
from mne_denoise.viz import plot_signal_overlay

rng = np.random.default_rng(17)
sfreq = 200.0
n_channels = 8
segment_seconds = 12.0
segment_samples = int(round(segment_seconds * sfreq))
test_times = np.arange(segment_samples) / sfreq

mixing = rng.standard_normal((n_channels, 3))
mixing /= np.linalg.norm(mixing, axis=0, keepdims=True)


def make_regime(rng, mixing, time, amplitudes, noise_scale):
    """Generate one independent realization of a covariance regime."""
    frequencies = (10.0, 6.0, 0.8)
    sources = np.asarray(
        [
            amplitude
            * np.sin(2.0 * np.pi * frequency * time + rng.uniform(0.0, 2.0 * np.pi))
            + 0.05 * rng.standard_normal(time.size)
            for frequency, amplitude in zip(frequencies, amplitudes)
        ]
    )
    sensor_noise = noise_scale * rng.standard_normal((mixing.shape[0], time.size))
    return mixing @ sources + sensor_noise


baseline = make_regime(
    rng,
    mixing,
    test_times,
    amplitudes=(0.35, 0.20, 0.08),
    noise_scale=0.05,
)
adaptation = make_regime(
    rng,
    mixing,
    test_times,
    amplitudes=(0.80, 0.45, 0.20),
    noise_scale=0.12,
)
test_clean = make_regime(
    rng,
    mixing,
    test_times,
    amplitudes=(0.80, 0.45, 0.20),
    noise_scale=0.12,
)

# Artifacts are injected only into the independent test chunk.
test_contaminated = test_clean.copy()
artifact_mask = np.zeros(segment_samples, dtype=bool)
artifact_spatial = rng.standard_normal(n_channels)
artifact_spatial /= np.linalg.norm(artifact_spatial)
for onset in (2.0, 6.0, 10.0):
    start = int(round(onset * sfreq))
    stop = min(segment_samples, start + int(round(0.6 * sfreq)))
    artifact_mask[start:stop] = True
    artifact_source = rng.standard_normal(stop - start)
    test_contaminated[:, start:stop] += 5.0 * np.outer(
        artifact_spatial, artifact_source
    )

# The adaptation and test chunks are separate calls to ``make_regime`` with
# the same changed-regime parameters, so they have the same regime but
# independent phases, noise, and time-series realizations.

Calibrate once, adapt once, and evaluate only on the independent test chunk#

AdaptiveASR uses unfiltered statistics internally. Match that setting in the frozen comparator so the comparison isolates the calibration update.

Frozen ASR: artifact residual ratio=0.152, quiet test error=0.985
Adaptive ASR: artifact residual ratio=0.049, quiet test error=0.117
Adaptation chunks supplied: 1

Compare frozen and adaptive outputs on the independent test chunk#

channel = int(np.argmax(np.abs(artifact_spatial)))
plot_signal_overlay(
    frozen_clean,
    adaptive_clean,
    test_times,
    pick=channel,
    scale_after=False,
    before_label="frozen ASR",
    after_label="adaptive ASR",
    reference=test_clean[channel],
    reference_label="clean substrate",
    highlight_mask=artifact_mask,
    highlight_label="artifact",
    x_label="Time (s)",
    y_label="Amplitude (a.u.)",
    title="Independent changed-regime test chunk",
    show=False,
)
Independent changed-regime test chunk
<Figure size 2400x800 with 1 Axes>

Total running time of the script: (0 minutes 1.255 seconds)