畳み込みとフーリエ変換の関係をわかりやすく解説

畳み込み(convolution)は、信号処理、画像処理、確率論、微分方程式など、理工学の広範な分野で現れる基本的な演算です。フーリエ変換との関係を理解すると、畳み込みの計算が劇的に簡単になります。

畳み込み定理は「時間領域の畳み込みは周波数領域の積に対応する」ことを主張しており、これはフーリエ変換の最も実用的な性質の1つです。

本記事の内容

  • 畳み込みの定義と直感的な理解
  • 畳み込み定理の導出
  • LTIシステムとインパルス応答
  • Pythonでの実装と可視化

前提知識

この記事を読む前に、以下の記事を読んでおくと理解が深まります。

畳み込みの定義

定義

2つの関数 $f(t)$ と $g(t)$ の畳み込み $(f * g)(t)$ は次のように定義されます。

$$ (f * g)(t) = \int_{-\infty}^{\infty} f(\tau) g(t – \tau) \, d\tau $$

直感的な理解

畳み込みの計算を直感的に理解するために、手順を分解してみましょう。

  1. $g(\tau)$ を左右反転して $g(-\tau)$ を作る
  2. $g(-\tau)$ を $t$ だけ右にずらして $g(t – \tau)$ を作る
  3. $f(\tau)$ と $g(t – \tau)$ の積を $\tau$ について積分する
  4. $t$ を変化させながら3を繰り返す

大雑把に言うと、畳み込みは「一方の関数をスライドさせながら、もう一方との重なり面積を計算する」操作です。

畳み込みの性質

交換法則

$$ f * g = g * f $$

導出

変数変換 $u = t – \tau$ とおくと $\tau = t – u$, $d\tau = -du$ なので、

$$ (f * g)(t) = \int_{-\infty}^{\infty} f(\tau) g(t – \tau) \, d\tau = \int_{\infty}^{-\infty} f(t – u) g(u) (-du) = \int_{-\infty}^{\infty} g(u) f(t – u) \, du = (g * f)(t) $$

結合法則

$$ (f * g) * h = f * (g * h) $$

分配法則

$$ f * (g + h) = f * g + f * h $$

デルタ関数との畳み込み

$$ f * \delta = f $$

デルタ関数 $\delta(t)$ は畳み込みの単位元です。

畳み込み定理

定理

$$ \mathcal{F}[f * g] = \hat{f}(\omega) \cdot \hat{g}(\omega) $$

つまり、時間領域の畳み込みは周波数領域の積に対応します。

逆もまた成り立ちます。

$$ \mathcal{F}[f \cdot g] = \frac{1}{2\pi} \hat{f} * \hat{g} $$

導出

$$ \begin{align} \mathcal{F}[f * g](\omega) &= \int_{-\infty}^{\infty} \left[\int_{-\infty}^{\infty} f(\tau) g(t – \tau) \, d\tau\right] e^{-i\omega t} \, dt \end{align} $$

積分の順序を交換します。

$$ \begin{align} &= \int_{-\infty}^{\infty} f(\tau) \left[\int_{-\infty}^{\infty} g(t – \tau) e^{-i\omega t} \, dt\right] d\tau \end{align} $$

内側の積分で $s = t – \tau$ と置換すると $t = s + \tau$, $dt = ds$ なので、

$$ \begin{align} \int_{-\infty}^{\infty} g(t – \tau) e^{-i\omega t} \, dt &= \int_{-\infty}^{\infty} g(s) e^{-i\omega(s + \tau)} \, ds \\ &= e^{-i\omega\tau} \int_{-\infty}^{\infty} g(s) e^{-i\omega s} \, ds \\ &= e^{-i\omega\tau} \hat{g}(\omega) \end{align} $$

したがって、

$$ \begin{align} \mathcal{F}[f * g](\omega) &= \int_{-\infty}^{\infty} f(\tau) e^{-i\omega\tau} \hat{g}(\omega) \, d\tau \\ &= \hat{g}(\omega) \int_{-\infty}^{\infty} f(\tau) e^{-i\omega\tau} \, d\tau \\ &= \hat{f}(\omega) \cdot \hat{g}(\omega) \end{align} $$

実用的な意味

畳み込み定理の実用的な意味は非常に大きいです。

