適応フィルタとは
適応フィルタ(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\) )は忘却係数で、古いデータの影響を指数的に減衰させます。
更新アルゴリズム
- ゲインベクトルの計算:
- 誤差の計算:
- フィルタ係数の更新:
- 逆相関行列の更新:
初期値は \(\mathbf{P}(0) = \delta^{-1}\mathbf{I}\) (\(\delta\) は小さな正の定数)とします。式(10)〜(13)は行列反転補題(Sherman-Morrison公式)から導出され、\(\lambda=1\) のRLSはカルマンフィルタと数値的に完全に一致します。この導出と等価性の実証は RLS(逐次最小二乗法)アルゴリズムの理論とPython実装 で扱っています。
アルゴリズムの比較
| 特性 | LMS | NLMS | RLS |
|---|---|---|---|
| 計算量(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よりやや劣る結果です(この理由は後述の「エッジケースと実務上の注意点」で数値的に検証します)。波形で確認すると次のようになります。

誤差信号 \(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実装 の実行検証で示されています。

エッジケースと実務上の注意点
ここまでの理論と実装だけでは見えてこない、実務でつまずきやすい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段(計算コストが低く収束条件が明示的な線形成分の除去)として、今なお実用上の価値を持ち続けています。具体的な手法や性能数値は年ごとに更新されるため、最新の一次情報(チャレンジの公式論文・リーダーボード)を参照することをお勧めします。
おすすめ書籍
ディジタルフィルタと FFT の原理を基礎から丁寧に解説した定番の入門書です。本記事で扱った処理の理論的背景を体系的に学べます。
※ 上記は Amazon アソシエイトのリンクです。
関連記事
- RLS(逐次最小二乗法)アルゴリズムの理論とPython実装 - 本記事で触れたRLSを行列反転補題から導出し、カルマンフィルタとの数値的等価性を実証した発展編です。
- 適応フィルタの基礎理論とデジタル信号処理への応用 - FIR/IIRフィルタの基礎からWiener-Hopf方程式・最急降下法までを導出した、本記事の理論的な出発点です。
- 指数移動平均(EMA)フィルタの周波数特性 - 固定パラメータのフィルタの代表例。適応フィルタとの対比で理解が深まります。
- ウィーナーフィルタの理論とPython実装 - 適応フィルタ(特にLMS)の理論的基盤となる最適フィルタです。
- カルマンフィルタの理論とPython実装 - RLSと密接な関係を持つ逐次推定アルゴリズム。状態空間モデルにおける適応的推定です。
- FIRフィルタとIIRフィルタの比較 - 適応フィルタの多くはFIR構造を採用しています。FIR/IIRの特性の違いを解説しています。
- 信号処理におけるフィルタリング手法の基礎 - カルマンフィルタ、EKF、UKF、粒子フィルタの概要を体系的に解説しています。
- SGD・Adamの理論と比較 - LMSと同じ確率的勾配降下法の枠組みで、深層学習の最適化アルゴリズムを解説しています。
- 移動平均フィルタの種類と比較 - 適応フィルタと対比される固定フィルタの代表例です。
- ローパスフィルタの設計と比較 - 各種ローパスフィルタの周波数応答を比較しています。
- Unscented Kalman Filter(UKF)の理論とPython実装 - カルマンフィルタの非線形拡張で、RLSの発展形として理解できます。
- LMS/NLMSアルゴリズムの理論とPython実装 - 本記事のLMS/NLMSの数学的導出(Wiener解への収束、定常誤差、ステップサイズ感度)に焦点を当てた姉妹記事です。
- バンドパスフィルタの設計とPython実装 - 固定設計のバンドパスを、信号統計のドリフトに合わせて適応化する出発点として有用です。
- デジタルフィルタ設計指針ハブ:選定の3軸と特性比較 — 本記事の適応フィルタを固定設計フィルタと比較しながら選定する際の指針として活用できます。
参考文献
- 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