適応フィルタ(LMS/RLS)の理論とPython実装

numpy / scipy.signal.lfilter でLMS・NLMS・RLS適応フィルタをPython実装し、ノイズキャンセレーション・エコー除去・システム同定の実例を解説。最小平均二乗・正規化LMS・再帰的最小二乗アルゴリズムの数学的導出と収束特性・計算量・選択指針までまとめます。

適応フィルタとは

適応フィルタ(Adaptive Filter)は、入力信号の統計的性質が時間とともに変化する環境に対応するため、フィルタ係数を自動的に更新するフィルタです。 EMAフィルタバターワースフィルタ のような固定パラメータのフィルタとは異なり、未知の環境やダイナミックに変化する信号に対して最適な性能を発揮します。

主な応用分野

  • ノイズキャンセレーション: マイクで拾った環境ノイズを除去(ANCヘッドホン等)
  • エコーキャンセレーション: 電話やビデオ会議のエコー除去
  • システム同定: 未知のシステムの伝達関数を推定
  • チャネル等化: 通信における伝送路歪みの補正

本記事は、LMS・NLMS・RLSという主要な適応フィルタアルゴリズムの比較概観と実践的な実装(ノイズキャンセレーションの実行検証)に主眼を置きます。各アルゴリズムの収束条件の厳密な証明・ミスアジャストメントの定量化・カルマンフィルタとの等価性といった深い理論は姉妹記事( LMS/NLMSアルゴリズムの理論とPython実装RLS(逐次最小二乗法)アルゴリズムの理論とPython実装 )に譲り、本記事末尾ではそれらの記事で扱っていない実務上の落とし穴(フィルタ次数の選び方・ステップサイズと定常誤差のトレードオフ・参照信号の相関に関する注意点)を実行検証付きで扱います。Wiener-Hopf方程式を最急降下法から導出する基礎部分は 適応フィルタの基礎理論とデジタル信号処理への応用 を参照してください。

ウィーナーフィルタとの関係

適応フィルタの理論的基盤は ウィーナーフィルタ です。ウィーナーフィルタは、入力信号 \(\mathbf{x}(n)\) から目的信号 \(d(n)\) を推定する際の平均二乗誤差(MSE)を最小化する最適フィルタです。

フィルタ出力を \(y(n) = \mathbf{w}^T \mathbf{x}(n)\) 、誤差を \(e(n) = d(n) - y(n)\) とすると、MSEは:

\[ J(\mathbf{w}) = E[e^2(n)] = E[(d(n) - \mathbf{w}^T \mathbf{x}(n))^2] \tag{1} \]

これを最小化する最適解(ウィーナー・ホフ解)は:

\[ \mathbf{w}^* = \mathbf{R}^{-1} \mathbf{p} \tag{2} \]

ここで、\(\mathbf{R} = E[\mathbf{x}(n)\mathbf{x}^T(n)]\) は入力の自己相関行列、\(\mathbf{p} = E[\mathbf{x}(n)d(n)]\) は相互相関ベクトルです。

実際には \(\mathbf{R}\) と \(\mathbf{p}\) が未知であるか、時間変化するため、適応アルゴリズムでウィーナー解に逐次的に近づけます。式(2)の導出過程(\(J(\mathbf{w})\) を展開し \(\mathbf{w}\) で微分してゼロと置く)は 適応フィルタの基礎理論とデジタル信号処理への応用 で最急降下法とあわせて詳しく扱っています。

LMS(Least Mean Squares)アルゴリズム

LMSアルゴリズムは、MSEの勾配を瞬時推定値で近似する確率的勾配降下法です。 SGD と同じ原理に基づいています。

導出

MSE \(J(\mathbf{w})\) の勾配は:

\[ \nabla J(\mathbf{w}) = -2E[\mathbf{x}(n)e(n)] \tag{3} \]

LMSでは期待値を瞬時値で置き換え、勾配の推定値を得ます:

\[ \hat{\nabla} J(\mathbf{w}) = -2\mathbf{x}(n)e(n) \tag{4} \]

