들어가며
지난 18편까지 우리는 FIR 필터와 IIR 필터의 원리를 배우고, 원하는 주파수 특성(LPF, HPF 등)을 수학적으로 고정하여 설계하는 방법을 익혔습니다. 하지만 우리가 살아가는 현실 세계의 신호 환경은 결코 정적이지 않습니다.
화상 회의 중 상대방의 목소리가 내 스피커로 나와서 다시 내 마이크로 들어가는 **에코(echo)**를 생각해 봅시다. 회의실의 크기, 화자의 위치, 가구 배치, 심지어 창문을 여닫는 동작 하나만으로도 에코 경로는 시시각각 바뀝니다. 또 어떤 노이즈는 차량 엔진처럼 RPM에 따라 주파수가 변하고, 어떤 진동은 기계의 마모 상태에 따라 특성이 달라집니다. 이런 환경에서 한 번 설계해 둔 고정 필터는 며칠만 지나도 무용지물이 되기 쉽습니다.
지금까지 배운 '계수가 고정된 필터'로는 이렇게 매 순간 변하는 노이즈나 에코를 쫓아갈 수 없습니다. 환경이 변하면 필터 스스로 자신의 특성을 바꾸며 적응해야 합니다.
이번 글에서는 적응 필터의 일반 구조부터 시작해서 LMS 알고리즘의 유도와 수렴 조건을 정리한 뒤, 실제 동작하는 에코 캔슬러를 Python으로 구현해 보겠습니다. 스텝 사이즈(μ)에 따라 수렴 속도와 정상 상태 오차가 어떻게 트레이드오프되는지, 그리고 LMS가 왜 "기계 학습의 시조" 중 하나로 불리는지도 함께 살펴보겠습니다.
1. 적응 필터: 환경에 적응하는 필터의 개념
1.1 고정 필터와의 차이
전통적인 FIR 필터의 출력은 다음과 같이 표현됩니다.
여기서 계수 $w_k$는 설계 단계에서 한 번 결정되면 변하지 않습니다. 반면 적응 필터에서는 계수가 시간의 함수가 됩니다.
즉, 매 시각 $n$마다 계수 벡터 $\mathbf{w}[n]$이 갱신되는 구조입니다. 그렇다면 "어떤 기준으로" 계수를 갱신해야 할까요? 이를 위해 참조 신호(desired signal) $d[n]$을 도입합니다.
1.2 일반 구조

오차 신호는 $e[n] = d[n] - y[n]$으로 정의하며, 알고리즘의 목표는 이 오차의 평균 제곱을 최소화하는 것입니다.
이 비용 함수를 최소로 만드는 최적 계수를 찾기 위한 가장 직관적인 접근이 바로 LMS입니다.
2. LMS 알고리즘: 가장 단순하고 강력한 학습 규칙
2.1 경사 하강법에서 출발
적응 필터의 핵심은 "어떻게 계수를 업데이트할 것인가?"입니다. 여기서 등장하는 것이 바로 딥러닝의 경사하강법(Gradient Descent)의 모태가 되는 LMS 알고리즘입니다.
LMS 알고리즘의 목표는 오차 신호의 제곱인 $e^2[n]$을 최소화하는 필터 계수 벡터 $\mathbf{w}$를 찾는 것입니다. 수식의 전개 과정은 복잡하지만, 놀랍게도 최종적으로 도출되는 업데이트 공식은 매우 직관적이고 단순합니다.
앞절에서 언급한 비용 함수 $J(\mathbf{w})$의 기울기를 계산해 보면 다음과 같습니다.
경사 하강법(steepest descent)을 적용하면 계수 갱신식은 아래 형태가 됩니다.
문제는 우변의 기댓값 $E[\cdot]$을 실시간으로 계산할 수 없다는 점입니다. Widrow와 Hoff의 결정적 통찰은 기댓값을 순간값으로 근사하자는 것이었습니다.
이 한 줄이 LMS 알고리즘의 전부입니다. 곱셈 한 번과 덧셈 한 번으로 계수를 갱신하기 때문에 연산량이 매우 작고, 따라서 임베디드 환경에서도 부담 없이 실시간 동작이 가능합니다.
2.2 스텝 사이즈($\mu$)의 의미와 수렴 조건
스텝 사이즈 $\mu$는 학습률입니다. 너무 작으면 수렴이 느리고, 너무 크면 발산합니다. 입력 신호의 분산을 $\sigma_x^2$, 필터 길이를 $L$이라 할 때 안정적인 수렴을 위한 일반적 상한은 다음과 같습니다.
이 상한을 넘기면 계수가 발산하고, 너무 보수적으로 작게 잡으면 환경 변화를 따라가지 못합니다. 즉, 학습률 $\mu$의 설정이 매우 중요합니다.
- $\mu$가 크면: 환경 변화에 빠르게 적응하지만, 오차가 튀거나 시스템이 발산(불안정)할 위험이 큽니다.
- $\mu$가 작으면: 시스템이 안정적으로 수렴하지만, 환경이 변했을 때 적응하는 속도가 너무 느려집니다.
실무에서는 입력 전력에 정규화한 NLMS(Normalized LMS)를 쓰는 경우가 많지만, 이번 글에서는 가장 기본형인 LMS에 집중하겠습니다.
3. 에코 캔슬링 시스템 모델
적응 필터의 대표적 응용은 시스템 식별(system identification) 입니다. 에코 캔슬링은 그중 가장 교과서적인 예시입니다.

