본문 바로가기
딥러닝 with Python

[딥러닝 with Python] Fourier Transform 비교: FFT, STFT, RFFT

by CodeCrafter 2025. 6. 28.
반응형

 

1. Fourier Transform이란?

- 시계열 x(t)는 시간의 흐름에 따라 변화하지만, 이 변화는 주기성(Periodicity)를 포함하고 있을 수 있습니다.

- 푸리에 변환은 이런 시계열을 다수의 정현파(sinusoids)의 합으로 분해하는 것입니다.

 

- 즉, 시계열은 시간의 조각이 아닌, 주파수의 합으로 보는 관점입니다.

 

- 이를 수식으로 나타내면 아래와 같습니다.

- 이때 

 * Sine파는 시간-불변 주기성의 기본 단위이며

 * 푸리에 변환은 신호가 어떤 사인파의 조합인지 분해하는 과정 입니다.

 

 

2. FFT, STFT, RFFT

1) FFT(Fast Fourier Transform)

- 시간 영역의 신호를 주파수 영역으로 변환하는 푸리에 변환을 빠르게 계산하는 방법입니다.

 * 이때 신호 전체를 분석해 어떤 주파수가 얼마나 포함되어 있는지를 알려주지만

 * 시간 정보는 제공하지 않고, 시간 평균적인 주파수 구성만을 보여줍니다.

 

- 푸리에 변환을 수치적으로 구현할 때는 DFT(Discrete Fourier Transform)을 활용합니다. 

 * 이 계산은 O(N^2) 복잡도

 * FFT는 동일한 결과를 O(NlogN)으로 계산

 

- DFT는 각 주파수 성분에 대해 모든 시간 성분을 한 번씩 곱하고 더함

- FFT는 대치성과 주기성을 활용해 중복 계산을 제거함 

 

파이썬 코드를 통해 예시를 알아보면 아래와 같습니다.

import numpy as np
import matplotlib.pyplot as plt

# 신호 생성
fs = 1000  # 샘플링 주파수 [Hz]
t = np.linspace(0, 1, fs, endpoint=False)  # 1초 길이의 시간 벡터
x = np.sin(2*np.pi*50*t) + 0.5*np.sin(2*np.pi*120*t)  # 50Hz + 120Hz 합성 신호

# FFT 계산
X = np.fft.fft(x)
freqs = np.fft.fftfreq(len(x), d=1/fs)

# 시각화
plt.figure(figsize=(12, 6))

# 1. 원본 시간 도메인 신호
plt.subplot(2, 1, 1)
plt.plot(t, x)
plt.title('Original Time-Domain Signal')
plt.xlabel('Time [s]')
plt.ylabel('Amplitude')
plt.grid()

# 2. 주파수 도메인 스펙트럼
plt.subplot(2, 1, 2)
plt.plot(freqs[:fs//2], np.abs(X[:fs//2]) / fs)
plt.title('FFT Spectrum')
plt.xlabel('Frequency [Hz]')
plt.ylabel('Magnitude')
plt.grid()

plt.tight_layout()
plt.show()

 

 

오리지날 원본시계열이 주파수 도메인의 스펙트럼으로 바뀌는 것을 보실 수 있습니다. 

 

 

2)STFT (Short Time Fourier Transform)

- STFT는 시간에 따라 변화하는 주파수 성분을 분석하기 위해 고안된 푸리에 기반 분석 기법입니다.

 * 실제 신호는 시간이 지남에 따라 주파수 구성이 변하는 비정상 신호인 경우가 많습니다.

 * STFT는 신호를 일정한 길이의 짧은 윈도우로 나누고, 각 윈도우에 대해 FFT를 수행하여 시간에 따른 주파수 변화를 분석하는 기법입니다.

 

- 일반 FFT는 신호 전체를 기준으로 평균적인 주파수 성분만을 보여주기 때문에, 다음 질문에 답이 제한됩니다

 * 언제 어떤 주파수가 나타났지?

 * 예를 들어 음성에서 ㅅ 발음은 고주파, ㅜ 는 저주파 성분이 강한데, 이들이 시간에 따라 어떻게 나타나는지를 분석하려면 시간-주파수 해석이 필요함. 

 * 이를 STFT가 가능하게 함 

 

- STFT의 동작원리는 다음과 같습니다.

 a) 신호를 고정된 길이의 윈도우로 나누고

 b) 각 윈도우에 대해 FFT를 수행하고

 c) 결과를 시간축에 따라 정렬 -> 2D 형태의 스펙토그램으로 생성

 

- 이를 수식으로 표현하면 아래와 같습니다.

 

이때 시간-주파수 해상도 트레이드오프가 발생하는데

 윈도우가 짧을수록 시간 해상도는 올라가지만 주파수 해상도는 떨어지고

 윈도우가 길어질수록 시간 해상도는 줄어들지만 주파수 해상도는 올라가게 됩니다. 

 

이를 파이썬 코드로 원본 시계열, fft, stft로 시각화해보면 다음과 같습니다.

시간에 따라 한번 주파수가 변화하게 설정해놓았습니다.

import numpy as np
import matplotlib.pyplot as plt
from scipy.signal import stft