これを用いた更新則:

\[ \mathbf{w}(n+1) = \mathbf{w}(n) + \mu \mathbf{x}(n) e(n) \tag{5} \]

ここで \(\mu\) はステップサイズ(学習率)です。

収束条件

LMSが収束するためには、ステップサイズ \(\mu\) が以下を満たす必要があります:

\[ 0 < \mu < \frac{2}{\lambda_{\max}} \tag{6} \]

\(\lambda_{\max}\) は自己相関行列 \(\mathbf{R}\) の最大固有値です。実用的には:

\[ 0 < \mu < \frac{2}{\text{tr}(\mathbf{R})} \approx \frac{2}{M \cdot \sigma_x^2} \tag{7} \]

\(M\) はフィルタ次数、\(\sigma_x^2\) は入力信号のパワーです。式(6)の収束条件は重み誤差ベクトルの漸化式から厳密に証明でき、収束後に残る定常誤差(ミスアジャストメント)も定量化できますが、その導出は LMS/NLMSアルゴリズムの理論とPython実装 に譲ります。

NLMS(Normalized LMS)アルゴリズム

LMSの問題点は、ステップサイズが入力信号のパワーに依存することです。NLMSは入力ベクトルのノルムで正規化することでこの問題を解決します:

\[ \mathbf{w}(n+1) = \mathbf{w}(n) + \frac{\mu}{\|\mathbf{x}(n)\|^2 + \epsilon} \mathbf{x}(n) e(n) \tag{8} \]

\(\epsilon\) はゼロ除算防止の小さな正の定数です。NLMSでは \(0 < \mu < 2\) で収束が保証されます。式(8)は天下り的に見えますが、実は「事後誤差をちょうどゼロにする」という最小擾乱原理から導出できます。この導出と、NLMSのステップサイズが定常誤差にどう効くか(本記事末尾の実行検証でも扱います)の詳細は LMS/NLMSアルゴリズムの理論とPython実装 を参照してください。

RLS(Recursive Least Squares)アルゴリズム

RLSは、過去の全データに対する重み付き最小二乗誤差を最小化するアルゴリズムです。LMSより高速に収束しますが、計算コストが高くなります。

コスト関数

\[ J(\mathbf{w}, n) = \sum_{i=1}^{n} \lambda^{n-i} |e(i)|^2 \tag{9} \]

\(\lambda\) (\(0 < \lambda \le 1\) )は忘却係数で、古いデータの影響を指数的に減衰させます。

更新アルゴリズム

  1. ゲインベクトルの計算:
\[ \mathbf{k}(n) = \frac{\mathbf{P}(n-1)\mathbf{x}(n)}{\lambda + \mathbf{x}^T(n)\mathbf{P}(n-1)\mathbf{x}(n)} \tag{10} \]
  1. 誤差の計算:
\[ e(n) = d(n) - \mathbf{w}^T(n-1)\mathbf{x}(n) \tag{11} \]
  1. フィルタ係数の更新:
\[ \mathbf{w}(n) = \mathbf{w}(n-1) + \mathbf{k}(n) e(n) \tag{12} \]
  1. 逆相関行列の更新:
\[ \mathbf{P}(n) = \frac{1}{\lambda}\left[\mathbf{P}(n-1) - \mathbf{k}(n)\mathbf{x}^T(n)\mathbf{P}(n-1)\right] \tag{13} \]

初期値は \(\mathbf{P}(0) = \delta^{-1}\mathbf{I}\) (\(\delta\) は小さな正の定数)とします。式(10)〜(13)は行列反転補題(Sherman-Morrison公式)から導出され、\(\lambda=1\) のRLSはカルマンフィルタと数値的に完全に一致します。この導出と等価性の実証は RLS(逐次最小二乗法)アルゴリズムの理論とPython実装 で扱っています。

アルゴリズムの比較

