简介:这份资源是面向深度学习初学者与科研人员的GAN生成对抗网络MATLAB实现代码包,旨在帮助读者在无需Python环境的前提下,直观理解生成器与判别器相互博弈的训练机制,并快速上手生成与训练数据分布相似的新样本。压缩包共14个文件,以13个m脚本和1个bmp图像为主,整体约65KB,脚本涵盖网络搭建、前向与反向传播、损失函数、优化更新及训练主循环等模块,bmp则作为真实样本数据参与训练。目前已有6526人学习下载,适合数据增强、图像合成等场景的入门实践。代码结构清晰,包含生成器与判别器的网络定义、Leaky ReLU与sigmoid等激活函数、交叉熵损失计算以及交替训练流程,并配有代码说明文件,逐项解释学习率、批次大小、训练轮数等参数配置方式,便于读者按需调整网络层数、滤波器大小与步长,训练完成后还可保存模型并生成新样本与真实数据对比,是理解GAN原理与MATLAB深度学习API的实用参考。
1. 从一次“生成器输出全灰图”说起:GAN 的 MATLAB 版本到底能跑出什么
如果你在 MATLAB 里手写过生成对抗网络,大概率经历过这个场景:训练了 200 个 epoch,生成器输出的还是一张全灰图,判别器 loss 稳如泰山,看起来一切正常,但就是什么都没学到。这不是 MATLAB 的锅,而是 GAN 本身对超参数、网络结构、训练节奏极度敏感。标题里说的“GAN生成对抗网络MATLAB版本(可直接运行)”,核心价值就在于:把那些容易翻车的细节——学习率配比、判别器更新频率、批归一化的位置、噪声输入的尺度——全部固定下来,让你拿到一个能出图的基线,再在这个基线上做自己的实验。
这篇文章面向两类人:一是需要在 MATLAB 环境里快速验证 GAN 想法、不想从零调参的算法工程师;二是做课程设计或科研复现、手头只有 MATLAB 授权、不想折腾 Python 环境的学生和研究者。我会用一个生成手写数字的 DCGAN 作为贯穿案例,把网络定义、训练循环、损失函数、可视化监控全部拆开讲清楚,最后给出一个可以直接复制运行的完整脚本结构。你不需要 GPU 集群,一块普通显卡甚至 CPU 都能跑起来看效果,只是速度差异。
2. MATLAB 里搭一个能收敛的 DCGAN:从网络层到损失函数
2.1 为什么选 DCGAN 结构而不是原始 GAN
原始 GAN 用全连接层就能实现,但在图像生成任务上极不稳定,生成器很容易退化成只输出少数几种样本。DCGAN 的核心改进是用卷积层替代全连接层,并在生成器和判别器中分别使用转置卷积和步长卷积,同时引入批归一化和 LeakyReLU。这些设计在 MATLAB 的 Deep Learning Toolbox 里都有现成层,不需要自己写 CUDA 核。
我一般会按这个比例来配生成器和判别器的容量:生成器从 100 维噪声映射到 7×7×256 的特征图,再经过两次转置卷积上采样到 28×28;判别器则反过来,两次步长卷积下采样后接全连接输出标量。这个规模在 MNIST 和类似尺寸的数据集上足够用,参数少、训练快,适合做基线。
2.2 用 dlnetwork 定义生成器和判别器
MATLAB 从 R2019b 开始推荐用dlnetwork配合自定义训练循环,而不是trainNetwork。原因是 GAN 需要交替更新两个网络,trainNetwork的封装程度太高,拿不到中间梯度。下面是我常用的生成器定义方式:
% 生成器:输入 1x1x100 的噪声,输出 28x28x1 的图像 latentDim = 100; genLayers = [ featureInputLayer(latentDim, 'Normalization', 'none', 'Name', 'noise') projectAndReshapeLayer([7 7 256], 'Name', 'project') batchNormalizationLayer('Name', 'bn1') reluLayer('Name', 'relu1') transposedConv2dLayer(5, 128, 'Stride', 2, 'Cropping', 2, ... 'Name', 'deconv1') batchNormalizationLayer('Name', 'bn2') reluLayer('Name', 'relu2') transposedConv2dLayer(5, 1, 'Stride', 2, 'Cropping', 2, ... 'Name', 'deconv2') tanhLayer('Name', 'tanh') ]; genNet = dlnetwork(genLayers);projectAndReshapeLayer是 MATLAB 专门为 GAN 提供的层,把 1×1×100 的噪声向量通过全连接映射成 7×7×256 的张量,省去了手动 reshape 的麻烦。转置卷积层的Cropping参数用来控制输出尺寸,这里设成 2 是为了让 7×7 经过 stride=2 的转置卷积后得到 14×14,再经过一次得到 28×28。如果你把 Cropping 设错,输出尺寸会对不上,后面判别器接收时直接报维度错误。
判别器的定义对称但方向相反:
% 判别器:输入 28x28x1 的图像,输出一个标量分数 discLayers = [ imageInputLayer([28 28 1], 'Normalization', 'none', 'Name', 'image') convolution2dLayer(5, 64, 'Stride', 2, 'Padding', 2, ... 'Name', 'conv1') leakyReluLayer(0.2, 'Name', 'lrelu1') convolution2dLayer(5, 128, 'Stride', 2, 'Padding', 2, ... 'Name', 'conv2') batchNormalizationLayer('Name', 'bn_d1') leakyReluLayer(0.2, 'Name', 'lrelu2') fullyConnectedLayer(1, 'Name', 'fc') ]; discNet = dlnetwork(discLayers);判别器里没有用 sigmoid,因为后面损失函数会用sigmoidCrossEntropy或者直接对 logits 做操作。LeakyReLU 的斜率设 0.2 是 DCGAN 论文里的推荐值,实测比 0.1 或 0.3 都稳。批归一化放在卷积之后、激活之前,这一点和生成器一致。
2.3 损失函数与梯度计算:别让判别器“赢太快”
GAN 的训练难点在于平衡。如果判别器太强,生成器梯度消失;如果判别器太弱,生成器学不到有意义的梯度。我一般用非饱和损失,也就是生成器的目标从“骗过判别器”改成“让判别器输出高分数”,这样梯度更稳定。
function [lossG, lossD, gradG, gradD] = ganLoss(genNet, discNet, realImages, latentDim, batchSize) % 生成噪声 Z = randn(1, 1, latentDim, batchSize, 'single'); Z = dlarray(Z, 'SSCB'); % 生成器前向 fakeImages = forward(genNet, Z); % 判别器对真实和生成图像的输出 realScores = forward(discNet, dlarray(realImages, 'SSCB')); fakeScores = forward(discNet, fakeImages); % 判别器损失:真实样本分数越高越好,生成样本分数越低越好 lossD = mean(sigmoidCrossEntropy(realScores, ones(size(realScores), 'like', realScores))) ... + mean(sigmoidCrossEntropy(fakeScores, zeros(size(fakeScores), 'like', fakeScores))); % 生成器损失:让生成样本的分数越高越好 lossG = mean(sigmoidCrossEntropy(fakeScores, ones(size(fakeScores), 'like', fakeScores))); % 计算梯度 gradG = dlgradient(lossG, genNet.Learnables); gradD = dlgradient(lossD, discNet.Learnables); end这里的关键参数是batchSize,我一般设 64 或 128。太小的话梯度噪声大,太大的话显存吃紧且更新次数少。sigmoidCrossEntropy内部会对 logits 做 sigmoid 再算交叉熵,数值上比先 sigmoid 再算 BCE 更稳定。注意dlarray的维度标记'SSCB'分别对应空间高、空间宽、通道、批,这是 MATLAB 自定义训练循环里的标准格式,标错会导致dlgradient报错。
2.4 训练循环:两个优化器的更新节奏
有了损失和梯度,训练循环本身不复杂,但有几个细节容易翻车。我用两个独立的adamupdate分别更新生成器和判别器,学习率都设 0.0002,beta1 设 0.5——这是 DCGAN 论文里的经典配置,比默认的 0.9 更适合 GAN。
numEpochs = 50; lr = 0.0002; beta1 = 0.5; avgG = []; avgGSq = []; avgD = []; avgDSq = []; for epoch = 1:numEpochs while hasdata(mnistTrain) [realImages, ~] = next(mnistTrain); realImages = single(realImages) / 127.5 - 1; % 归一化到 [-1,1] [lossG, lossD, gradG, gradD] = dlfeval(@ganLoss, ... genNet, discNet, realImages, latentDim, size(realImages, 4)); [genNet, avgG, avgGSq] = adamupdate(genNet, gradG, avgG, avgGSq, ... epoch, lr, beta1); [discNet, avgD, avgDSq] = adamupdate(discNet, gradD, avgD, avgDSq, ... epoch, lr, beta1); end endadamupdate的第三个参数是迭代次数,这里传 epoch 是为了让偏差校正正常工作。如果你在一个 epoch 内多次调用,应该传全局迭代计数而不是 epoch 数。归一化那一步把像素从 [0,255] 映射到 [-1,1],和生成器最后一层的 tanh 输出范围匹配,这一步漏掉的话生成器会一直输出灰蒙蒙的图。
3. 训练监控与调参:怎么判断 GAN 是在学习还是在崩溃
3.1 用固定噪声看生成质量的演变
GAN 训练不像分类任务那样看准确率就行,loss 曲线经常看不出问题。我的习惯是每 5 个 epoch 用同一组固定噪声生成一批图像,拼成网格保存下来。这样你能直观看到生成器是从噪声慢慢变成数字轮廓,还是突然崩成一片同色。
fixedZ = dlarray(randn(1, 1, latentDim, 25, 'single'), 'SSCB'); if mod(epoch, 5) == 0 generated = forward(genNet, fixedZ); generated = extractdata(generated); generated = (generated + 1) / 2; % 反归一化到 [0,1] montage(generated, 'Size', [5 5]); drawnow; endextractdata把 dlarray 转回普通数组,montage是 MATLAB 图像处理工具箱里的函数,没有的话可以用imtile替代。固定噪声的好处是排除随机性干扰,如果同一组噪声在不同 epoch 生成的图像越来越清晰,说明生成器确实在进步。
3.2 判别器 loss 趋近于零意味着什么
很多人看到判别器 loss 降到 0.01 以下就以为训练得很好,实际上这通常意味着判别器太强、生成器梯度消失。健康的训练里,判别器 loss 应该在 0.3 到 0.7 之间波动,生成器 loss 在 1.0 到 3.0 之间缓慢下降。如果判别器 loss 持续低于 0.1,我会做两件事:一是降低判别器的学习率,比如从 0.0002 降到 0.0001;二是给判别器的标签加一点噪声,把真实样本的标签从 1.0 改成 0.9,这叫标签平滑,能防止判别器过度自信。
% 标签平滑:真实样本用 0.9 而不是 1.0 realLabels = 0.9 * ones(size(realScores), 'like', realScores); lossD = mean(sigmoidCrossEntropy(realScores, realLabels)) ... + mean(sigmoidCrossEntropy(fakeScores, zeros(size(fakeScores), 'like', fakeScores)));标签平滑的代价是判别器输出不再那么极端,但换来的是更稳定的训练。这个技巧在 WGAN 里被更优雅地解决了,但如果你不想改损失函数结构,标签平滑是最省事的补救手段。
3.3 学习率和批大小的联动关系
学习率和批大小不是独立的。批大小翻倍时,梯度估计的方差减半,理论上学习率可以适当提高。但在 GAN 里我反而会保守一些:批大小从 64 提到 128 时,学习率保持 0.0002 不变,或者只提到 0.0003。因为判别器和生成器的平衡很脆弱,学习率太高会让某一方突然占优。
另一个容易忽略的参数是beta1。Adam 默认 beta1=0.9,动量很大,适合分类任务。但 GAN 的梯度方向变化剧烈,动量太大会导致更新方向滞后。DCGAN 论文推荐 0.5,我实测下来 0.5 确实比 0.9 稳,生成器不容易陷入模式崩溃。
4. 避坑与排查:MATLAB 跑 GAN 最常见的五个翻车现场
4.1 生成器输出全灰或全黑
现象:训练几十个 epoch 后,生成图像仍然是一张均匀的灰色或黑色图,没有任何数字轮廓。
原因:最常见的是归一化范围不匹配。生成器最后一层是 tanh,输出范围 [-1,1],但真实图像没有归一化到同一范围,判别器很容易区分真假,生成器梯度消失。另一个可能是噪声输入的尺度太小,randn默认方差为 1,如果生成器第一层没有合适的缩放,信号会逐层衰减。
解决:确认真实图像做了single(images)/127.5 - 1的归一化。检查生成器第一层projectAndReshapeLayer的输出是否用了批归一化,如果没有,加一个batchNormalizationLayer。噪声用randn而不是rand,后者范围 [0,1] 太小。
4.2 训练中途 loss 突然变成 NaN
现象:前几个 epoch 正常,突然某一批数据后 loss 变成 NaN,之后所有输出都是 NaN。
原因:梯度爆炸。GAN 的梯度有时会非常大,尤其是判别器更新次数多于生成器时。MATLAB 的dlgradient不会自动做梯度裁剪。
解决:在adamupdate之前手动裁剪梯度。可以用dlupdate对每个梯度张量做范数裁剪:
function grad = clipGradient(grad, maxNorm) grad = dlupdate(@(g) g * min(1, maxNorm / (norm(extractdata(g)) + 1e-8)), grad); end % 使用:gradG = clipGradient(gradG, 1.0);maxNorm设 1.0 到 5.0 之间,我一般用 1.0。裁剪后再传给adamupdate,NaN 基本不会再出现。
4.3 生成的数字只有一种或少数几种
现象:生成器能出清晰的数字,但翻来覆去只有 1 和 0,其他数字不出现。
原因:模式崩溃。生成器发现只要骗过判别器就行,不需要覆盖所有模式。判别器也没有足够的能力区分“多样性不足”和“真实”。
解决:一是用 minibatch discrimination,但 MATLAB 没有现成的层,需要自己写;更简单的做法是降低判别器的学习率,让生成器有更多机会探索。另外可以增大噪声维度,从 100 提到 128 或 256,给生成器更多自由度。如果还不行,考虑换成 WGAN-GP 损失,但那需要改判别器输出和损失函数结构。
4.4 GPU 显存不足或训练速度极慢
现象:batch size 设 64 就报显存错误,或者每个 epoch 要跑十几分钟。
原因:MATLAB 默认把数据放在 CPU 内存,每次迭代才传到 GPU。如果数据加载和预处理也在 CPU 上做,GPU 利用率很低。
解决:用gpuArray把数据和网络参数提前放到 GPU。dlnetwork的Learnables可以用dlupdate(@gpuArray, net.Learnables)迁移。数据方面,用minibatchqueue配合preprocessMiniBatch函数,设置OutputEnvironment为'gpu'。这样数据在后台预取,GPU 不会等数据。
4.5 换了数据集后完全训不起来
现象:在 MNIST 上跑得好好的代码,换成自己的 64×64 彩色图像后,生成器输出全是噪点。
原因:网络容量和数据集复杂度不匹配。28×28 灰度图的生成器只有两层转置卷积,换成 64×64 彩色图后感受野不够,生成器表达能力不足。
解决:增加转置卷积层数,把 7×7 的投影改成 4×4 或 8×8,中间加一层 16×16 的上采样。通道数也要相应增加,从 256 提到 512。判别器同样加深。另外学习率可能要降到 0.0001,因为参数多了之后更新幅度需要更保守。
5. 从能跑到好用:把 MATLAB GAN 嵌进自己的实验流程
5.1 用自定义数据存储替换 MNIST
MATLAB 自带的 MNIST 数据存储只是为了演示。实际项目里你大概率有自己的图像文件夹。用imageDatastore加transform和combine就能构造训练流:
imds = imageDatastore('your_image_folder', 'IncludeSubfolders', true, ... 'LabelSource', 'foldernames'); augImds = augmentedImageDatastore([64 64], imds, 'ColorPreprocessing', 'gray2rgb'); ds = transform(augImds, @(x) single(x)/127.5 - 1);augmentedImageDatastore会自动做 resize,省去手动预处理的代码。如果你的图像不是正方形,resize 会变形,这时候需要在transform里自己做 padding 或裁剪。
5.2 保存和加载训练好的生成器
训练完不保存等于白跑。dlnetwork可以直接save:
save('gan_generator.mat', 'genNet'); % 加载 loaded = load('gan_generator.mat'); genNet = loaded.genNet;注意保存的是dlnetwork对象,不是SeriesNetwork或DAGNetwork,加载后可以直接forward。如果你要把生成器导出成 ONNX 给其他框架用,MATLAB 也支持exportONNXNetwork,但自定义层可能不被支持,需要先替换成标准层。
5.3 用验证指标判断什么时候该停
GAN 没有天然的验证集准确率,我一般用两个指标:一是固定噪声生成图像的方差,方差太低说明模式崩溃;二是判别器对真实和生成样本的平均输出差,差值太小说明判别器已经分不清真假,再训练下去生成器也不会进步。
realScoreMean = mean(extractdata(forward(discNet, dlarray(realBatch, 'SSCB')))); fakeScoreMean = mean(extractdata(forward(discNet, fakeBatch))); scoreGap = realScoreMean - fakeScoreMean; % scoreGap 低于 0.1 时可以考虑停止这个判断不是绝对的,但比盯着 loss 曲线靠谱。我自己的习惯是每 10 个 epoch 算一次 scoreGap,如果连续三次都低于 0.1,就手动停掉,把当前生成器保存下来。
5.4 一个我踩过的坑:别在训练中途改网络结构
有一次我觉得生成器输出不够清晰,训练到一半给生成器加了一层转置卷积。结果dlnetwork的Learnables数量变了,之前累积的avgG和avgGSq维度对不上,adamupdate直接报错。更麻烦的是,即使我重新初始化优化器状态,生成器已经学到的特征也被打乱了,相当于从头开始。
血泪经验:网络结构在训练前定好,中途要改就重新开一个训练脚本,把之前训练好的权重用initialize迁移过去,但优化器状态必须重置。MATLAB 没有 PyTorch 那种load_state_dict的灵活机制,dlnetwork的层名和参数形状必须完全匹配才能加载。
5.5 把训练循环封装成函数的好处
最后分享一个习惯:把整个训练循环写成一个函数,输入是数据存储、网络结构参数、超参数,输出是训练好的生成器和判别器。这样你可以用不同的超参数组合批量跑实验,不用每次改脚本。
function [genNet, discNet, history] = trainGAN(ds, latentDim, numEpochs, lr, beta1) % 初始化网络 genNet = buildGenerator(latentDim); discNet = buildDiscriminator(); % 训练循环 history = struct('lossG', [], 'lossD', []); for epoch = 1:numEpochs % ... 训练代码 ... history.lossG(end+1) = lossG; history.lossD(end+1) = lossD; end endhistory结构体记录 loss 曲线,方便后面画图对比不同超参数的效果。这个封装方式让我在一个下午跑了 12 组不同学习率和批大小的组合,最后找到一组在 64×64 彩色数据集上稳定收敛的配置。MATLAB 的 GAN 训练确实比 PyTorch 啰嗦一些,但把模板搭好之后,调参效率并不低。希望帮到你。
本文还有配套的精品资源,点击获取