들어가며
지난 푸리에 변환(DFT/FFT) 편에서는 섞여 있는 복잡한 신호를 주파수 성분별로 분해하여 각 주파수의 '크기(진폭)'를 확인하는 방법을 알아보았습니다. 복잡한 수식 이면에 숨겨진 직관적인 의미와 FFT를 통한 노이즈 속 신호 탐지 실습까지 진행해 보았습니다.
하지만 우리가 얻은 FFT 결과 배열을 자세히 들여다보면, 단순한 실수가 아닌 복소수(Complex Number)로 이루어져 있다는 것을 알 수 있습니다. 지난 시간에는 복소수의 크기(절댓값)를 구해 진폭만을 다루었지만, 사실 복소수 데이터에는 위상(Phase)이라는 또 다른 반쪽의 진실이 숨겨져 있습니다.
오늘은 그 복소수가 품고 있는 두 가지 핵심 정보인 진폭(Amplitude)과 위상(Phase) 을 낱낱이 해부하고, 나아가 노이즈 분석과 신호 품질 평가의 핵심 도구인 전력 스펙트럼 밀도(PSD, Power Spectral Density) 까지 파헤쳐 보겠습니다.
1. 복소수 스펙트럼의 두 얼굴: 진폭(Magnitude)과 위상(Phase)
FFT를 계산하면 각 주파수 성분에 대해 복소수 $X[k] = a + jb$ 형태의 값이 반환됩니다. 복소평면 위에서 이 값은 원점으로부터의 '거리'와 '각도'로 표현될 수 있습니다.
- 진폭 스펙트럼 (Magnitude Spectrum): 특정 주파수 성분이 신호에 '얼마나 강하게(크게)' 들어 있는지를 나타냅니다.
$$|X[k]| = \sqrt{a^2 + b^2}$$ - 위상 스펙트럼 (Phase Spectrum): 특정 주파수 성분의 사인파가 **'어느 시점에서 시작되는가(시간적 지연)'**를 나타냅니다. 단위는 라디안(Radian) 또는 도(Degree)를 사용합니다.
$$\angle X[k] = \operatorname{atan2}(b, a)$$
두 정보 중 하나라도 잃으면 원래 신호를 완벽히 복원할 수 없습니다. 흔히 "위상은 중요하지 않다"고 오해하기 쉽지만, 이는 사실이 아닙니다. 동일한 주파수와 진폭을 가진 여러 사인파들을 더하더라도, 각 사인파의 '위상(시작점)'이 다르면 합성된 최종 파형의 모양은 완전히 달라집니다.
2. Python 실습: 진폭과 위상 스펙트럼 분석
위상이 파형에 미치는 영향을 확인하기 위해, 진폭은 같지만 위상이 서로 다른 파형을 생성하고 FFT를 통해 이를 분해해 보겠습니다.
import numpy as np
import matplotlib.pyplot as plt
# 한글 폰트 설정
plt.rcParams['font.family'] = 'Malgun Gothic'
plt.rcParams['axes.unicode_minus'] = False
fs = 1000
t = np.linspace(0, 1, fs, endpoint=False)
# 10Hz, 진폭 1, 위상 0 (사인파)
s1 = np.sin(2 * np.pi * 10 * t)
# 10Hz, 진폭 1, 위상 pi/2 (코사인파, 90도 앞섬)
s2 = np.sin(2 * np.pi * 10 * t + np.pi/2)
# FFT 연산
X1 = np.fft.fft(s1)
X2 = np.fft.fft(s2)
freqs = np.fft.fftfreq(fs, 1/fs)
# 진폭과 위상 계산
# 위상 비교가 목적이므로 상대적 크기만 사용 (절대 진폭 복원 불필요)
mag1 = np.abs(X1) / (fs/2)
mag2 = np.abs(X2) / (fs/2)
phase1 = np.angle(X1)
phase2 = np.angle(X2)
# 노이즈 위상을 제거하기 위한 마스킹 (진폭이 작은 곳의 위상은 0으로 처리)
threshold = 0.1
phase1[mag1 < threshold] = 0
phase2[mag2 < threshold] = 0
# 시각화 (양수 주파수 영역만)
half = fs // 2
f_pos = freqs[:half]
fig, axes = plt.subplots(3, 1, figsize=(10, 10))
# 시간 영역 파형
axes[0].plot(t[:100], s1[:100], label='Phase = 0')
axes[0].plot(t[:100], s2[:100], label='Phase = $\pi/2$ (90도)')
axes[0].set_title('시간 영역: 위상이 다른 두 파형')
axes[0].legend()
axes[0].grid(True)
# 진폭 스펙트럼
axes[1].plot(f_pos[:50], mag1[:half][:50], 'o-', label='s1 Magnitude')
axes[1].plot(f_pos[:50], mag2[:half][:50], 'x--', label='s2 Magnitude')
axes[1].set_title('진폭 스펙트럼 (Magnitude): 두 신호가 동일함')
axes[1].legend()
axes[1].grid(True)
# 위상 스펙트럼 (Radian)
axes[2].plot(f_pos[:50], phase1[:half][:50], 'o-', label='s1 Phase')
axes[2].plot(f_pos[:50], phase2[:half][:50], 'x--', label='s2 Phase')
axes[2].set_title('위상 스펙트럼 (Phase): 10Hz에서 1.57(pi/2) 차이 확인')
axes[2].set_ylabel('Radian')
axes[2].legend()
axes[2].grid(True)
plt.tight_layout()
plt.savefig('amplitude_phase_spectrum.png', dpi=150, bbox_inches='tight')
plt.show()