特性LMSNLMSRLS
計算量(1ステップ)\(O(M)\)\(O(M)\)\(O(M^2)\)
メモリ\(O(M)\)\(O(M)\)\(O(M^2)\)
収束速度遅い中程度速い
定常誤差ステップサイズ依存ステップサイズ依存小さい
数値安定性高い高いやや低い
調整パラメータ\(\mu\)\(\mu\)\(\lambda\) , \(\delta\)
適用場面リアルタイム処理入力パワー変動が大きい高速収束が必要

Python実装

LMS / NLMS

import numpy as np

def lms_filter(x, d, M, mu):
    """LMSアルゴリズムによる適応フィルタ

    Parameters
    ----------
    x : array_like
        入力信号(参照信号)
    d : array_like
        目的信号
    M : int
        フィルタ次数
    mu : float
        ステップサイズ

    Returns
    -------
    y : ndarray
        フィルタ出力
    e : ndarray
        誤差信号
    w_history : ndarray
        フィルタ係数の履歴
    """
    N = len(x)
    w = np.zeros(M)
    y = np.zeros(N)
    e = np.zeros(N)
    w_history = np.zeros((N, M))

    for n in range(M, N):
        x_vec = x[n:n-M:-1] if n >= M else np.pad(x[:n+1][::-1], (0, M-n-1))
        y[n] = np.dot(w, x_vec)
        e[n] = d[n] - y[n]
        w = w + mu * e[n] * x_vec
        w_history[n] = w

    return y, e, w_history

def nlms_filter(x, d, M, mu=0.5, eps=1e-8):
    """NLMSアルゴリズムによる適応フィルタ"""
    N = len(x)
    w = np.zeros(M)
    y = np.zeros(N)
    e = np.zeros(N)

    for n in range(M, N):
        x_vec = x[n:n-M:-1] if n >= M else np.pad(x[:n+1][::-1], (0, M-n-1))
        y[n] = np.dot(w, x_vec)
        e[n] = d[n] - y[n]
        norm = np.dot(x_vec, x_vec) + eps
        w = w + (mu / norm) * e[n] * x_vec

    return y, e

RLS

def rls_filter(x, d, M, lam=0.99, delta=1.0):
    """RLSアルゴリズムによる適応フィルタ

    Parameters
    ----------
    x : array_like
        入力信号
    d : array_like
        目的信号
    M : int
        フィルタ次数
    lam : float
        忘却係数(0 < lam <= 1)
    delta : float
        逆相関行列の初期スケーリング

    Returns
    -------
    y : ndarray
        フィルタ出力
    e : ndarray
        誤差信号
    """
    N = len(x)
    w = np.zeros(M)
    P = np.eye(M) / delta
    y = np.zeros(N)
    e = np.zeros(N)

    for n in range(M, N):
        x_vec = x[n:n-M:-1] if n >= M else np.pad(x[:n+1][::-1], (0, M-n-1))
        y[n] = np.dot(w, x_vec)
        e[n] = d[n] - y[n]

        # ゲインベクトル
        Px = P @ x_vec
        k = Px / (lam + x_vec @ Px)

        # フィルタ係数更新
        w = w + k * e[n]

        # 逆相関行列更新
        P = (P - np.outer(k, x_vec @ P)) / lam

    return y, e

実践例:ノイズキャンセレーション

適応フィルタの代表的な応用として、ノイズキャンセレーションを実装し、実際に3アルゴリズムを動かしてMSE(平均二乗誤差)を数値で比較します。

import numpy as np

def lms_filter(x, d, M, mu):
    N = len(x)
    w = np.zeros(M)
    y = np.zeros(N)
    e = np.zeros(N)
    for n in range(M, N):
        x_vec = x[n:n-M:-1]
        y[n] = np.dot(w, x_vec)
        e[n] = d[n] - y[n]
        w = w + mu * e[n] * x_vec
    return y, e

def nlms_filter(x, d, M, mu=0.5, eps=1e-8):
    N = len(x)
    w = np.zeros(M)
    y = np.zeros(N)
    e = np.zeros(N)
    for n in range(M, N):
        x_vec = x[n:n-M:-1]
        y[n] = np.dot(w, x_vec)
        e[n] = d[n] - y[n]
        norm = np.dot(x_vec, x_vec) + eps
        w = w + (mu / norm) * e[n] * x_vec
    return y, e

