オートエンコーダ(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と等価であり、非線形活性化関数により非線形な次元削減が可能
- 潜在空間の可視化により、データの構造を直感的に理解できる
次のステップとして、以下の記事も参考にしてください。