【初心者】Pythonでのフーリエ変換(FFT)の方法と注意点

# はじめに
Pythonには高速フーリエ変換が簡単にできる「FFT」というパッケージが存在します。
とても簡便な反面、初めて扱う際にはいくつか**分かりにくい点**や**注意が必要な点**がありました。

ということで、私がつまづいた箇所などを踏まえて、FFTの使い方を**できるだけ分かりやすく**本記事にまとめておこうと思います。<font color="silver">（クドいほど強調した箇所もあるので、必要がなければ読み飛ばしてもらって構いません。）</font>



# 用語
## FFTとは
FFTとは、**高速フーリエ変換**のことです。
しかし、Pythonの FFT を扱うにあたって、**高速フーリエ変換**の中身**を学ぶ必要はありません**。

実装レベルでは、「離散時間フーリエ変換」「離散フーリエ変換」までの知識があれば素晴らしいですが、そこまで必要ではないと思います。**「フーリエ級数展開」「フーリエ変換」はもちろん押さえておいてください**。
ちなみに高速フーリエ変換として知っておくべきことは、次のことです。

:::note info
**FFT**（**高速フーリエ変換**）
サンプル数 $N$ が**2の累乗**の場合のみ、高速フーリエ変換される。
そうでない場合は、離散フーリエ変換が行われる。
:::

高速フーリエ変換を使うと、離散フーリエ変換では $O(N^2)$ の時間かかってしまう計算が、$N = 2^k$ の場合にのみ、$O(N\log  N)$ の時間で計算できます。
今回は $N$ がそこまで大きくないので、自由に $N$ を指定しています。

また、以下にも注意してください。

:::note warn
**高速フーリエ変換は、離散フーリエ変換の仲間**である。
```math
(高速フーリエ変換) \subset (離散フーリエ変換)
```
:::
離散フーリエ変換と全く別の理論、というわけではありません。<font color="silver">（だいぶアクロバティックで異次元なことはしてますが…）</font>
「離散フーリエが分かれば大丈夫」と言うわけはここにあります。

## サンプリングレート（サンプリング周波数）

**サンプリングレートとは、**「波形をデジタルで扱う際に、**1秒間あたりいくつサンプリングするか**」を表した値のことです。

:::note
**例**
* CDのサンプリングレートは基本的に 44.1kHz である。
* 最近のストリーミング音楽では 48kHz のものも増えてきている。
:::

今回はサンプリングレート $f_s = 44.1$ [kHz] とします。


# FFTの使い方
## そのまえに…
高速フーリエ変換は、**$N$個のデータを、別の$N$個のデータに**変換するものです。
これからの説明では、便宜上 $y(t)$ のように関数として説明しますが、実際は $y_i \\; (i=1, 2,..., N)$ のように離散化されています。そこだけ注意して読んでいただけると幸いです。

## 波のグラフ y(t)
では本題に参りましょう。
まず、時間に関する波の式 $y(t)$ を準備します。今回は簡単な例として振幅が1のサイン波
```math
y(t) = \sin(2\pi f_0 t)
```
を用います。これを**音波の式**と思うことにすると、$f_0$ は（一定の）周波数です。
このグラフをPythonくんにお願いして描いてもらっちゃいます！


Python初心者のためにゴリゴリにメモをしておきました🦍📜

```python
import numpy as np # numpyをインポート
import matplotlib.pyplot as plt # matplotlibをインポート

### 量子化(離散化) ###
f_s = 44100 # サンプリングレート f_s[Hz] (任意)
t_fin = 1 # 収録終了時刻 [s] (任意)
dt = 1/f_s # サンプリング周期 dt[s]
N = int(f_s * t_fin) # サンプル数 [個]

### 入力信号 y(t) ###
f0 = 440 # 周波数 f0[Hz]
t = np.arange(0, t_fin, dt) # 離散時間 t[s]
y = np.sin(2*np.pi*f0*t) # y(t) を生成

### y(t) のグラフ ###
plt.plot(t, y) # y-t グラフのプロット
plt.xlim(0, 10*10**-3) # 横軸に関する描画範囲指定
plt.show() # グラフの表示
```
これを実行すると、以下のグラフが得られます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648376/74acc05e-f238-cc02-169b-dc8ae119d097.png)
これが周波数 440 Hz、ドレミで言うと「ラの音」の波形です🎶

