Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

高速フーリエ変換

高速フーリエ変換は離散フーリエ変換の高速なアルゴリズム

1の原始NN乗根による表現

ωN\omega_Nを用いると、離散フーリエ変換の式

fl=k=0N1Fkei2πkl/N,Fk=1Nl=0N1flei2πkl/Nf_l=\sum_{k=0}^{N-1} F_k e^{i 2 \pi k l / N}, \quad F_k=\frac{1}{N} \sum_{l=0}^{N-1} f_l e^{-i 2 \pi k l / N}

は次のように書くことができる

fl=k=0N1FkωNkl,Fk=1Nl=0N1flωNklf_l=\sum_{k=0}^{N-1} F_k \omega_N^{k l}, \quad F_k=\frac{1}{N} \sum_{l=0}^{N-1} f_l \omega_N^{-k l}

ベクトルと行列の形で書くと以下になる。

(f0f1f2fN1)=(11111ωNωN2ωNN11ωN2ωN4ωN2(N1)1ωNN1ωN2(N1)ωN(N1)(N1))(F0F1F2FN1)\left(\begin{array}{c} f_0 \\ f_1 \\ f_2 \\ \vdots \\ f_{N-1} \end{array}\right)=\left(\begin{array}{ccccc} 1 & 1 & 1 & \cdots & 1 \\ 1 & \omega_N & \omega_N^2 & \cdots & \omega_N^{N-1} \\ 1 & \omega_N^2 & \omega_N^4 & \cdots & \omega_N^{2(N-1)} \\ \vdots & \vdots & \vdots & & \vdots \\ 1 & \omega_N^{N-1} & \omega_N^{2(N-1)} & \cdots & \omega_N^{(N-1)(N-1)} \end{array}\right)\left(\begin{array}{c} F_0 \\ F_1 \\ F_2 \\ \vdots \\ F_{N-1} \end{array}\right)
(F0F1F2FN1)=1N(11111ωN1ωN2ωN(N1)1ωN2ωN4ωN2(N1)1ωN(N1)ωN2(N1)ωN(N1)(N1))(f0f1f2fN1)\left(\begin{array}{c} F_0 \\ F_1 \\ F_2 \\ \vdots \\ F_{N-1} \end{array}\right)=\frac{1}{N}\left(\begin{array}{ccccc} 1 & 1 & 1 & \cdots & 1 \\ 1 & \omega_N^{-1} & \omega_N^{-2} & \cdots & \omega_N^{-(N-1)} \\ 1 & \omega_N^{-2} & \omega_N^{-4} & \cdots & \omega_N^{-2(N-1)} \\ \vdots & \vdots & \vdots & & \vdots \\ 1 & \omega_N^{-(N-1)} & \omega_N^{-2(N-1)} & \cdots & \omega_N^{-(N-1)(N-1)} \end{array}\right)\left(\begin{array}{c} f_0 \\ f_1 \\ f_2 \\ \vdots \\ f_{N-1} \end{array}\right)

離散フーリエ変換を計算するには、長さNNの任意の数列a0,a1,a2,,aN1a_0, a_1, a_2, \ldots, a_{N-1}に対して

bk=l=0N1alωNkl,k=0,1,2,,N1b_k=\sum_{l=0}^{N-1} a_l \omega_N^{k l}, \quad k=0,1,2, \ldots, N-1

となる数列b0,b1,b2,,bN1b_0, b_1, b_2, \ldots, b_{N-1}が計算できればよい

{Fk}\{ F_k \} から {fl}\{f_l\}を計算するには、bk=flb_k = f_l, ak=Fka_k = F_kとおけば

fl=k=0N1FkωNklf_l =\sum_{k=0}^{N-1} F_k \omega_N^{k l}

が成り立つ。

{fl}\{f_l\} から {Fk}\{ F_k \} を計算するには、ωN=ωN1\overline{\omega_N}=\omega_N^{-1} であるから

Fk=1Nl=0N1flωNklF_k=\frac{1}{N} \overline{\sum_{l=0}^{N-1} \overline{f_l} \omega_N^{k l}}

高速フーリエ変換

NNが2の冪乗であれば、離散フーリエ変換の計算が効率化される(具体的にはO(N)O(N) から O(NlogN)O(N\log N)になる)これを使うのが高速フーリエ変換。

単位円での表現

1の原始NN乗根ωN\omega_Nを次々と冪乗すると、複素平面の単位円周上を回転する

Source
import cmath
import numpy as np
import matplotlib.pyplot as plt

