オートエンコーダの種類と理論を体系的に解説

オートエンコーダ(Autoencoder, AE)は、入力データをエンコーダで低次元の潜在表現に圧縮し、デコーダで元のデータを再構成するニューラルネットワークです。一見すると「入力を再現するだけ」の自明なタスクに見えますが、ボトルネック構造や正則化の工夫により、データの本質的な構造を学習する強力な表現学習の枠組みとなります。

オートエンコーダの理論的な深さは、不完全AEがPCA(主成分分析)と等価であるという古典的な結果から、デノイジングAEがスコア関数の推定と関連するという拡散モデルへの接続まで、多岐にわたります。本記事では、オートエンコーダの各種変種を統一的な視点で解説し、それぞれの数学的な根拠を省略なく導出します。

本記事の内容

  • オートエンコーダの基本構造(エンコーダ → 潜在表現 → デコーダ)
  • 損失関数 $L = \|\bm{x} – g(f(\bm{x}))\|^2$ の意味
  • 不完全AE(ボトルネック)によるPCAとの等価性の証明
  • 正則化AEの動機(過完備表現の学習)
  • スパースAE(KLペナルティの導出)
  • デノイジングAE(ノイズ付き入力からの復元とスコア関数推定との関係)
  • 収縮AE(ヤコビアンフロベニウスノルムの正則化と局所不変性)
  • 変分AE(VAE)への接続
  • Pythonで各種AEの実装と潜在空間の比較可視化

前提知識

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

オートエンコーダの基本構造

構成要素

オートエンコーダは以下の2つの関数から構成されます。

エンコーダ $f: \mathbb{R}^d \to \mathbb{R}^k$: 入力 $\bm{x} \in \mathbb{R}^d$ を潜在表現 $\bm{z} = f(\bm{x}) \in \mathbb{R}^k$ に変換します。

デコーダ $g: \mathbb{R}^k \to \mathbb{R}^d$: 潜在表現 $\bm{z}$ から再構成 $\hat{\bm{x}} = g(\bm{z}) = g(f(\bm{x}))$ を生成します。

損失関数

最も基本的な損失関数は再構成誤差(Mean Squared Error)です。

$$ L(\theta) = \frac{1}{N}\sum_{n=1}^{N}\|\bm{x}^{(n)} – g(f(\bm{x}^{(n)}))\|^2 $$

ここで $\theta$ はエンコーダとデコーダの全パラメータです。

この損失関数の意味は明快です。「エンコーダで圧縮し、デコーダで復元した結果が、元のデータにできるだけ近くなるように学習する」ということです。

なぜ自明でないのか

もし $k = d$(潜在空間がデータ空間と同じ次元)で制約がなければ、恒等写像 $f = g^{-1} = \text{id}$ が最適解であり、何も面白い表現は学習されません。

オートエンコーダが意味のある表現を学習するためには、何らかのボトルネック($k < d$)または正則化が必要です。

不完全AEとPCAの等価性

線形オートエンコーダの定義

エンコーダとデコーダがともに線形であるオートエンコーダを考えます。

$$ \bm{z} = \bm{W}_e \bm{x}, \quad \hat{\bm{x}} = \bm{W}_d \bm{z} = \bm{W}_d \bm{W}_e \bm{x} $$

ここで $\bm{W}_e \in \mathbb{R}^{k \times d}$、$\bm{W}_d \in \mathbb{R}^{d \times k}$、$k < d$(不完備)です。

データの平均が $\bm{0}$(中心化済み)と仮定します。再構成誤差は

$$ L = \frac{1}{N}\sum_{n=1}^{N}\|\bm{x}^{(n)} – \bm{W}_d\bm{W}_e\bm{x}^{(n)}\|^2 = \mathbb{E}\left[\|\bm{x} – \bm{W}_d\bm{W}_e\bm{x}\|^2\right] $$

PCAとの等価性の証明

定理: 線形不完全オートエンコーダの最適解 $\bm{W}_d^* \bm{W}_e^*$ は、データの共分散行列 $\bm{C} = \mathbb{E}[\bm{x}\bm{x}^T]$ の上位 $k$ 個の固有ベクトルへの射影と等価である。

証明:

$\bm{P} = \bm{W}_d\bm{W}_e \in \mathbb{R}^{d \times d}$ とおきます。$\bm{P}$ のランクは $\text{rank}(\bm{P}) \leq k$ です。

再構成誤差を展開します。