시간 영역에서는 두 파형이 분명히 다르게 보이지만, 진폭 스펙트럼은 둘 다 10 Hz에서 똑같이 1.0을 가리킵니다. 하지만 위상 스펙트럼을 확인해보면, s2의 10 Hz 성분이 s1보다 정확히 $\pi/2$ (약 1.57 라디안)만큼 높은 위상을 가지고 있음을 명확하게 확인할 수 있습니다.
3. 데시벨(dB) 스케일로 보는 스펙트럼
실무에서는 진폭 스펙트럼을 데시벨(dB) 스케일로 표현하는 경우가 많습니다. 넓은 다이나믹 레인지를 한 화면에 표현할 수 있기 때문입니다.
# 기본 파라미터 설정
fs = 1000 # 샘플링 주파수 (Hz)
T = 1.0 # 신호 길이 (초)
N = int(fs * T) # 총 샘플 수
t = np.linspace(0, T, N, endpoint=False)
# 복합 신호 생성: 50Hz + 120Hz + 노이즈
f1, f2 = 50, 120
A1, A2 = 1.0, 0.5
sig = A1 * np.sin(2 * np.pi * f1 * t) + A2 * np.sin(2 * np.pi * f2 * t)
sig += 0.2 * np.random.randn(N) # 노이즈 추가
# FFT 계산
X = np.fft.fft(sig)
freqs = np.fft.fftfreq(N, d=1/fs)
# 단측 스펙트럼 계산
N_half = N // 2
amplitude = np.abs(X[:N_half]) / N # 1/N 정규화
amplitude[1:] *= 2 # DC 제외 나머지 성분에 2배 보정 (단측 변환)
freqs_pos = freqs[:N_half]
amplitude_dB = 20 * np.log10(amplitude + 1e-10) # log(0) 방지
plt.figure(figsize=(10, 4))
plt.plot(freqs_pos, amplitude_dB)
plt.xlabel("주파수 (Hz)")
plt.ylabel("진폭 (dB)")
plt.title("진폭 스펙트럼 (dB 스케일)")
plt.xlim(0, 200)
plt.ylim(-70, 10)
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('dB_Scale_Spectrum.png', dpi=150, bbox_inches='tight')
plt.show()