直接計算では $O(N^2)$ かかる畳み込みが、FFTを使うと $O(N \log N)$ で計算できます。

  1. $f$ と $g$ をFFTで周波数領域に変換: $O(N \log N)$
  2. 周波数領域で要素ごとに積をとる: $O(N)$
  3. 逆FFTで時間領域に戻す: $O(N \log N)$

LTIシステムとインパルス応答

LTIシステムとは

線形時不変(Linear Time-Invariant, LTI)システムは、入力 $x(t)$ に対して出力 $y(t)$ を返すシステムで、以下の2つの性質を持ちます。

  1. 線形性: $\alpha x_1 + \beta x_2 \to \alpha y_1 + \beta y_2$
  2. 時不変性: $x(t – t_0) \to y(t – t_0)$

インパルス応答

LTIシステムのインパルス応答 $h(t)$ とは、入力がデルタ関数 $\delta(t)$ のときの出力です。

LTIシステムの出力は、入力とインパルス応答の畳み込みで表されます。

$$ y(t) = (x * h)(t) = \int_{-\infty}^{\infty} x(\tau) h(t – \tau) \, d\tau $$

伝達関数

フーリエ変換すると、

$$ \hat{y}(\omega) = \hat{x}(\omega) \cdot \hat{h}(\omega) $$

$\hat{h}(\omega)$ を伝達関数(transfer function)と呼びます。伝達関数は、各周波数成分に対するシステムの応答(増幅率と位相変化)を表します。

具体例: ガウスフィルタ

ガウス関数 $g(t) = \frac{1}{\sigma\sqrt{2\pi}} e^{-t^2/(2\sigma^2)}$ との畳み込みは、信号の平滑化(ぼかし)に相当します。

ガウス関数のフーリエ変換もガウス関数なので、周波数領域では高周波成分を減衰させるローパスフィルタとして機能します。

Pythonでの実装と可視化

畳み込みの可視化

import numpy as np
import matplotlib.pyplot as plt