$$ \begin{align} L &= \mathbb{E}\left[\|\bm{x} – \bm{P}\bm{x}\|^2\right] \\ &= \mathbb{E}\left[(\bm{x} – \bm{P}\bm{x})^T(\bm{x} – \bm{P}\bm{x})\right] \\ &= \mathbb{E}\left[\bm{x}^T(\bm{I} – \bm{P})^T(\bm{I} – \bm{P})\bm{x}\right] \\ &= \text{tr}\left((\bm{I} – \bm{P})^T(\bm{I} – \bm{P})\bm{C}\right) \end{align} $$

最後の等式は $\mathbb{E}[\bm{x}^T\bm{A}\bm{x}] = \text{tr}(\bm{A}\bm{C})$ を用いました。

最適化問題は「ランク $k$ 以下の行列 $\bm{P}$ で $\text{tr}((\bm{I}-\bm{P})^T(\bm{I}-\bm{P})\bm{C})$ を最小化する」です。

$\bm{P}$ が直交射影行列($\bm{P}^2 = \bm{P}$, $\bm{P}^T = \bm{P}$)であれば $(\bm{I}-\bm{P})^T(\bm{I}-\bm{P}) = \bm{I} – \bm{P}$ となるため

$$ L = \text{tr}((\bm{I} – \bm{P})\bm{C}) = \text{tr}(\bm{C}) – \text{tr}(\bm{P}\bm{C}) $$

$\text{tr}(\bm{C})$ は定数なので、$L$ の最小化は $\text{tr}(\bm{P}\bm{C})$ の最大化と等価です。

$\bm{C}$ の固有値分解を $\bm{C} = \bm{U}\bm{\Lambda}\bm{U}^T$ とします。$\bm{\Lambda} = \text{diag}(\lambda_1, \lambda_2, \dots, \lambda_d)$、$\lambda_1 \geq \lambda_2 \geq \dots \geq \lambda_d \geq 0$ です。

ランク $k$ の直交射影行列 $\bm{P}$ は $\bm{P} = \bm{V}\bm{V}^T$($\bm{V} \in \mathbb{R}^{d \times k}$、$\bm{V}^T\bm{V} = \bm{I}_k$)と書けます。

$$ \text{tr}(\bm{P}\bm{C}) = \text{tr}(\bm{V}\bm{V}^T\bm{U}\bm{\Lambda}\bm{U}^T) = \text{tr}(\bm{V}^T\bm{U}\bm{\Lambda}\bm{U}^T\bm{V}) $$

$\bm{Q} = \bm{U}^T\bm{V} \in \mathbb{R}^{d \times k}$ とおくと、$\bm{Q}^T\bm{Q} = \bm{V}^T\bm{U}\bm{U}^T\bm{V} = \bm{V}^T\bm{V} = \bm{I}_k$($\bm{U}$ は直交行列)なので

$$ \text{tr}(\bm{P}\bm{C}) = \text{tr}(\bm{Q}^T\bm{\Lambda}\bm{Q}) = \sum_{i=1}^{d}\sum_{j=1}^{k} \lambda_i Q_{ij}^2 $$

$\bm{Q}$ の各列は正規直交であるため、$\sum_{i=1}^d Q_{ij}^2 = 1$(各 $j$)かつ $\sum_{j=1}^k Q_{ij}^2 \leq 1$(各 $i$)です。

この和を最大化するには、大きい $\lambda_i$ に対応する $Q_{ij}^2$ の値を大きくすべきです。最適解は

$$ Q_{ij} = \delta_{ij} \quad (i = 1, \dots, k) $$

すなわち $\bm{V} = \bm{U}_{:, 1:k}$(上位 $k$ 個の固有ベクトル)のとき最大化されます。

このとき

$$ \bm{P}^* = \bm{U}_{:,1:k}\bm{U}_{:,1:k}^T $$

これはまさに PCA の射影行列です。$\square$

最小再構成誤差は

$$ L^* = \text{tr}(\bm{C}) – \sum_{i=1}^{k}\lambda_i = \sum_{i=k+1}^{d}\lambda_i $$

これは切り捨てた固有値の和、すなわちPCAで失われる分散に等しいです。

正則化AEの動機

過完備オートエンコーダ

$k \geq d$(潜在空間の次元がデータ空間以上)の場合、恒等写像が最適解となり得ます。しかし、適切な正則化を加えることで、過完備な潜在空間でも意味のある表現を学習できます。

正則化AEの一般的な損失関数は