fig, ax = plt.subplots(figsize=(4, 3))
ax.set_aspect('equal', 'box')

# 軸を描画
ax.arrow(-1.2, 0, 2.4, 0, head_width=0.05, head_length=0.1, fc='black', ec='black', length_includes_head=True)
ax.arrow(0, -1.2, 0, 2.4, head_width=0.05, head_length=0.1, fc='black', ec='black', length_includes_head=True)

ax.text(1.3, 0, "1", fontsize=14, color='black')
ax.text(0, 1.3, "i", fontsize=14, color='black')

ax.set_aspect('equal', adjustable='datalim')
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.spines['left'].set_visible(False)
ax.spines['bottom'].set_visible(False)
ax.set(xticks=[], yticks=[])
ax.set(xlim=(-1.2, 1.2), ylim=(-1.2, 1.2))

# 単位円
unit_circle = plt.Circle((0, 0), 1, color='gray', fill=False)
ax.add_artist(unit_circle)

# 偶数のN
n = 8
omega_n = cmath.exp(2j * cmath.pi / n)

ax.plot([0, omega_n.real], [0, omega_n.imag], color="gray", linestyle='-', linewidth=0.8)
t = 2 * np.pi / n
theta = np.linspace(0, t, 100)
x = np.cos(theta) * 0.2
y = np.sin(theta) * 0.2
ax.plot(x, y, color="gray", linewidth=1)

for k in range(n):
    omega = omega_n**k
    ax.plot(omega.real, omega.imag, 'o', color="gray")
    ax.text(omega.real + 0.02, omega.imag + 0.02, rf'$\omega_N^{k}$', fontsize=10, color="dimgray", ha='left')

plt.show()
<Figure size 400x300 with 1 Axes>
定理

NNが偶数のとき、次の関係が成り立つ

ωNN/2=1,ωNN/2+1=ωN,ωNN/2+2=ωN2,,ωNN1=ωNN/21\omega_N^{N / 2}=-1, \quad \omega_N^{N / 2+1}=-\omega_N, \quad \omega_N^{N / 2+2}=-\omega_N^2, \quad \ldots, \quad \omega_N^{N-1}=-\omega_N^{N / 2-1}
証明

定義 ωN:=ei2π/N\omega_N := e^{i 2 \pi / N} より、

ωNN/2=(ei2π/N)N/2=eiπ=1\omega_N^{N / 2} = \left(e^{i 2 \pi / N}\right)^{N / 2} =e^{i \pi}=-1

である。ゆえに

ωNN/2+k=ωNN/2ωNk=ωNk\omega_N^{N / 2+k} = \omega_N^{N / 2} \omega_N^k=-\omega_N^k

例えばN=8N=8のときは、ωNN/2=ωN4=1=ωN0\omega_{N}^{N/2} = \omega_{N}^{4} = -1 = -\omega_{N}^{0}であり、ωNN/2+1=ωN5=ωN1=ωN\omega_{N}^{N/2+1} = \omega_{N}^{5} = -\omega_{N}^{1} = -\omega_{N}である。以下同様。

単位円で描くとちょうど反対側に位置する。

そのため、 N/2N/2個のωN\omega_Nについてのみ考えればよい ことがわかる。

Source
import cmath
import numpy as np
import matplotlib.pyplot as plt

fig, ax = plt.subplots(figsize=(4, 3))
ax.set_aspect('equal', 'box')

# 軸を描画
ax.arrow(-1.2, 0, 2.4, 0, head_width=0.05, head_length=0.1, fc='black', ec='black', length_includes_head=True)
ax.arrow(0, -1.2, 0, 2.4, head_width=0.05, head_length=0.1, fc='black', ec='black', length_includes_head=True)

ax.text(1.3, 0, "1", fontsize=14, color='black')
ax.text(0, 1.3, "i", fontsize=14, color='black')

ax.set_aspect('equal', adjustable='datalim')
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.spines['left'].set_visible(False)
ax.spines['bottom'].set_visible(False)
ax.set(xticks=[], yticks=[])
ax.set(xlim=(-1.2, 1.2), ylim=(-1.2, 1.2))

# 単位円
unit_circle = plt.Circle((0, 0), 1, color='gray', fill=False)
ax.add_artist(unit_circle)

# 偶数のN
n = 8
omega_n = cmath.exp(2j * cmath.pi / n)

ax.plot([0, omega_n.real], [0, omega_n.imag], color="gray", linestyle='-', linewidth=0.8)
t = 2 * np.pi / n
theta = np.linspace(0, t, 100)
x = np.cos(theta) * 0.2
y = np.sin(theta) * 0.2
ax.plot(x, y, color="gray", linewidth=1)

