Chapter 11

DFT의 생각

신호를 주파수 점수표로 바꾸기
핵심 질문

어떤 주파수가 숨어 있는지 어떻게 점수로 볼까?

DFT의 생각

핵심 질문

신호 안에 어떤 주파수가 들어 있는지 어떻게 찾을 수 있을까?

교과서 설명

DFT는 길이 인 신호 을 주파수 성분 로 바꾼다.

1. 번호는 무엇을 뜻할까?

은 샘플 개수, 은 샘플 번호, 는 검사하는 주파수 번호다. 오른쪽의 은 복소평면에서 도는 회전 손잡이다.

개 샘플을 한 묶음으로 보았을 때 검사 파동이 그 안에서 몇 바퀴 도는지 세는 번호다. 은 돌지 않는 상수 성분, 은 한 바퀴, 는 두 바퀴를 뜻한다. 뒤쪽 번호는 반대 방향 회전처럼 볼 수 있다. 예를 들어 개 중 번은 번 도는 파동의 반대 방향 짝이다.

여기에는 중요한 약속이 숨어 있다. DFT는 길이 인 유한 배열을 한 주기로 반복한다고 약속하고, 그 한 주기 안에서 정수 번 회전하는 파동들과 비교한다. 이 말은 원래의 연속 신호가 실제로 영원히 반복된다고 단정하는 것이 아니다. 다만 지금 손에 든 개의 표본만으로 계산할 때, 양끝을 원형으로 이어 붙인 주기 신호처럼 다루겠다는 뜻이다. 그래서 은 이 한 주기 안에서 정확히 한 바퀴, 는 정확히 두 바퀴 도는 기준이 된다. 이 원형 해석은 뒤에서 길이를 고정한 DFT 곱셈이 순환 합성곱으로 나타나는 이유와도 연결된다.

여기서 기본 회전값을 라고 쓰면 DFT 식은 더 짧아진다.

이 장에서는 먼저 이 식을 “주파수별 점수”로 읽는다. 행렬과 다항식으로 보는 관점은 다음 장에서 한 번에 정리한다. 지금은 를 하나 고정하고, 모든 샘플 을 검사 회전값과 곱해 더한다고 생각하면 된다.

2. 검사 회전값을 곱해 더하기

각 항은 다음처럼 볼 수 있다.

즉, 이라는 막대 하나를 복소평면 위의 작은 벡터로 바꾼다. 그 벡터들을 모두 더했을 때 크게 남으면 그 주파수가 신호 안에 강하게 들어 있다는 뜻이다.

3. 복소 내적에서는 왜 켤레를 쓸까?

조금 더 수학적으로 말하면, DFT는 신호와 검사 파동을 비교하는 복소 내적처럼 볼 수 있다. 실수 배열에서는 같은 위치끼리 곱해 더하지만, 복소수에서는 검사 파동의 켤레를 곱해 방향을 맞춰 비교한다. 그래서 반대 방향 회전값 이 식에 들어간다고 이해할 수 있다.

검사 파동을

라고 두면, 그 켤레는 이다. 따라서 DFT는

로 쓸 수 있다. 즉 는 신호 번째 기본 파동 방향으로 비추어 본 점수다. 이 자료의 정규화에서는 기본 파동의 길이 제곱이 이므로, 실제 좌표 계수처럼 읽으려면 보통 이 나타난다.

복소 내적이 낯설다면 “검사 파동을 다시 기준 방향으로 돌려 놓고 더한다”고 생각하면 된다. 신호가 실제로 번째 기본 파동과 같은 방향이라면 각 샘플은 대략

꼴이다. 여기에 검사 파동의 켤레를 곱하면

가 된다. 단위원 위의 복소수는 길이가 이므로 이기 때문이다. 따라서 맞는 주파수에서는 여러 항이 같은 방향으로 쌓이고, 맞지 않는 주파수에서는 항들의 방향이 계속 달라져 서로 지워진다.

