简介:面向深度学习入门者与MATLAB实践者的GAN生成对抗网络可运行代码包,围绕生成器与判别器的博弈训练展开,适合想绕开Python环境、直接在MATLAB中复现GAN流程的读者。包内共14个文件,以13个.m函数脚本和1张bmp示例图像为主,压缩包约65KB:脚本按功能拆分为网络初始化、前向传播、生成器与判别器各自的反向传播、ReLU与sigmoid等激活函数、sigmoid交叉熵损失及其导数计算、权重更新以及主训练入口,串起一条可直接运行的训练链路。代码配有中文说明文件,逐项解释学习率、批次大小、训练轮数等参数含义及交替训练步骤,便于边读边改。已有6534人学习,适合作为理解GAN原理、做数据增强或图像合成小实验的起步样例。
1. 从一份能直接跑的 GAN MATLAB 工程说起:它替你省掉了哪几步
手头有一份 GAN 的 MATLAB 版本,双击就能跑,这件事本身值得先拆一拆。市面上讲 GAN 的材料大多停在公式和示意图,真到要改一行生成器结构、换一套数据、把震荡的训练曲线压平的时候,能落地的素材很少。这份工程的价值不在它多新,而在于它把生成对抗网络里最容易被略过的部分——权重初始化、对抗训练循环、损失项的组织顺序、MATLAB 里dlarray的维度约定——全部摊开成可以逐行改的脚本。它适合已经知道 GAN 大概在做什么、但从没亲手把一张噪声训到能认出来的读者,也适合手上有一批小样本图像、想用 MATLAB 生态做快速验证的工程人员。换句话说,这是一份用来看清「对抗训练到底在循环什么」的骨架,而不是一个调好参数的黑盒。
2. 对抗训练的骨架:生成器、判别器与损失函数怎么在 MATLAB 里对齐
2.1 生成器和判别器各自负责什么,边界在哪
GAN 的两个网络分工很干脆,但边界经常被误解。生成器吃一段低维随机噪声,输出一张和真实样本同尺寸的图;判别器吃一张图,输出一个标量分数,越高代表越像真样本。训练时两者交替更新,生成器的目标是让判别器给自己伪造的图打高分,判别器的目标是给真图打高分、给假图打低分。这不是一个最小化单一损失的优化问题,而是一个两人零和博弈,所以你会看到两条损失曲线经常此消彼长,这不是 bug。
在 MATLAB 里落地这个博弈,关键在于把两者的更新严格隔开。很多人第一次写会犯的错,是让生成器的梯度顺带更新了判别器的可学习参数,结果两个网络互相拖拽,损失曲线看起来像随机噪声。正确的做法是用dlgradient分别对两个dlnetwork的Learnables求导,并且在求第二个网络的梯度时保留计算图,否则第二次求导拿不到中间结果。
另一个边界是判别器的输出维度。二分类 GAN 的判别器最后接一个fullyConnectedLayer(1),输出一个标量;如果写成 2 类 softmax,本质上是在做真假二分类,数学上等价但损失形式要跟着换,混用会得到一张奇怪的曲线。这一点在改结构时最容易翻车。
2.2 用 dlnetwork 搭出两个网络:层序与维度约定
MATLAB 的dlnetwork是把层数组包成可自动求导对象的容器,相比旧版的trainNetwork,它允许你手写训练循环,这正是 GAN 需要的。下面这段是生成器的典型写法,把 100 维噪声映射到 28×28 的单通道灰度图。
% 生成器:100 维噪声 -> 28x28x1 灰度图,输出范围 [-1, 1] latentDim = 100; genLayers = [ featureInputLayer(latentDim, 'Name','noise', 'Normalization','none') fullyConnectedLayer(256, 'Name','g_fc1') batchNormalizationLayer('Name','g_bn1') reluLayer('Name','g_relu1') fullyConnectedLayer(512, 'Name','g_fc2') batchNormalizationLayer('Name','g_bn2') reluLayer('Name','g_relu2') fullyConnectedLayer(28*28, 'Name','g_fc3') % reshape 到图像维度,第四维是 batch,交给 dlnetwork 自动推断 functionLayer(@(x) reshape(x, 28, 28, 1, []), 'Name','g_reshape', 'Acceleratable', true) tanhLayer('Name','g_tanh') % 输出压到 [-1,1],必须和真实数据的归一化区间一致 ]; genNet = dlnetwork(genLayers);这里的逻辑链条是:噪声先升维到 256,再升到 512,最后摊到 784 个像素,中间用batchNormalizationLayer稳住每一层的输入分布,用reluLayer提供非线性,末端tanhLayer把输出锁死在[-1,1]。functionLayer里的reshape(x, 28, 28, 1, [])是把一维向量还原成图像张量,[]让 MATLAB 自动推断 batch 维,这样换 batch size 时不用改代码。最后一个参数Acceleratable置为true,是为了让这个自定义函数层在 GPU 上也能走加速路径,关掉它就只能退回 CPU 逐元素算,训练会慢一个量级。
判别器则是反过来,从图像张量一路压到单个标量。
% 判别器:28x28x1 -> 标量分数 discLayers = [ imageInputLayer([28 28 1], 'Name','img', 'Normalization','none') convolution2dLayer(4, 64, 'Stride',2, 'Padding','same', 'Name','d_conv1') leakyReluLayer(0.2, 'Name','d_lrelu1') dropoutLayer(0.3, 'Name','d_drop1') convolution2dLayer(4, 128, 'Stride',2, 'Padding','same', 'Name','d_conv2') batchNormalizationLayer('Name','d_bn1') leakyReluLayer(0.2, 'Name','d_lrelu2') dropoutLayer(0.3, 'Name','d_drop2') fullyConnectedLayer(1, 'Name','d_fc') % 输出单标量,不做 sigmoid ]; discNet = dlnetwork(discLayers);几个参数值得单独说。卷积核用 4×4、步长 2 是一种常见配置,两次下采样把 28×28 压到 7×7,感受野足够覆盖局部纹理又不会过早丢失结构。leakyReluLayer(0.2)的斜率 0.2 是经验值,它在负半轴保留一点梯度,避免判别器在假样本上梯度直接归零。dropoutLayer(0.3)是抑制判别器过强的重要手段,但注意判别器里batchNormalizationLayer用得要谨慎,原因在第 5 章会展开。最后fullyConnectedLayer(1)直接输出实数分数,不加 sigmoid,是因为损失函数用的是 hinge 形式,分数不需要被压到概率区间。
2.3 损失函数的选择:hinge 和 BCE 到底差在哪
原始 GAN 用的是二元交叉熵,判别器输出过 sigmoid 后算log(D(x))和log(1-D(G(z)))。这个形式在理论上很干净,但在实现里两个问题会咬人:一是 sigmoid 在饱和区梯度接近 0,判别器一旦太强,生成器就学不动;二是log(1-D(G(z)))在 D 输出接近 1 时数值下溢,容易出现-Inf。
工程上更稳的是 hinge 损失,判别器不接 sigmoid,直接用实数分数:
% hinge 损失辅助函数,softplus 用数值稳定的写法 sp = @(x) max(x, 0) + log1p(exp(-abs(x))); function [lossG, lossD] = ganLoss(dReal, dFake) % dReal: 判别器对真实样本的分数; dFake: 对生成样本的分数 lossD = mean(sp(-dReal)) + mean(sp(dFake)); % 真样本分越高越好,假样本分越低越好 lossG = -mean(dFake); % 生成器只关心把假样本分数顶上去 end逻辑上,sp(-dReal)在真实样本分数为正且较大时趋近 0,说明判别器把真图判对了;sp(dFake)在假样本分数为负时趋近 0,说明假图被判低了。两个均值相加就是判别器要最小化的总量。生成器那一项故意写得极简,只保留-mean(dFake),因为生成器不接触真实样本,它唯一能做的事情就是把假样本的分数往上顶。这种裁剪过的损失比原始形式少了一层对数,梯度幅值更平稳,初学者看到的曲线不至于像心电图。
需要提醒的是,hinge 损失下判别器的输出是无界实数,如果你在训练中途想把分数当概率看,得自己加 sigmoid 或者用排序来解读,直接和 0.5 比阈值是错的。
2.4 训练循环:dlfeval、dlgradient 与 adamupdate 怎么配合
把上面几块拼起来,一个完整的训练迭代是这样的。
function [gradG, gradD, lossG, lossD] = modelGradients(genNet, discNet, X, latentDim) % X 是真实图像,格式 'SSCB',已经归一化到 [-1,1] batchSize = size(X, 4); Z = dlarray(randn(latentDim, batchSize, 'single'), 'CB'); % 噪声格式 CB XGen = forward(genNet, Z); % 生成假样本 dReal = forward(discNet, X); % 真样本分数 dFake = forward(discNet, XGen); % 假样本分数 [lossG, lossD] = ganLoss(dReal, dFake); % 先对生成器求导并保留计算图,再对判别器求导 gradG = dlgradient(lossG, genNet.Learnables, 'RetainData', true); gradD = dlgradient(lossD, discNet.Learnables); enddlfeval是 MATLAB 自动微分的入口,它要求被调函数里所有参与求导的量都是dlarray。这里randn出来的噪声必须手动包一层dlarray(..., 'CB'),因为randn本身不产生带标签的数组,不标格式后面featureInputLayer会抱怨维度对不上。forward和predict的区别也在这里体现:forward走训练模式,dropoutLayer和batchNormalizationLayer会按训练态行为执行;predict走推理模式,dropout 会关掉,BN 用滑动统计量。训练循环里必须用forward,一不留神写成predict,判别器就等于没有正则化。
'RetainData', true是第二个关键点。dlgradient默认会在求导后释放中间结果以省内存,但这里要先算生成器的梯度、再算判别器的梯度,两次求导共享同一条前向计算图,不保留就会报「找不到梯度记录」。代价是显存占用会上升,所以在显存吃紧的机器上,把 batch size 调小比去掉这个参数更明智。
外层循环用adamupdate分别更新两个网络,注意两者的动量状态要分开存。
numEpochs = 50; miniBatchSize = 128; lrG = 2e-4; lrD = 2e-4; beta1 = 0.5; beta2 = 0.999; avgG = []; avgGSq = []; avgD = []; avgDSq = []; iter = 0; for epoch = 1:numEpochs idx = randperm(numTrain); % 每个 epoch 重新打乱 for b = 1:floor(numTrain/miniBatchSize) batchIdx = idx((b-1)*miniBatchSize+1 : b*miniBatchSize); X = dlarray(single(data(:,:,:,batchIdx)), 'SSCB'); [gradG, gradD, lossG, lossD] = dlfeval(@modelGradients, genNet, discNet, X, latentDim); [genNet, avgG, avgGSq] = adamupdate(genNet, gradG, avgG, avgGSq, iter, lrG, beta1, beta2); [discNet, avgD, avgDSq] = adamupdate(discNet, gradD, avgD, avgDSq, iter, lrD, beta1, beta2); iter = iter + 1; end % 每个 epoch 存一次生成样本快照,用于肉眼判断进展 saveSnapshot(genNet, latentDim, epoch); endadamupdate的第六个参数是迭代步数,它驱动偏差修正,所以iter必须从 0 单调递增、跨 epoch 不清零,很多人每个 epoch 重置一次,结果就是前几十步的更新幅度被放大,曲线锯齿明显。beta1 = 0.5而不是默认的 0.9,是 GAN 训练里一个流传很广的经验设置,动量小一点能让判别器和生成器的拉锯更灵敏,不至于一方冲太远。
3. 把工程跑起来:目录、依赖、首轮输出的三个验证点
3.1 依赖检查与首次启动
拿到工程后不要急着点运行,先把依赖过一遍。常见做法是开一个干净的 MATLAB 会话,在工程根目录执行下面几条命令,确认工具箱和硬件都到位。
% 1) 确认深度学习工具箱可用 ver('deeplearning') % 没有输出说明没装,后面所有 dlnetwork 都会报错 % 2) 确认 GPU 是否可用,没有 GPU 也能跑,只是慢 canUseGPU = (gpuDeviceCount > 0); disp("GPU 可用: " + canUseGPU) % 3) 把工程所有子目录加入路径,避免函数找不到 addpath(genpath(pwd)); savepath; % 可选,省得下次再 addpath第一条命令是硬性门槛,dlnetwork、dlarray、dlgradient全在这个工具箱里,缺了就只能看代码不能跑。第二条不是必需的,但值得知道,因为 GAN 的训练时间对硬件非常敏感,同一份脚本在 CPU 上跑 50 个 epoch 可能要几个小时,在 GPU 上常常是十几分钟的量级。第三条容易被忽略,工程里如果有modelGradients.m、ganLoss.m这类独立函数文件放在子目录里,不addpath就会报未定义函数。
还有一个隐性依赖是数据。工程如果自带数据加载脚本,先确认它读的是本地文件还是在线拉取。在线拉取在断网环境会静默失败或者抛一个看起来毫不相关的错误,先断网跑一次就知道。
3.2 首轮训练该看什么
第一次运行不建议直接上 50 个 epoch,先跑 2 到 3 个 epoch,盯住四个地方。
第一是损失的量级。hinge 损失下,判别器损失起步通常在 1.0 到 2.0 之间,生成器损失在 0.5 到 1.0 附近。如果第一个迭代打印出来是几万甚至NaN,基本可以断定数据归一化和tanh输出区间没对齐,这是最常见的首跑故障,第 5 章会细说。
第二是快照图。每轮存下来的生成图像应该从纯噪声逐渐出现结构,前几个 epoch 可能只是一团灰雾,这正常;如果到第 10 个 epoch 还是完全均匀的噪点,说明生成器根本没在学。
第三是判别器对真假的平均分数差。训练正常时,mean(dReal)略高于mean(dFake),差值是几十到几百这个量级;如果两者几乎相等,判别器没学到东西;如果差得离谱,判别器过强,生成器会崩。
第四是显存或内存占用是否随迭代线性增长。正常的训练循环内存在几十个迭代后应该趋于平稳,如果一路爬升,多半是快照或者中间变量没清理,跑久了必然 OOM。
3.3 关键参数逐个对齐
工程里的参数不少,但不是每个都需要动。下面这张表按「动了有用」的优先级排序,方便你在换数据或换结构时快速定位。
| 参数 | 工程里的典型值 | 调整方向与影响 |
|---|---|---|
latentDim | 100 | 调大增加多样性上限,但也更难训稳;小于 32 容易出重复样本 |
miniBatchSize | 128 | 显存不足时优先降这个;小于 32 时 BN 统计量不稳定 |
lrG/lrD | 各 2e-4 | 判别器过强就单独降lrD,不要两个一起降 |
beta1 | 0.5 | 收敛慢可试 0.9,震荡大就维持 0.5 |
dropoutLayer比例 | 0.3 | 判别器准确率长期贴近 100% 时加到 0.4~0.5 |
| 真实数据归一化区间 | [-1,1] | 必须和生成器末端tanh一致,改一个就得改另一个 |
表里最后一行是死约束,不是经验值。tanhLayer的输出域固定是[-1,1],真实数据如果按[0,1]归一化,判别器会看到两个明显不同的分布区间,直接靠亮度就能分真假,生成器再怎么训也追不上。这类「看起来能跑但永远训不好」的问题,比报错更难查。
4. 调参不靠玄学:损失曲线、模式崩溃与学习率配比的判读方法
4.1 判别器过强的三个信号
GAN 训练里最常出现的失衡是判别器太强。它有三个可以观测的信号,不需要猜。
第一个信号是判别器损失迅速掉到接近 0,而生成器损失持续上升。hinge 形式下,判别器把真假彻底分开,sp(-dReal)和sp(dFake)都趋近 0,损失曲线贴着横轴走;生成器的-mean(dFake)因为假样本分数被压得很低,绝对值越来越大。这条分叉一旦出现且持续超过十几个 epoch,训练基本就废了。
第二个信号是生成样本的多样性肉眼可见地下降。判别器太强时,生成器找到任何一个能骗过判别器的点就会死死咬住,输出逐渐收敛到少数几张图甚至一张。快照图从「一堆模糊但各不相同的形状」变成「同一张脸重复 64 次」,这是典型的模式崩溃前兆。
第三个信号是判别器对真样本的分数方差变得极大。正常训练时真样本分数分布相对集中,判别器过强时它会开始对真样本内部做细粒度区分,方差拉大,这意味着它已经把注意力从「真假」转移到「真假内部的子类别」,对生成器毫无帮助。
应对手段按代价从低到高排:先把lrD单独降到lrG的一半,观察 5 个 epoch;不行就把判别器的 dropout 比例从 0.3 提到 0.5;再不行给判别器的输入加一点高斯噪声,噪声幅度取数据标准差的 0.05 到 0.1。这三招的共同目标是削弱判别器的分辨能力,让博弈重新回到势均力敌。
4.2 模式崩溃的识别与缓解
模式崩溃的表现比失衡更隐蔽,因为损失曲线可能看起来很健康。识别它不能只看损失,要看生成样本的分布。
一个低成本的做法是每次快照时同时生成 64 张图并拼成网格,人眼扫一眼就能判断多样性。更进一步,可以对生成样本做一次简单的统计:把 64 张图展平成向量,两两算余弦相似度,如果平均相似度超过 0.9,说明样本高度雷同。这个方法不需要额外工具箱,几行代码就能加进训练循环。
% 生成一批样本并计算两两余弦相似度均值,作为多样性粗指标 Z = dlarray(randn(latentDim, 64, 'single'), 'CB'); fake = extractdata(predict(genNet, Z)); % 推理模式,关掉 dropout F = reshape(fake, [], 64); % 每列一张图 Fn = F ./ vecnorm(F); % 逐列归一化 S = Fn' * Fn; % 64x64 相似度矩阵 simMean = (sum(S(:)) - 64) / (64*63); % 去掉对角线后的均值 fprintf('epoch %d 多样性指标: %.3f\n', epoch, simMean);predict在这里必须用,不能用forward,因为要的是稳定的推理输出,训练态的 dropout 会让同一张 latent 每次生成不同结果,相似度算出来偏高。vecnorm对每列求范数,Fn' * Fn一步得到全部两两内积,64 张图的规模下开销可以忽略。
缓解模式崩溃的常见手段有三个。一是给判别器的真实样本标签做单边平滑,把目标从 1 改成 0.9,防止判别器过度自信;二是给生成器的输入噪声加一点方差,让同一个 latent 附近产生略有差异的输出;三是改用 WGAN-GP 那类带梯度惩罚的损失,它对模式崩溃的抵抗力明显更强,代价是每个迭代要多算一次梯度惩罚项,训练时间大约增加三成。我一般会先试前两个,省事,效果不够再上第三个。
4.3 学习率、batch 与 latent 维度的配比经验
这三者的关系不是独立的,改一个往往要动另一个。
latentDim决定生成器的信息入口宽度。100 维对应 28×28 灰度这种低分辨率数据是够的,换成 64×64 彩色图时常见做法是提到 128 甚至 256,否则生成器没有足够的自由度去编码颜色和纹理的组合。但latentDim不是越大越好,它和第一层fullyConnectedLayer的宽度要匹配,如果latentDim = 256而第一层还是 256 个单元,信息在入口就被压掉一半,训练会明显变慢。
miniBatchSize和batchNormalizationLayer强相关。BN 在训练时用当前 batch 的均值和方差做归一化,batch 太小时统计量本身噪声很大,判别器的 BN 层会把这种噪声传导到分数上,形成一种虚假的随机性。经验下限是 32,低于这个数就考虑把 BN 换成groupNormalizationLayer或者干脆去掉。
学习率的配比则要看具体数据。文本、简单图形这类结构规整的数据,lrG和lrD取相同值 2e-4 通常没问题;真实照片这类高熵数据,判别器更容易占优,把lrD设成lrG的 0.5 到 0.7 倍往往更稳。这个比例不是固定的,判断依据还是那三个失衡信号,信号出现了再调,没出现就别乱动,改得越多越难定位问题。
5. 踩坑排查:五个让训练白跑的真实故障与对应处理
5.1 现象:第一个迭代损失就是 NaN
原因几乎总是数据区间和输出激活不匹配。生成器末端是tanhLayer,输出[-1,1];如果真实数据按[0,1]甚至[0,255]喂进去,判别器面对两个数量级完全不同的分布,第一次前向的分数就会非常大,sp(dFake)里的exp直接溢出成Inf,反传回来就是NaN。
解决办法是在数据加载处统一做一次变换,把所有真实图像映射到[-1,1]:X = 2*(X/255) - 1。做完之后建议打印一次min(X(:))和max(X(:))确认,别信自己的记忆。同时检查ganLoss里的sp是不是用了数值稳定写法,log(1+exp(x))在x稍大时就会溢出,换成max(x,0) + log1p(exp(-abs(x)))能扛住更宽的范围。
5.2 现象:生成图像全是同一个结果
这是模式崩溃,但触发原因分两种,得先分清再动手。第一种是判别器过强,生成器只能找到唯一一个能骗过它的解,处理方式回到 4.1 的三招。第二种更隐蔽:生成器的输入噪声根本没接上。检查modelGradients里Z是不是每个迭代都重新randn生成,如果写在了循环外面,整轮训练用的是同一段固定噪声,生成器自然只会输出同一张图。
还有一种可能是batchNormalizationLayer在生成器里用得太靠后。如果 BN 放在最后一层fullyConnectedLayer之后、tanh之前,它会把 batch 内的样本强行拉到同一分布,多样性被抹掉。生成器里的 BN 应该放在中间层,末端保持一个不带归一化的全连接到输出。判断方法很简单,把 BN 层临时注释掉跑 5 个 epoch,如果多样性立刻回来,就是这个原因。
5.3 现象:dlarray 维度报错,提示格式串不匹配
MATLAB 的dlarray靠格式串('SSCB'、'CB')来追踪维度语义,报错信息通常是「Expected input to be a dlarray with format ...」。最常见的来源是噪声和图像的格式串用混了。
featureInputLayer对应'CB',imageInputLayer对应'SSCB',convolution2dLayer要求输入是'SSCB'或'CBT'。生成器输出的reshape之后如果没有被正确识别成图像格式,传给判别器的imageInputLayer就会报错。解决方式是在 reshape 之后显式标注格式,或者在functionLayer后面加一层imageInputLayer来重新声明。另一个高频来源是dlarray(single(...), 'SSCB')里数据本身是H×W×C×N,但 MATLAB 索引顺序写反了,变成N×H×W×C,格式串标得再对也没用,先用size(X)确认一遍。
5.4 现象:显存随 epoch 线性上涨,最后崩溃
这个问题的根源通常是dlgradient的中间结果没释放。前面提过'RetainData', true是为了两次求导,但它保留的数据如果跨迭代累积,显存就一路涨。正常的训练循环里,每次dlfeval结束后这些中间结果应该被回收,如果没有,检查是不是在循环里把gradG、gradD存进了某个数组做记录。存梯度做可视化是可以的,但要extractdata之后再存,直接存dlarray会把整条计算图一起留在内存里。
另一个来源是快照。saveSnapshot如果把每轮的生成结果都堆在一个变量里不清理,50 轮下来就是几十万个dlarray。正确做法是每轮存成图片文件然后清掉变量,或者只保留最近 N 轮。还要注意gpuArray数据的回收,在 GPU 上算完的结果如果没gather回来,它会一直占着显存。
5.5 现象:判别器准确率卡在 50%,训练毫无进展
判别器完全分不出真假,意味着它没学到任何东西,生成器也就没有有效梯度。先检查学习率是不是设得太小,1e-5这个量级在 hinge 损失下常常推不动,先从2e-4起步。再检查判别器的第一层卷积有没有把信息压没,Stride = 2配4×4核在 28×28 输入上是够的,但如果输入被提前 resize 到更小,比如 16×16,两次下采样后只剩 4×4,特征基本没了。
还有一个容易被忽略的原因是标签或分数被写反了。lossD里如果写成sp(dReal) + sp(-dFake),判别器会朝着「把真图判低、假图判高」的方向优化,结果就是它主动放弃分辨能力,准确率稳定在 50%。这类错误不会报错,只能靠逐行核对损失表达式发现。判断方法是在训练几十个迭代后,单独算一次mean(dReal)和mean(dFake),正常的判别器应该前者明显大于后者,反了就是符号写错。
6. 从能出图到能交付:EMA 权重、latent 插值与快照留存
训练能跑通、图能生成,和「这份结果能拿去用」之间还差几步。最省事也最见效的一步是 EMA 权重,全称是指数滑动平均,做法是维护一份生成器权重的滑动副本,每次更新后按θ_ema = decay * θ_ema + (1-decay) * θ混合,decay取 0.999 左右。它的作用是抹掉训练后期的权重抖动,让同一段 latent 生成的结果更稳定。实现只需要几行,而且几乎不增加训练时间。
decay = 0.999; emaNet = genNet; % 初始复制一份 emaLearnables = emaNet.Learnables; % 在每个迭代更新完 genNet 之后插入 newL = genNet.Learnables; emaLearnables.Value = decay * emaLearnables.Value + (1-decay) * newL.Value; emaNet.Learnables = emaLearnables;Learnables是一张带Layer、Parameter、Value三列的表,直接对Value列做逐元素混合就行。注意初始阶段decay设得太大时,EMA 权重会严重滞后于真实权重,前几十个迭代生成的图会明显更差,这正常,等到几百个迭代之后它才会反超。评估和出图时统一用emaNet,训练继续用genNet,两者不要混。
第二步是 latent 插值。随机采样只能看出多样性的有无,插值能看出生成器学到的空间是不是连续的。做法是取两个随机 latent 向量,在它们之间线性插值 8 到 10 个点,一次性生成并拼成一条横条图。如果中间帧出现明显的断裂、突变或者退化成噪声,说明生成器的隐空间还没被训练得足够平滑,这时候继续加训练轮次比调结构更有效。这个方法判断「训没训够」比看损失曲线直观得多。
第三步是快照留存策略。每轮存一张 64 宫格图,命名带上 epoch 和当前的损失值,训练中断时能快速定位到最好的那一轮。工程里常见的问题是只存最后一份权重,跑到一半崩了就得从头再来,这在长训练里是血泪教训。我一般会在每 5 个 epoch 存一份genNet和emaNet,磁盘占用不大,但省下的重跑时间很可观。
最后说一个我自己的习惯:从那以后我每次改完生成器或判别器的结构,都会先只跑 3 个 epoch、把快照图打开看一眼,确认梯度能正常传导、输出不是一片死灰,再放手跑长训练。这个三分钟的动作挡掉过我绝大多数的白跑。希望帮到你。
本文还有配套的精品资源,点击获取