### 補足
:::note
**サンプリング周期 $dt$**
サンプリング周期 $dt$ [s] は、サンプリング周波数 $f_s$ を用いて、
```math
dt = \frac{1}{f_s} 
```
と表せる。
:::
:::note
**サンプル数 $N$**
サンプル数 $N$ は、$\newcommand{\tfin}{t_\mathrm{fin}} \tfin$ と $dt$ を用いて、
```math
N = \frac{\tfin}{dt} = f_s \tfin
```
と表せる。
:::

## 波のグラフを周波数のグラフに（フーリエ変換）
音の正体を知らずにこの波形だけを見たとき、「コイツは本当に 440 Hz の音なのか？」と疑いたくなりますよね。
そこで登場するのが FFT です。

FFTを実行することで、**音のスペクトル**のグラフを得ることができます！
<font color="silver">音楽をやっている人なら、"イコライザー(EQ) のグラフ" として見たことがあるかもしれません。
（あるいは、"Photoshopのあのグラフ" として見たことが以下略…）</font>

FFTの実行と、スペクトルを得るプログラムは以下のようになります。
```python
### FFT: tの関数をfの関数にする ###
y_fft = np.fft.fft(y) # 離散フーリエ変換
freq = np.fft.fftfreq(N, d=dt) # 周波数を割り当てる（※後述）
Amp = abs(y_fft/(N/2)) # 音の大きさ（振幅の大きさ）
```
やっていることは単純で、

:::note
1. 時間 $t$ の関数 $y(t)$ を**フーリエ変換**し、周波数 $f$ の関数 $\newcommand{\ty}{\tilde y_\mathrm{fft}} \ty(f)$ を得る。
1. サンプル数 $N$ と、**$f$ を関係づける**。$\ty(f)$ の周波数 $f$ には `freq` に格納した値を用いる。
1. 各周波数 $f$ ごとの**音の大きさ $A$ を算出**する。
:::

の3つです。この3ステップを詳しく見ていきましょう。

### 1. フーリエ変換
フーリエ変換は
```math
\ty(f) = \mathcal{F}[y(t)]
```
と書けるのでした。（$\mathcal{F}$ は、「 $[\quad ]$ 内の関数をフーリエ変換しますよ～」という演算子です。）
具体的な表式（$e^{-i\omega t}$とか）は今回あまり意識しなくてもOKです。

Python では、 `np.fft.fft(y)` のように書き、`y` 部分に変換したいデータを入力します。

:::note
とにかく、**時間の関数が周波数の関数に変換される**ことが大事です。
:::

### 2. 周波数 f の関係づけ
離散フーリエ変換を学ぶと分かりますが、フーリエ変換で生まれた関数 $\ty(f)$ の 周波数 $f$ は、実は $t$ と同様に離散化されています。これにともなって、**$f$ は $N, dt$ に依存**しているのです。
つまり、$f$ は $N, dt$ の値（すなわちサンプル数が多いか少ないか）によって関係が変化します。固定された $N$ の場合、$f$ は、サンプルの $i$ 番目の関数$(= f(i))$ です。

:::note
サンプルの数 $N$ とサンプリング周期 $dt$ によって、 $f$ が変わってくる。
:::

なので、`np.fft.fftfreq(N, d=dt)` にサンプル数 `N` とサンプリング周期 `dt` を代入する、というわけなんですね。



また、$f/2$ [Hz] の周波数を、**ナイキスト周波数** と呼び、それより大きい周波数 $f$ に関して、$\ty(f)$ はオバケのような値をとります。理論的に変な値になってしまうことが知られているということです。したがって、グラフで表す際はその値を除いてやる必要があります。

今回、サンプリングレートを $f_s = 44100$ [Hz] に設定したので、 $f = 22050$ [Hz] がナイキスト周波数となります。

:::note
**ナイキスト周波数**：  $f_s/2$
これより大きい周波数では怪奇現象が起こる👻<font color="gray">（や、ちゃんとした理論があります）</font>
:::

### 3. 音（振幅）の大きさ
PythonライブラリnumpyのFFTの仕様では、$f = f_1$ 地点での**振幅 $A$ は、次のように表されます**。

:::note
**$f = f_1$ 地点での振幅 $A$**
```math
A = \left| \frac{\ty(f_1)}{N/2} \right|
```
:::

ナイキスト周波数以下のデータ数 $N/2$ で割ることにより、$y(t)$ の振幅と一致します。
また、$\ty(f_1)$ は複素数なので、絶対値をつけなければなりません。

