ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

MATLAB复现DCGAN:生成器判别器设计要点与稳定训练实践

MATLAB复现DCGAN:生成器判别器设计要点与稳定训练实践 简介面向本科、硕士教研学习的DCGAN对抗网络Matlab实现包含可直接运行的代码与训练结果帮助深度学习者快速理解生成对抗网络的原理与训练流程。资源包共4个文件包括Matlab脚本、MNIST数据集、文本说明和训练过程动态演示图压缩包大小约14.67MB结构清晰、便于复现。已有319人学习下载在相关领域具有较好的参考价值。借助该代码读者可在Matlab2014a/2019a环境下运行直接观察生成图像的迭代变化并可在现有基础上调整网络结构、超参数进一步开展实验对比。该资源可作为深度学习课程设计、毕业设计或科研入门的参考尤其适合对图像生成感兴趣的读者团队长期涉及神经网络预测、图像处理、路径规划等Matlab仿真研究适合希望结合其他算法或扩展应用场景的高校学习者。1. 为什么用MATLAB复现DCGAN一个zip里的生成器与判别器拿到这份DCGAN对抗网络资源时压缩包里只有四个东西DCGAN.m、DCGANmnist.gif、mnistAll.mat和一份说明txt。很多人跑完主程序用implay把动画看一遍就关掉了其实这个包最值得研究的是两件事一是MATLAB的深度学习工具箱如何把transposedConv2dLayer与convolution2dLayer拼成完整的GAN链路二是在CPU环境下训练MNIST这种小数据集时DCGAN到底需要怎样的调参节奏。我自己的经验是MATLAB写GAN并不比PyTorch啰嗦太多dlnetwork把前向传播、自动微分都封装好了处理28×28灰度图时训练一个epoch通常只要十几秒非常适合当作理解生成对抗网络的实验平台。这套代码面向教研场景读研究生在复现深度卷积GAN论文里的若干结论时可以直接拿它改生成器深度和损失函数不必从零搭环境。2. DCGAN网络结构拆解转置卷积与strided卷积的MATLAB定义2.1 设计原则全卷积、无池化、批量归一化DCGAN原文里提出了几个影响后续所有GAN变体的设计原则用strided卷积替代池化做下采样用转置卷积做上采样在生成器和判别器里都加批量归一化Batch Normalization去掉全连接层生成器输出层用Tanh激活判别器隐藏层统一用LeakyReLU。这些原则直接决定了我们等下在MATLAB里定义各个层的方式。如果你直接拿图像分类网络比如LeNet当作判别器主干不加修改地接进对抗训练框架很快就会遇到判别器loss压到底、生成器梯度消失的问题因为分类网络的结构里带了太多pooling层梯度回传到生成器时已经被削得很弱。2.2 生成器从100维噪声到28×28图像生成器的作用是把一个100维的高斯噪声向量映射成一张28×28×1的灰度图。在MATLAB的深度学习工具箱中我们用dlnetwork来组装这个映射。第一层是featureInputLayer接收噪声输入然后用一个自定义的projectAndReshapeLayer把100维向量变换成7×7×128的特征图接着叠两个转置卷积把空间尺寸逐步放大最后一层不加批量归一化而是直接用tanhLayer把输出压缩到[-1, 1]。代码结构大致是这样的filterSize 5; numFilters 64; generatorLG [ featureInputLayer(100, Name, noiseIn) projectAndReshapeLayer(7*7*128, 128, 7, 7, proj) transposedConv2dLayer(filterSize, numFilters*2, Stride, 2, Cropping, same, Name, tconv1) batchNormalizationLayer(Name, bn1) reluLayer(Name, relu1) transposedConv2dLayer(filterSize, 1, Stride, 2, Cropping, same, Name, tconv2) tanhLayer(Name, tanhOut) ]; lgraphGenerator layerGraph(generatorLG); dlnetGenerator dlnetwork(lgraphGenerator);这段代码里projectAndReshapeLayer负责把噪声向量reshape成特征图其内部通常用fullyConnectedLayer先做线性变换再reshape两个transposedConv2dLayer的Stride都设为2每次转置卷积会把特征图边长放大一倍所以特征图尺寸从7×7变成14×14最后变成28×28。注意第一个转置卷积的滤波器数量是numFilters*2即128个第二个则直接输出单通道图像。Cropping设为same可以保证输出尺寸恰好是输入的两倍这是DCGAN实现里最容易出错的地方如果你发现输出是29×29或者27×27优先检查这个参数。2.3 判别器strided卷积代替pooling判别器可以看作是生成器的镜像结构但方向相反输入一张28×28图像输出一个标量表示输入是真实MNIST图像的概率。我们用步长为2的普通卷积逐层缩小空间尺寸每层卷积之后接批量归一化和LeakyReLU最后一层用convolution2dLayer把特征图压缩为1×1的logit值。这里的logit不经过sigmoid因为后面计算损失时我们直接调用交叉熵函数它内部会做数值稳定的sigmoid处理。scale 0.2; discriminatorLG [ imageInputLayer([28 28 1], Normalization, none, Name, imgIn) convolution2dLayer(filterSize, numFilters, Stride, 2, Padding, same, Name, conv1) leakyReluLayer(scale, Name, lrelu1) convolution2dLayer(filterSize, numFilters*2, Stride, 2, Padding, same, Name, conv2) batchNormalizationLayer(Name, bn2) leakyReluLayer(scale, Name, lrelu2) convolution2dLayer(filterSize, 1, Stride, 1, Padding, same, Name, conv3) ]; lgraphDiscriminator layerGraph(discriminatorLG); dlnetDiscriminator dlnetwork(lgraphDiscriminator);leakyReluLayer(scale)中scale就是负斜率通常取0.2。conv1和conv2的Stride都设为2特征图从28×28降到14×14再到7×7最后conv3用1×1大小的感受野把每个空间位置映射成一个标量。实际上conv3可以当作一个共享的全连接层在使用它把7×7×128的特征图逐位置打分。和生成器不同判别器第一层不接批量归一化这符合DCGAN原文的设计因为BN在判别器输入层容易让训练产生震荡。2.4 两个网络的结构对比模块生成器判别器输入100维噪声向量28×28×1图像上/下采样方式转置卷积Stride2普通卷积Stride2激活函数ReLU 输出层TanhLeakyReLU(0.2)批量归一化除输出层外全部使用输入层不使用其余使用输出28×28×1图像值域[-1,1]1×1标量logit需要说明的是dlnetwork接口要求输入数据是dlarray格式维度顺序默认为SSCB空间-空间-通道-批。很多人在老版本MATLAB跑惯了脚本式网络容易忘记把普通数组转成dlarray这一步会在后面训练里统一处理。另外若你的机器有NVIDIA显卡并且安装了GPU版本的深度学习工具箱训练时会自动走GPU速度会快数十倍没有的话CPU模式也足以跑完20个epoch。3. 训练循环与关键参数adam、批大小与噪声维度的搭配3.1 数据加载与预处理mnistAll.mat里保存的是原始MNIST数据集常见的布局是train_x、train_y、test_x、test_y四个变量其中图像以向量形式存储。加载后先用whos确认变量名和数据类型再把它重排成28×28×1×N的四维数组。需要注意这个文件里的图像通常保存为uint8类型值域在0到255之间而DCGAN生成器输出的是[-1, 1]区间的tanh值所以真实图像也要归一化到同一量纲不能让判别器通过数值范围就能轻易区分真伪。load mnistAll.mat; trainX double(train_x) / 127.5 - 1; % 映射到[-1,1] trainX reshape(trainX, 28, 28, 1, []); trainX dlarray(trainX, SSCB); % 指定维度顺序 numTrain size(trainX, 4); numEpochs 20; miniBatchSize 128; numIterationsPerEpoch floor(numTrain / miniBatchSize);这里double(train_x) / 127.5 - 1是关键的一行除以127.5而不是255减1之后把[0,255]映射到[-1,1]。如果写成/255会得到[0,1]区间这就和生成器的tanh输出范围不一致相当于人为降低了判别器的难度训练前期看着loss正常后期生成图像的对比度会异常。dlarray(trainX, SSCB)中的格式标签告诉MATLAB各维度的含义S代表空间维度宽和高C代表通道B代表批次后续dlgradient依赖这个信息计算梯度。3.2 对抗损失与adam更新训练循环的思路是交替更新判别器和生成器。每次迭代先随机抽一个真实图像批次同时从标准正态分布中采样一批100维噪声用生成器产出假图。判别器的损失是真实图像与假图像两组二分类交叉熵之和生成器的损失则是假图像被判别器判定为真类的交叉熵。在MATLAB里我们自定义一个模型函数来执行前向传播和损失计算然后用dlgradient自动求梯度最后用adamupdate更新网络参数。paramsG dlnetGenerator.Learnables; paramsD dlnetDiscriminator.Learnables; avgG []; avgSqG []; avgD []; avgSqD []; for epoch 1:numEpochs idx randperm(numTrain); for i 1:numIterationsPerEpoch idxBatch idx((i-1)*miniBatchSize 1 : i*miniBatchSize); XReal trainX(:, :, :, idxBatch); Z dlarray(randn(100, miniBatchSize), CB); [~, gradD, gradG] dlfeval(modelLoss, dlnetGenerator, dlnetDiscriminator, XReal, Z); [dlnetDiscriminator, avgD, avgSqD] adamupdate(dlnetDiscriminator, gradD, avgD, avgSqD, epoch); [dlnetGenerator, avgG, avgSqG] adamupdate(dlnetGenerator, gradG, avgG, avgSqG, epoch); end endrandperm(numTrain)打乱索引保证每个epoch拿到的mini-batch顺序不同这对BN层的统计量更新很重要。randn(100, miniBatchSize)生成标准正态分布的噪声CB标签表示第一维是通道100维潜变量第二维是批次。dlfeval和modelLoss配合完成前向计算与反向求导gradD和gradG里保存了所有可学习参数的梯度。adamupdate在2019a版本的深度学习工具箱里已经内置不需要自己维护一阶矩和二阶矩估计值。3.3 modelLoss函数内部实现modelLoss是训练循环的核心它把真实图像和假图像分别送入判别器然后返回两个网络的梯度和生成器的损失值。实现过程中有一个细节生成器和判别器的前向计算要在同一次dlfeval调用里完成确保梯度能跨网络回传。function [lossG, gradD, gradG] modelLoss(dlnetGenerator, dlnetDiscriminator, XReal, Z) XFake forward(dlnetGenerator, Z); YReal forward(dlnetDiscriminator, XReal); YFake forward(dlnetDiscriminator, XFake); lossD -mean(log(sigmoid(YReal)) log(1 - sigmoid(YFake))); lossG -mean(log(sigmoid(YFake))); gradD dlgradient(lossD, dlnetDiscriminator.Learnables); gradG dlgradient(lossG, dlnetGenerator.Learnables); endYReal和YFake是判别器输出的logit值sigmoid把它们压缩到(0,1)之间。这里的lossD用了原始GAN的公式等价于crossentropy对两个批次求平均。注意生成器损失没有直接使用真实图像的信息它只关心YFake能否骗过判别器如果lossD下降缓慢而lossG持续上升通常说明生成器学习率偏大或者判别器表达能力太强。dlgradient是针对Learnables求梯度返回结果的结构和dlnetGenerator.Learnables完全一致可以直接喂给adamupdate。3.4 关键参数经验表参数推荐值调整方向噪声维度100加大到256可增加生成样本多样性但训练更慢mini-batch size128减小到64让loss更震荡增大到256更稳定但显存占用高学习率0.0002大于0.001极易发散小于0.0001收敛过慢Adam beta10.5默认0.9会让训练震荡DCGAN原文建议0.5Adam beta20.999一般不用动LeakyReLU斜率0.2太小0.01会接近ReLU梯度稀疏性变强把学习率固定在0.0002、beta1调整到0.5是DCGAN原文里反复强调的一组搭配。beta1控制一阶动量衰减0.9会保留过长的历史梯度方向导致判别器在真实样本和假样本之间来回摇摆loss曲线呈现锯齿状。如果你看DCGANmnist.gif发现生成图像在几个数字之间跳来跳去建议首先检查beta1是否被改回了默认值。4. 用DCGANmnist.gif追踪生成质量识别模式崩塌与loss失效4.1 逐帧分析训练动态包里自带的DCGANmnist.gif记录了训练过程中生成图像的变化这是一个多帧动画每一帧通常是当前epoch用同一批固定噪声生成的9张或16张图像拼成的网格。用MATLAB读出来可以逐帧分析[imgFrames, cmap] imread(DCGANmnist.gif); for k 1:size(imgFrames, 4) imshow(imgFrames(:, :, :, k)); title([Frame num2str(k)]); pause(0.2); end除了直接看gif更推荐在训练脚本里自己保存每个epoch的生成图。做法是训练前用rng固定一个噪声矩阵ZFixed每个epoch结束后对这个固定噪声调用predict生成图像用imwrite写入文件。用imwrite写gif前三帧需要设置LoopCount和DelayTime后续帧用WriteMode设为append。这里的关键点是必须用predict而不是forward因为predict会跳过批量归一化层对当前batch统计量的依赖直接使用训练累积的均值和方差生成的图像才有意义。4.2 通过loss曲线判断训练状态判别器和生成器的loss在GAN里并不能直观反映生成质量尤其当判别器过于强大时它的loss可以降到0.01而生成器还在产出随机噪声。所以只盯着loss数值没有意义更好的办法是同时记录真实图像被判别器判为真实的概率和假图像被判为真实的概率把这两个概率画成曲线dRealProb mean(sigmoid(YReal), all); dFakeProb mean(sigmoid(YFake), all);在训练早期第1到第5个epochdRealProb应该维持在0.9以上dFakeProb缓慢上升。如果dFakeProb在第一个epoch就超过0.5说明判别器被生成器压制了常见原因是判别器学习率设置低于生成器或者判别器的结构比生成器浅太多。反之如果dRealProb在几个epoch后掉到0.6以下说明判别器无法区分真实图像训练已经失衡需要降低判别器学习率或给它增加一层卷积。4.3 不同epoch的生成质量特征训练阶段gif画面特征可能的问题Epoch 1-3带噪点的模糊轮廓没有数字实体正常现象无需干预Epoch 4-8数字轮廓显现边缘有重影说明特征学习有效观察是否出现全部图像趋同Epoch 9-15数字清晰笔画锐利有少量坏样本若所有样本都像同一个数字进入模式崩塌Epoch 16-20图像细节稳定背景干净若出现彩色噪点检查是否有数值溢出第9到第15个epoch是模式崩塌的高发期。所谓模式崩塌是指生成器找到了一个能稳定骗过判别器的数字比如1于是所有噪声输入都倾向输出这个数字。此时判别器的loss可能还在正常波动但生成的图像多样性消失了。要验证是否崩塌可以统计固定噪声ZFixed生成的图像之间两两的像素差如果方差小于某个阈值基本可以确认崩塌。4.4 用真实样本对比生成分布一个很实用的验证方式是把真实MNIST样本和生成样本各取16张拼在一个画布里用montage函数比较sampleReal extractdata(XReal(:, :, :, 1:16)); sampleFake extractdata(XFake(:, :, :, 1:16)); montage({sampleReal, sampleFake}, Size, [2 1]);montage会自动把两个cell数组里的图像按指定网格排列。观察时重点看两点生成图像的笔画粗细是否和真实样本差不多以及生成的数字中是否缺少某几个类别。如果40张生成图里完全没有0说明数据分布没有被完整覆盖这时增加噪声维度并调大beta1通常能得到改善。5. 排错清单与稳定收敛标签平滑与固定噪声可视化5.1 运行时的常见报错与处理这类资源包在别人机器上能跑换到你的环境就报错绝大多数问题出在维度声明和数据格式上而不是网络结构本身。下面是我实际排查中遇到频率最高的三类情况按出现概率排序。第一类是dlarray维度不匹配。dlnetwork的前向传播严格要求输入格式与imageInputLayer声明一致如果模型里写的是[28 28 1]那么输入dlarray的格式必须是SSCB且前两维必须是28、28。如果你把训练集的维度重排成[784, numTrain]直接塞进去会看到Incorrect input size一类报错。解决办法是严格使用reshape(trainX, 28, 28, 1, [])并且紧跟着声明dlarray格式。第二类是adamupdate梯度结构不一致。dlgradient返回的梯度结构必须和dlnet.Learnables的结构完全对应如果你用手动方式改了Learnables里的某个参数比如给某个层单独设了学习率梯度结构就会出现偏差。一个容易踩的坑是用了freezeWeights这类函数冻结判别器前几层后Learnables里仍然保留那些参数而dlgradient不会为你冻结的参数计算梯度导致更新时报结构不匹配。解决方法是冻结层时直接把这些参数从Learnables中移除。第三类是gif保存报错。imwrite写gif时如果第一个参数是RGB图像会提示索引图像格式问题。正确做法是先把图像转成uint8灰度图再写或者固定cmap为gray(256)。我一般会用exportgraphics配合gcf逐帧保存兼容性更好。5.2 让训练更稳定标签平滑原始GAN的判别器目标是硬标签真实样本标签为1假样本标签为0。在MNIST这种模式相对清晰的数据集上硬标签容易让判别器输出置信度过高梯度传到生成器时被压缩成接近0的饱和区。一个非常廉价有效的改进是标签平滑把真实样本的标签从1改成0.9代码里只需要改一行yReal 0.9 * ones(size(YReal), like, YReal); yFake zeros(size(YFake), like, YFake); lossD -mean(yReal .* log(sigmoid(YReal)) (1 - yReal) .* log(1 - sigmoid(YReal)) ... yFake .* log(sigmoid(YFake)) (1 - yFake) .* log(1 - sigmoid(YFake)));真实标签取0.9而不是1相当于告诉判别器真实样本也有小概率是假的这会促使判别器的决策边界不要过于靠近真实样本从而给生成器留下更大的梯度空间。实际操作中把平滑值从0.9调到0.95会发现loss曲线更平缓但生成图像的锐利度略降调到0.8则生成图像笔画更粗但容易出现背景噪点0.9是个不需要纠结的默认值。5.3 固定噪声可视化技巧训练结束后评估生成器的质量不要每次重新采样噪声因为不同噪声的生成难度不一样你觉得模型变好了可能只是抽到了一组容易生成的噪声向量。正确做法是在训练开始前用rng(seed)固定随机种子并生成一批ZFixed比如4×4共16个噪声向量每次评估都用这16个。这样一来不同epoch生成的图像可以直接进行像素级对比模型有没有进步一目了然。检查DCGANmnist.gif也一样看看第5帧和第20帧的左上角那张图是不是同一个数字并且笔画越来越干净如果是说明生成器学习到了有效的映射关系而不是简单记住了训练样本。predict(dlnetGenerator, ZFixed)返回的dlarray需要extractdata转成普通数组再通过(out1)/2把[-1,1]映射回[0,1]区间最后用imshow显示才能得到视觉上正确的灰度图。把这一小段可视化逻辑封装成visualizeGeneration.m函数每次调参后只需跑一次就能确认改动是否生效。本文还有配套的精品资源点击获取
返回列表