조금 더 천천히 말하면, 켤레는 검사 파동의 회전을 거꾸로 돌리는 장치다. 신호가 정말 방향으로 돌고 있었다면 를 곱하는 순간 그 회전이 멈춘 것처럼 같은 방향 값들이 남는다. 반대로 맞지 않는 주파수라면 거꾸로 돌려도 여전히 여러 방향으로 흩어지므로 합이 작아진다.

작은 예로 , 이면 검사 파동은

이고 켤레는

이다. 신호 와 비교하면

가 되어 점수가 남는다. 반대로 맞지 않는 검사 파동을 쓰면 양수와 음수, 서로 다른 회전 방향이 섞여 합이 작아진다.

4. 선형성과 행렬 관점

중요한 성질: 선형성

DFT는 더하기와 상수배를 보존한다. 두 신호 , 의 DFT를 각각 , 라고 하면

상수 에 대해서도

즉 신호를 더한 뒤 DFT한 결과는 각각 DFT한 결과를 더한 것과 같다. 여러 파동을 더한 신호에서 여러 주파수 막대가 함께 나타나는 이유도 이 성질과 잘 맞는다.

선형성은 합 기호의 분배법칙에서 바로 나온다. 예를 들어 의 DFT를 계산하면

이다. 상수배도 같은 방식으로

가 된다.

심화 관점으로는 DFT를 행렬 곱셈으로도 볼 수 있다. 이 문단은 지금 완전히 이해하지 못해도 뒤의 FFT 흐름을 따라가는 데 큰 문제는 없다.

로 정의하면

이다. 역 DFT가 가능한 이유는 이 행렬의 행들이 서로 직교하기 때문이다. 더 정확히는 이 거의 켤레전치 행렬과 같고, 크기를 맞추기 위해 이 붙는다.

5. 크기, 위상, 실제 진폭 읽기

는 보통 복소수다. 그래서 두 가지 정보를 가진다.

는 그 주파수가 얼마나 강한지를 나타내고, 의 방향 각도는 그 주파수가 어디서 시작했는지를 나타낸다. 이 시작 위치 정보를 위상이라고 부른다.

다만 가 항상 원래 사인파의 진폭과 바로 같은 것은 아니다. 이 자료의 정의에서는 DFT에는 을 붙이지 않으므로, 값의 크기는 샘플 개수 의 영향을 받는다. 또 실수 신호의 코사인 성분은 보통 의 켤레복소수 짝으로 나뉘어 나타난다. 처음에는 정확한 진폭 환산보다 “어느 막대가 상대적으로 큰가”를 먼저 보자.

주의: DFT 크기와 실제 진폭은 다르다

이 자료의 약속에서는 DFT 쪽에 을 붙이지 않는다. 그래서 같은 진폭의 파동이라도 샘플 개수 이 커지면 도 커질 수 있다. 실수 코사인은 보통 두 칸에 나뉘므로, 일반적인 코사인 진폭은 처럼 환산해서 읽는다. 평균 칸과 짝수 의 Nyquist 칸은 예외다.

실수 신호의 스펙트럼을 조금 더 정확히 읽으려면 앞쪽 막대와 뒤쪽 막대를 한 쌍으로 보아야 한다. 예를 들어 길이 인 배열 안에 정확히 번 도는 코사인 하나만 있다고 하자.

오일러 공식으로 코사인을 두 방향 회전의 평균으로 쓸 수 있다.

그래서 진폭 는 보통 두 칸에 반씩 나뉘어 나타난다.

따라서 도 아니고, 아래에서 말할 Nyquist 칸도 아니라면 코사인의 진폭은 대략 다음처럼 읽을 수 있다.

예를 들어 이고 이면 진폭은 이다. 이때 이 된다.

실수 신호에서는 이 두 칸이 항상 서로 짝을 이룬다. 수식으로는

이다. 켤레복소수는 크기가 같고 허수부 부호만 반대이므로, 실수 신호의 스펙트럼은 앞쪽 절반과 뒤쪽 절반이 거울처럼 대응된다.