def rls_filter(x, d, M, lam=0.99, delta=1.0):
    N = len(x)
    w = np.zeros(M)
    P = np.eye(M) / delta
    y = np.zeros(N)
    e = np.zeros(N)
    for n in range(M, N):
        x_vec = x[n:n-M:-1]
        y[n] = np.dot(w, x_vec)
        e[n] = d[n] - y[n]
        Px = P @ x_vec
        k = Px / (lam + x_vec @ Px)
        w = w + k * e[n]
        P = (P - np.outer(k, x_vec @ P)) / lam
    return y, e

np.random.seed(42)

# 信号生成
N = 1000
t = np.arange(N)
signal = np.sin(2 * np.pi * 0.01 * t)  # 目的信号
noise_ref = np.random.randn(N)  # 参照ノイズ

# 未知のシステムを通ったノイズ(FIRフィルタで模擬、真の次数は4)
h_true = np.array([0.5, -0.3, 0.2, -0.1])
noise_filtered = np.convolve(noise_ref, h_true, mode='full')[:N]

# 観測信号 = 目的信号 + フィルタされたノイズ
observed = signal + noise_filtered

# 各アルゴリズムでノイズ除去(フィルタ次数M=8)
M = 8
y_lms, e_lms = lms_filter(noise_ref, observed, M, mu=0.01)
y_nlms, e_nlms = nlms_filter(noise_ref, observed, M, mu=0.1)
y_rls, e_rls = rls_filter(noise_ref, observed, M, lam=0.99)

# 定常状態(後半500サンプル)における、真の目的信号との誤差
steady = slice(500, 1000)
mse_before = np.mean((observed[steady] - signal[steady]) ** 2)
mse_lms = np.mean((e_lms[steady] - signal[steady]) ** 2)
mse_nlms = np.mean((e_nlms[steady] - signal[steady]) ** 2)
mse_rls = np.mean((e_rls[steady] - signal[steady]) ** 2)

print(f"cancellation前 MSE: {mse_before:.5f}")
print(f"LMS  (mu=0.01)      MSE: {mse_lms:.6f}  ({10*np.log10(mse_before/mse_lms):.2f} dB改善)")
print(f"NLMS (mu~=0.1)      MSE: {mse_nlms:.6f}  ({10*np.log10(mse_before/mse_nlms):.2f} dB改善)")
print(f"RLS  (lambda=0.99)  MSE: {mse_rls:.6f}  ({10*np.log10(mse_before/mse_rls):.2f} dB改善)")

実行結果は次の通りでした。

cancellation前 MSE: 0.37745
LMS  (mu=0.01)      MSE: 0.014198  (14.25 dB改善)
NLMS (mu~=0.1)      MSE: 0.028662  (11.20 dB改善)
RLS  (lambda=0.99)  MSE: 0.015131  (13.97 dB改善)

ノイズキャンセレーション前は観測信号のMSEが \(0.377\) (雑音がほぼ目的信号と同レベルに埋もれている)のに対し、LMSは\(14.25\) dB、RLSは\(13.97\) dB改善し、MSEを\(1/25\) 程度まで低減できています。NLMSは\(\tilde\mu=0.1\) の設定では\(11.20\) dB改善にとどまり、LMS・RLSよりやや劣る結果です(この理由は後述の「エッジケースと実務上の注意点」で数値的に検証します)。波形で確認すると次のようになります。

観測信号(信号+ノイズ)とLMS/NLMS/RLSそれぞれの出力を目的信号と重ねた比較。RLSとLMSはノイズ除去後の波形が目的信号にほぼ重なるのに対し、NLMSはやや残留ノイズが多い