$$ L(\theta) = \mathbb{E}\left[\|\bm{x} – g(f(\bm{x}))\|^2\right] + \lambda \cdot \Omega(f, g) $$

ここで $\Omega$ は正則化項で、$\lambda > 0$ はその強さを制御するハイパーパラメータです。

以下、3つの代表的な正則化AEを詳しく解説します。

スパースオートエンコーダ

動機

スパースAE(Sparse Autoencoder)は、潜在表現のスパース性(ほとんどのニューロンが非活性)を促す正則化を加えます。これは、データの各サンプルが少数の特徴の組み合わせで表現されるべきだという仮定に基づきます。

スパース性制約

潜在層のニューロン $j$ の平均活性度を定義します。

$$ \hat{\rho}_j = \frac{1}{N}\sum_{n=1}^{N} h_j(\bm{x}^{(n)}) $$

ここで $h_j(\bm{x})$ はエンコーダの $j$ 番目のユニットの出力(シグモイド活性化関数の場合 $h_j \in [0, 1]$)です。

目標は、すべてのニューロン $j$ について $\hat{\rho}_j \approx \rho$ を達成することです。ここで $\rho$ は目標スパース度(例: $\rho = 0.05$)です。

KLペナルティの導出

各ニューロンの活性/非活性をベルヌーイ分布と見なし、目標分布 $\text{Bernoulli}(\rho)$ と経験的分布 $\text{Bernoulli}(\hat{\rho}_j)$ のKLダイバージェンスをペナルティとして使います。

$$ \begin{align} D_{\text{KL}}(\rho \| \hat{\rho}_j) &= \rho \log \frac{\rho}{\hat{\rho}_j} + (1-\rho) \log \frac{1-\rho}{1-\hat{\rho}_j} \end{align} $$

導出を確認しましょう。ベルヌーイ分布 $P \sim \text{Bernoulli}(\rho)$ と $Q \sim \text{Bernoulli}(\hat{\rho}_j)$ のKLダイバージェンスは

$$ \begin{align} D_{\text{KL}}(P \| Q) &= \sum_{x \in \{0,1\}} P(x) \log \frac{P(x)}{Q(x)} \\ &= \rho \log \frac{\rho}{\hat{\rho}_j} + (1-\rho) \log \frac{1-\rho}{1-\hat{\rho}_j} \end{align} $$

このKLダイバージェンスは $\hat{\rho}_j = \rho$ のとき最小値0を取り、$\hat{\rho}_j$ が $\rho$ から離れるほど大きくなります。

スパースAEの損失関数:

$$ \boxed{L_{\text{sparse}} = \frac{1}{N}\sum_{n=1}^{N}\|\bm{x}^{(n)} – g(f(\bm{x}^{(n)}))\|^2 + \beta \sum_{j=1}^{k} D_{\text{KL}}(\rho \| \hat{\rho}_j)} $$

ここで $\beta$ はスパースペナルティの重みです。

デノイジングオートエンコーダ(DAE)

動機と定義

Vincent et al. (2008) が提案したデノイジングAE(Denoising Autoencoder, DAE)は、入力にノイズを加えた $\tilde{\bm{x}}$ から元の(ノイズなしの)$\bm{x}$ を復元するように学習します。

$$ \tilde{\bm{x}} = \bm{x} + \bm{\epsilon}, \quad \bm{\epsilon} \sim \mathcal{N}(\bm{0}, \sigma^2\bm{I}) $$

$$ L_{\text{DAE}} = \mathbb{E}_{\bm{x}, \bm{\epsilon}}\left[\|\bm{x} – g(f(\tilde{\bm{x}}))\|^2\right] $$

直感的に、ノイズを除去するためにはデータの構造を「理解」する必要があります。ノイズで汚された入力から元の信号を復元することで、データの本質的な構造(データ多様体)を学習します。

スコア関数推定との関係

DAEの理論的に最も深い結果は、最適なデノイジング関数がスコア関数の推定と関係するという点です。

定理(Alain & Bengio, 2014): ガウスノイズ $\bm{\epsilon} \sim \mathcal{N}(\bm{0}, \sigma^2\bm{I})$ を用いたDAEの最適なデノイジング関数 $r^*(\tilde{\bm{x}})$ は、ノイズ付きデータの分布 $q_\sigma(\tilde{\bm{x}}) = \int p(\bm{x})\mathcal{N}(\tilde{\bm{x}}; \bm{x}, \sigma^2\bm{I})\,d\bm{x}$ について

