高速フーリエ変換は離散フーリエ変換の高速なアルゴリズム
1の原始乗根による表現¶
高速フーリエ変換¶
が2の冪乗であれば、離散フーリエ変換の計算が効率化される(具体的には から になる)これを使うのが高速フーリエ変換。
単位円での表現¶
1の原始乗根を次々と冪乗すると、複素平面の単位円周上を回転する
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()
例えばのときは、であり、である。以下同様。
単位円で描くとちょうど反対側に位置する。
そのため、 個のについてのみ考えればよい ことがわかる。
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()
証明
となる
前出の定理より、となっている。例えばとなっている
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()
はが複素平面上の単位円周の等分点を順にたどって1周するとき はそれらを1つおきに進んで2周する 。そのため 前半の個のみを計算すればよい から、
となる。
また「が偶数のとき」という定理より以下のように書き直せる。
→ データ数個の離散フーリエ変換 を計算すればいい。
についても同様に得られる。
これら個の離散フーリエ変換で得られるものたちを以下のようにと表記する
考察:なぜ引数はなのに回転因子はなのか?
なのと同様の形になっていると思われる。
考察:でとマイナスがつくのはなぜ?
前述の定理より、
高速フーリエ変換¶
は係数の間引き(Decimation-In-Time, DIT)を行ってを実行し、バタフライを施す。
間引きによってと分解していくと最終的にの計算になり、0次式(定数)の計算になる。
その後バタフライによって統合していく。
そのためは間引きの繰り返しとバタフライの繰り返しで分割統治法によって計算できる。
この方法を 高速フーリエ変換 (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()
