chakokuのブログ(rev4)

テック・コミック・DTM・・・ごくまれにチャリ

もろもろをすっ飛ばしてDFTを実装する

背景:MicroPython上で動かせる軽量FFTライブラリを作りたい
課題:微積三角関数フーリエ級数フーリエ変換→離散フーリエ変換と積み上げて理解したかったけど、フーリエ変換→離散フーリエ変換の間がどうにも埋まらない
取り組み:途中の証明をすっ飛ばして、離散フーリエ変換(DFT)の公式ありきで、実装する
結論:愚直実装版だがDFTは動いた(サンプル数8でテスト)
詳細:

これまで式を追いかけてなんとかDFT/FFTを理解したいと思ってきたが、フーリエ変換と離散フーリエ変換(DFT)の間はかなりの思考過程を経る必要があるように思えてこの間のギャップが埋まらない。世の中の記事や資料には、離散フーリエ変換(DFT)の公式からソフトウエア実装を説明する記事は結構あるので、式を追いかけて理解するのは一旦やめて、公式ありき*1で実装してみる。DFTを実装して動作確認したら、次は計算量を減らすためのバタフライ演算を導入して、FFTになると思う。一旦FFTは置いといて、愚直に2重ループを回すDFTをPython等で実装してみる。

参考資料をいろいろ見ていると、DFTの公式としては以下

eの-2πitk/nはsin/cosで表現すると、cos(2 * pie * t * k / n) + i * sin(- 2 * pie * t * k / n )
これをサンプル実装すると以下のような2重ループになると。

for (k = 0; k < N; k++) {     //  k倍の周波数を計算

    X.real = 0;  // 実数部
    X.imag = 0;  // 虚数部

    for (t = 0; t < N; t++) {   //  Nサンプル分を加算
        angle = 2.0 * Pie * t * k  /  n ;
        X.real +=  x[t].real * cos(angle) + x[t].imag * sin(angle);    // 複素数の実数部計算
        X.imag +=  x[t].imag * cos(angle) - x[t].real * sin(angle);   // 複素数の虚数部計算
    }

    save(X);
}

Pythonではライブラリを入れなくても複素数表現が可能

>>> angle = complex(1,2)
>>> angle
(1+2j)
>>> angle.real
1.0
>>> angle.imag
2.0

上記プログラムをPythonで実装すると以下*2

import math

def DFT(x, k):

    n = len(x)
    for k in range(n):   #  calc each freq
        sum_real  = 0    #  real number of sum
        sum_imag  = 0    #  imaginary number of sum
        for t in range(n):    # sum for N samples 
            angle = 2.0 * math.pi * t * k  /  n
            sum_real += x[t].real * math.cos(angle) + x[t].imag * math.sin(angle)
            sum_imag += x[t].imag * math.cos(angle) - x[t].real * math.sin(angle)
        sum = complex(sum_real, sum_imag)
        print(k, sum)

本のサンプルデータを使ってテスト

#!/usr/bin/python3

import math

def DFT(x, k):
    output = []
    n = len(x)
    for k in range(n):   #  calc each freq
        sum_real  = 0    #  real number part of sum
        sum_imag  = 0    #  imaginary number part of sum
        for t in range(n):    # sum for samples (N)
            angle = 2.0 * math.pi * t * k  /  n
            sum_real += x[t].real * math.cos(angle) + x[t].imag * math.sin(angle)
            sum_imag += x[t].imag * math.cos(angle) - x[t].real * math.sin(angle)
        output.append(complex(sum_real, sum_imag))
    return output

#
# test
#

# set sampling data
x = [0j] * 4
x[0] = complex(1,0)
x[1] = complex(1,0)
x[2] = complex(-1,0)
x[3] = complex(-1,0)

print("*** input data ***")
for data in x:
   print(data)

# confert DFT
out = DFT(x, 4)

print("*** DFT output ***")
for k  in range(len(out)):
   print(k, out[k], abs(out[k]))

実行結果(本の説明とは合っているが。。)

$ ./dft.py
*** input data ***
(1+0j)
(1+0j)
(-1+0j)
(-1+0j)
*** DFT output ***
0 0j 0.0
1 (2-2j) 2.8284271247461903
2 0j 0.0
3 (1.9999999999999993+2.0000000000000004j) 2.82842712474619

周期1のSin波の場合

samples = 8
x = [0j] * samples
for i in range(samples):
   x[i] = complex(math.sin(math.pi * 2 * i / samples),0)


# confert DFT
out = DFT(x, samples)

print("*** DFT output ***")
for k  in range(len(out)):
   print(k, out[k], abs(out[k]))

実行結果、1倍の周波数で絶対値4と検出(複素数虚数部が4)*3

$ ./dft.py
*** DFT output ***
0 (-2.220446049250313e-16+0j) 2.220446049250313e-16
1 (3.3306690738754696e-16-4j) 4.0
2 (8.104001475739143e-17+1.1102230246251565e-16j) 1.3745339441409415e-16
3 (5.551115123125783e-17-7.771561172376096e-16j) 7.791361360319881e-16
4 (2.220446049250313e-16+2.029061253294536e-16j) 3.007901299453244e-16
5 (1.3877787807814457e-15+1.7763568394002505e-15j) 2.2541902238434278e-15
6 (2.567266157150065e-15+8.881784197001252e-16j) 2.7165633485838455e-15
7 (-3.3861802251067274e-15+3.999999999999999j) 3.999999999999999