誤差信号 \(e(n)\) がノイズ除去後の信号に対応します。上から2〜4段目を見ると、LMSとRLSは目的信号の正弦波形にほぼ重なるまで残留ノイズが減少しているのに対し、NLMSはやや振幅の揺らぎが残っていることが視覚的にも確認できます。

収束曲線の可視化

各アルゴリズムの収束速度を学習曲線(目的信号からの誤差の移動平均)で比較します。

import numpy as np

def lms_filter(x, d, M, mu):
    N = len(x)
    w = np.zeros(M)
    e = np.zeros(N)
    for n in range(M, N):
        x_vec = x[n:n-M:-1]
        e[n] = d[n] - np.dot(w, x_vec)
        w = w + mu * e[n] * x_vec
    return e

def nlms_filter(x, d, M, mu=0.5, eps=1e-8):
    N = len(x)
    w = np.zeros(M)
    e = np.zeros(N)
    for n in range(M, N):
        x_vec = x[n:n-M:-1]
        e[n] = d[n] - np.dot(w, x_vec)
        norm = np.dot(x_vec, x_vec) + eps
        w = w + (mu / norm) * e[n] * x_vec
    return e

def rls_filter(x, d, M, lam=0.99, delta=1.0):
    N = len(x)
    w = np.zeros(M)
    P = np.eye(M) / delta
    e = np.zeros(N)
    for n in range(M, N):
        x_vec = x[n:n-M:-1]
        e[n] = d[n] - np.dot(w, x_vec)
        Px = P @ x_vec
        k = Px / (lam + x_vec @ Px)
        w = w + k * e[n]
        P = (P - np.outer(k, x_vec @ P)) / lam
    return e

np.random.seed(42)
N = 1000
t = np.arange(N)
signal = np.sin(2 * np.pi * 0.01 * t)
noise_ref = np.random.randn(N)
h_true = np.array([0.5, -0.3, 0.2, -0.1])
observed = signal + np.convolve(noise_ref, h_true, mode='full')[:N]

M = 8
e_lms = lms_filter(noise_ref, observed, M, mu=0.01)
e_nlms = nlms_filter(noise_ref, observed, M, mu=0.1)
e_rls = rls_filter(noise_ref, observed, M, lam=0.99)

# 学習曲線: 目的信号からの偏差二乗を移動平均で平滑化
window = 30
def smooth_dev(e):
    return np.convolve((e - signal) ** 2, np.ones(window) / window, mode='valid')

sm_lms, sm_nlms, sm_rls = smooth_dev(e_lms), smooth_dev(e_nlms), smooth_dev(e_rls)

# 収束到達点: 漸近誤差(末尾200点平均)の1.5倍を初めて下回るサンプル
def convergence_sample(sm, factor=1.5, tail=200):
    asymptote = np.mean(sm[-tail:])
    idx = np.where(sm < asymptote * factor)[0]
    return idx[0] + window, asymptote

for name, sm in [("LMS", sm_lms), ("NLMS", sm_nlms), ("RLS", sm_rls)]:
    n_conv, asym = convergence_sample(sm)
    print(f"{name:4s}: 収束到達 n={n_conv:4d}  (漸近誤差 {asym:.5f})")

実行結果は次の通りでした。

LMS : 収束到達 n= 165  (漸近誤差 0.02036)
NLMS: 収束到達 n= 153  (漸近誤差 0.02534)
RLS : 収束到達 n= 136  (漸近誤差 0.02359)

漸近誤差の1.5倍を初めて下回るサンプル数(収束速度の目安)はRLSが\(136\) サンプルで最速、次いでNLMSが\(153\) サンプル、LMSが\(165\) サンプルで最も遅いという結果でした。理論通りRLSが最速ですが、この設定(\(M=8\) 、白色雑音入力)ではLMSとの差はそれほど大きくありません。固有値拡散が大きい入力(強く自己相関した信号)ではこの差がさらに拡大することが LMS/NLMSアルゴリズムの理論とPython実装 の実行検証で示されています。

LMS・NLMS・RLSの学習曲線(目的信号からの二乗誤差、30サンプル移動平均、対数軸)。RLSが最も早く低いフロアに到達し、NLMSは他の2つよりやや高いフロアに留まる