에코 캔슬링(AEC) 시스템을 기준으로 적응 필터의 주요 4개의 핵심 신호를 아래 표에 정리했습니다.
| 기호 | 설명 (에코 캔슬링 관점) |
|---|---|
| 입력 신호 x[n] | 스피커로 출력되는 원격지 화자의 목소리 (Far-end signal). 에코를 발생시키는 원인 신호이며, 공간을 거쳐 마이크에 에코로 되돌아옵니다. |
| 목표 신호 d[n] | 마이크로 입력되는 신호. 내 목소리인 Near-end 신호 s[n]과 공간을 울리고 들어온 에코(Echo)가 혼합된 상태입니다. 즉, d[n] = s[n] + echo 로 표현할 수 있습니다. |
| 출력 신호 y[n] | 적응 필터가 x[n]을 가공하여 추정한 에코(Estimated Echo)입니다. y[n]이 실제 에코에 가까워질수록 d[n]에서 에코를 정확히 제거하고 s[n]만 남길 수 있습니다. |
| 오차 신호 e[n] | 마이크 입력 d[n]에서 추정 에코 y[n]을 뺀 신호입니다. 즉, e[n] = d[n] − y[n] = s[n] + echo − y[n]. 적응 필터가 수렴하면 echo ≈ y[n]이 되어 e[n] ≈ s[n], 즉 순수한 내 목소리만 남게 됩니다. |
여기서 핵심은 다음과 같습니다.
- 알 수 있는 것: 원단 신호 $x[n]$ (우리 시스템이 보내는 신호)과 마이크 입력 $d[n]$
- 알 수 없는 것: 실제 에코 경로 $\mathbf{h}$ (방의 음향 특성)
- 하고 싶은 것: 마이크 입력에서 에코 성분만 빼고 근단 음성 $s[n]$ 만 상대방에게 전송
LMS 필터는 원단 신호 $x[n]$ 을 입력으로 받아, 그 신호가 어떻게 변형되어 에코로 돌아오는지를 학습합니다. 학습이 충분히 진행되면 $\mathbf{w}[n] \approx \mathbf{h}$ 가 되고, 필터 출력 $y[n]$ 은 실제 에코의 좋은 추정치가 됩니다. 따라서 오차 $e[n] = d[n] - y[n] \approx s[n]$ 이 됩니다.
4. Python 실습: LMS 기반 에코 캔슬링 구현
이제 이론을 바탕으로 Python을 활용하여 가상의 통화 환경을 시뮬레이션하고 에코 캔슬러(AEC)를 구현해 보겠습니다.
4.1 Double-talk 문제가 고려되지 않은 시나리오
이 시나리오에서는 사실 처음부터 끝까지 double-talk이 일어나고 있지만, 두 신호가 통계적으로 무상관(white noise vs 50Hz 정현파)이라 LMS의 평균화 과정에서 자연스럽게 분리됩니다. 그러나 실제 통화에서는 두 화자의 음성이 비슷한 스펙트럼을 가지므로 이런 우연에 기댈 수 없습니다. 다음 시나리오에서 그 문제를 다루겠습니다. 시나리오 설정은 다음과 같습니다.
- 상대방의 목소리(Far-end)는 백색 잡음으로 가정합니다.
- 내 목소리(Near-end)는 명확히 구분되도록 50 Hz의 정현파로 가정합니다.
- 방의 울림(Room Impulse Response)은 지수적으로 감소하는 임의의 필터로 모델링합니다.
시뮬레이션 환경 구축 및 LMS 구현
import numpy as np
import matplotlib.pyplot as plt
# 한글 폰트 및 마이너스 기호 깨짐 방지
plt.rcParams['font.family'] = 'Malgun Gothic'
plt.rcParams['axes.unicode_minus'] = False
np.random.seed(42)
# 1. 시뮬레이션 파라미터 설정
fs = 8000 # 샘플링 주파수
N = 8000 # 총 샘플 수 (1초 분량)
t = np.arange(N) / fs
# 2. 신호 생성
# x[n]: 원격지 화자 신호 (Far-end, 백색 잡음)
x = np.random.randn(N)
# 실제 공간의 임펄스 응답 (Room Impulse Response) 생성
# 지수적으로 감소하는 에코 모델링 (길이 64)
true_rir_length = 64
true_rir = np.random.randn(true_rir_length) * np.exp(-np.arange(true_rir_length) / 10)
# 에코 생성 (Far-end 신호가 공간을 거침 = 컨볼루션)
echo = np.convolve(x, true_rir, mode='full')[:N]
# v[n]: 근거리 화자 신호 (Near-end, 순수한 내 목소리를 50Hz 사인파로 가정)
v = 0.5 * np.sin(2 * np.pi * 50 * t)
# d[n]: 마이크 입력 신호 = 에코 + 내 목소리
d = echo + v
# 3. LMS 적응 필터 구현
L = 64 # 적응 필터의 탭 수 (공간 임펄스 응답 길이에 맞춤)
mu = 0.01 # 학습률 (Step size)
w = np.zeros(L) # 필터 계수 초기화 (0으로 시작)
y = np.zeros(N) # 추정된 에코 신호
e = np.zeros(N) # 오차 신호 (우리가 얻고자 하는 에코가 제거된 신호)
# 컨볼루션 연산을 위해 입력 신호 x의 앞부분에 0 패딩
x_pad = np.pad(x, (L-1, 0), 'constant')
# 실시간 LMS 업데이트 루프
for n in range(N):
# 최근 L개의 입력 신호 추출 (최신 샘플이 앞에 오도록 역순 배치)
x_vec = x_pad[n : n+L][::-1]
# 추정 에코 계산 (필터링)
y[n] = np.dot(w, x_vec)
# 오차 계산 (마이크 신호 - 추정 에코)
e[n] = d[n] - y[n]
# LMS 필터 계수 업데이트
w = w + mu * e[n] * x_vec
위의 루프를 보면 e[n]을 계산한 후, 즉시 다음 연산을 위해 필터 계수 w를 업데이트하는 것을 볼 수 있습니다. 이것이 실시간 적응 필터의 기본 구조입니다.
결과 시각화 및 검증
적응 필터가 에코를 얼마나 잘 제거했는지, 그리고 오차가 어떻게 수렴해 가는지 시각화해 보겠습니다.
# 4. 결과 시각화
fig, axes = plt.subplots(3, 1, figsize=(12, 10))
fig.suptitle('LMS 알고리즘을 이용한 에코 캔슬링(AEC) 시뮬레이션', fontsize=14)
# [1] 마이크로 들어온 원래 신호 (에코 + 내 목소리)
axes[0].plot(t, d, color='tomato', alpha=0.8)
axes[0].set_title('마이크 입력 신호 d[n] (에코에 묻힌 내 목소리)')
axes[0].set_ylabel('진폭')
axes[0].grid(True, alpha=0.3)
axes[0].set_xlim(0, 1.0)
# [2] 필터링 후 신호 vs 실제 내 목소리
axes[1].plot(t, v, color='gray', linestyle='--', label='순수 내 목소리 (목표)', linewidth=2)
axes[1].plot(t, e, color='steelblue', alpha=0.7, label='에코 캔슬링 결과 e[n]')
axes[1].set_title('에코가 제거된 신호 e[n] (학습이 진행되며 내 목소리 복원)')
axes[1].set_ylabel('진폭')
axes[1].legend(loc='upper right')
axes[1].grid(True, alpha=0.3)
axes[1].set_xlim(0, 1.0)
# [3] 학습 오차 제곱 곡선 (Learning Curve)
# 학습 과정을 잘 보여주기 위해 100샘플 단위로 이동 평균 적용
e_squared = e**2
window = np.ones(100) / 100
e_smooth = np.convolve(e_squared, window, mode='valid')
axes[2].plot(np.arange(len(e_smooth))/fs, 10 * np.log10(e_smooth + 1e-10), color='purple')
axes[2].set_title('학습 곡선 (Learning Curve) - 오차 전력의 감소')
axes[2].set_xlabel('시간 (초)')
axes[2].set_ylabel('Mean Square Error (dB)')
axes[2].grid(True, alpha=0.3)
axes[2].set_xlim(0, 1.0)
plt.tight_layout()
plt.savefig('Adaptive_Echo_Cancellation_Simple.png', dpi=150, bbox_inches='tight')
plt.show()