예외도 있다. 은 평균 성분이라 짝이 따로 없다. 이때는

으로 읽는다. 또 이 짝수일 때 는 Nyquist 칸이라고 부르는 특수한 칸이다. 이 파동은 한 칸마다 부호가 바뀐다.

는 자기 자신이 반대 방향 짝이므로 두 칸으로 나뉘지 않는다. 그래서 이 칸의 크기는 일반적인 코사인 쌍처럼 두 배해서 읽지 않는다.

6. 역 DFT로 되돌리기

DFT는 정보를 버리는 계산이 아니다. 모든 를 알고 있으면 원래 신호로 되돌릴 수 있다.

이 식을 역 DFT라고 한다. DFT의 지수에는 가 있고, 역 DFT에는 가 있다는 점을 기억해 두자.

왜 되돌릴 수 있을까? 핵심은 서로 다른 검사 회전들이 한 바퀴 동안 서로 지워진다는 성질이다. 부터 사이의 번호일 때 다음이 성립한다.

같은 주파수끼리는 모든 항이 같은 방향으로 쌓여 이 되고, 다른 주파수끼리는 원 위를 고르게 돌아 합이 이 된다. 그래서 역 DFT에서 모든 를 다시 섞으면 맞는 성분만 남고, 마지막에 을 곱해 원래 크기로 되돌린다.

역 DFT가 실제로 원래 값을 되돌리는 과정도 한 번 펼쳐 보자. 복원한 값을 이라고 쓰면

괄호 안의 합은 일 때만 이 되고, 일 때는 이 된다. 즉 다른 위치에서 온 성분은 직교합 때문에 사라지고, 인 항만 남는다.

그래서 DFT는 단순히 “정보를 요약해서 버리는 계산”이 아니라, 서로 직교하는 회전 기준으로 좌표를 바꾸었다가 다시 원래 좌표로 돌아올 수 있는 변환이다.

직관 비유

친구들이 같은 박자로 박수를 치면 소리가 크게 들린다. 서로 다른 박자로 치면 소리가 섞여서 잘 들리지 않는다. DFT는 각 박자에 맞춰 봤을 때 얼마나 잘 맞는지를 재는 과정이다.

예제

상수 신호는 변화가 없으므로 성분이 크다.

이번 챕터의 실험실에서는 길이 인 신호를 사용한다.

그래서 가 잘 맞는다. 실수 신호에서는 반대 방향 회전 짝도 같이 나타나므로 도 크게 보인다.

여기서 이므로 이다. 이것은 바퀴를 반대 방향으로 도는 검사 파동처럼 읽을 수 있다.

이때 은 켤레복소수 짝으로 나타난다. 크기는 같고, 회전 방향 정보는 반대로 들어 있다.

신호가 여러 기본 파동의 합이면 DFT 점수표에서도 여러 막대가 동시에 커진다. 예를 들어

계열과 계열이 함께 섞인 신호다. DFT는 “가장 큰 하나”만 찾는 도구가 아니라, 각 가 얼마나 들어 있는지 모두 재는 점수표다.

다만 실제 신호가 항상 정수 번호 에 딱 맞는 것은 아니다. 길이 인 관측 구간 안에서 2.5바퀴 도는 파동을 잘라 보면, DFT가 검사하는 어느 한쪽과도 완전히 일치하지 않는다. 이때 에너지가 한 막대에만 모이지 않고 주변 여러 로 퍼져 보이는데, 이를 스펙트럼 누설 또는 leakage라고 부른다.

누설이 생기는 이유는 DFT가 개의 샘플을 한 주기로 반복한다고 약속하기 때문이다. 관측 구간 안에 2.5바퀴만 들어 있으면, 마지막 샘플 뒤에 같은 배열을 다시 붙일 때 끝점과 시작점이 매끄럽게 이어지지 않는다. 그래프를 반복해서 붙인 그림에는 갑작스러운 꺾임이 생기고, 이런 꺾임은 여러 주파수를 섞어야 표현된다. 따라서 “파동 하나면 막대 하나”라고 외우기보다, 관측 구간 안에 정수 개의 주기가 딱 들어올 때 가장 깨끗하게 한두 막대가 선명해진다고 이해해야 한다.