$$ r^*(\tilde{\bm{x}}) = \tilde{\bm{x}} + \sigma^2 \nabla_{\tilde{\bm{x}}} \log q_\sigma(\tilde{\bm{x}}) $$

を満たす。

証明: 最適なデノイジング関数は、損失を最小化する条件付き期待値です。

$$ r^*(\tilde{\bm{x}}) = \mathbb{E}[\bm{x} | \tilde{\bm{x}}] = \int \bm{x}\, p(\bm{x}|\tilde{\bm{x}})\,d\bm{x} $$

ベイズの定理より

$$ p(\bm{x}|\tilde{\bm{x}}) = \frac{q_\sigma(\tilde{\bm{x}}|\bm{x})\, p(\bm{x})}{q_\sigma(\tilde{\bm{x}})} $$

$q_\sigma(\tilde{\bm{x}}|\bm{x}) = \mathcal{N}(\tilde{\bm{x}}; \bm{x}, \sigma^2\bm{I})$ なので

$$ \begin{align} r^*(\tilde{\bm{x}}) &= \frac{1}{q_\sigma(\tilde{\bm{x}})}\int \bm{x}\, \mathcal{N}(\tilde{\bm{x}}; \bm{x}, \sigma^2\bm{I})\, p(\bm{x})\,d\bm{x} \end{align} $$

一方、$q_\sigma(\tilde{\bm{x}})$ のスコア関数を計算します。

$$ \begin{align} \nabla_{\tilde{\bm{x}}} \log q_\sigma(\tilde{\bm{x}}) &= \frac{\nabla_{\tilde{\bm{x}}} q_\sigma(\tilde{\bm{x}})}{q_\sigma(\tilde{\bm{x}})} \\ &= \frac{1}{q_\sigma(\tilde{\bm{x}})}\int \nabla_{\tilde{\bm{x}}} \mathcal{N}(\tilde{\bm{x}}; \bm{x}, \sigma^2\bm{I})\, p(\bm{x})\,d\bm{x} \end{align} $$

ガウス分布の勾配は

$$ \nabla_{\tilde{\bm{x}}} \mathcal{N}(\tilde{\bm{x}}; \bm{x}, \sigma^2\bm{I}) = \mathcal{N}(\tilde{\bm{x}}; \bm{x}, \sigma^2\bm{I}) \cdot \frac{\bm{x} – \tilde{\bm{x}}}{\sigma^2} $$

代入すると

$$ \nabla_{\tilde{\bm{x}}} \log q_\sigma(\tilde{\bm{x}}) = \frac{1}{q_\sigma(\tilde{\bm{x}})}\int \frac{\bm{x} – \tilde{\bm{x}}}{\sigma^2}\, \mathcal{N}(\tilde{\bm{x}}; \bm{x}, \sigma^2\bm{I})\, p(\bm{x})\,d\bm{x} = \frac{\mathbb{E}[\bm{x}|\tilde{\bm{x}}] – \tilde{\bm{x}}}{\sigma^2} $$

整理すると

$$ \mathbb{E}[\bm{x}|\tilde{\bm{x}}] = \tilde{\bm{x}} + \sigma^2 \nabla_{\tilde{\bm{x}}} \log q_\sigma(\tilde{\bm{x}}) $$

すなわち

$$ \boxed{r^*(\tilde{\bm{x}}) – \tilde{\bm{x}} = \sigma^2 \nabla_{\tilde{\bm{x}}} \log q_\sigma(\tilde{\bm{x}})} $$

$\square$

この結果は深遠です。最適なDAEの出力と入力の差は、ノイズ付きデータのスコア関数にスケール $\sigma^2$ を掛けたものに等しいのです。これは、DAEが暗黙的にスコア関数を学習していることを意味し、デノイジングスコアマッチングや拡散モデルとの直接的なつながりを示しています。

収縮オートエンコーダ(CAE)

動機

Rifai et al. (2011) が提案した収縮AE(Contractive Autoencoder, CAE)は、エンコーダの写像がデータの微小な摂動に対して安定(収縮的)であることを正則化として要求します。

ヤコビアンフロベニウスノルムの正則化

エンコーダ $f: \mathbb{R}^d \to \mathbb{R}^k$ のヤコビ行列は

$$ \bm{J}_f(\bm{x}) = \frac{\partial f(\bm{x})}{\partial \bm{x}} \in \mathbb{R}^{k \times d} $$