# 1. 시계열 생성 (시간에 따라 주파수 변화)
fs = 1000
t = np.linspace(0, 1, fs, endpoint=False)
x = np.where(t < 0.5,
             np.sin(2 * np.pi * 50 * t),
             np.sin(2 * np.pi * 120 * t))  # 0~0.5초: 50Hz / 0.5~1초: 120Hz

# 2. FFT
X = np.fft.fft(x)
freqs = np.fft.fftfreq(len(x), d=1/fs)

# 3. STFT
f_stft, t_stft, Zxx = stft(x, fs=fs, nperseg=128)

# 4. 시각화
plt.figure(figsize=(12, 10))

# (1) 원본 시계열
plt.subplot(3, 1, 1)
plt.plot(t, x)
plt.title('Time-Varying Signal: 50Hz (0~0.5s), 120Hz (0.5~1s)')
plt.xlabel('Time [s]')
plt.ylabel('Amplitude')
plt.grid()

# (2) FFT
plt.subplot(3, 1, 2)
plt.plot(freqs[:fs//2], np.abs(X[:fs//2]) / fs)
plt.title('FFT Spectrum (No Time Information)')
plt.xlabel('Frequency [Hz]')
plt.ylabel('Magnitude')
plt.grid()

# (3) STFT
plt.subplot(3, 1, 3)
plt.pcolormesh(t_stft, f_stft, np.abs(Zxx), shading='gouraud')
plt.title('STFT Spectrogram (Time-Frequency Analysis)')
plt.xlabel('Time [s]')
plt.ylabel('Frequency [Hz]')
plt.colorbar(label='Magnitude')

plt.tight_layout()
plt.show()

 

 

3) RFFT (Real FFT)

- RFFT는 실수 입력 신호에 특화된 푸리에 변환 알고리즘 입니다.

- 푸리에 변환의 대칭 성질을 활용해, 계산량과 메모리 사용을 절반으로 줄인 방식

 

-핵심 아이디어는 Hermitian Symmetry 입니다.

이는

 

즉 스펙트럼 켤레 대칭이므로, 전체 스펙트럼을 계산할 필요 없이 절반만 구해도 충분한것입니다.

 

 

먼저 이를 파이썬으로 시각화 해보면 아래와 같습니다.

import numpy as np
import matplotlib.pyplot as plt
from scipy.signal import stft

# 1. 시계열 생성 (시간에 따라 주파수 변화)
fs = 1000
t = np.linspace(0, 1, fs, endpoint=False)
x = np.where(t < 0.5,
             np.sin(2 * np.pi * 50 * t),
             np.sin(2 * np.pi * 120 * t))

# 2. FFT
X_fft = np.fft.fft(x)
freq_fft = np.fft.fftfreq(len(x), d=1/fs)

# 3. STFT
f_stft, t_stft, Zxx = stft(x, fs=fs, nperseg=128)

# 4. RFFT
X_rfft = np.fft.rfft(x)
freq_rfft = np.fft.rfftfreq(len(x), d=1/fs)

# 5. 시각화 (4행 1열 subplot)
plt.figure(figsize=(14, 12))

# (1) 원본 시계열
plt.subplot(4, 1, 1)
plt.plot(t, x)
plt.title('Original Time-Domain Signal')
plt.xlabel('Time [s]')
plt.ylabel('Amplitude')
plt.grid()

# (2) FFT 스펙트럼
plt.subplot(4, 1, 2)
plt.plot(freq_fft[:fs//2], np.abs(X_fft[:fs//2]) / fs)
plt.title('FFT Spectrum (Full, Symmetric)')
plt.xlabel('Frequency [Hz]')
plt.ylabel('Magnitude')
plt.grid()

# (3) STFT
plt.subplot(4, 1, 3)
plt.pcolormesh(t_stft, f_stft, np.abs(Zxx), shading='gouraud')
plt.title('STFT Spectrogram (Time-Frequency)')
plt.xlabel('Time [s]')
plt.ylabel('Frequency [Hz]')
plt.colorbar(label='Magnitude')

# (4) RFFT 스펙트럼
plt.subplot(4, 1, 4)
plt.plot(freq_rfft, np.abs(X_rfft) / fs)
plt.title('RFFT Spectrum (Optimized for Real Input)')
plt.xlabel('Frequency [Hz]')
plt.ylabel('Magnitude')
plt.grid()

plt.tight_layout()
plt.show()

 

결과에서 보이듯 fft와 rfft가 거의 동일한 결과를 나타내게 됩니다.

 

 

여기서 주목할 것은 계산량인데요

 

import numpy as np
import time

N = 2**20  # 큰 입력 크기 (1,048,576개 샘플)
x = np.random.rand(N)  # 실수 입력

# FFT 실행 시간 측정
start_fft = time.time()
_ = np.fft.fft(x)
end_fft = time.time()

# RFFT 실행 시간 측정
start_rfft = time.time()
_ = np.fft.rfft(x)
end_rfft = time.time()

print(f"FFT execution time:  {end_fft - start_fft:.5f} seconds")
print(f"RFFT execution time: {end_rfft - start_rfft:.5f} seconds")

 

다음 결과를 보면 실행 시간이 절반 이하로 줄어듦을 알 수 있습니다.

 

반응형

댓글