아주 작은 예시로 에서 한 칸만 직접 계산해 보자.

일 때는 다음 합을 계산한다.

각 항을 표로 쓰면 이렇다.

따라서 이다. 직접 한 칸을 계산해 보면 DFT가 “검사 회전값을 곱해 더하는 점수”라는 뜻이 더 선명해진다.

정규화 위치는 책마다 조금 다를 수 있다. 이 자료에서는 DFT에는 을 붙이지 않고, 역 DFT에만 을 붙인다. 어떤 책은 DFT와 역 DFT에 씩 나누기도 한다.

눈으로 확인하는 순서

복소평면 화면에서는 각 항 을 벡터로 본다.

파동 비교 화면에서는 원래 신호와 검사 파동을 같은 좌표평면에 겹쳐 본다.

벡터 합 화면에서는 벡터들을 꼬리물기로 이어 붙여 가 어디에 도착하는지 작도한다.

주파수 점수표에서는 모든 를 막대로 비교한다.

3D 지형 화면에서는 검사 주파수와 위상을 조금씩 바꿨을 때 점수가 어떤 모양을 만드는지 본다.

손풀이 체크

  1. 일 때 은?

    답 보기

    1+1+1+1=41+1+1+1=4

  2. 일 때 위 예시처럼 은?

    답 보기

    22

  3. 가 DFT 결과의 크기라면 값은?

    답 보기

    55

  4. 이고 이면 큰 막대는 어느 칸에 나타나고, 각 크기는 얼마일까?

    답 보기

    k=2k=2k=6k=6에 나타나며, 각 크기는 282=8\frac{2\cdot8}{2}=8이다.

다음으로 이어지는 생각

DFT 결과를 이용하면 다항식과 배열 곱셈도 빠르게 계산할 수 있다. 그 전에 같은 DFT 식을 점수, 행렬, 다항식이라는 세 언어로 한 번 묶어 보자.

이번 장에서 기억할 3문장

  1. DFT는 각 kk에 대해 신호와 검사 회전이 얼마나 잘 맞는지 점수를 매긴다.
  2. DFT 결과의 크기는 샘플 개수 NN과 켤레 짝의 영향을 받으므로 진폭과 바로 같지 않을 수 있다.
  3. 역 DFT가 되는 이유는 서로 다른 검사 회전들이 한 주기 동안 서로 지워지기 때문이다.

C++ Practice

C++로 확인하기

DFT X[k]=n=0N1x[n]e2πikn/NX[k]=\sum_{n=0}^{N-1}x[n]e^{-2\pi i kn/N}와 역 DFT를 작은 배열에서 직접 계산하기

#include <cmath>
#include <complex>
#include <iomanip>
#include <iostream>
#include <vector>

using namespace std;

using Complex = complex<double>;

vector<Complex> dft(const vector<Complex>& x, bool inverse = false) {
    const double pi = acos(-1.0);
    int N = static_cast<int>(x.size());
    vector<Complex> X(N);

    for (int k = 0; k < N; ++k) {
        for (int n = 0; n < N; ++n) {
            double sign = inverse ? 1.0 : -1.0;
            double angle = sign * 2.0 * pi * k * n / N;
            X[k] += x[n] * polar(1.0, angle);
        }
        if (inverse) {
            X[k] /= N;
        }
    }
    return X;
}

int main() {
    vector<Complex> x = {1, 0, -1, 0};
    auto X = dft(x);
    auto back = dft(X, true);

    cout << fixed << setprecision(2);
    cout << "X[1] = " << X[1].real() << " + " << X[1].imag() << "i\n";
    cout << "back[2] = " << back[2].real() << "\n";
}

연습: x=[1,1,1,1]x=[1,1,1,1]로 바꾸면 어느 kk가 커지는지 확인해 보자.