オートエンコーダ(Auto Encoder)の理論と実装をわかりやすく解説

オートエンコーダ(Auto Encoder, AE)は、ニューラルネットワークを利用した次元削減・特徴抽出の手法です。入力データをそのまま出力として再構成するように学習することで、データの本質的な特徴を低次元の潜在空間に圧縮します。

主成分分析(PCA)が線形な次元削減であるのに対し、オートエンコーダは非線形な次元削減が可能です。また、変分オートエンコーダ(VAE)やデノイジングオートエンコーダなどの発展形は、生成モデルや異常検知にも応用されています。

本記事の内容

  • オートエンコーダのアーキテクチャと損失関数
  • PCA(線形な次元削減)との関係
  • PyTorchでのMNIST実装
  • 潜在空間の可視化

前提知識

この記事を読む前に、ニューラルネットワークの基礎(全結合層、活性化関数、誤差逆伝播法)を押さえておくと理解が深まります。

オートエンコーダとは

オートエンコーダは、エンコーダ(Encoder)とデコーダ(Decoder)の2つの部分から構成されます。

エンコーダ: 入力 $\bm{x} \in \mathbb{R}^d$ を低次元の潜在表現 $\bm{z} \in \mathbb{R}^q$ ($q < d$) に圧縮

$$ \bm{z} = f_{\text{enc}}(\bm{x}; \bm{\theta}_e) $$

デコーダ: 潜在表現 $\bm{z}$ から入力を再構成

$$ \hat{\bm{x}} = f_{\text{dec}}(\bm{z}; \bm{\theta}_d) $$

学習の目的は、入力と再構成された出力の誤差を最小化することです。

$$ L(\bm{\theta}_e, \bm{\theta}_d) = \frac{1}{n}\sum_{i=1}^{n} \|\bm{x}_i – \hat{\bm{x}}_i\|^2 $$

PCAとの関係

オートエンコーダのエンコーダとデコーダが共に線形関数(活性化関数なし)で、損失関数が二乗誤差の場合、オートエンコーダの最適解はPCAの解と一致することが知られています。

すなわち、線形オートエンコーダのエンコーダの重み行列は、データの上位 $q$ 個の主成分方向を張る行列と同じ部分空間を張ります。

非線形活性化関数を導入することで、PCAでは捉えられない非線形な構造を学習できるようになります。

ボトルネック構造

オートエンコーダの潜在空間の次元 $q$ を入力次元 $d$ よりも小さくすることで、ボトルネック(情報のくびれ)を作ります。

$$ d \to h_1 \to h_2 \to q \to h_2 \to h_1 \to d $$

ボトルネックにより、モデルはデータの最も重要な特徴のみを保持するように強制され、入力をそのまま記憶することが防がれます。

PyTorchでの実装

MNISTデータセットを用いてオートエンコーダを実装します。

import numpy as np
import matplotlib.pyplot as plt
import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets, transforms
from torch.utils.data import DataLoader

# デバイス設定
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

# --- データの準備 ---
transform = transforms.Compose([
    transforms.ToTensor(),
])

train_dataset = datasets.MNIST(root='./data', train=True, download=True, transform=transform)
test_dataset = datasets.MNIST(root='./data', train=False, download=True, transform=transform)

train_loader = DataLoader(train_dataset, batch_size=256, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=256, shuffle=False)

# --- オートエンコーダの定義 ---
class AutoEncoder(nn.Module):
    def __init__(self, input_dim=784, latent_dim=2):
        super().__init__()
        # エンコーダ
        self.encoder = nn.Sequential(
            nn.Linear(input_dim, 256),
            nn.ReLU(),
            nn.Linear(256, 64),
            nn.ReLU(),
            nn.Linear(64, latent_dim),
        )
        # デコーダ
        self.decoder = nn.Sequential(
            nn.Linear(latent_dim, 64),
            nn.ReLU(),
            nn.Linear(64, 256),
            nn.ReLU(),
            nn.Linear(256, input_dim),
            nn.Sigmoid(),
        )

    def forward(self, x):
        z = self.encoder(x)
        x_hat = self.decoder(z)
        return x_hat, z

# --- 学習 ---
model = AutoEncoder(input_dim=784, latent_dim=2).to(device)
optimizer = optim.Adam(model.parameters(), lr=1e-3)
criterion = nn.MSELoss()

n_epochs = 20
train_losses = []

for epoch in range(n_epochs):
    model.train()
    epoch_loss = 0
    for batch_x, _ in train_loader:
        batch_x = batch_x.view(-1, 784).to(device)
        x_hat, z = model(batch_x)
        loss = criterion(x_hat, batch_x)

        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        epoch_loss += loss.item()

    avg_loss = epoch_loss / len(train_loader)
    train_losses.append(avg_loss)
    if (epoch + 1) % 5 == 0:
        print(f"Epoch [{epoch+1}/{n_epochs}], Loss: {avg_loss:.6f}")

# --- 可視化 ---
# 学習曲線
fig, ax = plt.subplots(figsize=(8, 4))
ax.plot(train_losses, 'b-')
ax.set_xlabel('Epoch')
ax.set_ylabel('MSE Loss')
ax.set_title('Training Loss')
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

# 再構成結果
model.eval()
with torch.no_grad():
    test_x, test_y = next(iter(test_loader))
    test_x_flat = test_x.view(-1, 784).to(device)
    recon, latent = model(test_x_flat)

fig, axes = plt.subplots(2, 10, figsize=(15, 3))
for i in range(10):
    axes[0, i].imshow(test_x[i].squeeze(), cmap='gray')
    axes[0, i].axis('off')
    axes[1, i].imshow(recon[i].cpu().view(28, 28), cmap='gray')
    axes[1, i].axis('off')
axes[0, 0].set_ylabel('Original')
axes[1, 0].set_ylabel('Reconstructed')
plt.suptitle('AutoEncoder Reconstruction', fontsize=14)
plt.tight_layout()
plt.show()

# 潜在空間の可視化(2次元)
model.eval()
all_z = []
all_labels = []
with torch.no_grad():
    for batch_x, batch_y in test_loader:
        batch_x = batch_x.view(-1, 784).to(device)
        _, z = model(batch_x)
        all_z.append(z.cpu().numpy())
        all_labels.append(batch_y.numpy())

all_z = np.concatenate(all_z)
all_labels = np.concatenate(all_labels)

fig, ax = plt.subplots(figsize=(8, 8))
scatter = ax.scatter(all_z[:, 0], all_z[:, 1], c=all_labels,
                     cmap='tab10', s=5, alpha=0.5)
plt.colorbar(scatter, ax=ax, label='Digit')
ax.set_xlabel('Latent dim 1')
ax.set_ylabel('Latent dim 2')
ax.set_title('Latent Space Visualization (2D)')
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

潜在空間を2次元に設定しているため、数字ごとにクラスタが形成される様子が可視化できます。同じ数字は潜在空間上で近くに配置されることがわかります。

まとめ

本記事では、オートエンコーダの理論と実装について解説しました。

  • オートエンコーダはエンコーダ・デコーダ構造で入力を再構成するように学習する
  • ボトルネック構造によりデータの本質的な特徴が潜在空間に圧縮される
  • 線形オートエンコーダはPCAと等価であり、非線形活性化関数により非線形な次元削減が可能
  • 潜在空間の可視化により、データの構造を直感的に理解できる

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