CS231n这门课我在深度之眼第七期里已经跟了大半程。前二十篇笔记一直在补图像分类、反向传播、卷积结构这些基础到了第二十一篇笔记内容明显换挡从“判断一张图是什么”转向“让模型自己画出图来”。这篇笔记我前后翻了三遍才把公式和直觉对齐趁热把它整理出来当作对自己的记录也给同在刷课的同学一条比较完整的参考。如果你正好走到这一课或者你只是想搞懂GAN和VAE到底在做什么这篇笔记应该能省你不少查资料的力气。我会把课上推导的逻辑链、我自己复现时踩过的坑还有整理笔记的方法一次讲清楚。1. 第二十一篇笔记正好卡在“从判别到生成”的拐点1.1 课程到这一步在讲什么CS231n前面的大部分内容核心都是判别模型。给一张猫的图片模型输出标签给一张街景模型框出行人车辆。这是把高维输入压缩成一个决策结果的过程本质上是在学习“输入到输出”的映射。而第二十一篇开始视角完全反过来给一个随机的隐变量让模型生成一张合理的图片。数据从哪里来、图片的分布长什么样成为了主角。这个转变非常重要。判别式模型只要学出决策边界就行生成式模型却要逼近真实数据分布p(x)。课堂上引入GAN和VAE正是瞄准这个目标的两条代表性技术路线。我当时的第一反应是“生成图片能不能商用”但课程更想传递的是生成模型不只会画画它逼着我们把“概率分布”这个概念落到实处。你想让模型伪造出以假乱真的图片首先得让它的损失函数和训练逻辑符合分布估计的本质这跟分类用的交叉熵完全是两码事。1.2 笔记里列出的四个核心知识点我在笔记二十一里把内容分成四条主线后面所有推导和代码都围绕它们展开显式密度模型以VAE为代表明确建模p(x)并最大化似然这时候你本身就有一个关于分布的表达隐式密度模型以GAN为代表不直接写p(x)而是通过生成器G(z)和判别器D(x)的对抗逼出分布训练博弈思想判别器要分辨真实数据和生成数据生成器要骗过判别器这是minimax问题的典型结构重参数化的工程意义让“采样”能传递梯度是VAE能反向传播的关键细节。这四件事串起来就是本讲笔记的主干。我在学习后期回头看发现这一讲其实是一个分水岭如果能把GAN的博弈关系、VAE的变分下界这两件事真正内化后面再去看图像翻译、视频预测甚至扩散模型都会顺很多。因为它们都是“如何建模分布”这个问题的不同解法CS231n在第二十一篇把基础范式一次给齐了。2. 核心演算笔记GAN的博弈和VAE的变分2.1 GAN目标函数的完整推导链GAN的出发点特别简单。生成器G把随机噪声z映射到图片空间得到G(z)判别器D是一台二分类器给真实图片高分给生成图片低分。整个模型的对抗目标写成V(D, G) E[log D(x)] E[log(1 - D(G(z)))]第一项期望在真实数据上第二项期望在噪声分布上。判别器想让V变大尽量给真图高分、假图低分生成器想让V变小努力让假图骗过D。这就是一个典型的minimax问题。推导的时候需要明确一点对于固定的G判别器的最优解可以直接求出来。把期望展开成积分对被积函数关于D(x)求变分令导数为零得到判别器的最优形式是D*(x) p_data(x) / (p_data(x) p_g(x))也就是说给定一个样本x如果来自真实分布D的输出接近1如果来自生成分布输出接近0。把这个最优D回代到V中经过整理会看到生成器的损失和两个分布的JS散度挂钩。这解释了为什么GAN能做到“分布逼近”在理想情况下生成器的目标就是最小化真实分布和生成分布之间的距离直到两个分布重合。不过课上和论文里都提到一个关键陷阱原来的目标函数中生成器用的是log(1 - D(G(z)))。当D很强、一眼识破假图时这个损失接近于0梯度很小生成器在初期几乎学不动。所以实践里普遍改成“非饱和损失”让生成器去最大化log D(G(z))。这个改动虽然形式上只是把负号换了个位置实际效果却天差地别我复现时对这一点体会特别深。2.2 VAE的ELBO和重参数化怎么推导才不晕VAE的思路是显式地把数据建模成“先验采样生成”的过程。假设隐变量z服从标准正态分布图片x由条件分布p(x|z)生成。我们想最大化log p(x)但这个积分无法直接算所以引入一个近似后验q(z|x)来逼近真实后验p(z|x)。从贝叶斯公式出发log p(x)可以拆成两项。让q(z|x)参与进来经过一番整理得到log p(x) ELBO KL(q(z|x) || p(z|x))其中ELBO等于“重建期望”减去“KL散度项”ELBO E[log p(x|z)] - KL(q(z|x) || p(z))最大化ELBO就是在同时做两件事第一让生成分布尽量重建原图第二让编码器的输出分布尽量贴近标准正态先验。当近似后验足够好时ELBO会逼近真实的log p(x)。推到这里有一个工程上的拦路虎z是通过高斯分布采样得到的而采样过程本身不可导梯度没法穿过它到达编码器。解决办法就是课上反复强调的重参数化技巧。把采样改写成z μ σ * εε服从标准正态分布这样随机性全部落在ε上而μ和σ成为可导的确定性变换。反向传播时梯度就能顺畅地流到编码器的网络参数上。我刚开始看这段总觉得“反正z的分布没变换种写法就能训练”太玄直到自己动手把计算图画了一遍才明白重参数化相当于把随机源从“需要求梯度的路径”上剥离出去这是工程实现上极其聪明的一步。2.3 两个模型的定性对比笔记里我做了一个很粗的对齐表把两代生成模型的性格差异写清楚方便自己以后查阅维度GANVAE分布表达隐式不直接写密度显式定义p(x训练目标对抗minimax最大化ELBO输出质量更锐利、细节丰富经常偏模糊覆盖度容易模式崩塌覆盖不全更能覆盖多模态但整体模糊可解释性隐空间没有明确约束隐空间被KL项拉向先验语义更紧凑这张表不是评价谁好谁坏而是说它们的偏好不同。GAN像一个追求“以假乱真”的画家样样细节都要画到位代价是可能只反复画自己擅长的内容VAE像一个“求稳”的记录员什么场景都能画但笔触总是偏虚。两种基础范式后来衍生出无数变体理解这层对比再看变体就清楚它们各自在修复什么短板。3. 从演算到代码DCGAN的PyTorch复现3.1 网络骨架为什么DCGAN比原始GAN稳原始GAN用全连接网络搭G和D在小数据集上勉强能跑但图像尺寸一大就各种不稳定。DCGAN的关键改变是把全连接换成卷积结构。生成器用转置卷积不断上采样从100维噪声一路扩到64x64的图片判别器用普通步长卷积下采样最后输出一个标量真伪分数。我在笔记里整理了DCGAN的几个设计要点后来复现时基本照单全收生成器每一层都用BatchNorm但判别器第一层故意不加因为输入分布已经过归一化再加BN会放大内部协变量偏移激活函数统一用ReLU生成器和LeakyReLU判别器判别器不输出概率而是原始logit交给损失函数处理全局平均池化替代全连接层减少参数量也降低过拟合风险生成器最后一层用tanh把像素输出限制在[-1, 1]输入图片要做同样的归一化。我当时问过一个问题这些设计是理论推导出来的还是实验试出来的根据课程内容和论文说明绝大多数是实践总结比如BatchNorm让训练更稳、LeakyReLU避免判别器对负输入完全失去梯度。所以这是一套“工程经验堆出来的结构”照着做通常不会太差但别指望有严格证明保证收敛。3.2 损失函数和训练循环的细节复现时最容易出错的就是判别器的更新顺序。通用做法是先更新判别器再更新生成器但判别器不能一次更新太猛否则生成器梯度就会失效。我的核心训练循环是这样的for epoch in range(epochs): for real_imgs, _ in dataloader: batch_size real_imgs.size(0) real_imgs real_imgs.to(device) z torch.randn(batch_size, z_dim).to(device) # 训练判别器真图分高假图分低 d_optimizer.zero_grad() logits_real discriminator(real_imgs) logits_fake discriminator(generator(z).detach()) loss_d bce(logits_real, ones) bce(logits_fake, zeros) loss_d.backward() d_optimizer.step() # 训练生成器让假图骗过判别器 g_optimizer.zero_grad() z torch.randn(batch_size, z_dim).to(device) fake_imgs generator(z) logits_fake discriminator(fake_imgs) loss_g bce(logits_fake, ones) loss_g.backward() g_optimizer.step()两个最关键的细节是detach和BCEWithLogitsLoss。判别器看到假图时如果不加detach梯度会从判别器一路流回生成器把生成器的梯度也顺带更新了加了detach判别器只更新自己的参数不会干扰生成器那一半。BCEWithLogitsLoss把sigmoid和交叉熵合在一起算数值稳定性比手动算log(1-D)好得多。手动写法容易在D接近1时出现log 0损失直接变NaN这是新手最容易碰到的坑。3.3 我记录的超参数清单在笔记里我把一组经过验证的常见超参单独列出来复现时不用每次从头试参数名数值说明优化器Adam比SGD稳对超参敏感度低学习率0.0002DCGAN论文中推荐的默认值beta10.5Adam第一矩衰减系数默认0.9在某些GAN上会震荡z维度100噪声向量长度太小则多样性差batch size64或128越大判别器越好但消耗显存图片尺寸64x64保留足够细节又不会训练过慢为什么学习率和beta1要特别讲究Adam默认的beta10.9会把历史梯度的权重拉得很高在对抗博弈中容易让判别器在较长时间里保持“刻板印象”导致训练反复横跳。调成0.5历史动量变短参数更新更灵活生成器才有机会追赶。这个点在只看公式时感觉不到实际训练几 epoch 后对比loss曲线就一目了然。4. 训练生成模型的典型翻车现场4.1 模式崩塌最常见也最难缠模式崩塌的表现是生成器反复输出同一张图或少数几张图比如手写数字明明有十类它只学会写其中一个数字。原因在于如果某一种输出能稳定骗过D生成器就没有动力去尝试其他可能性因为它去开拓多样性反而会增加被识破的风险。排查方式分两步。第一步看生成图如果连续几个epoch输出几乎一致基本可以断定崩塌第二步看loss趋势判别器loss居高不下与生成器loss持续走低同时出现需要警惕。缓解手段我试过几种比较有效的是给判别器加标签平滑把真图标签从1改成0.9让判别器不要把判别边界推得过于绝对再就是把判别器和生成器的更新比例从1:1改成5:1或1:3看哪边过度强势。这两种都是调GAN时的常规手段虽然不能百分之百消除崩塌但能把风险压到可接受范围。4.2 loss乱跳但图在变好有一个非常迷惑的现象训练过程中整体loss没有明显下降甚至上下跳得很厉害但每隔十来个epoch去看生成图发现图的质量确实在提高。我开始以为代码有bug后来查资料才发现这是对抗训练的常态。判别器和生成器在互相追赶任何一方的损失剧烈变化都代表另一方正在反击loss曲线不能像分类任务那样作为收敛的唯一凭证。遇到这种情况我的建议是只看真实图像质量不看loss曲线。固定几个随机种子生成固定的样本集合每次测试都用同一批种子然后把图像拼接保存。这样你能肉眼对比“同一批噪声”下模型的表现比盯着一堆数字靠谱得多。4.3 数值稳定性问题数值问题在GAN训练里非常常见。手动实现损失函数时log(1-D(G(z)))在D输出接近1时会饱和梯度趋近于零生成器原地踏步。更严重的是如果判别器输出一个极端数值比如1e-6log直接在数值上变成负无穷反向传播立刻得到NaN。解决办法就是用BCEWithLogitsLoss统一处理logit和标签避免手工做sigmoid再取log。另一个容易被忽视的点是生成器输出层用了tanh假图范围在[-1,1]如果真实图片没做同样范围的归一化判别器很容易凭取值区间判断真伪导致训练假收敛。我第一次复现时就把真实图片直接喂进去结果判别器五分钟内就“赢”了生成器完全学不到东西。5. 从笔记二十一延伸出去的学习方法论5.1 推导卡 实验对照表双轨记法刷到这一篇之后我明显感到课上公式密度上升纯抄PPT已经记不住了。于是我把笔记体系改成双轨制推导卡专门记录“从公式到结论”的中间步骤实验对照表专门记录“改了哪个参数→效果如何”。推导卡的核心不是把板书原样抄下来而是逼自己重新写一遍。比如GAN的最优判别器推导我会从目标函数出发重新做一次变分求导然后标注每一步用了什么性质。整个过程很慢但效果显著原本只记住结论的同学在推导卡协助下能把“为什么要引入p_data和p_g的比值”讲清楚。实验对照表听起来更简单实际坚持记录的人很少。我每次改超参都会把日期、改动项、训练轮数、可视化效果截图路径写进表里。这样过了一周再翻笔记能立刻知道当时试过什么、为什么不成立。5.2 实验记录的具体格式笔记里的实验对照表大致长这样实验序号改动项具体参数观察结果结论01基线DCGAN默认参数50 epoch后出现模糊图片结构基本可用02判别器学习率0.0008D迅速碾压G生成图无变化学习率不平衡03标签平滑0.9训练稳定性提升图片细节更丰富推荐保留04更新比例D:G1:3生成器更新过多多样性下降维持1:1更稳这个格式的妙处在于它把“我调过什么”和“效果如何”固化成可追踪的历史。以后再遇到别人问“你的DCGAN为什么崩了”你直接打开表看哪一次实验印证了假设比临时复现快太多。5.3 从老架构走向新架构的阅读路线学完笔记二十一如果你不满足于DCGAN接下来可以按时间线去读几类代表性工作。先读原始GAN论文把博弈目标吃透然后读DCGAN论文理解网络结构设计再去读图像翻译类的经典工作比如pix2pix和CycleGAN它们把GAN从“无中生有”扩展到“图到图的转换”。如果对显式生成感兴趣还可以沿着VAE的方向去看后续的改进工作。扩散模型是最近非常重要的一条新路线它不再用对抗博弈而是用逐步去噪的方式来生成图像训练稳定性比GAN好很多。理解VAE之后再去接触扩散模型会轻松不少因为两者都在处理“先验到数据”的路径只是VAE一步到位扩散模型把它拆成多步。这条阅读路径不需要一口气读完每看完一篇回去更新一次推导卡和实验对照表会比单纯追求阅读数量有意义得多。刷到笔记二十一我在CS231n的体验发生了一个明显变化前面的章节教你怎样读懂图像这一章教你怎样制造图像。对我来说最大收获反而不是GAN或者VAE本身而是第一次看清“分布”这个概念在深度学习中到底扮演什么角色。训练生成模型时你不可能靠调参绕过对分布的思考每个参数的作用、每种损失的走向最终都回到你对数据分布的理解。最后分享一个我踩过多次才养成的习惯调GAN前先固定随机种子固定测试噪声固定可视化脚本。没有这三样你所有的“调参经验”都只是模棱两可的直觉出了问题也没法复盘。先把这三步固化下来再去碰任何模型你会发现自己学得快一倍。
阅读完成 · 觉得有帮助?