エッジケースと実務上の注意点

ここまでの理論と実装だけでは見えてこない、実務でつまずきやすい3つの注意点を数値で確認します。

1. NLMSのステップサイズと定常誤差のトレードオフ

NLMSの収束条件は入力パワーに依存しない \(0 < \tilde\mu < 2\) ですが、「\(2\) 未満なら安全」という理解だけでは不十分です。定常状態でのミスアジャストメント(超過MSE比)は近似的に \(M \approx \tilde\mu / (2 - \tilde\mu)\) で与えられ、\(\tilde\mu\) を大きくするほど急激に悪化します。先ほどの実践例で \(\tilde\mu=0.1\) を選んだ根拠を、\(\tilde\mu\) を振って確認します。

import numpy as np

def nlms_filter(x, d, M, mu=0.5, eps=1e-8):
    N = len(x)
    w = np.zeros(M)
    e = np.zeros(N)
    for n in range(M, N):
        x_vec = x[n:n-M:-1]
        e[n] = d[n] - np.dot(w, x_vec)
        norm = np.dot(x_vec, x_vec) + eps
        w = w + (mu / norm) * e[n] * x_vec
    return e

np.random.seed(42)
N = 1000
t = np.arange(N)
signal = np.sin(2 * np.pi * 0.01 * t)
noise_ref = np.random.randn(N)
h_true = np.array([0.5, -0.3, 0.2, -0.1])
observed = signal + np.convolve(noise_ref, h_true, mode='full')[:N]

M = 8
steady = slice(500, 1000)
mse_before = np.mean((observed[steady] - signal[steady]) ** 2)

for mu_t in [0.5, 0.3, 0.1, 0.08, 0.05, 0.02]:
    e = nlms_filter(noise_ref, observed, M, mu=mu_t)
    mse = np.mean((e[steady] - signal[steady]) ** 2)
    pred_M = mu_t / (2 - mu_t)
    print(f"mu_tilde={mu_t:.2f}: MSE={mse:.5f}, 改善={10*np.log10(mse_before/mse):.2f} dB, "
          f"予測ミスアジャストメント~{pred_M:.3f}")

実行結果は次の通りでした。

mu_tilde=0.50: MSE=0.21244, 改善=2.50 dB, 予測ミスアジャストメント~0.333
mu_tilde=0.30: MSE=0.11465, 改善=5.17 dB, 予測ミスアジャストメント~0.176
mu_tilde=0.10: MSE=0.02866, 改善=11.20 dB, 予測ミスアジャストメント~0.053
mu_tilde=0.08: MSE=0.02136, 改善=12.47 dB, 予測ミスアジャストメント~0.042
mu_tilde=0.05: MSE=0.01223, 改善=14.89 dB, 予測ミスアジャストメント~0.026
mu_tilde=0.02: MSE=0.02052, 改善=12.65 dB, 予測ミスアジャストメント~0.010

安定条件の上限(\(\tilde\mu<2\) )の\(1/4\) に過ぎない \(\tilde\mu=0.5\) でも、改善量はわずか\(2.50\) dBしかありません。\(\tilde\mu\) を\(0.05\) まで下げると改善量は\(14.89\) dBまで向上し、LMS・RLSと同水準に達します。ただし \(\tilde\mu=0.02\) ではむしろ悪化しており(\(N=1000\) サンプルという有限区間では収束速度が追いつかず、定常誤差の低さより収束の遅さが支配的になるため)、実務では収束速度と定常誤差のトレードオフを両にらみでチューニングする必要があります。「安定条件を満たす」ことと「性能が良い」ことは別問題である点に注意してください。

2. フィルタ次数Mの選び方

未知システムの次数を過小評価すると係数が真値を表現しきれずバイアスが残り、過大評価すると不要な自由度が増えて定常誤差(ミスアジャストメント)が増加します。真の次数が\(4\) (h_true の長さ)の系に対し、LMSのフィルタ次数\(M\) を変えて確認します。

