AI for Beginners 第 9 课:自编码器与变分自编码器(Autoencoder / VAE)原理与实战指南

发布时间:2026/10/5 13:25:10
AI for Beginners 第 9 课:自编码器与变分自编码器(Autoencoder / VAE)原理与实战指南 教程人工智能机器学习深度学习【免费下载链接】AI-For-Beginners12 Weeks, 24 Lessons, AI for All!项目地址https://gitcode.com/GitHub_Trending/ai/AI-For-Beginners点击查看免费下载自编码器Autoencoder是「AI for Beginners」系列课程《4-ComputerVision》中第 9 课的绝对核心它通过让神经网络学习输入即输出的重构任务在无标注数据上完成特征提取是自监督学习Self-Supervised Learning最简单也最具代表性的模型家族。读完本篇文章你将掌握自编码器从原理到实战的完整链路——包括卷积自编码器的 TensorFlow / PyTorch 双框架实现、去噪与超分辨率两大经典变体、变分自编码器VAE的概率建模与损失函数设计以及对抗自编码器AAE的扩展思路并能直接运行仓库中的两份 Notebook 完成动手实验。为什么训练 CNN 需要自编码器自监督学习的动机在本课程前几课中我们用卷积神经网络CNN做图像分类时面临的普遍难题是需要大量带标签数据要把图片划分到不同类别通常意味着大量人工标注工作见本课英文原版 lessons/4-ComputerVision/09-Autoencoders/README.md 的开篇说明。然而我们完全可以利用原始未标注数据来训练 CNN 特征提取器这种思路被称为自监督学习self-supervised learning。做法非常巧妙不再使用标签而是把训练图像同时当作网络的输入与输出。**自编码器Autoencoder**的核心思想因此诞生一个编码器网络encoder network把输入图像转换到某种潜在空间latent space通常是一个尺寸更小的向量一个解码器网络decoder network负责从潜在向量中重构出原始图像。因为自编码器被训练为尽可能多地保留原始图像中的信息以便精确重构网络实际上被迫寻找输入图像最有意义的嵌入embedding——这正是降维与特征学习的过程。图片来源于本课 README原图引用自 Keras 官方博客的《Building Autoencoders in Keras》一文。自编码器的四大典型应用场景单纯把图像重构出来本身似乎用处不大但正是这个朴素目标让自编码器在以下四个场景中表现出独特价值1. 降维与图像嵌入把图像维度压缩到低维空间用于可视化或训练图像嵌入image embeddings。自编码器通常能给出比 PCA主成分分析更好的结果因为它天然考虑了图像的空间性质与层次化特征而不是像 PCA 那样只做线性投影。2. 去噪Denoising从图像中移除噪声。原理是噪声携带了大量无用信息自编码器无法把它全部塞进相对较小的潜在空间于是网络只保留了图像中真正重要的部分。训练去噪器时我们以原始图像为输出以人工添加了噪声的图像为输入让网络学会无视噪声。3. 超分辨率Super-resolution提升图像分辨率。做法与去噪对称以高分辨率图像为训练输出以降采样后的低分辨率图像作为自编码器输入网络学会从模糊输入中恢复细节。4. 生成模型Generative Models训练完成后解码器部分可以被单独取出从随机潜在向量出发生成全新的对象。这是自编码器走向生成能力的起点也直接引出了下文的主角——变分自编码器。实战一在 MNIST 上搭建最简卷积自编码器TensorFlow 实现仓库提供了完整的 AutoencodersTF.ipynb 实验笔记本。其首个示例就在 MNIST 上搭建了一个最简单的卷积自编码器完整代码如下import tensorflow as tf from tensorflow.keras.datasets import mnist import numpy as np import matplotlib.pyplot as plt (x_train, y_trainclass), (x_test, y_testclass) mnist.load_data() from tensorflow.keras.layers import Input, Dense, Conv2D, MaxPooling2D, UpSampling2D, Lambda from tensorflow.keras.models import Model from tensorflow.keras.losses import binary_crossentropy, mse # —— 编码器28x28x1 - (4, 4, 8) —— input_img Input(shape(28, 28, 1)) x Conv2D(16, (3, 3), activationrelu, paddingsame)(input_img) x MaxPooling2D((2, 2), paddingsame)(x) x Conv2D(8, (3, 3), activationrelu, paddingsame)(x) x MaxPooling2D((2, 2), paddingsame)(x) x Conv2D(8, (3, 3), activationrelu, paddingsame)(x) encoded MaxPooling2D((2, 2), paddingsame)(x) encoder Model(input_img, encoded) # —— 解码器(4, 4, 8) - 28x28x1 —— input_rep Input(shape(4, 4, 8)) x Conv2D(8, (3, 3), activationrelu, paddingsame)(input_rep) x UpSampling2D((2, 2))(x) x Conv2D(8, (3, 3), activationrelu, paddingsame)(x) x UpSampling2D((2, 2))(x) x Conv2D(16, (3, 3), activationrelu)(x) x UpSampling2D((2, 2))(x) decoded Conv2D(1, (3, 3), activationsigmoid, paddingsame)(x) decoder Model(input_rep, decoded) autoencoder Model(input_img, decoder(encoder(input_img))) autoencoder.compile(optimizeradam, lossbinary_crossentropy)训练前需要对数据做归一化并补齐通道维度x_train x_train.astype(float32) / 255. x_test x_test.astype(float32) / 255. x_train np.reshape(x_train, (len(x_train), 28, 28, 1)) x_test np.reshape(x_test, (len(x_test), 28, 28, 1))训练时让输入等于输出x_train同时作为x_train传入关键超参数如下autoencoder.fit(x_train, x_train, epochs25, batch_size128, shuffleTrue, validation_data(x_test, x_test))笔记本中记录了 25 个 epoch 的实际训练曲线训练损失从第 1 个 epoch 的 0.2130 稳步下降到第 25 个 epoch 的 0.0963验证损失收敛到 0.0953 左右。几个值得注意的实现要点逐层池化压缩经过 3 次MaxPooling2D((2,2))后28×28 的输入被压缩到 4×4×8 128 维的潜在表示这正是瓶颈结构迫使网络学习紧凑特征的关键。对称上采样解码器使用UpSampling2D逐层放大最后用sigmoid激活把输出限制在 [0,1] 区间与归一化后的像素值匹配。二分类交叉熵损失binary_crossentropy常用于像素级重构任务把每个像素当作独立的二值概率。实战二PyTorch 中的 Encoder / Decoder 与统一训练循环仓库的另一份笔记本 AutoEncodersPyTorch.ipynb 用 PyTorch 复现了同样结构并给出了一份可同时驱动基础自编码器、去噪和超分辨率三种任务的统一训练循环非常适合作为自己的实验脚手架。首先定义训练参数与数据加载device cuda:0 if torch.cuda.is_available() else cpu train_size 0.9 lr 1e-3 eps 1e-8 batch_size 256 epochs 30 def mnist(train_part, transformNone): dataset torchvision.datasets.MNIST(., downloadTrue, transformtransform) train_part int(train_part * len(dataset)) train_dataset, test_dataset torch.utils.data.random_split( dataset, [train_part, len(dataset) - train_part]) return train_dataset, test_dataset transform transforms.Compose([transforms.ToTensor()]) train_dataset, test_dataset mnist(train_size, transform) train_dataloader torch.utils.data.DataLoader( train_dataset, drop_lastTrue, batch_sizebatch_size, shuffleTrue) test_dataloader torch.utils.data.DataLoader(test_dataset, batch_size1, shuffleFalse)编码器与解码器通过继承nn.Module实现class Encoder(nn.Module): def __init__(self): super().__init__() self.conv1 nn.Conv2d(1, 16, kernel_size(3, 3), paddingsame) self.maxpool1 nn.MaxPool2d(kernel_size(2, 2)) self.conv2 nn.Conv2d(16, 8, kernel_size(3, 3), paddingsame) self.maxpool2 nn.MaxPool2d(kernel_size(2, 2)) self.conv3 nn.Conv2d(8, 8, kernel_size(3, 3), paddingsame) self.maxpool3 nn.MaxPool2d(kernel_size(2, 2), padding(1, 1)) self.relu nn.ReLU() def forward(self, input): hidden1 self.maxpool1(self.relu(self.conv1(input))) hidden2 self.maxpool2(self.relu(self.conv2(hidden1))) encoded self.maxpool3(self.relu(self.conv3(hidden2))) return encoded class Decoder(nn.Module): def __init__(self): super().__init__() self.conv1 nn.Conv2d(8, 8, kernel_size(3, 3), paddingsame) self.upsample1 nn.Upsample(scale_factor(2, 2)) self.conv2 nn.Conv2d(8, 8, kernel_size(3, 3), paddingsame) self.upsample2 nn.Upsample(scale_factor(2, 2)) self.conv3 nn.Conv2d(8, 16, kernel_size(3, 3)) self.upsample3 nn.Upsample(scale_factor(2, 2)) self.conv4 nn.Conv2d(16, 1, kernel_size(3, 3), paddingsame) self.relu nn.ReLU() self.sigmoid nn.Sigmoid() def forward(self, input): hidden1 self.upsample1(self.relu(self.conv1(input))) hidden2 self.upsample2(self.relu(self.conv2(hidden1))) hidden3 self.upsample3(self.relu(self.conv3(hidden2))) decoded self.sigmoid(self.conv4(hidden3)) return decoded class AutoEncoder(nn.Module): def __init__(self, super_resolutionFalse): super().__init__() if not super_resolution: self.encoder Encoder() else: self.encoder SuperResolutionEncoder() self.decoder Decoder() def forward(self, input): encoded self.encoder(input) decoded self.decoder(encoded) return decoded注意AutoEncoder构造器中的super_resolution开关为超分辨率任务预留下一个更浅的专用编码器SuperResolutionEncoder见下文。基础自编码器的训练配置为model AutoEncoder().to(device) optimizer optim.Adam(model.parameters(), lrlr, epseps) loss_fn nn.BCELoss()统一训练循环的关键在于noisy与super_res两个可选参数def train(dataloaders, model, loss_fn, optimizer, epochs, device, noisyNone, super_resNone): tqdm_iter tqdm(range(epochs)) train_dataloader, test_dataloader dataloaders[0], dataloaders[1] for epoch in tqdm_iter: model.train() train_loss 0.0 test_loss 0.0 for batch in train_dataloader: imgs, labels batch shapes list(imgs.shape) if super_res is not None: shapes[2], shapes[3] int(shapes[2] / super_res), int(shapes[3] / super_res) _transform transforms.Resize((shapes[2], shapes[3])) imgs_transformed _transform(imgs).to(device) imgs imgs.to(device) noisy_tensor noisy[0] if noisy is not None \ else torch.zeros(tuple(shapes)).to(device) imgs_noisy imgs noisy_tensor if super_res is None \ else imgs_transformed noisy_tensor imgs_noisy torch.clamp(imgs_noisy, 0., 1.) preds model(imgs_noisy) loss loss_fn(preds, imgs) optimizer.zero_grad() loss.backward() optimizer.step() train_loss loss.item() # ...测试集评估与 tqdm 进度打印略基础版本 30 个 epoch 后train loss 与 test loss 均收敛到约 0.104。去噪自编码器让网络学会忽略噪声去噪训练的精髓在于输入与输出不再相同输入是加噪图像输出是干净图像。噪声由正态分布采样生成笔记本中的noisify函数使用np.random.normal(loc0.5, scale0.3)按 batch 尺寸生成独立的噪声张量def noisify(shapes): return np.random.normal(loc0.5, scale0.3, sizeshapes) noisy_tensor torch.FloatTensor(noisify([256, 1, 28, 28])).to(device) test_noisy_tensor torch.FloatTensor(noisify([1, 1, 28, 28])).to(device) noisy_tensors (noisy_tensor, test_noisy_tensor) model AutoEncoder().to(device) optimizer optim.Adam(model.parameters(), lrlr, epseps) loss_fn nn.BCELoss() train(dataloaders, model, loss_fn, optimizer, 100, device, noisynoisy_tensors)去噪任务训练 100 个 epochtrain/test loss 收敛到约 0.134 / 0.133。笔记本在验证阶段用test_noisy_tensor给测试图片加噪再对比加噪输入与去噪输出可以直观看到数字轮廓被清晰还原。笔记本中特别提示了一个重要边界原实验说明去噪器只在与训练数据同分布的图像上表现良好。例如用 MNIST 训练的降噪器即使输入 Fashion-MNIST同样 28×28 尺寸效果也会显著变差——这正是下文数据特异性特性的具体体现。超分辨率自编码器从 14×14 还原 28×28超分辨率与去噪的训练套路完全对称把高分辨率图像自动降采样作为输入高分辨率原图作为输出。笔记本中设定super_res_koeff 2.0即训练时把 28×28 图像缩放到 14×14 喂给网络。为此引入了一个更浅的专用编码器class SuperResolutionEncoder(nn.Module): def __init__(self): super().__init__() self.conv1 nn.Conv2d(1, 16, kernel_size(3, 3), paddingsame) self.maxpool1 nn.MaxPool2d(kernel_size(2, 2)) self.conv2 nn.Conv2d(16, 8, kernel_size(3, 3), paddingsame) self.maxpool2 nn.MaxPool2d(kernel_size(2, 2), padding(1, 1)) self.relu nn.ReLU() def forward(self, input): hidden1 self.maxpool1(self.relu(self.conv1(input))) encoded self.maxpool2(self.relu(self.conv2(hidden1))) return encoded model AutoEncoder(super_resolutionTrue).to(device) optimizer optim.Adam(model.parameters(), lrlr, epseps) loss_fn nn.BCELoss() train(dataloaders, model, loss_fn, optimizer, epochs, device, super_res2.0)30 个 epoch 后 train loss 收敛到 0.102、test loss 到 0.103。验证时同样先把测试图降采样到 14×14 再送入网络即可对比模糊输入 → 超分输出的效果。笔记本还留了一个进阶实验题尝试在 CIFAR-10 上训练 2 倍与 4 倍上采样并观察以噪声为输入时 4 倍模型的行为。变分自编码器VAE从重构走向生成传统自编码器的局限传统自编码器虽然完成了降维并抓住了重要特征但它的潜在向量往往没有太多语义。以 MNIST 为例你很难判断某一潜在向量对应哪个数字因为距离相近的潜在向量并不一定对应相同的数字。而对训练生成式模型而言理解潜在空间的结构是必要的——这一动机直接催生了变分自编码器Variational Auto-Encoder, VAE。VAE 的核心学习潜在分布的统计参数VAE 是一类学习潜在参数统计分布即所谓潜在分布latent distribution的自编码器。例如我们期望潜在向量服从正态分布 N(z_mean, exp(z_log_sigma))其中均值 z_mean 与标准差以对数形式 z_log_sigma 预测都是维度为 d 的向量。编码器负责预测这些分布参数解码器则从该分布中随机采样一个向量来重构对象。归纳起来就是三步从输入向量预测z_mean和z_log_sigma不直接预测标准差而是预测其对数以保证标准差恒为正从分布 N(z_mean, exp(z_log_sigma)) 中采样向量sample解码器以sample为输入向量尝试解码出原始图像。图中展示的 VAE 采样与重构流程原图出自 Isaak Dykeman 关于条件变分自编码器的博客。PyTorch 中的 VAE 实现笔记本中的 VAE 使用全连接网络构建潜在维度latent_dim 2便于二维可视化。编码器在输出z_mean与z_log的同时完成重参数化采样class VAEEncoder(nn.Module): def __init__(self, device): super().__init__() self.intermediate_dim 512 self.latent_dim 2 self.linear nn.Linear(784, self.intermediate_dim) self.z_mean nn.Linear(self.intermediate_dim, self.latent_dim) self.z_log nn.Linear(self.intermediate_dim, self.latent_dim) self.relu nn.ReLU() self.device device def forward(self, input): bs input.shape[0] hidden self.relu(self.linear(input)) z_mean self.z_mean(hidden) z_log self.z_log(hidden) eps torch.FloatTensor(np.random.normal(size(bs, self.latent_dim))).to(device) z_val z_mean torch.exp(z_log) * eps return z_mean, z_log, z_val注意这里正是 VAE 的重参数化技巧reparameterization trick随机性被隔离到独立的eps上z_val z_mean exp(z_log) * eps既保留了采样随机性又使梯度可以正常反传到z_mean与z_log从而可以端到端地训练。解码器结构完全对称class VAEDecoder(nn.Module): def __init__(self): super().__init__() self.intermediate_dim 512 self.latent_dim 2 self.linear nn.Linear(self.latent_dim, self.intermediate_dim) self.output nn.Linear(self.intermediate_dim, 784) self.relu nn.ReLU() self.sigmoid nn.Sigmoid() def forward(self, input): hidden self.relu(self.linear(input)) decoded self.sigmoid(self.output(hidden)) return decodedVAE 的两部分损失函数VAE 使用一个由两部分组成的复合损失函数重构损失Reconstruction loss衡量重构图像与目标图像的接近程度常用均方误差MSE与普通自编码器中的损失一致KL 损失KL loss确保潜在变量的分布始终接近标准正态分布基于Kullback-Leibler 散度——一种衡量两个统计分布相似程度的度量。两者在代码中实现如下注意重构损失按 784 个像素做了缩放KL 项对所有潜在维度求和def vae_loss(preds, targets, z_vals): mse nn.MSELoss() reconstruction_loss mse(preds, targets.view(targets.shape[0], -1)) * 784.0 temp 1.0 z_vals[1] - torch.square(z_vals[0]) - torch.exp(z_vals[1]) kl_loss -0.5 * torch.sum(temp, axis-1) return torch.mean(reconstruction_loss kl_loss)训练使用 RMSprop 优化器lr1e-3, eps1e-830 个 epoch 后 train loss 收敛到约 35.1、test loss 约 35.6。笔记本在文末留了一个作业把基础自编码器中的卷积结构搬进 VAE构建一个CNN-based VAE。VAE 的生成优势潜在空间可视化VAE 的一大优势是生成新图像相对容易因为我们知道该从哪个分布中采样潜在向量。例如在 MNIST 上训练一个 2D 潜在向量的 VAE 后可以改变潜在向量的两个分量得到不同的数字当潜在向量从参数空间的不同区域取值时生成的图像会平滑地相互过渡——这正是潜在空间连续性的体现。潜在空间还可以直接投射到二维平面进行可视化图中不同数字0–9的潜在编码在二维平面上形成清晰聚类图像出自 Dmitry Soshnikov。延伸对抗自编码器AAE在 PyTorch 笔记本的末尾课程还介绍了自编码器的又一个变体——对抗自编码器Adversarial Auto-Encoder, AAE。AAE 是生成对抗网络GAN与 VAE 的组合编码器充当 GAN 中的生成器判别器Discriminator学习区分真实图像编码与编码器生成的编码编码器的输出即一个分布解码器从该输出出发解码图像。因此 AAE 需要维护三个损失函数来自 GAN 的生成器损失、判别器损失以及来自 VAE 的重构损失。笔记本中的实现使用 784→1000→1000→1000→150 的编码器结构、镜像的解码器与带 5 层线性层的判别器并用两组不同的学习率分别优化重构与对抗正则化lr1e-4regularization_lr5e-530 个 epoch 后重构损失收敛到约 0.092。相关架构示意图见 lessons/4-ComputerVision/09-Autoencoders/images/aae.png。自编码器的三条重要特性无论哪种变体自编码器都共享以下三条值得牢记的特性数据特异性Data Specific它们只对自己训练过的那类图像效果良好。例如用花朵训练的超分辨率网络在人物肖像上表现会很差——因为网络是靠从训练数据集中学到的精细特征来脑补高分辨率细节的本课英文原版对这条特性的解释见 lessons/4-ComputerVision/09-Autoencoders/README.md。有损Lossy重构图像永远不会与原始图像完全相同损失的形态由训练时选择的损失函数决定。工作在无标注数据上Unlabeled Data这是自编码器作为自监督学习工具的立身之本。动手挑战与课后作业挑战Challenge自编码器不只适用于图像同样可以用于音乐。课程推荐探索 Magenta 项目的 MusicVAE——一个用自编码器学习重构音乐的项目可以在 Colab 上用它做各种实验看看自己能创作出什么。作业Assignment在 TensorFlow 笔记本 AutoencodersTF.ipynb 的结尾有一份任务清单直接把它当作本课的课后作业。其中两道核心题目是把潜在向量压缩到极小例如 2 维并用全连接层把卷积部分的输出降到目标维度然后将不同数字对应的潜在点绘制成散点图从不同数字出发获取它们的潜在空间表示并观察给潜在向量加入噪声后重构出的数字会发生什么变化。总结在本课中我们从CNN 需要大量标注数据这一现实痛点出发掌握了自编码器的完整知识体系它用无标注数据做输入-输出恒等重构在压缩的潜在空间里强迫网络学会最有意义的特征表示基于这一思想衍生出降维嵌入、去噪、超分辨率与生成模型四大应用而通过为潜在空间引入概率分布建模变分自编码器让生成新图像变得自然可行对抗自编码器则进一步用判别器约束潜在分布的形态。配合仓库中的两份完整 NotebookTensorFlow 版 与 PyTorch 版你可以把从理论到代码的每一步都亲手跑通。赞分享教程人工智能机器学习深度学习【免费下载链接】AI-For-Beginners12 Weeks, 24 Lessons, AI for All!项目地址https://gitcode.com/GitHub_Trending/ai/AI-For-Beginners点击查看免费下载相关推荐AI-For-Beginners 第 9 课自监督表示学习与图像重建——深入解析 Autoencoder 与变分自编码器VAEAI For Beginners 第 9 课自监督表示学习与图像重建——深入解析 Autoencoder 与变分自编码器VAE 本课围绕 AI For B教程人工智能机器学习深度学习AI-For-Beginners 自编码器实战指南从无监督表示学习到变分自编码器VAEAI For Beginners 自编码器实战指南从无监督表示学习到变分自编码器VAE 自编码器Autoencoder是深度学习中最基础也最具代表性的教程人工智能机器学习深度学习AI-For-Beginners 教程Autoencoder 与 VAE 变分自编码器实战指南——基于无标签数据的图像重建、去噪与生成AI For Beginners 教程Autoencoder 与 VAE 变分自编码器实战指南——基于无标签数据的图像重建、去噪与生成 导读 本篇文章以 AI教程人工智能机器学习深度学习创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考