def convolve_continuous(f, g, t, dt):
    """数値的な畳み込み(直接計算)"""
    n = len(t)
    result = np.zeros(n)
    for i in range(n):
        integrand = f * np.roll(g[::-1], i - n // 2 + 1)
        result[i] = np.sum(integrand) * dt
    return result

# 矩形パルスとガウス関数の畳み込み
t = np.linspace(-5, 5, 1000)
dt = t[1] - t[0]

# 矩形パルス
rect = np.where(np.abs(t) < 1, 1.0, 0.0)

# ガウス関数
sigma = 0.5
gauss = (1 / (sigma * np.sqrt(2 * np.pi))) * np.exp(-t**2 / (2 * sigma**2))

# 畳み込み(NumPyのconvolveを使用)
conv = np.convolve(rect, gauss, mode='same') * dt

fig, axes = plt.subplots(2, 2, figsize=(12, 8))

ax = axes[0, 0]
ax.plot(t, rect, 'b-', linewidth=2)
ax.set_title('$f(t)$: Rectangular pulse')
ax.set_xlabel('$t$')
ax.grid(True, alpha=0.3)

ax = axes[0, 1]
ax.plot(t, gauss, 'r-', linewidth=2)
ax.set_title(f'$g(t)$: Gaussian ($\\sigma = {sigma}$)')
ax.set_xlabel('$t$')
ax.grid(True, alpha=0.3)

ax = axes[1, 0]
ax.plot(t, conv, 'g-', linewidth=2)
ax.set_title('$(f * g)(t)$: Convolution')
ax.set_xlabel('$t$')
ax.grid(True, alpha=0.3)

# 周波数領域での確認
ax = axes[1, 1]
F_rect = np.fft.fftshift(np.fft.fft(rect) * dt)
F_gauss = np.fft.fftshift(np.fft.fft(gauss) * dt)
F_conv = np.fft.fftshift(np.fft.fft(conv) * dt)
F_product = F_rect * F_gauss
omega = np.fft.fftshift(2 * np.pi * np.fft.fftfreq(len(t), d=dt))

ax.plot(omega, np.abs(F_conv), 'g-', linewidth=2, label='$\\mathcal{F}[f*g]$')
ax.plot(omega, np.abs(F_product), 'k--', linewidth=2, label='$\\hat{f} \\cdot \\hat{g}$', alpha=0.7)
ax.set_title('Convolution theorem verification')
ax.set_xlabel('$\\omega$')
ax.set_xlim(-20, 20)
ax.legend()
ax.grid(True, alpha=0.3)

plt.tight_layout()
plt.savefig('convolution_demo.png', dpi=150, bbox_inches='tight')
plt.show()

FFTによる高速畳み込み

import numpy as np
import matplotlib.pyplot as plt
import time

def convolve_direct(f, g):
    """直接畳み込み O(N^2)"""
    return np.convolve(f, g, mode='same')

def convolve_fft(f, g):
    """FFTを使った畳み込み O(N log N)"""
    n = len(f) + len(g) - 1
    # 2の冪に切り上げ(FFTの効率化)
    n_fft = 2**int(np.ceil(np.log2(n)))
    F = np.fft.fft(f, n_fft)
    G = np.fft.fft(g, n_fft)
    result = np.fft.ifft(F * G).real
    # 'same'モードに合わせてトリミング
    start = (len(g) - 1) // 2
    return result[start:start + len(f)]

# 計算時間の比較
sizes = [100, 500, 1000, 5000, 10000]
times_direct = []
times_fft = []

for N in sizes:
    f = np.random.randn(N)
    g = np.random.randn(min(N, 100))

    start = time.time()
    for _ in range(3):
        convolve_direct(f, g)
    times_direct.append((time.time() - start) / 3)

    start = time.time()
    for _ in range(3):
        convolve_fft(f, g)
    times_fft.append((time.time() - start) / 3)

fig, ax = plt.subplots(figsize=(8, 5))
ax.plot(sizes, times_direct, 'bo-', label='Direct convolution $O(N^2)$')
ax.plot(sizes, times_fft, 'r^-', label='FFT convolution $O(N \\log N)$')
ax.set_xlabel('Signal length $N$')
ax.set_ylabel('Time (s)')
ax.set_title('Convolution: Direct vs FFT')
ax.legend()
ax.grid(True, alpha=0.3)
ax.set_yscale('log')
ax.set_xscale('log')

plt.tight_layout()
plt.savefig('convolution_speed.png', dpi=150, bbox_inches='tight')
plt.show()

LTIシステムのフィルタリング

import numpy as np
import matplotlib.pyplot as plt

# ノイズ付き信号の生成
np.random.seed(42)
t = np.linspace(0, 2, 1000)
dt = t[1] - t[0]
signal_clean = np.sin(2 * np.pi * 5 * t) + 0.5 * np.sin(2 * np.pi * 20 * t)
noise = 0.3 * np.random.randn(len(t))
signal_noisy = signal_clean + noise

# ローパスフィルタ(ガウスフィルタ)
sigma = 0.01
t_kernel = np.linspace(-0.1, 0.1, 201)
kernel = np.exp(-t_kernel**2 / (2 * sigma**2))
kernel /= kernel.sum()  # 正規化

# 畳み込みによるフィルタリング
signal_filtered = np.convolve(signal_noisy, kernel, mode='same')

fig, axes = plt.subplots(3, 1, figsize=(12, 9))

axes[0].plot(t, signal_clean, 'b-', linewidth=1)
axes[0].set_title('Clean signal')
axes[0].set_ylabel('Amplitude')
axes[0].grid(True, alpha=0.3)

axes[1].plot(t, signal_noisy, 'gray', linewidth=0.5)
axes[1].set_title('Noisy signal')
axes[1].set_ylabel('Amplitude')
axes[1].grid(True, alpha=0.3)

axes[2].plot(t, signal_filtered, 'r-', linewidth=1)
axes[2].set_title('Filtered signal (Gaussian low-pass filter)')
axes[2].set_ylabel('Amplitude')
axes[2].set_xlabel('$t$ (s)')
axes[2].grid(True, alpha=0.3)

plt.tight_layout()
plt.savefig('lti_filtering.png', dpi=150, bbox_inches='tight')
plt.show()

まとめ

本記事では、畳み込みとフーリエ変換の関係について解説しました。

  • 畳み込み: $(f * g)(t) = \int f(\tau) g(t-\tau) d\tau$ — 一方をスライドさせながら重なりを計算
  • 畳み込み定理: $\mathcal{F}[f * g] = \hat{f} \cdot \hat{g}$ — 時間領域の畳み込みは周波数領域の積
  • LTIシステム: 出力 $= $ 入力とインパルス応答の畳み込み
  • FFTによる高速化: $O(N^2)$ の畳み込みを $O(N \log N)$ で計算可能

畳み込み定理は信号処理やフィルタリングの基礎であり、ディープラーニングのCNN(畳み込みニューラルネットワーク)の数学的背景でもあります。