4. 노이즈 분석의 핵심: 전력 스펙트럼 밀도 (PSD)
백색 잡음(White Noise), 뇌파(EEG), 지진파처럼 불규칙하게 계속되는 무작위 신호 (Random signal)를 분석할 때는 단순한 FFT 진폭 스펙트럼이 적합하지 않습니다. 결정론적 신호(Deterministic Signal)와 달리, 무작위 신호는 시간이 지날수록 에너지가 무한대로 발산하기 때문에, 단위 시간당 에너지인 전력(Power) 개념을 사용해야 합니다. PSD(Power Spectral Density)는 신호의 전력이 주파수 대역별로 어떻게 분포(밀도)되어 있는지를 나타냅니다. 일반적인 단위는 $V^2/\text{Hz}$ 또는 $\text{dB/Hz}$입니다.
단순히 FFT 결과의 절댓값을 제곱하여 PSD를 구하는 방식(Periodogram)은 노이즈가 심해 그래프가 매우 거칠게 튀는(분산이 큰) 단점이 있습니다. 실무에서는 신호를 겹쳐서(Overlap) 여러 구간으로 자르고, 각 구간에 윈도우(Window)를 씌워 평균을 내는 Welch 방법을 표준적으로 사용합니다.
주의: 아래 코드에서 한글 폰트와 수식의 마이너스 첨자가 충돌을 일으켜 이를 회피하기 위해 fontdict를 중간에 사용하였습니다.
import numpy as np
import matplotlib.pyplot as plt
from scipy import signal
# =====================================================================
# 전역 폰트를 기본 영문으로 초기화 (수식/마이너스 에러 방지)
plt.rcParams['font.family'] = 'sans-serif'
plt.rcParams['axes.unicode_minus'] = True
# =====================================================================
# 데이터 생성
fs = 1000
np.random.seed(0)
t = np.linspace(0, 5, 5*fs, endpoint=False)
x = 2.0 * np.sin(2 * np.pi * 50 * t) + np.random.normal(scale=3.0, size=len(t))
f_per, Pxx_per = signal.periodogram(x, fs)
f_welch, Pxx_welch = signal.welch(x, fs, nperseg=1024)
# 시각화
plt.figure(figsize=(10, 5))
plt.semilogy(f_per, Pxx_per, alpha=0.5, label='Periodogram (단순 FFT)')
plt.semilogy(f_welch, Pxx_welch, color='red', linewidth=2, label="Welch's Method")
# 한글이 필요한 요소에만 개별적으로 맑은 고딕 폰트 지정
plt.title('PSD (Power Spectral Density) 추정 비교', fontdict={'family': 'Malgun Gothic'})
plt.xlabel('Frequency (Hz)')
plt.ylabel('PSD ($V^2$/Hz)')
plt.xlim(0, 100)
plt.ylim(1e-5, 1e2)
# 범례(legend) 안의 텍스트에도 한글 폰트 적용
plt.legend(prop={'family': 'Malgun Gothic'})
plt.grid(True, alpha=0.3)
plt.savefig('PSD_Welch_Periodogram.png', dpi=150, bbox_inches='tight')
plt.show()

코드를 실행해 보면, 단순 FFT 기반의 Periodogram(파란색)은 노이즈 성분 때문에 위아래로 심하게 요동치지만, Welch 방법(빨간색)은 여러 구간의 평균을 내어 매끄럽고 안정적인 노이즈 플로어(Noise Floor)와 50 Hz에서의 명확한 신호 피크를 보여줍니다.
[Insight]
지금까지 살펴본 스펙트럼의 관계를 정리하면 다음과 같습니다.

- 위상(Phase)의 숨겨진 힘: 오디오 분야에서는 사람의 귀가 위상 변화에 둔감하여 주로 진폭 스펙트럼이 중요하게 다뤄지는 경우가 많지만, 음성 합성(TTS)이나 신호 복원 태스크에서는 위상 추정이 핵심 난제로 남아 있습니다. 반면 이미지 처리(2D FFT)에서는 주로 위상이 중요하게 다루어집니다. 이미지의 윤곽선(Edge)과 구조에 대한 핵심 정보는 진폭이 아닌 위상에 담겨 있습니다. 또한 레이더, 소나 신호 처리나 최신 통신 기술(QPSK, QAM 등)에서는 전파의 위상 변화에 데이터를 싣기 때문에 위상 분석이 절대적입니다.
- PSD(전력 스펙트럼 밀도)의 중요성: 통신 시스템 설계 엔지니어들은 대역폭 내에 노이즈가 어느 정도 깔려 있는지(Noise Floor)를 파악할 때 항상 PSD를 봅니다. 기계의 모터 진동 데이터 센서를 분석할 때도 특정 주파수 대역의 진동 에너지가 커지는 것을 PSD로 모니터링하여 고장을 예측(Predictive Maintenance)합니다.
다음 글 예고
이번 시간에 PSD를 실용적으로 구하기 위해 Welch 방법을 사용하면서 신호를 자르고 윈도우(Window)를 씌운다는 언급을 잠시 했습니다. 현실의 신호는 무한하지 않기에 유한한 길이로 잘라서 컴퓨터에 넣어야 합니다. 그런데 신호를 강제로 싹둑 자를 때 발생하는 양끝의 불연속성 때문에, 주파수 영역에서 에너지가 주변으로 새어나가는 스펙트럴 누설(Spectral Leakage) 현상이 필연적으로 발생합니다.
이를 부드럽게 막아주는 방패가 바로 윈도우 함수입니다. 다음 시간에는 [11. 윈도우 함수(Windowing): Hamming, Hann 등 윈도우별 분해능 비교] 편을 통해 주파수 분석의 정밀도를 끌어올리는 다양한 윈도우 기법 및 어떤 윈도우를 언제 선택해야 하는지를 알아보겠습니다.
📌 이전 연재 글 보기