import numpy as np

def lms_filter(x, d, M, mu):
    N = len(x)
    w = np.zeros(M)
    e = np.zeros(N)
    for n in range(M, N):
        x_vec = x[n:n-M:-1]
        e[n] = d[n] - np.dot(w, x_vec)
        w = w + mu * e[n] * x_vec
    return e

np.random.seed(42)
N = 1000
t = np.arange(N)
signal = np.sin(2 * np.pi * 0.01 * t)
noise_ref = np.random.randn(N)
h_true = np.array([0.5, -0.3, 0.2, -0.1])  # 真の次数は4
observed = signal + np.convolve(noise_ref, h_true, mode='full')[:N]
steady = slice(500, 1000)
mse_before = np.mean((observed[steady] - signal[steady]) ** 2)

for M in [2, 4, 8, 16, 32]:
    mu = 0.01 if M <= 8 else 0.01 * 8 / M  # tr(R)の増加分だけmuを下げて公平に比較
    e = lms_filter(noise_ref, observed, M, mu=mu)
    mse = np.mean((e[steady] - signal[steady]) ** 2)
    print(f"M={M:>3d} (mu={mu:.4f}): MSE={mse:.5f}  ({10*np.log10(mse_before/mse):.2f} dB改善)")

実行結果は次の通りでした。

M=  2 (mu=0.0100): MSE=0.05377  (8.46 dB改善)
M=  4 (mu=0.0100): MSE=0.00891  (16.27 dB改善)
M=  8 (mu=0.0100): MSE=0.01420  (14.25 dB改善)
M= 16 (mu=0.0050): MSE=0.01166  (15.10 dB改善)
M= 32 (mu=0.0025): MSE=0.04388  (9.35 dB改善)

真の次数と一致する \(M=4\) が最良(\(16.27\) dB改善)で、過小な \(M=2\) は未知システムを表現しきれず\(8.46\) dBまで悪化します。\(M\) を\(8\) 、\(16\) と増やすと、\(\mu\) を \(\mathrm{tr}(\mathbf{R}) \approx M\sigma_x^2\) の増加に合わせて縮小しているにもかかわらず性能はやや低下し、\(M=32\) では\(9.35\) dBまで悪化しました。これは、不要な自由度が増えるほど瞬時勾配のばらつき(ミスアジャストメント)を抑えきれなくなるためです。実務では次数を大きめに取れば安全というわけではなく、対象システムのインパルス応答長(エコーパスの残響時間など)を事前に見積もり、それに見合った次数を選ぶことが重要です。

3. 参照信号と目的信号の相関(信号のキャンセル事故)

ノイズキャンセレーションが正しく機能する前提は「参照信号 \(x(n)\) がノイズ成分とのみ相関し、目的信号とは無相関である」ことです。この前提が崩れる、すなわち参照信号に目的信号が漏れ込む(マイクの回り込みなど)と、適応フィルタは目的信号自体を「除去すべきノイズ」と誤認し、意図せず目的信号を減衰させてしまいます。

import numpy as np

def lms_filter(x, d, M, mu):
    N = len(x)
    w = np.zeros(M)
    e = np.zeros(N)
    for n in range(M, N):
        x_vec = x[n:n-M:-1]
        e[n] = d[n] - np.dot(w, x_vec)
        w = w + mu * e[n] * x_vec
    return e

np.random.seed(42)
N = 1000
t = np.arange(N)
signal = np.sin(2 * np.pi * 0.01 * t)
noise_ref = np.random.randn(N)
h_true = np.array([0.5, -0.3, 0.2, -0.1])
steady = slice(500, 1000)
M = 8

for leak in [0.0, 0.1, 0.3, 0.5, 1.0, 2.0]:
    noise_ref_leak = noise_ref + leak * signal  # 参照信号への目的信号の漏れ込み
    observed_leak = signal + np.convolve(noise_ref_leak, h_true, mode='full')[:N]
    e = lms_filter(noise_ref_leak, observed_leak, M, mu=0.01)
    amp_ratio = np.std(e[steady]) / np.std(signal[steady])
    print(f"leak={leak:.2f}: 出力振幅/目的信号振幅={amp_ratio:.4f}  "
          f"({20*np.log10(amp_ratio):+.2f} dB)")

