高速フーリエ変換#
高速フーリエ変換は離散フーリエ変換の高速なアルゴリズム
1の原始 乗根による表現#
1の原始
これは
上の式を満たす
は次のように書くことができる
Tip
フーリエ変換は線形変換
上記だけだと「
ベクトルと行列の形で書くと以下になる。
離散フーリエ変換を計算するには、長さ
となる数列
が成り立つ。
高速フーリエ変換#
単位円での表現#
1の原始
Show code cell 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()
Ignoring fixed x limits to fulfill fixed data aspect with adjustable data limits.

証明
定義
である。ゆえに
例えば
単位円で描くとちょうど反対側に位置する。
そのため、
Show code cell 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()
Ignoring fixed x limits to fulfill fixed data aspect with adjustable data limits.

証明
前出の定理より、
Show code cell 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()
Ignoring fixed x limits to fulfill fixed data aspect with adjustable data limits.

を定義すると
は
と書くことができる。
つまり、 複素平面上の単位円周の
この計算を
間引き#
上記の多項式は次のように書き直せる
ただし、
とおいた。この係数の 間引き は高速フーリエ変換における重要な計算手順の一つ。
となる。
また「
→ データ数
これら
バタフライ#
この計算を バタフライ と呼び、式中の
考察:なぜ引数は なのに回転因子は なのか?
考察: で とマイナスがつくのはなぜ?
前述の定理より、
高速フーリエ変換#
間引きによって
その後バタフライによって統合していく。
そのため
この方法を 高速フーリエ変換 (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)]
Show code cell 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
Show code cell 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()
