参考文献
まとめ
- numpy.fft.fft は、長さ の入力系列 を渡すと、これを 式のように分解したときの係数、つまり 式の右辺の各列ベクトルの成分の和で表したときの係数 を返してくれる。それが実は 式で計算できるので実際は 式をやってくれている(高速なアルゴリズムで)。
- 入力系列 を numpy.fft.fft に渡して出た出力系列 を numpy.fft.ifft に渡すと元の入力系列 に戻る。単に 式をやってくれているだけである。元に戻ることは計算すればわかる。
- 入力系列 を numpy.fft.fft に渡して出た出力系列 は 式の右辺の各列ベクトルの成分の和で表したときの係数なので、周波数 の波の振幅と解釈できる。ただし、整数でしか値を取らない波なので、周波数には整数を足し引きしても値は変わらない。なので、 以上の周波数成分からは を差し引いて周波数の取りうる範囲を (これは が偶数のとき)と を中心にしてもよい。こちらの流儀で numpy.fft.fft の出力系列がそれぞれどんな周波数の波の振幅と解釈できるかを取得できるのが numpy.fft.fftfreq である。入力系列のサイズ を渡すだけである。
- 入力系列 が時間 ごとにサンプリングされたものであるとき、これを反映した周波数にしたいならば、 より、 ごとの波を ごとの波にする代わりに周波数 を にすればよい。numpy.fft.fftfreq に も一緒に渡せばそれも踏まえて はそれぞれどんな周波数の波の振幅なのかを返してくれる。もっとも、numpy.fft.fft や numpy.fft.ifft は 式と 式をやるだけなので を気にすることはない。 や をグラフにプロットしたときの横軸の目盛にのみ が関わってくる。
Discrete Fourier Transform (numpy.fft) — NumPy v1.17 Manual によると、numpy.fft における離散フーリエ変換と逆離散フーリエ変換の定義は以下であるとありますね。
を に代入したら戻ってくるね。
「元に戻ったというより元々そういう係数を出していたのだ」といわれても、 で係数が出てくること自体が非自明なんですがそれは…。まあ御託はいいです。とりあえず numpy.fft を使ってみましょう。とりあえず正弦波からサンプリングした系列を numpy.fft.fft して、再び numpy.fft.ifft してみればよいでしょう。
import numpy as np %matplotlib inline import matplotlib.pyplot as plt from pylab import rcParams rcParams['figure.figsize'] = 9, 3 rcParams['font.family']='Ume Hy Gothic O5' rcParams['font.size'] = 15 T = 4 f = 1 / T dt = 0.5 x = np.arange(-2, 2, dt) y = np.sin(2 * np.pi * f * x) X = np.fft.fftfreq(y.size, d=dt) Y = np.fft.fft(y) y_ = np.fft.ifft(Y) print('周期 {}'.format(T)) print('周波数 {}'.format(f)) print('サンプル数 {}'.format(y.size)) print('サンプリング時間間隔 {}'.format(dt)) print('サンプリングレート {}'.format(1/dt)) fig, ax = plt.subplots(5, 1, figsize=(7, 11)) ax[0].scatter(x, y) ax[0].set_ylabel('元の信号') ax[1].scatter(X, Y.real) ax[1].set_ylabel('fftの実部') ax[2].scatter(X, Y.imag) ax[2].set_ylabel('fftの虚部') ax[3].scatter(x, y_.real) ax[3].set_ylabel('ifftの実部') ax[4].scatter(x, y_.imag) ax[4].set_ylabel('ifftの虚部') plt.subplots_adjust(hspace=0.4) plt.show()
周期 4 周波数 0.25 サンプル数 8 サンプリング時間間隔 0.5 サンプリングレート 2.0
みづらいけど、-0.25 ヘルツの振幅が 、+0.25 ヘルツの振幅が になっているね。確かに周期4の正弦波が復元されるね。
では、上の例は ですから、周波数 0, 1/8=0.125, …, 7/8=0.875 ヘルツの成分の振幅になって…いませんね。出ているのは -1, -0.75, -0.5, -0.25, 0, 0.25, 0.5, 0.75 ヘルツの成分の振幅ですよ?
2点あって、まず、 が整数しか取らない世界では の に整数を足し引きしても値は変わらない。だから、0, 1/8, 2/8, 3/8, 4/8, 5/8, 6/8, 7/8 ヘルツの成分を、4/8, 5/8, 6/8, 7/8, 0, 1/8, 2/8, 3/8 ヘルツと並び替えて、さらに -4/8, -3/8, -2/8, -1/8, 0, 1/8, 2/8, 3/8 ヘルツだと考えていい。描画するならこう並べ替えた方が低周波数成分が真ん中に集まってみやすいだろう。加えてもう1点は、もし元の入力系列に対して「これは時間 ごとにサンプリングしたものだ」という思いを込めたいなら、 と、周波数側にも を押し付けることができる。部長は にしたから、これを反映して周波数を1/0.5倍=2倍にする。つまり、-8/8, -6/8, -4/8, -2/8, 0, 2/8, 4/8, 6/8 ヘルツになる。-1, -0.75, -0.5, -0.25, 0, 0.25, 0.5, 0.75 ヘルツと一致するね。これまで単位時間に起きていたと思っていたことを「やっぱり時間 0.5 の間に起きていたことにして」としたんだから、周波数は2倍になるよね。でも、それらは人間がグラフの軸目盛にかく数字の問題でしかない。入力の点数が同じ8点なら、 がどうあったって基底ベクトルは同じだからね(記事の下部の2つのケース;横軸の目盛と何ヘルツと解釈するかが違うだけで、絵は全く同じ)。まとめると、numpy.fft.fft の返り値は素朴には 0, 1/8, 2/8, 3/8, 4/8, 5/8, 6/8, 7/8 ヘルツに対応する振幅と受け取れるんだけど、この2点を反映すると何ヘルツに対応する振幅と受け取るべきかを取得できるのが、部長が上のスクリプトでよんでいる np.fft.fftfreq だね。
numpy.fft.fft に8点の系列を渡したときの基底(左列が実部、右列が虚部)
各行が の各列ベクトルに対応。x = np.linspace(0.0, 7.0, 8) x_ = np.arange(0.0, 8.0, 0.01) # 点だけだとよくわからないのでcos, sinカーブを描画する用 vec_f = np.sort(np.fft.fftfreq(8, d=1)) # 8点の系列を渡したときの周波数成分 fig, ax = plt.subplots(8, 2, figsize=(11, 13)) for i, f in enumerate(vec_f): ax[i, 0].plot(x_, np.cos(2 * np.pi * f * x_), linestyle='dotted') ax[i, 1].plot(x_, np.sin(2 * np.pi * f * x_), linestyle='dotted') ax[i, 0].scatter(x, np.cos(2 * np.pi * f * x)) ax[i, 1].scatter(x, np.sin(2 * np.pi * f * x)) ax[i, 0].text(-0.5, 0.75, '{} Hz'.format(f), transform=ax[i, 0].transAxes) plt.subplots_adjust(hspace=0.4) plt.show()
numpy.fft.fft に8点の系列を渡してΔt=0.5と指定したときの基底(左列が実部、右列が虚部)
各行が の各列ベクトルに対応。x = np.linspace(0.0, 3.5, 8) x_ = np.arange(0.0, 4.0, 0.01) # 点だけだとよくわからないのでcos, sinカーブを描画する用 vec_f = np.sort(np.fft.fftfreq(8, d=0.5)) # 8点の系列を渡してかつΔt=0.5としたときの周波数成分 fig, ax = plt.subplots(8, 2, figsize=(11, 13)) for i, f in enumerate(vec_f): ax[i, 0].plot(x_, np.cos(2 * np.pi * f * x_), linestyle='dotted') ax[i, 1].plot(x_, np.sin(2 * np.pi * f * x_), linestyle='dotted') ax[i, 0].scatter(x, np.cos(2 * np.pi * f * x)) ax[i, 1].scatter(x, np.sin(2 * np.pi * f * x)) ax[i, 0].text(-0.5, 0.75, '{} Hz'.format(f), transform=ax[i, 0].transAxes) plt.subplots_adjust(hspace=0.4) plt.show()