CAEは、このヤコビ行列のフロベニウスノルムを正則化項として加えます。

$$ \boxed{L_{\text{CAE}} = \frac{1}{N}\sum_{n=1}^{N}\left[\|\bm{x}^{(n)} – g(f(\bm{x}^{(n)}))\|^2 + \lambda \|\bm{J}_f(\bm{x}^{(n)})\|_F^2\right]} $$

ここで

$$ \|\bm{J}_f(\bm{x})\|_F^2 = \sum_{i=1}^{k}\sum_{j=1}^{d}\left(\frac{\partial f_i(\bm{x})}{\partial x_j}\right)^2 = \text{tr}(\bm{J}_f^T \bm{J}_f) $$

局所不変性の意味

ヤコビアンの大きさを制限することの幾何学的な意味を理解しましょう。ヤコビ行列は線形近似

$$ f(\bm{x} + \bm{\delta}) \approx f(\bm{x}) + \bm{J}_f(\bm{x})\bm{\delta} $$

を定義します。$\|\bm{J}_f\|_F$ が小さいということは、入力の微小な変化 $\bm{\delta}$ に対する出力の変化が小さいことを意味します。

直感的に、CAEはデータ多様体に接する方向の変動には敏感であり(再構成誤差を下げるために必要)、直交する方向の変動には不変な(ヤコビアン正則化で抑制される)表現を学習します。

シグモイドエンコーダの場合の具体的な計算

エンコーダが $f(\bm{x}) = \sigma(\bm{W}\bm{x} + \bm{b})$($\sigma$ はシグモイド関数、要素ごとに適用)の場合、ヤコビ行列は

