畳み込み(convolution)は、2つの関数から新しい関数を作り出す演算で、信号処理やシステム理論の根幹をなす概念です。そして、畳み込みとフーリエ変換を結びつける 畳み込み定理 は、数学とエンジニアリングの両方において最も重要な定理の1つです。
畳み込み定理は、「時間領域での畳み込みは、周波数領域での単なる乗算に等しい」と述べています。この性質のおかげで、計算量が膨大な畳み込み演算をフーリエ変換を用いて高速に実行でき、これが FFT ベースのフィルタリングの理論的基礎となっています。本記事では、畳み込みの定義から始めて、畳み込み定理を厳密に証明し、信号処理への応用を具体例とともに解説します。
本記事の内容
- 畳み込みの定義と直感的な理解
- 畳み込みの基本的性質
- 畳み込み定理の証明
- 相関と畳み込みの関係
- 信号処理(フィルタリング)への応用
- Pythonでの実装と可視化
前提知識
この記事を読む前に、以下の記事を読んでおくと理解が深まります。
畳み込みとは
直感的な理解
畳み込みとは、ある関数を別の関数で「ぼかす」あるいは「フィルタリングする」操作と考えることができます。身近な例で言えば、
- カメラのピンぼけ: 各点の光がレンズの特性(ぼかし関数)に従って広がる
- 移動平均: 過去のデータに対して重みをつけて平均を取る
- エコー: 音が壁に反射して遅れた信号が重なる
これらはすべて畳み込みとして数学的にモデル化できます。
数学的定義
2つの関数 $f(t)$ と $g(t)$ の畳み込みは以下のように定義されます。
$$ \begin{equation} (f * g)(t) = \int_{-\infty}^{\infty} f(\tau) \, g(t – \tau) \, d\tau \end{equation} $$
この積分の意味を直感的に理解しましょう。畳み込み $(f * g)(t)$ を時刻 $t$ で評価するとき、
- 関数 $g$ を時間反転する: $g(\tau) \to g(-\tau)$
- 反転した $g$ を $t$ だけシフトする: $g(-\tau) \to g(t – \tau)$
- シフトした $g$ と $f$ を掛け合わせる: $f(\tau) \cdot g(t – \tau)$
- その積を全区間で積分する: $\int f(\tau) g(t-\tau) d\tau$
つまり、$g$ をスライドさせながら $f$ との重なりの度合いを測る操作です。
畳み込みの基本的性質
交換法則
$$ f * g = g * f $$
証明:
$$ \begin{align} (f * g)(t) &= \int_{-\infty}^{\infty} f(\tau) g(t – \tau) \, d\tau \\ &= \int_{-\infty}^{\infty} f(t – s) g(s) \, ds \quad (s = t – \tau) \\ &= (g * f)(t) \end{align} $$
結合法則
$$ (f * g) * h = f * (g * h) $$
分配法則
$$ f * (g + h) = f * g + f * h $$
デルタ関数との畳み込み
$$ f * \delta = f $$
デルタ関数は畳み込みの単位元です。これは直感的にも、「ぼかしなし」のフィルタを適用することに対応します。
微分との関係
$$ \frac{d}{dt}(f * g) = f’ * g = f * g’ $$
畳み込みの微分は、どちらか一方の関数の微分で計算できます。
畳み込み定理
定理の主張
$\mathcal{F}[f(t)] = F(\nu)$, $\mathcal{F}[g(t)] = G(\nu)$ とするとき、
$$ \begin{equation} \mathcal{F}[f * g] = F(\nu) \cdot G(\nu) \end{equation} $$
すなわち、時間領域での畳み込みは、周波数領域での乗算に等しい です。
証明
$$ \begin{align} \mathcal{F}[f * g](\nu) &= \int_{-\infty}^{\infty} \left[ \int_{-\infty}^{\infty} f(\tau) g(t – \tau) \, d\tau \right] e^{-i2\pi\nu t} \, dt \\ &= \int_{-\infty}^{\infty} f(\tau) \left[ \int_{-\infty}^{\infty} g(t – \tau) e^{-i2\pi\nu t} \, dt \right] d\tau \quad (\text{積分順序の交換}) \end{align} $$
内側の積分について $s = t – \tau$ と置換すると、$dt = ds$ であり、
$$ \begin{align} \int_{-\infty}^{\infty} g(t – \tau) e^{-i2\pi\nu t} \, dt &= \int_{-\infty}^{\infty} g(s) e^{-i2\pi\nu(s + \tau)} \, ds \\ &= e^{-i2\pi\nu\tau} \int_{-\infty}^{\infty} g(s) e^{-i2\pi\nu s} \, ds \\ &= e^{-i2\pi\nu\tau} G(\nu) \end{align} $$
これを元の式に代入すると、
$$ \begin{align} \mathcal{F}[f * g](\nu) &= \int_{-\infty}^{\infty} f(\tau) e^{-i2\pi\nu\tau} G(\nu) \, d\tau \\ &= G(\nu) \int_{-\infty}^{\infty} f(\tau) e^{-i2\pi\nu\tau} \, d\tau \\ &= G(\nu) \cdot F(\nu) \\ &= F(\nu) \cdot G(\nu) \end{align} $$
これで畳み込み定理が証明されました。
逆の関係: 乗算定理
畳み込み定理の逆も成り立ちます。
$$ \begin{equation} \mathcal{F}[f(t) \cdot g(t)] = F * G = \int_{-\infty}^{\infty} F(\mu) G(\nu – \mu) \, d\mu \end{equation} $$
時間領域での乗算は、周波数領域での畳み込みに等しい です。
相関と畳み込みの関係
畳み込みに関連する重要な演算として 相互相関(cross-correlation)があります。
$$ (f \star g)(t) = \int_{-\infty}^{\infty} f^*(\tau) \, g(t + \tau) \, d\tau $$
ここで $f^*$ は複素共役です。相関は $g$ を反転せずにスライドさせる点が畳み込みとの違いです。フーリエ変換との関係は、
$$ \mathcal{F}[f \star g] = F^*(\nu) \cdot G(\nu) $$
$f = g$ の場合は 自己相関 となり、パワースペクトル密度と結びつきます(ウィーナー・ヒンチンの定理)。
信号処理への応用: フィルタリング
線形時不変(LTI)システム
入力 $x(t)$ に対する出力 $y(t)$ が畳み込みで表されるシステムを 線形時不変(LTI)システム と呼びます。
$$ y(t) = (x * h)(t) = \int_{-\infty}^{\infty} x(\tau) h(t – \tau) \, d\tau $$
ここで $h(t)$ は インパルス応答 です。畳み込み定理より、
$$ Y(\nu) = X(\nu) \cdot H(\nu) $$
$H(\nu)$ は 伝達関数(周波数応答)と呼ばれ、各周波数をどれだけ通すかを決めるフィルタの特性です。
フィルタの種類
| フィルタ | $H(\nu)$ の特性 | 役割 |
|---|---|---|
| ローパスフィルタ | 低周波を通す | ノイズ除去、平滑化 |
| ハイパスフィルタ | 高周波を通す | エッジ検出 |
| バンドパスフィルタ | 特定帯域を通す | 特定周波数の抽出 |
具体例: ガウシアンフィルタによる平滑化
入力信号 $x(t)$ にガウシアンフィルタ $h(t) = \frac{1}{\sqrt{2\pi}\sigma} e^{-t^2/(2\sigma^2)}$ を適用する場合を考えます。
ガウス関数のフーリエ変換もガウス関数なので、
$$ H(\nu) = e^{-2\pi^2 \sigma^2 \nu^2} $$
これは低周波成分をほぼそのまま通し、高周波成分を指数関数的に減衰させるローパスフィルタです。$\sigma$ が大きいほど(時間領域でのフィルタが広いほど)、周波数領域での帯域幅が狭くなり、より強い平滑化が行われます。
Pythonでの実装
畳み込みの基本的な可視化
import numpy as np
import matplotlib.pyplot as plt
# 矩形パルスとガウス関数の畳み込み
t = np.linspace(-5, 5, 1000)
dt = t[1] - t[0]
# 矩形パルス
f = np.where(np.abs(t) < 1, 1.0, 0.0)
# ガウス関数(フィルタ)
sigma = 0.5
g = (1 / (np.sqrt(2 * np.pi) * sigma)) * np.exp(-t ** 2 / (2 * sigma ** 2))
# 畳み込み(数値計算)
conv_fg = np.convolve(f, g, mode='same') * dt
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
axes[0].plot(t, f, 'b-', linewidth=2)
axes[0].set_title('$f(t)$: Rectangular Pulse')
axes[0].set_xlabel('t')
axes[0].set_ylim(-0.3, 1.3)
axes[0].grid(True, alpha=0.3)
axes[1].plot(t, g, 'r-', linewidth=2)
axes[1].set_title(f'$g(t)$: Gaussian ($\\sigma$ = {sigma})')
axes[1].set_xlabel('t')
axes[1].grid(True, alpha=0.3)
axes[2].plot(t, conv_fg, 'g-', linewidth=2)
axes[2].set_title('$(f * g)(t)$: Convolution')
axes[2].set_xlabel('t')
axes[2].grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig("convolution_basic.png", dpi=150, bbox_inches="tight")
plt.show()
矩形パルスとガウス関数を畳み込むと、矩形の鋭いエッジが滑らかになります。これが「ぼかし」の効果です。
畳み込み定理の数値検証
import numpy as np
import matplotlib.pyplot as plt
# 時間軸の設定
N = 1024
dt = 0.01
t = np.arange(N) * dt - N * dt / 2
# 信号: 2つのガウスパルスの合成
f = np.exp(-((t - 0.5) / 0.3) ** 2) + 0.5 * np.exp(-((t + 1.0) / 0.2) ** 2)
# フィルタ: ガウシアン
sigma = 0.2
g = (1 / (np.sqrt(2 * np.pi) * sigma)) * np.exp(-t ** 2 / (2 * sigma ** 2))
# 方法1: 時間領域での畳み込み
conv_time = np.convolve(f, g, mode='same') * dt
# 方法2: 周波数領域での乗算(畳み込み定理)
F = np.fft.fft(f)
G = np.fft.fft(g)
conv_freq = np.real(np.fft.ifft(F * G)) * dt
# 結果の比較
fig, axes = plt.subplots(2, 2, figsize=(12, 8))
# 時間領域
axes[0, 0].plot(t, f, 'b-', linewidth=1.5, label='f(t)')
axes[0, 0].plot(t, g * 0.5, 'r-', linewidth=1.5, label='g(t) (scaled)')
axes[0, 0].set_title('Input signal and filter')
axes[0, 0].legend()
axes[0, 0].grid(True, alpha=0.3)
# 畳み込み結果の比較
axes[0, 1].plot(t, conv_time, 'g-', linewidth=2, label='Time domain conv.')
axes[0, 1].plot(t, conv_freq, 'r--', linewidth=2, label='Freq. domain (FFT)')
axes[0, 1].set_title('Convolution Theorem Verification')
axes[0, 1].legend()
axes[0, 1].grid(True, alpha=0.3)
# 周波数領域
freq = np.fft.fftfreq(N, dt)
axes[1, 0].plot(np.fft.fftshift(freq), np.fft.fftshift(np.abs(F)),
'b-', linewidth=1, label='|F($\\nu$)|')
axes[1, 0].plot(np.fft.fftshift(freq), np.fft.fftshift(np.abs(G)),
'r-', linewidth=1, label='|G($\\nu$)|')
axes[1, 0].set_title('Frequency Domain')
axes[1, 0].set_xlim(-20, 20)
axes[1, 0].legend()
axes[1, 0].grid(True, alpha=0.3)
# 誤差
error = np.abs(conv_time - conv_freq)
axes[1, 1].plot(t, error, 'k-', linewidth=1)
axes[1, 1].set_title('Error: |Time conv. - Freq. conv.|')
axes[1, 1].set_yscale('log')
axes[1, 1].grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig("convolution_theorem_verification.png", dpi=150, bbox_inches="tight")
plt.show()
時間領域での畳み込みと周波数領域での乗算+逆フーリエ変換の結果がよく一致しており、畳み込み定理が数値的にも成り立つことが確認できます。
ローパスフィルタの適用
import numpy as np
import matplotlib.pyplot as plt
# ノイズ付き信号の生成
np.random.seed(42)
N = 1024
dt = 0.001
t = np.arange(N) * dt
# 元の信号: 正弦波の合成
signal_clean = np.sin(2 * np.pi * 50 * t) + 0.5 * np.sin(2 * np.pi * 120 * t)
# ノイズの追加
noise = 0.8 * np.random.randn(N)
signal_noisy = signal_clean + noise
# 理想ローパスフィルタ(カットオフ周波数 100 Hz)
freq = np.fft.fftfreq(N, dt)
cutoff = 100 # Hz
# 周波数領域でフィルタリング
F_noisy = np.fft.fft(signal_noisy)
H_lowpass = np.where(np.abs(freq) <= cutoff, 1.0, 0.0)
F_filtered = F_noisy * H_lowpass
signal_filtered = np.real(np.fft.ifft(F_filtered))
fig, axes = plt.subplots(3, 2, figsize=(14, 10))
# 元の信号
axes[0, 0].plot(t, signal_clean, 'b-', linewidth=1)
axes[0, 0].set_title('Clean Signal (50 Hz + 120 Hz)')
axes[0, 0].set_xlabel('Time [s]')
axes[0, 0].grid(True, alpha=0.3)
axes[0, 1].plot(np.fft.fftshift(freq), np.fft.fftshift(np.abs(np.fft.fft(signal_clean))),
'b-', linewidth=1)
axes[0, 1].set_xlim(-200, 200)
axes[0, 1].set_title('Clean Signal Spectrum')
axes[0, 1].set_xlabel('Frequency [Hz]')
axes[0, 1].grid(True, alpha=0.3)
# ノイズ付き信号
axes[1, 0].plot(t, signal_noisy, 'r-', linewidth=0.5)
axes[1, 0].set_title('Noisy Signal')
axes[1, 0].set_xlabel('Time [s]')
axes[1, 0].grid(True, alpha=0.3)
axes[1, 1].plot(np.fft.fftshift(freq), np.fft.fftshift(np.abs(F_noisy)),
'r-', linewidth=0.5)
axes[1, 1].set_xlim(-200, 200)
axes[1, 1].set_title('Noisy Signal Spectrum')
axes[1, 1].set_xlabel('Frequency [Hz]')
axes[1, 1].grid(True, alpha=0.3)
# フィルタリング後
axes[2, 0].plot(t, signal_filtered, 'g-', linewidth=1)
axes[2, 0].plot(t, signal_clean, 'b--', linewidth=1, alpha=0.5, label='Original')
axes[2, 0].set_title('Filtered Signal (Lowpass, fc = 100 Hz)')
axes[2, 0].set_xlabel('Time [s]')
axes[2, 0].legend()
axes[2, 0].grid(True, alpha=0.3)
axes[2, 1].plot(np.fft.fftshift(freq), np.fft.fftshift(np.abs(F_filtered)),
'g-', linewidth=1)
axes[2, 1].set_xlim(-200, 200)
axes[2, 1].set_title('Filtered Signal Spectrum')
axes[2, 1].set_xlabel('Frequency [Hz]')
axes[2, 1].grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig("lowpass_filter_application.png", dpi=150, bbox_inches="tight")
plt.show()
ノイズが除去されて50 Hzの正弦波成分のみが残り、120 Hzの成分はカットオフ周波数以上なので除去されています。このように、畳み込み定理を利用することで、周波数領域でフィルタ特性を乗算するだけで効率的にフィルタリングが実現できます。
まとめ
本記事では、畳み込みとフーリエ変換の関係について解説しました。
- 畳み込みは、一方の関数を反転・シフトしながら他方と重ね合わせて積分する演算です
- 畳み込み定理: 時間領域の畳み込みは周波数領域の乗算に等しい($\mathcal{F}[f * g] = F \cdot G$)
- 乗算定理: 時間領域の乗算は周波数領域の畳み込みに等しい
- LTIシステムの出力は入力とインパルス応答の畳み込みで表され、伝達関数 $H(\nu)$ がフィルタ特性を決定します
- 畳み込み定理により、FFT を用いた高速フィルタリングが可能です
次のステップとして、以下の記事も参考にしてください。