for k in range(n // 2):
    omega = omega_n**k
    ax.plot(omega.real, omega.imag, 'o', color="gray")
    ax.text(omega.real + 0.02, omega.imag + 0.02, rf'$\omega_N^{k}$', fontsize=10, color="dimgray", ha='left')

    omega = -1 * omega
    k2 = k + n // 2
    ax.plot(omega.real, omega.imag, 'o', color="steelblue")
    ax.text(omega.real + 0.02, omega.imag + 0.02, rf'$\omega_N^{k2} = -\omega_N^{k}$', fontsize=10, color="steelblue", ha='left')

plt.show()
<Figure size 400x300 with 1 Axes>
定理

NNが偶数のとき、ωN2\omega^2_Nは1の原始N/2N/2乗根として表せる。

ωN2=ωN/2\omega_N^2=\omega_{N / 2}
証明

ωN2=(ei2π/N)2=ei4π/N=ei2π/(N/2)=ωN/2\omega_N^2=\left(e^{i 2 \pi / N}\right)^2=e^{i 4 \pi / N}=e^{i 2 \pi /(N / 2)}=\omega_{N / 2} となる

前出の定理より、ωN2k=ωN/2k\omega_{N}^{2k} = \omega_{N / 2}^kとなっている。例えばωN4=ωN/22\omega_{N}^{4} = \omega_{N / 2}^2となっている

Source
import cmath
import numpy as np
import matplotlib.pyplot as plt

fig, ax = plt.subplots(figsize=(4, 3))
ax.set_aspect('equal', 'box')

# 軸を描画
ax.arrow(-1.2, 0, 2.4, 0, head_width=0.05, head_length=0.1, fc='black', ec='black', length_includes_head=True)
ax.arrow(0, -1.2, 0, 2.4, head_width=0.05, head_length=0.1, fc='black', ec='black', length_includes_head=True)

ax.text(1.3, 0, "1", fontsize=14, color='black')
ax.text(0, 1.3, "i", fontsize=14, color='black')

ax.set_aspect('equal', adjustable='datalim')
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.spines['left'].set_visible(False)
ax.spines['bottom'].set_visible(False)
ax.set(xticks=[], yticks=[])
ax.set(xlim=(-1.2, 1.2), ylim=(-1.2, 1.2))

# 単位円
unit_circle = plt.Circle((0, 0), 1, color='gray', fill=False)
ax.add_artist(unit_circle)

# 偶数のN
n = 4
omega_n = cmath.exp(2j * cmath.pi / n)

ax.plot([0, omega_n.real], [0, omega_n.imag], color="gray", linestyle='-', linewidth=0.8)
t = 2 * np.pi / n
theta = np.linspace(0, t, 100)
x = np.cos(theta) * 0.2
y = np.sin(theta) * 0.2
ax.plot(x, y, color="gray", linewidth=1)

for k in range(n):
    omega = omega_n**k
    ax.plot(omega.real, omega.imag, 'o', color="gray")
    ax.text(omega.real + 0.02, omega.imag + 0.02, rf'$\omega_{{N/2}}^{k}$', fontsize=10, color="dimgray", ha='left')

plt.show()
<Figure size 400x300 with 1 Axes>

N1N-1 次多項式

f(x)=a0+a1x+a2x2++aN1xN1=l=0N1alxlf(x)=a_0+a_1 x+a_2 x^2+\cdots+a_{N-1} x^{N-1}=\sum_{l=0}^{N-1} a_l x^l

を定義すると

bk=l=0N1alωNkl,k=0,1,2,,N1b_k=\sum_{l=0}^{N-1} a_l \omega_N^{k l}, \quad k=0,1,2, \ldots, N-1

bk=f(ωNk),k=0,1,2,,N1b_k=f\left(\omega_N^k\right), \quad k=0,1,2, \ldots, N-1

と書くことができる。

つまり、 複素平面上の単位円周のNN等分点でN1N-1次多項式f(x)f(x)を計算すればb0,b1,,bNb_0,b_1,\dots,b_Nが得られる

この計算をFFTN[f(x)]\operatorname{FFT}_N[f(x)]と表す。

FFTN[f(x)]={f(1),f(ωN),f(ωN2),,f(ωNN1)}\operatorname{FFT}_N[f(x)]=\{f(1), f(\omega_N), f(\omega_N^2), \ldots, f(\omega_N^{N-1})\}

間引き

上記の多項式は次のように書き直せる

f(x)=a0+a1x+a2x2++aN1xN1=a0+a2x2+a4x4++aN2xN2+x(a1+a3x2+a5x4++aN1xN2)=p(x2)+xq(x2)\begin{aligned} f(x)= & a_0+a_1 x+a_2 x^2+\cdots+a_{N-1} x^{N-1}\\ = & a_0+a_2 x^2+a_4 x^4+\cdots+a_{N-2} x^{N-2} \\ & +x\left(a_1+a_3 x^2+a_5 x^4+\cdots+a_{N-1} x^{N-2}\right) \\ = & p(x^2)+x q(x^2) \end{aligned}

ただし、

{p(x)=a0+a2x+a4x2++aN2xN/21q(x)=a1+a3x+a5x2++aN1xN/21\left\{\begin{array}{l} p(x)=a_0+a_2 x+a_4 x^2+\cdots+a_{N-2} x^{N / 2-1} \\ q(x)=a_1+a_3 x+a_5 x^2+\cdots+a_{N-1} x^{N / 2-1} \end{array}\right.

とおいた。この係数の 間引き は高速フーリエ変換における重要な計算手順の一つ。

p(x2)p(x^2)xxが複素平面上の単位円周のNN等分点を順にたどって1周するとき x2x^2はそれらを1つおきに進んで2周する 。そのため 前半のN/2N/2個のみを計算すればよい から、

FFTN[p(x2)]={p(1),p(ωN2),p(ωN4),,p(ωNN2)}\operatorname{FFT}_{N}[p(x^2)]=\{p(1), p(\omega_N^2), p(\omega_N^4), \ldots, p(\omega_N^{N-2})\}

となる。

また「NNが偶数のときωN2=ωN/2\omega_N^2=\omega_{N / 2}」という定理より以下のように書き直せる。

FFTN[p(x2)]={p(1),p(ωN/2),p(ωN/22),,p(ωN/2N/21)}\operatorname{FFT}_{N}[p(x^2)]=\{p(1), p(\omega_{N/2}), p(\omega_{N/2}^2), \ldots, p(\omega_{N/2}^{N/2-1})\}

データ数N/2N/2個の離散フーリエ変換 を計算すればいい。

q(x2)q(x^2)についても同様に得られる。

FFTN[q(x2)]={q(1),q(ωN/2),q(ωN/22),,q(ωN/2N/21)}\operatorname{FFT}_{N}[q(x^2)]=\{q(1), q(\omega_{N/2}), q(\omega_{N/2}^2), \ldots, q(\omega_{N/2}^{N/2-1})\}

これらN/2N/2個の離散フーリエ変換で得られるものたちを以下のようにFFTN/2\operatorname{FFT}_{N / 2}と表記する

FFTN[p(x2)]=FFTN/2[p(x)],FFTN[q(x2)]=FFTN/2[q(x)]\operatorname{FFT}_N[p(x^2)]=\operatorname{FFT}_{N / 2}[p(x)], \quad \operatorname{FFT}_N[q(x^2)]=\operatorname{FFT}_{N / 2}[q(x)]

バタフライ

f(x)f(x)は「NNが偶数のとき、ωNN/2=1,ωNN/2+1=ωN,ωNN/2+2=ωN2,,ωNN1=ωNN/21\omega_N^{N / 2}=-1, \quad \omega_N^{N / 2+1}=-\omega_N, \quad \omega_N^{N / 2+2}=-\omega_N^2, \quad \ldots, \quad \omega_N^{N-1}=-\omega_N^{N / 2-1}」という定理と f(x)=p(x2)+xq(x2)f(x) = p(x^2)+x q(x^2) により、以下のように計算できる。

{f(ωNk)=p(ωN/2k)+ωNkq(ωN/2k),k=0,1,,N/21f(ωNN/2+k)=p(ωN/2k)ωNkq(ωN/2k),k=0,1,,N/21\begin{cases} f(\omega_N^k)=p(\omega_{N / 2}^k)+\omega_N^k q(\omega_{N / 2}^k), & k=0,1, \ldots, N / 2-1 \\ f(\omega_N^{N / 2+k})=p(\omega_{N / 2}^k)-\omega_N^k q(\omega_{N / 2}^k), & k=0,1, \ldots, N / 2-1 \end{cases}

この計算を バタフライ と呼び、式中のωNk\omega_N^k回転因子(ひねり因子) と呼ぶ。

考察:なぜ引数はωN/2k\omega_{N / 2}^kなのに回転因子はωNk\omega_N^kなのか?

f(x)=p(x2)+xq(x2)f(x)= p(x^2)+x q(x^2)なのと同様の形になっていると思われる。

考察:f(ωNN/2+k)=p(ωN/2k)ωNkq(ωN/2k)f(\omega_N^{N / 2+k})=p(\omega_{N / 2}^k)-\omega_N^k q(\omega_{N / 2}^k)ωNk-\omega_N^kとマイナスがつくのはなぜ?

前述の定理より、ωNN/2+k=ωNk\omega_N^{N / 2+k} = -\omega_N^{k}

高速フーリエ変換

FFTN\operatorname{FFT}_Nは係数の間引き(Decimation-In-Time, DIT)DND_Nを行ってFFTN/2\operatorname{FFT}_{N/2}を実行し、バタフライBNB_Nを施す。

間引きによってFFTN/4,FFTN/8,FFTN/16,\operatorname{FFT}_{N/4}, \operatorname{FFT}_{N/8}, \operatorname{FFT}_{N/16}, \dotsと分解していくと最終的にFFT1\operatorname{FFT}_{1}の計算になり、0次式(定数)の計算になる。

その後バタフライによって統合していく。

そのためFFTN\operatorname{FFT}_{N}は間引きの繰り返しとバタフライの繰り返しで分割統治法によって計算できる。

この方法を 高速フーリエ変換 (FFT)と呼ぶ。

Pythonの実装

by chatGPT

import cmath

def fft_recursive(x: list[complex]) -> list[complex]:
    """
    Args: 入力信号(複素数リスト)
    Returns: FFTの結果(周波数成分Fのリスト)(複素数のリスト)
    """
    N = len(x)
    if N <= 1:  # 再帰の終了条件
        return x

    # 偶数番目と奇数番目に分割
    even = fft_recursive(x[0::2])
    odd = fft_recursive(x[1::2])

    # バタフライ操作
    T = [cmath.exp(-2j * cmath.pi * k / N) * odd[k] for k in range(N // 2)]
    return [even[k] + T[k] for k in range(N // 2)] + \
           [even[k] - T[k] for k in range(N // 2)]
Source
# テスト用データ
input_signal = [0, 1, 2, 3, 4, 5, 6, 7]  # サンプルデータ

# 信号を複素数に変換
input_signal = [complex(x) for x in input_signal]

# FFTを計算
fft_result = fft_recursive(input_signal)

# 結果を表示
print("FFTの結果:")
for i, value in enumerate(fft_result):
    print(f"X[{i}] = {value:.3f}")

# NumPyによるFFT計算
import numpy as np
fft_result = np.fft.fft(input_signal)

# 結果を表示
print("NumPy FFT結果:")
for i, value in enumerate(fft_result):
    print(f"X[{i}] = {value:.3f}")
FFTの結果:
X[0] = 28.000+0.000j
X[1] = -4.000+9.657j
X[2] = -4.000+4.000j
X[3] = -4.000+1.657j
X[4] = -4.000+0.000j
X[5] = -4.000-1.657j
X[6] = -4.000-4.000j
X[7] = -4.000-9.657j
NumPy FFT結果:
X[0] = 28.000+0.000j
X[1] = -4.000+9.657j
X[2] = -4.000+4.000j
X[3] = -4.000+1.657j
X[4] = -4.000+0.000j
X[5] = -4.000-1.657j
X[6] = -4.000-4.000j
X[7] = -4.000-9.657j
Source
import numpy as np
import matplotlib.pyplot as plt

# サンプル信号を作成(サンプリング周波数: 1000 Hz)
fs = 1000  # サンプリング周波数
t = np.linspace(0, 1, fs, endpoint=False)  # 時間軸
frequencies = [100, 200]
signal = np.sin(2 * np.pi * frequencies[0] * t) + 0.5 * np.sin(2 * np.pi * frequencies[1] * t)

# FFTを計算
N = len(signal)
fft_result = np.fft.fft(signal)
frequencies = np.fft.fftfreq(N, 1/fs)  # 周波数軸

# 振幅スペクトルを取得
amplitude = np.abs(fft_result) / N  # 振幅正規化

# 結果をプロット
plt.figure(figsize=(8, 4))
plt.plot(frequencies[:N//2], amplitude[:N//2])  # 正の周波数成分のみプロット
plt.title("Frequency Spectrum")
plt.xlabel("Frequency (Hz)")
plt.ylabel("Amplitude")
plt.grid()
plt.show()
<Figure size 800x400 with 1 Axes>