$$ \bm{J}_f(\bm{x}) = \text{diag}(\sigma'(\bm{W}\bm{x} + \bm{b})) \cdot \bm{W} $$

ここで $\sigma'(a) = \sigma(a)(1-\sigma(a))$ です。したがって

$$ \|\bm{J}_f\|_F^2 = \sum_{i=1}^{k} h_i(1-h_i)^2 \|\bm{w}_i\|^2 $$

ここで $h_i = \sigma(\bm{w}_i^T\bm{x} + b_i)$ は $i$ 番目のユニットの出力、$\bm{w}_i$ は $\bm{W}$ の $i$ 行目です。

この正則化は、ユニットの出力 $h_i$ が0または1に近い(飽和している)ときにヤコビアンのノルムが自然に小さくなるため、ユニットの飽和を促す効果があります。

DAEとCAEの関係

Alain & Bengio (2014) は、DAE(小さなガウスノイズ $\sigma \to 0$ の場合)とCAEの間に理論的な関係があることを示しました。

ガウスノイズ $\bm{\epsilon} \sim \mathcal{N}(\bm{0}, \sigma^2\bm{I})$ を用いたDAEの損失を $\sigma$ について展開すると

$$ L_{\text{DAE}} \approx L_{\text{reconstruct}} + \sigma^2 \|\bm{J}_r(\bm{x})\|_F^2 + O(\sigma^4) $$

ここで $r(\tilde{\bm{x}}) = g(f(\tilde{\bm{x}}))$ はオートエンコーダ全体の写像です。これは、小さなノイズの DAE が暗黙的にヤコビアン正則化(CAEと類似の効果)を行っていることを意味します。

変分AE(VAE)への接続

通常のAEと VAE の最も重要な違いは、潜在空間の構造です。

通常のAE: エンコーダは決定論的な写像 $\bm{z} = f(\bm{x})$ で、潜在空間に確率的な構造はありません。

VAE: エンコーダは確率分布 $q_\phi(\bm{z}|\bm{x}) = \mathcal{N}(\bm{\mu}_\phi(\bm{x}), \text{diag}(\bm{\sigma}_\phi^2(\bm{x})))$ を出力し、KLダイバージェンス正則化により潜在空間に構造を付与します。

$$ L_{\text{VAE}} = -\mathbb{E}_{q_\phi(\bm{z}|\bm{x})}[\log p_\theta(\bm{x}|\bm{z})] + D_{\text{KL}}(q_\phi(\bm{z}|\bm{x}) \| p(\bm{z})) $$

第1項は再構成誤差(通常のAEの損失関数に対応)、第2項はKL正則化です。VAEは通常のAEに確率論的な基盤を与え、生成モデルとしての能力を持たせた拡張と見ることができます。

Pythonでの実装

各種オートエンコーダを NumPy で実装し、2Dデータの潜在空間を比較可視化します。

import numpy as np
import matplotlib.pyplot as plt

np.random.seed(42)

# --- データ生成(3クラスの2Dガウス) ---
n_per_class = 300
means = [np.array([0, 2]), np.array([-2, -1]), np.array([2, -1])]
data_list = []
labels_list = []
for c, m in enumerate(means):
    samples = np.random.randn(n_per_class, 2) * 0.4 + m
    data_list.append(samples)
    labels_list.append(np.full(n_per_class, c))

X = np.vstack(data_list)
labels = np.concatenate(labels_list)

# シャッフル
perm = np.random.permutation(len(X))
X = X[perm]
labels = labels[perm]

# --- 基本オートエンコーダ(NumPy実装) ---
class BasicAE:
    """2D -> 1D -> 2D の基本オートエンコーダ"""
    def __init__(self, d_in=2, d_latent=1, hidden=32, lr=1e-3):
        self.d_in = d_in
        self.d_latent = d_latent
        # エンコーダ: d_in -> hidden -> d_latent
        self.We1 = np.random.randn(d_in, hidden) * 0.3
        self.be1 = np.zeros(hidden)
        self.We2 = np.random.randn(hidden, d_latent) * 0.3
        self.be2 = np.zeros(d_latent)
        # デコーダ: d_latent -> hidden -> d_in
        self.Wd1 = np.random.randn(d_latent, hidden) * 0.3
        self.bd1 = np.zeros(hidden)
        self.Wd2 = np.random.randn(hidden, d_in) * 0.3
        self.bd2 = np.zeros(d_in)
        self.lr = lr
        self.params = [self.We1, self.be1, self.We2, self.be2,
                       self.Wd1, self.bd1, self.Wd2, self.bd2]
        self.m = [np.zeros_like(p) for p in self.params]
        self.v = [np.zeros_like(p) for p in self.params]
        self.step = 0

    def encode(self, x):
        """エンコード"""
        self.e_h1_pre = x @ self.We1 + self.be1
        self.e_h1 = np.maximum(0, self.e_h1_pre)  # ReLU
        z = self.e_h1 @ self.We2 + self.be2  # 線形出力
        self.z = z
        return z

    def decode(self, z):
        """デコード"""
        self.d_h1_pre = z @ self.Wd1 + self.bd1
        self.d_h1 = np.maximum(0, self.d_h1_pre)  # ReLU
        x_hat = self.d_h1 @ self.Wd2 + self.bd2  # 線形出力
        return x_hat

    def forward(self, x):
        """フォワードパス"""
        z = self.encode(x)
        x_hat = self.decode(z)
        self.x_in = x
        self.x_hat = x_hat
        return x_hat, z

    def compute_loss(self, x, x_hat):
        """再構成損失"""
        return np.mean((x - x_hat)**2)

    def backward(self):
        """逆伝播 + Adam更新"""
        N = self.x_in.shape[0]
        # dL/dx_hat
        dx_hat = 2.0 / N * (self.x_hat - self.x_in)
        # デコーダ逆伝播
        dWd2 = self.d_h1.T @ dx_hat
        dbd2 = dx_hat.sum(axis=0)
        d_d_h1 = dx_hat @ self.Wd2.T
        d_d_h1 *= (self.d_h1_pre > 0)
        dWd1 = self.z.T @ d_d_h1
        dbd1 = d_d_h1.sum(axis=0)
        dz = d_d_h1 @ self.Wd1.T
        # エンコーダ逆伝播
        dWe2 = self.e_h1.T @ dz
        dbe2 = dz.sum(axis=0)
        d_e_h1 = dz @ self.We2.T
        d_e_h1 *= (self.e_h1_pre > 0)
        dWe1 = self.x_in.T @ d_e_h1
        dbe1 = d_e_h1.sum(axis=0)
        grads = [dWe1, dbe1, dWe2, dbe2, dWd1, dbd1, dWd2, dbd2]
        self._adam_update(grads)

    def _adam_update(self, grads):
        """Adam更新"""
        self.step += 1
        b1, b2, eps = 0.9, 0.999, 1e-8
        for i, g in enumerate(grads):
            self.m[i] = b1 * self.m[i] + (1 - b1) * g
            self.v[i] = b2 * self.v[i] + (1 - b2) * g**2
            mh = self.m[i] / (1 - b1**self.step)
            vh = self.v[i] / (1 - b2**self.step)
            self.params[i] -= self.lr * mh / (np.sqrt(vh) + eps)
        (self.We1, self.be1, self.We2, self.be2,
         self.Wd1, self.bd1, self.Wd2, self.bd2) = self.params


class SparseAE(BasicAE):
    """スパースオートエンコーダ(KLペナルティ付き)"""
    def __init__(self, rho=0.05, beta=1.0, **kwargs):
        super().__init__(**kwargs)
        self.rho = rho  # 目標スパース度
        self.beta = beta  # ペナルティ重み

    def compute_loss(self, x, x_hat):
        """再構成損失 + KLスパースペナルティ"""
        mse = np.mean((x - x_hat)**2)
        # 潜在層の平均活性度(シグモイド変換後)
        z_sigmoid = 1.0 / (1.0 + np.exp(-self.z))
        rho_hat = np.mean(z_sigmoid, axis=0)
        rho = self.rho
        kl = rho * np.log(rho / (rho_hat + 1e-10)) + \
             (1 - rho) * np.log((1 - rho) / (1 - rho_hat + 1e-10))
        return mse + self.beta * np.sum(kl)


class DenoisingAE(BasicAE):
    """デノイジングオートエンコーダ"""
    def __init__(self, noise_std=0.3, **kwargs):
        super().__init__(**kwargs)
        self.noise_std = noise_std

    def forward_noisy(self, x):
        """ノイズ付き入力でフォワードパス"""
        noise = np.random.randn(*x.shape) * self.noise_std
        x_noisy = x + noise
        z = self.encode(x_noisy)
        x_hat = self.decode(z)
        self.x_in = x  # ターゲットはノイズなし
        self.x_hat = x_hat
        return x_hat, z


# --- 学習と比較 ---
def train_ae(model, X, n_epochs=500, batch_size=128, denoising=False):
    """オートエンコーダの学習"""
    losses = []
    for epoch in range(n_epochs):
        idx = np.random.choice(len(X), batch_size)
        x_batch = X[idx]
        if denoising:
            x_hat, z = model.forward_noisy(x_batch)
        else:
            x_hat, z = model.forward(x_batch)
        loss = model.compute_loss(x_batch, x_hat)
        losses.append(loss)
        model.backward()
    return losses

# 各AEの学習
ae_basic = BasicAE(d_in=2, d_latent=1, hidden=32, lr=1e-3)
losses_basic = train_ae(ae_basic, X, n_epochs=800)

ae_sparse = SparseAE(rho=0.05, beta=0.5, d_in=2, d_latent=1, hidden=32, lr=1e-3)
losses_sparse = train_ae(ae_sparse, X, n_epochs=800)

ae_dae = DenoisingAE(noise_std=0.3, d_in=2, d_latent=1, hidden=32, lr=1e-3)
losses_dae = train_ae(ae_dae, X, n_epochs=800, denoising=True)

# --- 潜在空間の可視化 ---
fig, axes = plt.subplots(2, 4, figsize=(20, 10))

# 元データ
colors = ['#e74c3c', '#3498db', '#2ecc71']
for c in range(3):
    mask = labels == c
    axes[0, 0].scatter(X[mask, 0], X[mask, 1], s=5, alpha=0.5, c=colors[c])
axes[0, 0].set_title('Original Data')
axes[0, 0].set_aspect('equal')

# 各AEの再構成
models = [ae_basic, ae_sparse, ae_dae]
titles = ['Basic AE', 'Sparse AE', 'Denoising AE']

for idx, (model, title) in enumerate(zip(models, titles)):
    x_hat, z = model.forward(X)
    # 再構成
    for c in range(3):
        mask = labels == c
        axes[0, idx + 1].scatter(x_hat[mask, 0], x_hat[mask, 1],
                                  s=5, alpha=0.5, c=colors[c])
    axes[0, idx + 1].set_title(f'{title} (Reconstruction)')
    axes[0, idx + 1].set_aspect('equal')
    axes[0, idx + 1].set_xlim(-4, 4)
    axes[0, idx + 1].set_ylim(-3, 4)

    # 潜在空間(1D → ヒストグラム)
    for c in range(3):
        mask = labels == c
        axes[1, idx + 1].hist(z[mask].ravel(), bins=30, alpha=0.5,
                               color=colors[c], density=True, label=f'Class {c}')
    axes[1, idx + 1].set_title(f'{title} (Latent Space)')
    axes[1, idx + 1].legend()

# 損失関数の比較
axes[1, 0].plot(losses_basic, label='Basic AE', alpha=0.7)
axes[1, 0].plot(losses_sparse, label='Sparse AE', alpha=0.7)
axes[1, 0].plot(losses_dae, label='Denoising AE', alpha=0.7)
axes[1, 0].set_xlabel('Iteration')
axes[1, 0].set_ylabel('Loss')
axes[1, 0].set_title('Training Loss')
axes[1, 0].legend()
axes[1, 0].set_yscale('log')

plt.tight_layout()
plt.show()

次に、収縮AEのヤコビアンノルムの効果を可視化します。

import numpy as np
import matplotlib.pyplot as plt

np.random.seed(42)

# --- 収縮AEのヤコビアンノルムの可視化 ---
# 学習済みの BasicAE を使って、入力空間上のヤコビアンノルムを計算

def compute_jacobian_norm(model, x, eps=1e-4):
    """数値微分でエンコーダのヤコビアンフロベニウスノルムを計算"""
    d_in = x.shape[1]
    norms = np.zeros(len(x))
    for j in range(d_in):
        x_plus = x.copy()
        x_plus[:, j] += eps
        x_minus = x.copy()
        x_minus[:, j] -= eps
        z_plus = model.encode(x_plus)
        z_minus = model.encode(x_minus)
        dz_dxj = (z_plus - z_minus) / (2 * eps)
        norms += np.sum(dz_dxj**2, axis=1)
    return np.sqrt(norms)

# グリッド上でヤコビアンノルムを計算
xx, yy = np.meshgrid(np.linspace(-4, 4, 50), np.linspace(-4, 4, 50))
grid = np.stack([xx.ravel(), yy.ravel()], axis=1)

fig, axes = plt.subplots(1, 3, figsize=(18, 5))

for idx, (model, title) in enumerate(zip(
    [ae_basic, ae_sparse, ae_dae],
    ['Basic AE', 'Sparse AE', 'Denoising AE']
)):
    jac_norms = compute_jacobian_norm(model, grid)
    im = axes[idx].contourf(xx, yy, jac_norms.reshape(xx.shape),
                             levels=20, cmap='viridis')
    plt.colorbar(im, ax=axes[idx])
    # データ点をオーバーレイ
    axes[idx].scatter(X[:, 0], X[:, 1], s=1, alpha=0.3, c='white')
    axes[idx].set_title(f'{title} - Jacobian Norm')
    axes[idx].set_aspect('equal')

plt.suptitle('Encoder Jacobian Frobenius Norm across Input Space', fontsize=14)
plt.tight_layout()
plt.show()

このヤコビアンノルムの可視化から、デノイジングAEではデータ多様体上でのみヤコビアンノルムが大きく、データから離れた領域では小さい(収縮的)傾向が見られます。これは、DAEが暗黙的にCAEと類似の正則化効果を持つことを示唆しています。

各種AEの比較まとめ

種類 正則化 数学的特性 主な用途
不完全AE ボトルネック ($k < d$) 線形の場合PCAと等価 次元削減
スパースAE $D_{\text{KL}}(\rho\|\hat{\rho}_j)$ スパース特徴表現 特徴抽出
デノイジングAE ノイズ付加 スコア関数推定と関連 表現学習・前処理
収縮AE $\|\bm{J}_f\|_F^2$ 局所不変性 ロバスト表現
VAE $D_{\text{KL}}(q_\phi\|p)$ 確率モデル・ELBO 生成モデル

まとめ

本記事では、オートエンコーダの種類と理論を体系的に解説しました。

  • オートエンコーダは再構成誤差 $\|\bm{x} – g(f(\bm{x}))\|^2$ を最小化する
  • 線形不完全AEはPCAと等価であり、上位 $k$ 個の主成分への射影を学習する
  • スパースAEはKLダイバージェンスペナルティ $D_{\text{KL}}(\rho\|\hat{\rho}_j)$ でスパースな表現を促す
  • デノイジングAEの最適なデノイジング関数はスコア関数と $r^*(\tilde{\bm{x}}) = \tilde{\bm{x}} + \sigma^2 \nabla \log q_\sigma(\tilde{\bm{x}})$ の関係にある
  • 収縮AEはヤコビアンフロベニウスノルム $\|\bm{J}_f\|_F^2$ の正則化でデータ多様体に直交する方向の不変性を学習する
  • VAEは確率的な潜在空間を持ち、生成モデルとしての能力を付与する

次のステップとして、以下の記事も参考にしてください。