Autoencoder
Bộ tự mã hóa
Hãy tưởng tượng bạn cần gửi một tấm ảnh 1 MB qua đường truyền chỉ cho phép 10 KB mỗi gói. Bạn nén ảnh xuống, gửi đi, rồi giải nén ở đầu bên kia. Câu hỏi là...
Ảnh giải nén có giống 100% ảnh gốc không? Tại sao?
Bên dưới là một autoencoder mô phỏng trên ảnh chữ số 28×28 (784 pixel). Kéo slider để thay đổi kích thước bottleneck — cổ chai nén dữ liệu trước khi giải nén lại.
Hình minh họa
Kiến trúc: 784 → 128 → 32 → 32 → 32 → 128 → 784
Tốt — hình dáng rõ, hơi mờ
So sánh input vs reconstruction
latent code
Bottleneck càng nhỏ → càng mờ, nhưng mạng buộc phải học bản chất của chữ số (đường cong, hình dạng) thay vì nhớ từng pixel.
Latent space 2D — các chữ số tự phân cụm
Mỗi điểm là một ảnh. Hai trục là hai chiều của bottleneck. Autoencoder không hề được "dạy" nhãn, nhưng các chữ số tự phân cụm vì ảnh giống nhau sẽ có latent code gần nhau.
Ghi chú:giữa các cluster có "vùng trống" — nếu sample z từ đó, decoder sinh ra ảnh không giống chữ số nào. Đây là lí do vanilla AE không phải generative model tốt; VAE giải quyết bằng cách ép latent space liên tục.
Autoencoder không chỉ nén — nó học ra bản chất. Bottleneck buộc mạng phải tìm ra đặc trưng quan trọng nhất của dữ liệu. Với chữ số viết tay, 2 chiều latent có thể tự học ra độ nghiêng và nét dày. Không ai dạy mạng khái niệm "độ nghiêng" — nó tự khám phá vì đó là chiều biến đổi lớn nhất trong dữ liệu.
Đây là một ví dụ đẹp của unsupervised learning: mạng tự tìm ra cấu trúc ẩn trong dữ liệu, chỉ bằng một nguyên tắc duy nhất — tái tạo lại chính mình.
Vanilla Autoencoder
Nén → giải nén. Học biểu diễn nén. Dùng cho giảm chiều, trích xuất đặc trưng, pretrain encoder.
Denoising AE
Input = ảnh bị nhiễu, target = ảnh sạch. Học lọc nhiễu tự động, biểu diễn robust hơn.
Sparse AE
Thêm ràng buộc: phần lớn neuron bottleneck phải = 0. Mỗi input chỉ kích hoạt vài neuron. Gần với lí thuyết về "neuron khái niệm" trong não.
VAE (Variational)
Latent space = phân phối xác suất (thường là Gaussian). Có thể sinh dữ liệu mới bằng lấy mẫu. Nền tảng cho nhiều generative model.
Contractive AE
Thêm phạt Jacobian: ép encoder ít nhạy với biến đổi nhỏ của input. Kết quả: biểu diễn ổn định, gần như bất biến với nhiễu nhỏ.
Masked AE (MAE)
Xóa ngẫu nhiên 75% patch của ảnh, bắt mạng tái tạo lại. Phương pháp self-supervised mạnh cho pretrain Vision Transformer (He et al. 2022).
Autoencoder có bottleneck = kích thước input (784 → 784 → 784). Mạng sẽ học gì?
Giải thích
Autoencoder là mạng nơ-ron học ra một cặp hàm: một encoder nén dữ liệu và một decoder tái tạo lại. Mục tiêu là output giống input nhất có thể, với điều kiện phải đi qua một bottleneck thấp chiều.
Trong đó d << D (bottleneck nhỏ hơn input). Mạng phải tìm cách nén D chiều vào d chiều mà vẫn tái tạo được. Cả θ và φ được học qua stochastic gradient descent — target chính là input, nên autoencoder là self-supervised: không cần nhãn.
Autoencoder 1 lớp tuyến tính (không activation, MSE loss) có nghiệm tối ưu là phép chiếu lên không gian con của k principal component hàng đầu — chính xác tương đương PCA. Nhiều lớp + activation phi tuyến (ReLU, tanh) → autoencoder mạnh hơn PCA rất nhiều, có thể nắm bắt cấu trúc cong trong dữ liệu (manifold). Đây là lí do AE còn được gọi là nonlinear PCA.
Phát hiện bất thường: Train AE trên dữ liệu bình thường (máy móc công nghiệp, giao dịch ngân hàng). Khi gặp dữ liệu lạ → reconstruction error cao → flag là anomaly.
Giảm chiều cho visualization: Latent 2D/3D dùng để vẽ dữ liệu phức tạp (giống t-SNE nhưng có encoder học được, áp dụng cho điểm mới).
Khử nhiễu: Denoising AE lọc nhiễu ảnh/audio tự động. Dùng rộng rãi trong tiền xử lí ảnh y tế.
Pretrain encoder: Trong kỉ nguyên chưa có ImageNet, AE được dùng để khởi tạo trọng số encoder trước khi fine-tune cho task supervised.
Nén đặc biệt: Autoencoder neural có thể nén ảnh y tế hoặc dữ liệu khoa học tốt hơn JPEG cho domain cụ thể.
1. Identity mapping: Bottleneck quá to → mạng học hàm đồng nhất. Kiểm tra: nếu loss → 0 cực nhanh, có thể đây là vấn đề.
2. Posterior collapse (với VAE): Decoder mạnh đến mức bỏ qua z — mạng chỉ học phân phối trung bình.
3. Loss function sai: MSE giả định Gaussian, không phù hợp với ảnh nhị phân — BCE thường tốt hơn. Với ảnh màu, perceptual loss (VGG features) cho kết quả rõ nét hơn.
Nếu reconstruction mờ đều trên mọi mẫu: bottleneck quá nhỏ hoặc model under-fit. Nếu reconstruction khớp train set nhưng xấu trên test: over-fit — thêm dropout / weight decay. Nếu latent code collapse (mọi x cho z gần giống nhau): encoder quá yếu hoặc decoder quá mạnh. Visualize latent 2D bằng t-SNE/UMAP để phát hiện collapse.
Về mặt công thức, loss tổng quát của autoencoder có dạng:
Số hạng regularizer thay đổi theo biến thể: sparsity (sparse AE), KL divergence (VAE), Jacobian norm (contractive AE), adversarial (AAE), v.v.
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader
from torchvision import datasets, transforms
class Autoencoder(nn.Module):
"""MNIST autoencoder: 784 → 128 → 32 → 8 → 32 → 128 → 784."""
def __init__(self, latent_dim: int = 8):
super().__init__()
self.latent_dim = latent_dim
# ─── Encoder ───
self.encoder = nn.Sequential(
nn.Linear(784, 128),
nn.ReLU(),
nn.Linear(128, 32),
nn.ReLU(),
nn.Linear(32, latent_dim), # ← Bottleneck!
)
# ─── Decoder (đối xứng) ───
self.decoder = nn.Sequential(
nn.Linear(latent_dim, 32),
nn.ReLU(),
nn.Linear(32, 128),
nn.ReLU(),
nn.Linear(128, 784),
nn.Sigmoid(), # Output ∈ [0, 1] — phù hợp với ảnh normalized
)
def forward(self, x: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
# x: [B, 784] — flatten ảnh 28×28
z = self.encoder(x) # [B, latent_dim]
x_hat = self.decoder(z) # [B, 784]
return x_hat, z
def encode(self, x: torch.Tensor) -> torch.Tensor:
"""Chỉ lấy latent code — dùng cho giảm chiều / anomaly detection."""
return self.encoder(x)
def decode(self, z: torch.Tensor) -> torch.Tensor:
"""Generate từ latent — lưu ý: vanilla AE không generative tốt."""
return self.decoder(z)
# ─── Training loop ───
def train(latent_dim: int = 8, epochs: int = 20, batch_size: int = 256):
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
transform = transforms.Compose([
transforms.ToTensor(),
transforms.Lambda(lambda x: x.view(-1)), # flatten 28×28 → 784
])
train_ds = datasets.MNIST("./data", train=True, download=True, transform=transform)
loader = DataLoader(train_ds, batch_size=batch_size, shuffle=True)
model = Autoencoder(latent_dim=latent_dim).to(device)
optimizer = optim.Adam(model.parameters(), lr=1e-3)
# BCE phù hợp với ảnh normalized [0, 1]
loss_fn = nn.BCELoss()
for epoch in range(epochs):
total = 0.0
for x, _ in loader: # bỏ qua nhãn — self-supervised!
x = x.to(device)
x_hat, _ = model(x)
loss = loss_fn(x_hat, x)
optimizer.zero_grad()
loss.backward()
optimizer.step()
total += loss.item() * x.size(0)
avg = total / len(train_ds)
print(f"Epoch {epoch + 1:2d}/{epochs} | loss = {avg:.4f}")
return model
# ─── Sử dụng latent code cho anomaly detection ───
@torch.no_grad()
def anomaly_score(model: Autoencoder, x: torch.Tensor) -> torch.Tensor:
"""Reconstruction error cao → anomaly."""
x_hat, _ = model(x)
# Per-sample MSE
return ((x - x_hat) ** 2).mean(dim=-1)
if __name__ == "__main__":
model = train(latent_dim=8, epochs=10)
# Kiểm tra: sample 1 mẫu, xem loss
x_sample = next(iter(DataLoader(
datasets.MNIST(
"./data", train=False, download=True,
transform=transforms.Compose([
transforms.ToTensor(),
transforms.Lambda(lambda x: x.view(-1)),
]),
),
batch_size=1,
)))[0]
scores = anomaly_score(model, x_sample)
print(f"Anomaly score: {scores.item():.4f}")"""Denoising Autoencoder — variant quan trọng:
input = ảnh + nhiễu, target = ảnh sạch.
Mạng phải hiểu bản chất dữ liệu để lọc nhiễu."""
import torch
import torch.nn as nn
class DenoisingAE(nn.Module):
"""Convolutional Denoising AE cho MNIST."""
def __init__(self, latent_dim: int = 32):
super().__init__()
# Encoder: 28×28 → 14×14 → 7×7 → latent
self.encoder = nn.Sequential(
nn.Conv2d(1, 16, 3, stride=2, padding=1), # 28 → 14
nn.ReLU(),
nn.Conv2d(16, 32, 3, stride=2, padding=1), # 14 → 7
nn.ReLU(),
nn.Flatten(),
nn.Linear(32 * 7 * 7, latent_dim),
)
# Decoder: latent → 7×7 → 14×14 → 28×28
self.decoder = nn.Sequential(
nn.Linear(latent_dim, 32 * 7 * 7),
nn.ReLU(),
nn.Unflatten(1, (32, 7, 7)),
nn.ConvTranspose2d(32, 16, 3, stride=2, padding=1, output_padding=1),
nn.ReLU(),
nn.ConvTranspose2d(16, 1, 3, stride=2, padding=1, output_padding=1),
nn.Sigmoid(),
)
def forward(self, x_noisy):
z = self.encoder(x_noisy)
x_hat = self.decoder(z)
return x_hat
def add_gaussian_noise(x: torch.Tensor, sigma: float = 0.3) -> torch.Tensor:
"""Thêm nhiễu Gaussian. sigma lớn → nhiễu mạnh."""
return (x + sigma * torch.randn_like(x)).clamp(0, 1)
def train_denoising(model, loader, optimizer, loss_fn, epochs=20, sigma=0.3):
model.train()
for epoch in range(epochs):
total = 0.0
for x, _ in loader:
x_clean = x # target là ảnh sạch
x_noisy = add_gaussian_noise(x_clean, sigma=sigma)
x_hat = model(x_noisy)
# Loss giữa output và ảnh SẠCH (không phải noisy input!)
loss = loss_fn(x_hat, x_clean)
optimizer.zero_grad()
loss.backward()
optimizer.step()
total += loss.item() * x.size(0)
print(f"Epoch {epoch + 1:2d} | loss = {total / len(loader.dataset):.4f}")
# Điểm mấu chốt: mạng không thể chỉ copy input sang output
# (vì input ≠ target). Nó BUỘC phải học lọc nhiễu → biểu diễn robust.
#
# Denoising AE là tiền thân của:
# - BERT: mask 15% token, dự đoán lại → "denoising" trong NLP
# - MAE (Masked AE): mask 75% patch ảnh → pretrain cho ViT
# - Diffusion models: denoising lặp lại thành generative mạnh nhất hiện nayMột cách nhìn đang rất thịnh hành gần đây: mọi mô hình denoising đủ mạnh đều trở thành generative model. Diffusion models (Stable Diffusion, DALL-E 3, Imagen) về cơ bản là chuỗi các denoising autoencoder lặp lại — mỗi bước khử một ít nhiễu. Bằng cách học phân phối của noise ở mọi mức, chúng có thể sinh ảnh từ pure noise. Autoencoder — tưởng chừng chỉ là kĩ thuật nén — đang là gốc rễ của generative AI hiện đại.
Bạn train AE trên máy bình thường (không bị lỗi). Sau 100 epoch, reconstruction gần như hoàn hảo nhưng sample z ngẫu nhiên từ N(0, I) lại cho ra ảnh vô nghĩa. Lí do?
- Encoder + Bottleneck + Decoder. Mục tiêu: x̂ ≈ x qua một cổ chai d << D.
- Bottleneck buộc mạng học đặc trưng quan trọng nhất — nonlinear PCA.
- AE tuyến tính + MSE ≡ PCA. Phi tuyến → mạnh hơn PCA, học được manifold cong.
- Ứng dụng: anomaly detection, giảm chiều, khử nhiễu, pretrain encoder.
- Điểm yếu: latent space rời rạc → không sinh dữ liệu mới tốt → cần VAE.
- Biến thể: Denoising, Sparse, Contractive, VAE, MAE — đều dùng lại khung chung.
Kiểm tra hiểu biết
Autoencoder bottleneck có 2 neuron. Input là ảnh 784 pixel (28×28). Mạng học được gì?