- 첫 번째 그래프: 에코(백색 잡음 형태)의 진폭이 너무 커서, 정작 전달해야 할 50 Hz의 내 목소리는 흔적조차 보이지 않습니다.
- 두 번째 그래프: 0초에서 0.2초 사이에는 필터가 아직 주 환경(임펄스 응답)을 학습하는 중이라 노이즈가 남아있습니다. 하지만 0.2초가 넘어가면서 에코가 깔끔하게 지워지고 회색 점선(순수한 내 목소리)과 거의 완벽하게 일치하는 파란색 선이 나타납니다.
- 세 번째 그래프 (Learning Curve): 시작 직후 오차 전력이 급격히 감소하여 약 -10 dB 부근에 수렴하는 모습을 볼 수 있습니다. "어? 왜 오차가 0($-\infty\text{ dB}$)으로 떨어지지 않지?"라고 생각하실 수 있습니다. 오차가 -10 dB 부근에 머무는 이유는, 필터가 에코를 완벽히 걷어낸 후 남은 '순수한 내 목소리(50Hz 사인파)'의 에너지가 바로 -9 dB 수준이기 때문입니다. 즉, 이 그래프는 시스템이 에코만 정확하게 발라내어 제거하고, 우리가 지키고자 했던 내 목소리는 온전히 보존했음을 증명하는 아주 훌륭한 결과입니다.
4.2 Double-talk 문제가 고려된 시나리오
원단 신호와 근단 음성이 동시에 존재하면 근단 음성이 계수 갱신을 교란시켜 필터가 잘못 학습할 수 있습니다. 실제 에코 캔슬러는 이중 통화 검출기(Double-Talk Detector, DTD)를 두어 그 구간에서는 계수 갱신을 일시 정지(freeze)합니다. 이번 구현에서는 단순화한 DTD 동작을 함께 보여드리겠습니다.
시나리오 설정은 다음과 같습니다.
- 처음에는 내 목소리는 발생하지 않고, 1.5초 이후에 발생하며 이중 통화 검출기가 이를 정확히 검출하는 것으로 가정합니다.
- 상대방의 목소리(Far-end)는 300 Hz와 800 Hz의 정현파 조합과 가우시안 잡음이 합쳐진 것으로 가정합니다.
- 내 목소리(Near-end)는 1.5초 이후에 나타나며, 500 Hz의 정현파로 가정합니다.
- 방의 울림(Room Impulse Response)은 총 길이 64탭 중 5개 위치(인덱스 0, 5, 15, 30, 50)에 주요 반사 성분이 있는 필터로 가정합니다. 직접파(인덱스 0)와 후기 반사파(인덱스 50)까지의 거리가 약 6.25 ms (50 / 8000 Hz)에 해당합니다.
시뮬레이션 환경 구축 및 LMS 구현
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.ticker import FuncFormatter
# 한글 폰트 및 마이너스 기호 설정
plt.rcParams['font.family'] = 'Malgun Gothic'
plt.rcParams['axes.unicode_minus'] = False
neg_fmt = FuncFormatter(lambda x, _: f"${x:.0f}$" if x >= 0 else f"$-{abs(x):.0f}$")
np.random.seed(42)
# 시뮬레이션 파라미터
fs = 8000 # 샘플링 주파수 (Hz)
duration = 3.0 # 길이 (s)
N = int(fs * duration)
t = np.arange(N) / fs
# 원단(far-end) 신호: 음성을 모사한 다중 톤 + 가우시안 잡음
far_end = (0.5 * np.sin(2*np.pi*300*t)
+ 0.3 * np.sin(2*np.pi*800*t)
+ 0.2 * np.random.randn(N))
# 실제 에코 경로(room impulse response): 5개의 주요 탭
M = 64
true_h = np.zeros(M)
true_h[0] = 0.50 # 직접 반사
true_h[5] = 0.30
true_h[15] = -0.20
true_h[30] = 0.15
true_h[50] = -0.10 # 후기 반사
# 에코 신호 생성
echo = np.convolve(far_end, true_h)[:N]
# 근단(near-end) 음성: 1.5초 이후에만 등장 (DTD 검증용)
near_end = np.zeros(N)
mask = t > 1.5
near_end[mask] = 0.3 * np.sin(2*np.pi*500*t[mask])
# 마이크 입력 d[n] = 에코 + 근단 음성
mic = echo + near_end
##################################################################
# LMS 필터 파라미터
L = 64 # 적응 필터 길이 (실제 에코 경로와 동일)
mu = 0.01 # 스텝 사이즈
w = np.zeros(L) # 계수 초기화
y = np.zeros(N) # 필터 출력 (에코 추정치)
e = np.zeros(N) # 오차 신호 (에코 제거 후)
for n in range(L, N):
# 1) 입력 벡터 구성 (가장 최근 샘플이 맨 앞)
x_vec = far_end[n-L+1:n+1][::-1]
# 2) 필터 출력 (현재 추정 에코)
y[n] = w @ x_vec
# 3) 오차 계산
e[n] = mic[n] - y[n]
# 4) 계수 갱신 (단순 DTD: 근단 음성 구간에서는 동결)
if t[n] <= 1.5:
w = w + mu * e[n] * x_vec
여기서도 핵심은 단 한 줄의 갱신식 "w = w + mu * e[n] * x_vec" 입니다. 또한 이전 코드와 달리 "y[n] = w @ x_vec"을 사용했습니다. "@"는 Python 3.5부터 도입된 행렬 곱셈 연산자로 np.dot(w, x_vec)와 동일하게 두 벡터의 내적(inner product) 을 계산합니다.
결과 시각화 및 검증
def db(x):
return 10 * np.log10(np.mean(x**2) + 1e-12)
# ERLE(Echo Return Loss Enhancement) 계산
idx_start = (t > 0.0) & (t < 0.1) # 미수렴 초기 구간
idx_converged = (t > 1.0) & (t < 1.5) # 수렴 후 구간
erle_start = db(mic[idx_start]) - db(e[idx_start])
erle_conv = db(mic[idx_converged]) - db(e[idx_converged])
# 계수 추정 정확도 (정규화 오차)
coef_err_db = 10 * np.log10(np.sum((w - true_h)**2) / np.sum(true_h**2))
print(f"ERLE (초기 0~0.1s) : {erle_start:.2f} dB")
print(f"ERLE (수렴 1.0~1.5s) : {erle_conv:.2f} dB")
print(f"계수 추정 정규화 오차 : {coef_err_db:.2f} dB")
여기서 ERLE(Echo Return Loss Enhancement)는 마이크 입력 전력 대비 잔여 오차 전력의 비율을 dB로 나타낸 값으로, 적응 필터가 에코를 얼마나 억제했는지를 정량화하는 표준 지표입니다.
ERLE (초기 0~0.1s) : 10.19 dB
ERLE (수렴 1.0~1.5s) : 41.86 dB
계수 추정 정규화 오차 : -38.62 dB
학습 초기 약 10 dB에 불과하던 억제량이 1초가 지난 시점에서는 약 42 dB까지 향상되었습니다. 통신 품질 기준으로 30 dB 이상의 ERLE면 사람 귀로는 거의 들리지 않는 수준입니다.
fig, axes = plt.subplots(3, 1, figsize=(12, 9))
# (a) 마이크 입력 vs 에코 제거 후 신호
axes[0].plot(t, mic, color='tomato', alpha=0.7, label='마이크 입력 d[n]')
axes[0].plot(t, e, color='steelblue', alpha=0.9, label='에코 제거 후 e[n]')
axes[0].axvline(1.5, color='gray', linestyle='--', alpha=0.5, label='근단 음성 시작')
axes[0].set_title('(a) 시간 영역 비교')
axes[0].set_xlabel('시간 (s)'); axes[0].set_ylabel('진폭')
axes[0].legend(loc='upper right'); axes[0].grid(alpha=0.3)
# (b) 학습 곡선 (MSE의 이동 평균)
win = 200
mse_curve = np.convolve(e**2, np.ones(win)/win, mode='valid')
mse_db = 10 * np.log10(mse_curve + 1e-12)
axes[1].plot(t[:len(mse_db)], mse_db, color='darkgreen')
axes[1].set_title('(b) 학습 곡선: 순간 MSE (dB)')
axes[1].set_xlabel('시간 (s)'); axes[1].set_ylabel('MSE (dB)')
axes[1].yaxis.set_major_formatter(neg_fmt)
axes[1].grid(alpha=0.3)
# (c) 실제 에코 경로 vs 추정된 계수
axes[2].stem(np.arange(M), true_h, linefmt='C0-', markerfmt='C0o',
basefmt=' ', label='실제 h')
axes[2].stem(np.arange(L), w, linefmt='C3--', markerfmt='C3x',
basefmt=' ', label='추정 w')
axes[2].set_title('(c) 에코 경로 추정 결과')
axes[2].set_xlabel('탭 인덱스'); axes[2].set_ylabel('계수 값')
axes[2].legend(); axes[2].grid(alpha=0.3)
plt.tight_layout();
plt.savefig('Adaptive_Echo_Cancellation_Double_Talk.png', dpi=150, bbox_inches='tight')
plt.show()