sin(Θ) + sin(3Θ)の場合、

1倍の周波数と3倍の周波数で絶対値4と検出(複素数虚数部が4)

*** DFT output ***
0 (-8.881784197001252e-16+0j) 8.881784197001252e-16
1 (4.440892098500626e-16-4j) 4.0
2 (4.898587196589412e-16+4.440892098500626e-16j) 6.611934599881869e-16
3 (7.771561172376096e-16-4.000000000000002j) 4.000000000000002
4 (1.7763568394002505e-15+1.3855296899767904e-15j) 2.2528062816609927e-15
5 (-4.440892098500626e-15+4.0000000000000036j) 4.0000000000000036
6 (5.5141545874470235e-15-4.440892098500626e-16j) 5.5320083189171275e-15
7 (-6.328271240363392e-15+3.9999999999999987j) 3.9999999999999987

とまぁここまではそうとして、、元波形がcosの場合は??実数部分で検知?
cos(Θ) + cos(3Θ)の場合...

以下のように1倍周波数と3倍周波数の実数部分で検知されている。。なるほど。

*** DFT output ***
0 (-7.428289848809505e-16+0j) 7.428289848809505e-16
1 (4.000000000000001-2.857720186560418e-16j) 4.000000000000001
2 (-3.6739403974420633e-16+7.313866074287308e-16j) 8.184770919003633e-16
3 (3.999999999999999-7.756307383149842e-16j) 3.999999999999999
4 (1.0335278545193e-15-9.79717439317882e-16j) 1.424087808729102e-15
5 (3.999999999999999+2.7350656169507486e-15j) 3.999999999999999
6 (-3.67394039744203e-16-2.6908214860644963e-15j) 2.715786930211188e-15
7 (4.000000000000001+2.2452068972918088e-15j) 4.000000000000001

位相ずれは、実数部と虚数部の比率が変わると。。。
sin(Θ + π/4 ) + sin(3Θ)の場合

以下の通り、1倍の周波数の実数部と虚数部で、同じ比率になっている。2√2か。。なるほど。。

*** DFT output ***
0 (-1.1102230246251565e-15+0j) 1.1102230246251565e-15
1 (2.8284271247461903-2.828427124746191j) 4.000000000000001
2 (2.4757346845116496e-16+1.1102230246251565e-16j) 2.713274293635243e-16
3 (9.43689570931383e-16-4.000000000000002j) 4.000000000000002
4 (1.5543122344752192e-15+9.797174393178836e-16j) 1.837316734573258e-15
5 (-3.2751579226442118e-15+4.000000000000003j) 4.000000000000003
6 (5.50935717240673e-15-1.3322676295501878e-15j) 5.6681525640985315e-15
7 (2.828427124746186+2.8284271247461925j) 3.9999999999999987

DC成分が含まれている場合、、例えば、 sin(x) + 2 の場合

DFTの結果は以下、DC成分を示すk=0の出力が16になっている

*** DFT output ***
0 (16+0j) 16.0
1 (-7.771561172376096e-16-4j) 4.0
2 (-5.541681397207245e-16-6.661338147750939e-16j) 8.665082724754263e-16
3 (-3.3306690738754696e-16+9.992007221626409e-16j) 1.0532500405730103e-15
4 (2.220446049250313e-16-7.768113139884291e-16j) 8.07923031059731e-16
5 (-2.7755575615628914e-15-6.661338147750939e-16j) 2.8543745438774784e-15
6 (-3.1876482493291672e-15-2.220446049250313e-16j) 3.1953724615492976e-15
7 (3.6637359812630166e-15+4.000000000000002j) 4.000000000000002

周期が整数倍ではなかった場合(計測範囲が周期からずれている場合) sin(1.5x)で1.5周期計測してしまった場合

DFTを計算すると、DC成分、1倍成分、2倍成分が検知されている。

*** DFT output ***
0 (1.414213562373095+0j) 1.414213562373095
1 (2.707106781186547-0.2928932188134523j) 2.722905353179411
2 (-2.4142135623730954+0.9999999999999998j) 2.613125929752753
3 (-0.7071067811865462-0.29289321881345254j) 0.7653668647301783
4 (-0.585786437626905-3.0796421157949656e-16j) 0.585786437626905
5 (-0.7071067811865479+0.29289321881345154j) 0.7653668647301796
6 (-2.4142135623730936-1.0000000000000022j) 2.6131259297527527
7 (2.707106781186549+0.29289321881345765j) 2.722905353179413

証明すっ飛ばしてサンプルコードを書いた。細かい理屈は追いつかないけど、まぁ挙動は分かった。この実装だと愚直に同じ計算を何度も行っているため、サンプル数が大きくなると実行時間が問題になる(サンプル数8の場合、8x8で64回)。再計算を削減するためFFTを使うことになるのだろう(多分)

■参考資料
How to implement the discrete Fourier transform
https://www.robots.ox.ac.uk/~sjrob/Teaching/SP/l7.pdf

*1:公式の証明はあとから理解する

*2:補足:DFT関数の引数kは使っていません(後日修正します)

*3:絶対値がなぜ4なのか?は勉強不足で分からず