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

適応フィルタの基本概念から、LMS(最小平均二乗)アルゴリズム、NLMS、RLS(再帰的最小二乗)アルゴリズムの数学的導出とPython実装を解説します。ノイズキャンセレーションの実践例も紹介。

適応フィルタとは

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

主な応用分野

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

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

適応フィルタの理論的基盤はウィーナーフィルタです。ウィーナーフィルタは、入力信号 \(\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}\) が未知であるか、時間変化するため、適応アルゴリズムでウィーナー解に逐次的に近づけます。

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\) は入力信号のパワーです。

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\) で収束が保証されます。

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\) は小さな正の定数)とします。

アルゴリズムの比較

特性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

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

適応フィルタの代表的な応用として、ノイズキャンセレーションを実装します。

import matplotlib.pyplot as plt

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フィルタで模擬)
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  # フィルタ次数
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.5)
y_rls, e_rls = rls_filter(noise_ref, observed, M, lam=0.99)

# プロット
fig, axes = plt.subplots(4, 1, figsize=(12, 10), sharex=True)

axes[0].plot(t, signal, 'g', label='Original signal')
axes[0].plot(t, observed, 'r', alpha=0.5, label='Observed (signal + noise)')
axes[0].set_title('Input Signals')
axes[0].legend()

axes[1].plot(t, signal, 'g', alpha=0.5, label='Original')
axes[1].plot(t, e_lms, 'b', label='LMS output')
axes[1].set_title('LMS Noise Cancellation')
axes[1].legend()

axes[2].plot(t, signal, 'g', alpha=0.5, label='Original')
axes[2].plot(t, e_nlms, 'b', label='NLMS output')
axes[2].set_title('NLMS Noise Cancellation')
axes[2].legend()

axes[3].plot(t, signal, 'g', alpha=0.5, label='Original')
axes[3].plot(t, e_rls, 'b', label='RLS output')
axes[3].set_title('RLS Noise Cancellation')
axes[3].legend()

plt.tight_layout()
plt.show()

ノイズキャンセレーションでは、誤差信号 \(e(n)\) がノイズ除去後の信号に対応します。RLSはLMSより高速に収束し、初期段階からノイズ除去性能が高いことが確認できます。

収束曲線の可視化

各アルゴリズムの収束速度を学習曲線(MSEの推移)で比較します。

# 学習曲線(MSEの移動平均)
window = 50
mse_lms = np.convolve(e_lms**2, np.ones(window)/window, mode='valid')
mse_nlms = np.convolve(e_nlms**2, np.ones(window)/window, mode='valid')
mse_rls = np.convolve(e_rls**2, np.ones(window)/window, mode='valid')

plt.figure(figsize=(10, 5))
plt.semilogy(mse_lms, label='LMS')
plt.semilogy(mse_nlms, label='NLMS')
plt.semilogy(mse_rls, label='RLS')
plt.xlabel('Sample')
plt.ylabel('MSE')
plt.title('Learning Curves')
plt.legend()
plt.grid(True)
plt.tight_layout()
plt.show()

関連記事

参考文献

  • 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.