実行結果は次の通りでした。

leak=0.00: 出力振幅/目的信号振幅=1.0291  (+0.25 dB)
leak=0.10: 出力振幅/目的信号振幅=1.0333  (+0.28 dB)
leak=0.30: 出力振幅/目的信号振幅=0.9321  (-0.61 dB)
leak=0.50: 出力振幅/目的信号振幅=0.7755  (-2.21 dB)
leak=1.00: 出力振幅/目的信号振幅=0.4900  (-6.20 dB)
leak=2.00: 出力振幅/目的信号振幅=0.2772  (-11.14 dB)

参照信号への漏れ込みが \(0\) (理想条件)では出力振幅は目的信号とほぼ同じ(\(+0.25\) dB)ですが、漏れ込み係数を\(1.0\) まで増やすと出力振幅は元の\(49\) %まで、\(2.0\) では\(28\) %まで減衰しました。これは適応フィルタが「除去可能な相関成分」として目的信号の一部まで巻き込んで消してしまうためで、ハウリングとは逆の意味で実務上深刻な不具合です(ANCヘッドホンやエコーキャンセラでは、参照マイクと目的音源の物理的な分離設計、あるいはダブルトーク検出による適応停止でこの問題を回避します)。

最新研究動向:ハイブリッドAECにおける適応フィルタの位置づけ

伝統的な適応フィルタ(NLMSやKalmanフィルタ系の手法)は、2023年以降も音響エコーキャンセレーション(AEC)研究の主要な土台であり続けています。Microsoft Researchが主催する ICASSP 2023 Acoustic Echo Cancellation Challenge(Cutler et al., 2023, arXiv:2309.12553)は、10,000台以上の実デバイス・話者による実録音データと合成データの両方を公開し、パーソナライズドAECのトラック新設・レイテンシ20msへの短縮・フルバンドAECMOS評価指標の導入など、実運用に近い条件でAEC手法を競わせた第4回チャレンジです。この分野の報告では、伝統的な信号処理ベースの適応フィルタで線形エコー成分の大部分を先に除去し、その残差信号に対して2段目のニューラルネットワークで非線形歪みや残留エコーを段階的に抑圧する、ハイブリッド構成(hybrid signal processing + deep echo cancellation)を採用する手法が有力なアプローチの一つとして報告されています。本記事で扱ったNLMS/RLSのような軽量な線形適応フィルタは、この種のハイブリッド構成における第1段(計算コストが低く収束条件が明示的な線形成分の除去)として、今なお実用上の価値を持ち続けています。具体的な手法や性能数値は年ごとに更新されるため、最新の一次情報(チャレンジの公式論文・リーダーボード)を参照することをお勧めします。

おすすめ書籍

はじめて学ぶディジタル・フィルタと高速フーリエ変換(三上直樹、CQ出版)

ディジタルフィルタと FFT の原理を基礎から丁寧に解説した定番の入門書です。本記事で扱った処理の理論的背景を体系的に学べます。

※ 上記は Amazon アソシエイトのリンクです。

関連記事

参考文献

  • Haykin, S. (2014). Adaptive Filter Theory (5th ed.). Pearson.
  • Widrow, B., & Stearns, S. D. (1985). Adaptive Signal Processing. Prentice-Hall.
  • Sayed, A. H. (2008). Adaptive Filters. Wiley-IEEE Press.
  • Cutler, R., Saabas, A., Parnamaa, T., Purin, M., Indenbom, E., Ristea, N.-C., Guzvin, J., Gamper, H., Braun, S., & Aichner, R. (2023). ICASSP 2023 Acoustic Echo Cancellation Challenge. arXiv:2309.12553. https://arxiv.org/abs/2309.12553