## 音波のスペクトル
前節《周波数 f の関係づけ》でも説明したように、$\ty(f)$ のうち、意味のあるデータというのは、ナイキスト周波数以下のデータのみです。つまり、各データの（1から数えて）**$2$ 番目から $N/2$ 番目までのデータが意味のある値**です。
したがって、Pythonの基本技「スライス」を利用して、グラフに用いるデータを制限します！

### Pythonでの処理
周波数のデータ `freq` を、（`0`から数えて） `1` 番目から `int(N/2) - 1` 番目までのデータにスライスすると、 `freq[1:int(N/2)]` となり、振幅のデータ `Amp` に対しても同様に、 `Amp[1:int(N/2)]` となります。
ここで、 `0` 番目のデータを除く理由は、次のとおりです。

:::note warn
**`0` 番目のデータは** 0 Hz なので、**使わない！**
:::

以上の点に注意して、スペクトルのグラフを描くと以下のようになります。

```python
### 音波のスペクトル ###
plt.plot(freq[1:int(N/2)], Amp[1:int(N/2)]) # A-f グラフのプロット
plt.xscale("log") # 横軸を対数軸にセット
plt.show()
```

これを実行すると、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648376/37b5b167-480d-305c-2df7-746429054c6a.png)
のグラフが得られますが、分かりにくいので拡大すると、

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648376/2feb807e-dfb4-ae5f-ea56-6148bd09ed14.png)

のようになっています。ちゃんと $f = 440$ [Hz] のところに、振幅 $A = 1$ のピークが立っていますね。


## フーリエ逆変換でもとの波形に戻す

$\newcommand{\iy}{y_\mathrm{ifft}}$フーリエ逆変換は、
```math
\iy(t) = \mathcal{F}^{-1}[\tilde y(f)]
```
で表せました。これも具体的な表式（$e^{i\omega t}$とか）を意識しなくて大丈夫です。
Python では、 `np.fft.ifft(y)` のように書いて、`y` 部分に変換したいデータを入力します。
今回は、$\ty$ を $\iy$ に変換したいから、

```python
### IFFT: fの関数をtの関数にする ###
y_ifft = np.fft.ifft(y_fft)
```
と入力すればよいですね。
このグラフを出力するには、以下のコードを食わせましょう。
```python
### 逆変換したグラフ: もとの波形を復元 ###
plt.plot(t, y_ifft.real) # 実部しかいらない
plt.xlim(0, 10*10**-3)
plt.show()
```

このとき、$\iy$ は**実部をとって、 `y_ifft.real` とする**ことに注意。<font color="silver">（ダッテ私タチ実数世界ニ住ンデルカラ…<font color="white">虚数ノ波ッテ見エナイデショ？</font>）</font>
これを実行すると、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648376/74acc05e-f238-cc02-169b-dc8ae119d097.png)
となり、**$\iy(t)$ が、最初に設定した $y(t)$ と一致**したことがわかります。

## あらためて
高速フーリエ変換は、$N$個のデータを、別の$N$個のデータに変換するものです。
あたかも滑らかな関数 $y(t)$ かのように説明してしまいましたが、実際は $y_i \\; (i=1, 2,..., N)$ のように離散化されています。そこだけ注意して理解してください。

# あとがき
これでPythonでのフーリエ変換/逆変換ができるようになりました。お疲れさまでした。
余裕があれば、応用として $y(t)$ が三角波・のこぎり波・矩形波の場合もやってみると面白いと思います。
なにか誤りや質問等ありましたら、気軽にコメントいただけると嬉しいです。

# 参考文献
コグニカルは理論を知るのに分かりやすいのでおすすめです。
* [フーリエ変換とフーリエ逆変換 - Cognicull](https://cognicull.com/ja/2miy1idq)
* [離散時間フーリエ変換（DTFT）とは - Cognicull](https://cognicull.com/ja/y8yl6cxa)
* [離散フーリエ変換（DFT）とは - Cognicull](https://cognicull.com/ja/oyvddk65)
* [高速フーリエ変換（FFT）とは - Cognicull](https://cognicull.com/ja/f5q2jl62)
* [NumPyを使った高速フーリエ変換による周波数解析 – Helve Tech Blog](https://helve-blog.com/posts/python/numpy-fast-fourier-transform/)
* [Python NumPy SciPy サンプルコード: フーリエ変換処理 その 3 | org-技術](https://org-technology.com/posts/fft-03.html)