그래프 (a)는 LMS 에코 캔슬러의 동작을 한눈에 보여주는 가장 핵심적인 결과 그래프입니다. 시간을 따라 세 구간으로 나누어 설명드리겠습니다.
구간 1 (0 ~ 약 0.5초): 학습이 진행되는 구간
- 빨간색(마이크 입력 $d[n]$): 시작부터 끝까지 진폭이 거의 일정하게 ±1 부근을 유지합니다. 원단 신호가 에코 경로를 통해 만들어낸 에코가 그대로 마이크에 잡히고 있는 상태입니다.
- 파란색(에코 제거 후 $e[n]$): 0초 시점에서는 빨간색과 거의 같은 진폭에서 시작하지만, 시간이 지나면서 부드럽게 0을 향해 줄어드는 모습이 보입니다.
이 "파란색이 점점 작아지는" 현상이 바로 LMS 알고리즘이 에코 경로를 학습하는 과정입니다. 초기 계수가 모두 0이므로 처음에는 필터가 아무것도 추정하지 못해 $e[n] \approx d[n]$ 이지만, 매 샘플마다 $\mathbf{w}[n + 1] = \mathbf{w}[n] + \mu e[n] \mathbf{x}[n]$ 갱신이 누적되면서 점점 정확한 에코 추정치를 만들어내고, 그 결과 오차가 빠르게 감소합니다.
구간 2 (약 0.5 ~ 1.5초): 완벽한 에코 제거 구간
- 빨간색: 여전히 ±1 부근의 강한 에코가 마이크에 들어오고 있습니다.
- 파란색: 거의 0에 붙어 있는 평탄한 선으로 보입니다.
이 구간이 적응 필터가 진정 빛을 발하는 부분입니다. 필터가 실제 에코 경로 $\mathbf{h}$를 거의 완벽하게 추정해냈기 때문에($\mathbf{w} \approx \mathbf{h}$, 계수 정규화 오차 약 -38 dB) 마이크 입력에서 추정 에코를 빼면 거의 아무것도 남지 않습니다. 수치로는 이 구간의 ERLE가 약 42 dB이며, 이는 앞서 설명한 것처럼 청각적으로는 "완전히 깨끗한" 상태에 도달했다고 볼 수 있습니다.
구간 3 (1.5초 이후): 근단 음성이 깨끗하게 살아나는 구간
이 구간이 사실 가장 중요합니다. 회색 점선이 표시된 1.5초 시점에서 근단 음성 $s[n] = 0.3 \sin(2\pi \cdot 500t)$ 가 시작됩니다.
- 빨간색: 진폭이 약간 더 커진 듯한 두꺼운 띠로 보입니다. 에코와 근단 음성이 함께 섞여 들어오기 때문입니다 ($d[n] = \text{echo} + s[n]$).
- 파란색: 갑자기 진폭 약 $\pm 0.3$의 선명한 사인파 띠가 나타납니다.
여기서 파란색의 진폭이 정확히 0.3인 점에 주목하실 만합니다. 이는 코드에서 설정한 근단 음성의 진폭과 정확히 일치합니다. 즉, 에코는 깨끗이 제거되었고 근단 화자의 목소리만 그대로 살아남아 상대방에게 전송되고 있다는 뜻입니다. 만약 필터가 근단 음성까지 함께 제거해버렸다면 통화가 끊긴 것처럼 들렸을 텐데, 그런 일은 일어나지 않았습니다.
적응 필터는 원단 신호와 상관관계가 있는 성분(에코)만 골라서 제거하고, 그와 무관한 신호(근단 음성)는 건드리지 않습니다. 이것이 적응 필터가 단순한 노이즈 게이트나 고정 필터와 본질적으로 다른 점입니다. LMS는 "에코가 무엇인지" 알지 못한 채로도, 단지 원단 신호 $x[n]$과 상관관계가 있는 성분만 학습한다는 원리만으로 에코와 음성을 자동으로 분리해냅니다. 이 "상관관계 기반 분리" 원리는 ANC(능동 소음 제어), 빔포밍, 채널 등화 등 적응 신호처리 응용 전반에 그대로 적용되는 핵심 아이디어입니다.
그래프 (b)는 그래프 (a)와 짝을 이루는 학습 곡선(learning curve) 으로, LMS의 수렴 과정을 정량적으로 보여주는 가장 중요한 진단 도구입니다. 두 구간으로 나누어 설명드리겠습니다.
구간 1 (0 ~ 1.5초): 교과서적 수렴 곡선
- 시작점 약 -16 dB 에서 1.5초 직전 약 -60 dB 까지 단조 감소합니다.
- 곡선이 dB 축에서 거의 직선적으로 떨어지는 점에 주목해 주시기 바랍니다.
이것이 LMS 학습 곡선의 가장 중요한 특징입니다. dB 스케일에서 선형이라는 것은 선형 스케일에서 지수적(exponential) 으로 감소한다는 뜻이고, 이는 LMS의 이론적 수렴 거동과 정확히 일치합니다. 비용 함수 $J(\mathbf{w}) = E[e^2[n]]$에 대한 경사 하강이 평균적으로 만들어내는 궤적이 바로 이 지수 감소 형태입니다.
또한 곡선에 보이는 잔잔한 잔물결(고주파 미세 진동) 도 의미가 있습니다. 이는 LMS가 진짜 경사가 아닌 순간 경사(instantaneous gradient) 를 사용하기 때문에 발생하는 gradient noise 로, 이론적으로 예측되는 현상입니다. $\mu$를 더 크게 잡으면 수렴은 빨라지지만 이 잔물결이 더 커집니다.
총 약 44 dB의 감소량(-16 → -60 dB)은 선형 스케일로 환산하면 오차 전력이 약 25,000배 줄어들었다는 뜻입니다. 단순한 한 줄짜리 갱신식이 이 정도 결과를 만들어낸다는 점이 LMS의 매력입니다.
구간 2 (1.5초 이후): 갑작스러운 점프, 그러나 실패가 아닌 성공
1.5초 시점에서 MSE가 -60 dB에서 약 -14 dB로 급격히 점프한 뒤, 그 이후로는 변동 없이 평탄한 직선을 유지합니다. 이 점프를 처음 보면 "필터가 망가졌나?" 싶지만, 사실은 정반대입니다.
핵심은 MSE의 정의에 있습니다. 이 그래프가 측정하는 것은 $e[n]^2$ 의 이동 평균이지, "에코 추정 오차"가 아닙니다. 1.5초 이전에는 $e[n] \approx 0$ 이 정답이었지만 (에코만 있었으므로), 1.5초 이후에는 $e[n] \approx s[n]$ 이 정답입니다 (근단 음성은 보존되어야 하므로).
근단 음성의 전력을 계산해 보면 정확히 들어맞습니다.
- $s[n] = 0.3 \sin(2\pi \cdot 500t)$
- 전력 $= 0.3^2 / 2 = 0.045$
- dB 환산: $10 \log_{10}(0.045) \approx -13.5$ dB
그래프의 평탄 구간이 거의 정확히 -14 dB에 위치하고 있습니다. 즉 이 "점프"는 필터의 실패가 아니라, 에코는 여전히 완벽히 제거되고 있는 가운데 근단 음성이 그대로 살아남아 있다는 증거입니다. (a) 그래프에서 1.5초 이후 파란색 신호가 ±0.3 진폭의 사인파였던 것을 떠올리시면 두 그래프가 같은 사실을 다른 방식으로 보여주고 있다는 점이 명확해집니다.
평탄 구간이 완전히 일직선인 이유는 코드에서 DTD 동결 로직(if t[n] <= 1.5: w = w + ...)으로 1.5초 이후 계수 갱신을 멈췄기 때문입니다. 계수가 고정되었으므로 에코 추정도 안정적이고, 따라서 $e[n]$ 의 전력도 근단 음성 전력으로 일정하게 유지됩니다.
그래프 (a)와 그래프 (b)는 같은 시뮬레이션의 서로 다른 관점입니다.
두 그래프를 시점별로 정리하면 같은 사실을 다른 측면에서 보여준다는 점이 더 명확해집니다.
그래프 (c)에서 주요 탭의 추정 정확도를 보면 LMS가 실제 에코 경로를 매우 정밀하게 복원했음을 알 수 있습니다. −38.62 dB의 정규화 오차는 실제 계수와 추정 계수의 차이가 평균적으로 약 1% 수준임을 의미합니다.
| 탭 인덱스 | 실제 h[k] | 추정 w[k] | 상대 오차 |
|---|---|---|---|
| 0 (직접파) | +0.5000 | +0.4973 | 0.5% |
| 5 | +0.3000 | +0.2985 | 0.5% |
| 15 | −0.2000 | −0.1981 | 1.0% |
| 30 | +0.1500 | +0.1484 | 1.1% |
| 50 (후기 반사) | −0.1000 | −0.0987 | 1.3% |
후기 반사(인덱스 50)일수록 추정 오차가 약간 커지는 경향이 보이는데, 이는 LMS가 시간 지연이 큰 성분일수록 학습에 더 많은 샘플이 필요하기 때문입니다.
5. 스텝 사이즈에 따른 수렴 거동
LMS의 실무적 핵심은 결국 스텝 사이즈 μ의 선택으로 귀결됩니다. 동일한 시나리오에서 μ만 바꿔 가며 성능을 비교한 결과는 다음과 같습니다.
| μ | 정상 상태 MSE | -30 dB 도달 시간 | -50 dB 도달 시간 | 비고 |
|---|---|---|---|---|
| 0.001 | -23.20 dB | 미달 | 미달 | 너무 작아 환경 변화 추종 불가 |
| 0.005 | -43.63 dB | 0.81 s | 미달 | 안정적이나 느림; 정상 상태도 얕음 |
| 0.010 | -64.15 dB | 0.43 s | 1.15 s | 본문 (b) 그래프 기준값 |
| 0.050 | -95.02 dB | 0.10 s | 0.62 s | 빠른 수렴, 가장 깊은 정상 상태 |
| 0.100 | -77.55 dB | 0.09 s | 0.63 s | 빠르지만 정상 상태 jitter 증가 |
| 0.200 이상 | 발산 | - | - | 안정 한계 초과 |
여기서 두 가지 중요한 관찰이 나옵니다.
첫째, $\mu$가 커질수록 수렴은 빠르지만 어느 지점을 지나면 정상 상태 오차가 오히려 커집니다. $\mu=0.05$에서 -95 dB까지 떨어졌던 MSE가 $\mu=0.1$에서 -78 dB로 악화되었습니다. 이는 큰 $\mu$가 최적점 주변에서 계수를 계속 흔들리게 하기 때문이며, 이론적으로 excess MSE(과잉 평균제곱오차)라고 부릅니다.
둘째, $\mu$가 안정 한계를 넘으면 계수가 폭발적으로 발산합니다. 본 시뮬레이션에서는 $\mu=0.2$부터 오버플로가 발생했습니다. 입력 신호 전력이 변하는 실제 환경에서는 이 안정 한계도 함께 변하므로, 입력 전력으로 $\mu$를 정규화하는 NLMS가 사실상 표준이 됩니다. NLMS는 다음과 같이 LMS의 갱신식에서 스텝 사이즈를 입력 벡터의 순간 전력으로 정규화한 것입니다.
NLMS는 LMS의 갱신식에서 분모만 바뀐 정도지만, 입력 신호 진폭에 무관하게 안정적으로 동작하기 때문에 실무 에코 캔슬러는 거의 대부분 NLMS 계열을 사용합니다.
[Insight]
1. LMS는 가장 단순한 형태의 "온라인 학습"이다. 오차를 계산하고 입력에 비례해 가중치를 갱신하는 LMS의 형태는, 사실 신경망의 확률적 경사 하강법(SGD) 과 본질적으로 동일합니다. Widrow는 LMS를 발표한 직후 이를 이용한 적응형 뉴런 ADALINE을 설계했고, 이는 현대 딥러닝의 직계 조상이기도 합니다. 신호 처리와 기계 학습이 같은 뿌리에서 출발했음을 보여주는 가장 명료한 사례입니다.
2. 적응 필터의 본질은 "시스템 식별"이다. 이번 구현에서 LMS가 한 일은 결국 알 수 없는 시스템(방의 에코 경로)을 입력과 출력의 관측만으로 복원해 낸 것입니다. 이 관점은 능동 소음 제어(ANC), 채널 등화(Channel Equalization), 라인 에코 캔슬링, 심지어 빔포밍에까지 그대로 확장됩니다. 즉 적응 필터는 "필터"라기보다는 "환경 추정기"에 가깝다고 보는 편이 더 본질적입니다.
3. 스텝 사이즈는 학습 시스템 공통의 트레이드오프이다. 빠른 수렴과 낮은 정상 상태 오차는 본질적으로 양립할 수 없습니다. LMS에서는 μ가, SGD에서는 learning rate가, Kalman 필터에서는 프로세스 노이즈와 측정 노이즈의 비율이 같은 역할을 합니다. 이번 글의 μ 비교 실험을 통해 얻은 직관은 다음 글의 Kalman 필터에도, 그리고 모든 적응형 시스템에도 동일하게 적용됩니다.
4. LMS는 끝이 아니라 시작점이다. 실무에서는 입력 신호의 색깔(Coloration)이 LMS의 수렴을 심하게 저해하기 때문에, NLMS, 어파인 프로젝션(APA), RLS(Recursive Least Squares), 주파수 영역 적응 필터(FDAF) 등 더 정교한 알고리즘이 사용됩니다. 그러나 이 모든 변형의 출발점은 "오차에 비례해 가중치를 갱신한다"는 LMS의 단순한 아이디어입니다. 단순함이 곧 견고함의 다른 이름임을 보여주는 알고리즘이라 하겠습니다.
이처럼 적응 필터는 "고정된 시스템"을 다루던 전통적인 신호 처리 체계를 넘어, "데이터를 보고 스스로 진화하는 시스템" 즉 머신러닝의 철학을 신호 처리에 도입한 훌륭한 가교 역할을 합니다.
다음 글 예고
지금까지 우리는 1차원적인 노이즈를 다루거나 신호의 주파수 대역을 걸러내는 '필터링'에 집중했습니다. 그렇다면, 자동차의 GPS 센서나 드론의 자이로 센서처럼 센서 자체에 측정 노이즈가 심할 때, 현재 기기의 정확한 '위치'나 '속도'를 추정하려면 어떻게 해야 할까요?
단순히 주파수를 자르는 것만으로는 불규칙한 센서 노이즈를 다룰 수 없습니다.
다음 시간에는 [20. 칼만 필터(Kalman): 센서 데이터 예측 및 필터링 기초] 편을 통해, 이전 상태를 기반으로 미래를 예측하고 현재의 불확실한 측정값을 융합하여 최적의 상태를 추정해 내는 확률적 필터링의 꽃, 칼만 필터의 세계를 파헤쳐 보겠습니다.
📌 이전 연재 글 보기
- [16] Z-변환 응용: 전달함수 H(z), 극점·영점과 필터 안정성
- [17] FIR 필터 설계: 윈도우 기법을 이용한 설계와 노이즈 제거 실전
- [18] IIR 필터 설계: Butterworth, Chebyshev 필터 및 안정성 분석
