简介:本资源为Pix2Pix对抗网络Matlab实现配套资料,面向本科、硕士及科研人员进行图像到图像翻译的教研学习。包内提供Pix2Pix核心训练脚本与Facade数据集加载程序,并附有运行结果图与动态演示文件,可帮助读者理解条件生成对抗网络在图像生成任务中的完整流程。资源共5个文件,以m脚本文件为主,另含txt说明文档、jpg结果图与gif演示动画,压缩包约28.78MB,结构精简便于快速上手。目前已有148人学习下载。通过该资料,读者可掌握Pix2Pix的生成器与判别器搭建、损失函数设计及训练迭代过程,对照运行结果验证模型效果,适合作为对抗网络入门与课程实验的参考素材。
1. Pix2Pix 对抗网络在 MATLAB 里到底能跑出什么:从一张标签图到一张实景图
手里有一批成对的图像——卫星图配地图、线稿配彩图、黑白配彩色、建筑立面线框配渲染效果图——想让模型学会这种「A 到 B」的映射,Pix2Pix 是最经典也最容易复现的一条路。它属于条件生成对抗网络(cGAN)的一支,判别器不再只判断「真不真」,而是判断「给定输入 A,输出 B 是否匹配」,这一点是它和普通 GAN 最大的区别。MATLAB 从 R2019b 起在 Deep Learning Toolbox 里提供了dlnetwork、自定义训练循环、dlgradient自动微分,加上 Image Processing Toolbox 做配对图像的预处理,完全可以在本地把 Pix2Pix 从零搭起来,不需要 Python 环境。这篇笔记面向的是手上有成对数据、想用 MATLAB 跑通 Pix2Pix 并看到实际生成结果的工程师,从网络结构、数据组织、训练循环、参数设置一路讲到翻车点,代码可以直接抄进.m文件跑。
2. Pix2Pix 的网络结构与 MATLAB 实现选型:U-Net 生成器 + PatchGAN 判别器
2.1 为什么生成器用 U-Net 而不是普通编码解码器
Pix2Pix 原文的生成器是 U-Net 结构,编码器逐层下采样提取语义,解码器逐层上采样恢复分辨率,关键在于跳跃连接把编码器每一层的高频细节直接拼到对应解码层。对于图像到图像的翻译任务,输入和输出在空间结构上高度对齐(比如边缘位置、建筑轮廓),如果只靠瓶颈层的语义向量重建,细节会糊掉。U-Net 的 skip connection 让低层纹理信息绕过瓶颈直接传到输出端,这是 Pix2Pix 生成清晰结果的核心。
在 MATLAB 里搭 U-Net 生成器,用convolution2dLayer、batchNormalizationLayer、reluLayer堆编码块,用transposedConv2dLayer或resize2dLayer+ 卷积做上采样,跳跃连接用depthConcatenationLayer拼接。下面是一个 8 层下采样的生成器骨架:
function dlnetG = buildUNetGenerator(inputSize, outputChannels) % inputSize: [H W C],例如 [256 256 3] % outputChannels: 输出通道数,RGB 为 3 layers = [ imageInputLayer(inputSize, 'Normalization', 'none', 'Name', 'in') ]; % ---- 编码器 ---- filters = [64 128 256 512 512 512 512 512]; encNames = cell(1, numel(filters)); for i = 1:numel(filters) f = filters(i); blockName = sprintf('enc%d', i); encNames{i} = [blockName '_concat']; layers = [layers convolution2dLayer(4, f, 'Padding', 'same', 'Stride', 2, ... 'Name', [blockName '_conv'], 'WeightsInitializer', 'he') batchNormalizationLayer('Name', [blockName '_bn']) reluLayer('Name', [blockName '_relu']) ]; end % ---- 解码器(带跳跃连接)---- decFilters = [512 512 512 512 256 128 64]; for i = 1:numel(decFilters) f = decFilters(i); blockName = sprintf('dec%d', i); layers = [layers transposedConv2dLayer(4, f, 'Stride', 2, 'Cropping', 'same', ... 'Name', [blockName '_deconv'], 'WeightsInitializer', 'he') batchNormalizationLayer('Name', [blockName '_bn']) ]; % 跳跃连接:与对应编码层拼接 layers = [layers depthConcatenationLayer(2, 'Name', [blockName '_concat']) reluLayer('Name', [blockName '_relu']) ]; end % ---- 输出层 ---- layers = [layers convolution2dLayer(4, outputChannels, 'Padding', 'same', ... 'Name', 'out_conv', 'WeightsInitializer', 'he') tanhLayer('Name', 'out_tanh') ]; lgraph = layerGraph(layers); % 建立跳跃连接:enc_i 的输出接到 dec 对应 concat 的第二输入 % 编码器第 i 层输出 -> 解码器第 (numel(filters)-i+1) 层 concat for i = 1:numel(filters) decIdx = numel(filters) - i + 1; srcName = sprintf('enc%d_relu', i); dstName = sprintf('dec%d_concat', decIdx); lgraph = connectLayers(lgraph, srcName, [dstName '/in2']); end dlnetG = dlnetwork(lgraph); end逻辑说明:编码器每层 stride=2 下采样,滤波器数从 64 翻倍到 512;解码器用transposedConv2dLayer上采样,每层后接depthConcatenationLayer把对应编码层输出拼进来。connectLayers把enc_i_relu接到dec_(N-i+1)_concat的第二个输入端口。输出层用tanh把像素值压到 [-1, 1],这是 Pix2Pix 的标准做法,训练时数据也要归一化到同一区间。
参数说明:filters数组控制每层通道数,显存不够可以整体减半(如 [32 64 128 256 256 256 256 256]);WeightsInitializer设为he适配 ReLU;transposedConv2dLayer的Cropping设为same保证输出尺寸和输入对齐。如果输入不是 256×256,编码器层数要相应调整,保证最底层特征图不小于 1×1。
2.2 PatchGAN 判别器:为什么不用整图判真假
Pix2Pix 的判别器不是输出一个标量判断整张图真假,而是输出一个 N×N 的 patch 矩阵,每个元素判断原图中对应感受野区域的真假,最后取均值。这叫 PatchGAN。好处是参数量小、训练快,而且对高频细节敏感——它强迫生成器在局部纹理上也逼近真实图像,而不是只把整体色调调对。对于 256×256 输入,常用 70×70 感受野的 PatchGAN,输出大约 30×30 的 patch 矩阵。
function dlnetD = buildPatchDiscriminator(inputSize, targetChannels) % 输入为 [A, B] 沿通道维拼接,通道数 = inputSize(3) + targetChannels inCh = inputSize(3) + targetChannels; layers = [ imageInputLayer([inputSize(1) inputSize(2) inCh], ... 'Normalization', 'none', 'Name', 'in') convolution2dLayer(4, 64, 'Stride', 2, 'Padding', 'same', ... 'Name', 'd1_conv', 'WeightsInitializer', 'he') leakyReluLayer(0.2, 'Name', 'd1_lrelu') convolution2dLayer(4, 128, 'Stride', 2, 'Padding', 'same', ... 'Name', 'd2_conv', 'WeightsInitializer', 'he') batchNormalizationLayer('Name', 'd2_bn') leakyReluLayer(0.2, 'Name', 'd2_lrelu') convolution2dLayer(4, 256, 'Stride', 2, 'Padding', 'same', ... 'Name', 'd3_conv', 'WeightsInitializer', 'he') batchNormalizationLayer('Name', 'd3_bn') leakyReluLayer(0.2, 'Name', 'd3_lrelu') convolution2dLayer(4, 512, 'Stride', 1, 'Padding', 'same', ... 'Name', 'd4_conv', 'WeightsInitializer', 'he') batchNormalizationLayer('Name', 'd4_bn') leakyReluLayer(0.2, 'Name', 'd4_lrelu') convolution2dLayer(4, 1, 'Stride', 1, 'Padding', 'same', ... 'Name', 'd5_conv') ]; dlnetD = dlnetwork(layers); end逻辑说明:判别器输入是「条件图 A」和「目标图 B」沿通道维拼接后的张量。前四层逐步下采样提取特征,最后一层 1 通道卷积输出 patch 得分矩阵。注意判别器没有全局池化也没有 sigmoid,输出的是 logits,损失函数里用sigmoidCrossEntropy或直接算最小二乘。
参数说明:leakyReluLayer(0.2)的斜率 0.2 是 Pix2Pix 原文设置,比 0.01 更常用;判别器第一层不做 batch norm(这是 GAN 训练的通用经验,避免把真假样本的统计量混在一起);最后一层 stride=1 保持 patch 输出。
2.3 损失函数:cGAN loss + L1 的加权组合
Pix2Pix 的生成器损失由两部分组成:对抗损失让输出看起来真实,L1 重建损失让输出在像素级靠近目标。L1 的权重 λ 通常设 100,这个数字很关键——太小则生成图模糊,太大则对抗训练失效、输出趋近于直接回归。
function [lossG, lossD, gradG, gradD] = modelLoss(dlnetG, dlnetD, ... inputA, targetB, lambda) % inputA: dlarray [H W C N] % targetB: dlarray [H W C N] % 生成器前向 fakeB = forward(dlnetG, inputA); % 判别器对真实对和生成对的输出 realPair = cat(3, inputA, targetB); fakePair = cat(3, inputA, fakeB); predReal = forward(dlnetD, realPair); predFake = forward(dlnetD, fakePair); % 对抗损失(最小二乘 GAN) lossD_real = mean((predReal - 1).^2, 'all'); lossD_fake = mean(predFake.^2, 'all'); lossD = 0.5 * (lossD_real + lossD_fake); lossG_adv = 0.5 * mean((predFake - 1).^2, 'all'); lossG_l1 = mean(abs(fakeB - targetB), 'all'); lossG = lossG_adv + lambda * lossG_l1; gradG = dlgradient(lossG, dlnetG.Learnables); gradD = dlgradient(lossD, dlnetD.Learnables); end逻辑说明:判别器要同时拉高真实对的得分、压低生成对的得分,所以lossD是两部分平均。生成器要骗过判别器(lossG_adv)同时逼近目标(lossG_l1)。用最小二乘形式(LSGAN)比原始对数损失训练更稳,梯度不会饱和。
参数说明:lambda默认 100,如果生成结果模糊可以试 200,如果训练震荡可以降到 50;cat(3, ...)是沿通道维拼接,要求 A 和 B 空间尺寸一致。
3. 数据准备与训练循环:从成对图像到可收敛的模型
3.1 成对图像的读取、对齐与归一化
Pix2Pix 要求训练数据严格成对。常见的数据组织方式有两种:一是 A、B 分别放在两个文件夹,文件名一一对应;二是 A、B 水平拼接成一张宽图,训练时切分。MATLAB 里用imageDatastore配合自定义ReadFcn最灵活。
function [A, B] = readPair(filename, targetSize) % filename 指向拼接图,左半为 A,右半为 B img = imread(filename); img = imresize(img, [targetSize(1), targetSize(2)*2]); w = targetSize(2); A = img(:, 1:w, :); B = img(:, w+1:end, :); % 归一化到 [-1, 1] A = single(A) / 127.5 - 1; B = single(B) / 127.5 - 1; % 转为 dlarray,维度 [H W C N] A = dlarray(permute(A, [1 2 3 4])); B = dlarray(permute(B, [1 2 3 4])); end逻辑说明:imresize保证所有样本尺寸一致,这是dlnetwork输入层的硬性要求。归一化用/127.5 - 1把 [0,255] 映射到 [-1,1],和生成器输出层的tanh对应。permute把 HWC 转成 HWCN,MATLAB 的dlnetwork默认维度顺序是 spatial-spatial-channel-batch。
参数说明:targetSize常用 [256 256],显存紧张可以降到 [128 128],但太小会导致细节丢失;如果原始数据是灰度图,通道数改为 1,生成器输出通道也改 1。
3.2 自定义训练循环:两个优化器、交替更新
MATLAB 没有现成的 Pix2Pix 训练函数,必须写自定义循环。核心是每步先更新判别器再更新生成器,用两个独立的adamupdate。
% ---- 训练配置 ---- numEpochs = 200; batchSize = 1; % Pix2Pix 常用 batch=1 learnRateG = 2e-4; learnRateD = 2e-4; lambda = 100; beta1 = 0.5; % Adam 动量,GAN 里常用 0.5 而非 0.9 % ---- 初始化 ---- dlnetG = buildUNetGenerator([256 256 3], 3); dlnetD = buildPatchDiscriminator([256 256 3], 3); avgG = []; avgSqG = []; avgD = []; avgSqD = []; files = dir(fullfile('data', '*.jpg')); numIter = floor(numel(files) / batchSize); for epoch = 1:numEpochs idx = randperm(numel(files)); for i = 1:numIter batchFiles = files(idx((i-1)*batchSize+1 : i*batchSize)); [A, B] = deal(cell(batchSize,1)); for j = 1:batchSize [A{j}, B{j}] = readPair(fullfile(batchFiles(j).folder, ... batchFiles(j).name), [256 256]); end A = cat(4, A{:}); B = cat(4, B{:}); % 计算梯度 [lossG, lossD, gradG, gradD] = dlfeval(@modelLoss, ... dlnetG, dlnetD, A, B, lambda); % 更新判别器 [dlnetD, avgD, avgSqD] = adamupdate(dlnetD, gradD, ... avgD, avgSqD, epoch*numIter+i, learnRateD, beta1); % 更新生成器 [dlnetG, avgG, avgSqG] = adamupdate(dlnetG, gradG, ... avgG, avgSqG, epoch*numIter+i, learnRateG, beta1); if mod(i, 50) == 0 fprintf('Epoch %d, Iter %d, LossG=%.4f, LossD=%.4f\n', ... epoch, i, double(gather(extractdata(lossG))), ... double(gather(extractdata(lossD)))); end end % 每 10 个 epoch 保存一次生成样本 if mod(epoch, 10) == 0 fakeB = forward(dlnetG, A); fakeB = extractdata(fakeB); fakeB = (fakeB + 1) * 127.5; fakeB = uint8(permute(fakeB, [1 2 3 4])); imwrite(fakeB(:,:,:,1), sprintf('sample_epoch%d.png', epoch)); save(sprintf('checkpoint_epoch%d.mat', epoch), 'dlnetG', 'dlnetD'); end end逻辑说明:dlfeval触发自动微分,modelLoss返回两个网络的梯度和损失。adamupdate分别更新两个网络的参数,beta1=0.5是 GAN 训练的经验值,比默认 0.9 更不容易震荡。每 50 步打印损失,每 10 个 epoch 保存生成样本和 checkpoint。
参数说明:batchSize=1是 Pix2Pix 原文设置,因为 batch norm 在 batch=1 时退化为 instance norm,反而效果稳定;learnRateG和learnRateD都设 2e-4,如果判别器 loss 快速趋近 0 说明它太强,可以降低 D 的学习率或给 D 加输入噪声;lambda=100是 L1 权重,控制生成图清晰度。
3.3 训练过程监控:看什么指标判断收敛
Pix2Pix 的损失曲线不像分类任务那样单调下降,判别器和生成器的 loss 会此消彼长。健康的训练状态是:lossD在 0.2~0.5 之间波动,lossG_adv在 0.3~0.8 之间,lossG_l1持续下降。如果lossD长期接近 0,说明判别器碾压生成器,生成结果会退化;如果lossD一直在 0.5 以上不降,说明判别器太弱,生成器学不到细节。
实际监控建议每 10 个 epoch 生成一批样本图肉眼检查。前 20 个 epoch 输出通常是模糊色块,50 个 epoch 后开始出现结构,100~200 个 epoch 细节逐渐清晰。如果 200 个 epoch 还是糊的,优先检查数据归一化是否一致、L1 权重是否太小、学习率是否过大。
4. 避坑与排查:Pix2Pix 在 MATLAB 里最容易翻车的 5 个地方
4.1 生成结果全是灰色或单一色调
现象:训练几十个 epoch 后,生成器输出几乎是一张纯灰或纯色图,完全没有结构。
原因:最常见的是 L1 权重过大导致生成器只学均值。L1 损失的最优解在条件分布的中位数,当模型不确定时输出灰色是最小化 L1 的策略。另一个可能是判别器太弱,对抗损失没有起到作用。
解决:先把lambda从 100 降到 50 甚至 20 试一轮,观察是否有结构出现;同时检查判别器的学习率是否被生成器压制,可以适当提高learnRateD。如果数据本身对比度低,先做直方图均衡化再送入网络。
4.2 训练中途 loss 突然变 NaN
现象:前几十个 epoch 正常,突然某一步lossG或lossD变成 NaN,之后全部 NaN。
原因:MATLAB 的dlgradient在梯度爆炸时会产生 Inf,下一步就变 NaN。Pix2Pix 里常见于判别器最后一层没有归一化、学习率过大、或者输入数据里有异常值(比如全黑或全白图)。
解决:在modelLoss里加梯度裁剪,用dlupdate把梯度范数限制在 1 以内;检查数据集中是否有损坏图像,用imread读一遍过滤掉;把学习率降到 1e-4 重跑。MATLAB 没有内置的clipGradients,需要手写:
function grad = clipGrad(grad, maxNorm) totalNorm = 0; for i = 1:numel(grad) totalNorm = totalNorm + sum(grad(i).Value.^2, 'all'); end totalNorm = sqrt(totalNorm); if totalNorm > maxNorm scale = maxNorm / totalNorm; for i = 1:numel(grad) grad(i).Value = grad(i).Value * scale; end end end4.3 显存不足(Out of Memory)
现象:跑几个 iteration 后 MATLAB 报显存错误,或者系统卡死。
原因:Pix2Pix 的 U-Net 生成器在 256×256 输入下参数量约 50M,加上判别器和中间激活值,batch=1 也需要 6~8GB 显存。如果同时保留了多个dlarray中间变量没释放,占用会翻倍。
解决:把输入尺寸降到 128×128;生成器通道数整体减半;在训练循环里用clear清理不再用的变量;开启executionEnvironment为'auto'让 MATLAB 自动选择 GPU/CPU。如果只有 CPU,训练会慢 10~20 倍,建议先用 128×128 小图验证流程。
4.4 生成的图和输入完全不对应
现象:生成器输出的图像质量还行,但和输入 A 的结构完全无关,像是随机生成的另一张图。
原因:跳跃连接没接对。U-Net 的 skip connection 如果接错了层,编码器的空间信息传不到解码器,生成器就退化成普通 GAN,只学数据分布不学条件映射。
解决:用analyzeNetwork(dlnetG)检查网络结构,确认每个depthConcatenationLayer的两个输入分别来自正确的编码层和解码层。重点检查connectLayers里的索引映射:enc_i应该接到dec_(N-i+1),不是dec_i。
4.5 训练速度异常慢
现象:每个 iteration 要好几秒,200 个 epoch 跑几天都跑不完。
原因:MATLAB 默认在 CPU 上执行dlnetwork的 forward,除非显式指定 GPU。另外readPair里每次都用imread+imresize做实时预处理,I/O 成为瓶颈。
解决:训练前把所有图像预处理成统一尺寸存成 MAT 文件或 HDF5,训练时直接load;用gpuArray把数据和网络参数搬到 GPU;把readPair里的imresize去掉,改为离线预处理。检查 GPU 是否可用用gpuDeviceCount,返回 0 说明没装 CUDA 或 GPU 不支持。
5. 进阶技巧:用感知损失和谱归一化把 Pix2Pix 的细节再拉一档
基础版 Pix2Pix 跑通之后,如果觉得生成图的纹理还是偏软、边缘不够锐,可以加两个改进。第一个是感知损失:不只用 L1 比较像素,还把生成图和目标图送进一个预训练的 VGG 网络,比较中间层的特征差异。MATLAB 可以用vgg19加载预训练模型,取'relu3_3'层的输出算 L2。这个损失对纹理和风格的约束比纯 L1 强得多,代价是每个 iteration 多一次 VGG 前向。
vgg = vgg19('Weights', 'imagenet'); vggFeatureLayer = 'relu3_3'; netVGG = dlnetwork(layerGraph(vgg.Layers(1:find( ... arrayfun(@(l) strcmp(l.Name, vggFeatureLayer), vgg.Layers), 1)))); function lossPerc = perceptualLoss(netVGG, fakeB, targetB) % VGG 输入需要 ImageNet 归一化 meanVGG = reshape([123.68 116.78 103.94], [1 1 3]); fakeNorm = (fakeB + 1) * 127.5 - meanVGG; realNorm = (targetB + 1) * 127.5 - meanVGG; featFake = forward(netVGG, fakeNorm); featReal = forward(netVGG, realNorm); lossPerc = mean((featFake - featReal).^2, 'all'); end第二个改进是谱归一化(Spectral Normalization),加在判别器每一层卷积上,限制判别器的 Lipschitz 常数,让训练更稳定、不容易模式崩溃。MATLAB 没有内置的谱归一化层,需要自定义层或者用dlupdate在每次更新后对权重做谱范数归一化。实操上更简单的替代是给判别器输入加高斯噪声(标准差 0.1),效果接近但实现成本低。
| 改进项 | 加在哪 | 代价 | 效果 |
|---|---|---|---|
| 感知损失 | 生成器损失 | 每步多一次 VGG 前向,显存 +2GB | 纹理锐利,风格更接近目标域 |
| 谱归一化 | 判别器每层卷积 | 每步多一次 SVD,速度 -20% | 训练稳定,减少模式崩溃 |
| 输入噪声 | 判别器输入 | 几乎无代价 | 判别器不会过强,生成器梯度更平滑 |
| 多尺度判别器 | 额外两个判别器 | 显存翻倍 | 兼顾全局结构和局部细节 |
我自己的习惯是先用基础版跑通 200 个 epoch 拿到 baseline,确认数据管线和损失函数没问题,再加感知损失微调 50 个 epoch。直接上全套改进容易在调试时分不清是哪个模块出的问题。另外 checkpoint 一定要存,Pix2Pix 的训练结果随机性不小,同一个配置跑两次可能一次好一次糊,保留中间结果能省很多重跑时间。希望帮到你。
本文还有配套的精品资源,点击获取