恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
SRGAN图像超分辨率Pytorch复现:感知损失与训练避坑指南
首页
资讯中心
/
SRGAN图像超分辨率Pytorch复现:感知损失与训练避坑指南
SRGAN图像超分辨率Pytorch复现:感知损失与训练避坑指南
发布时间:2026/10/11 20:18:22
简介面向图像超分辨率重建的Pytorch版SRGAN项目代码完整复现了生成对抗网络在x2、x4、x8三种倍率下的超分训练与推理流程适合正在学习GAN应用、复现论文或处理低分辨率图像复原需求的开发者使用。资源压缩包共375个文件以py源码、png/bmp图像样本、pth模型权重三类为主大小约231MB其中pth文件为不同倍数下的最优PSNR权重图像文件用于测试输入与超分结果的可视化展示。代码注释详细目录按benchmark_results、data、epochs、statistics、training_results等模块划分分别存放测试结果、数据集、逐轮模型权重、评估指标和验证集对比图同时提供图像、测试集、视频等多种测试入口可完成任意图片和视频的超分演示并附带绘制loss、PSNR、SSIM训练变化曲线的工具。已有696人浏览学习适合作为复现SRGAN以及扩展超分实验的完整参考。1. 图像超分辨率SRGAN的Pytorch版本复现代码为什么参数看着都对图还是糊很多同学拿到“图像超分辨率SRGAN的Pytorch版本复现代码”这个项目名第一反应是赶紧把生成器跑通结果训练几个小时后发现PSNR 怎么也上不去肉眼效果甚至比双三次插值还脏。这不是代码抄错了而是把 SRGAN 当成普通回归模型在调。SRGAN 的核心是让生成器学会“骗过”判别器用感知损失替代纯像素误差它的目标不是分数高而是看着像真实的高清图。这个标题背后的完整交付物应该是一份注释清晰的 Pytorch 训练管线、三档不同倍率的权重文件以及能帮你判断模型有没有学歪的曲线绘制和 PSNR 评估工具。适合图像重建方向入门、做数据预处理超分、以及想对比 GAN 和纯卷积超分差异的工程师。读懂本文你能从网络结构、损失配比、训练节奏到权重筛选走通一条不靠运气的复现路径。2. 拆解SRGAN的四个关键设计复现前先搞懂损失、训练和评估口径2.1 生成器和判别器选型残差堆叠与像素重排SRGAN 的生成器主体是 SRResNet16 个残差块加两个 2 倍 PixelShuffle 上采样。Pytorch 里 PixelShuffle 就是把[B, C*r*r, H, W]重排成[B, C, H*r, W*r]这一步替代了早年反卷积没有棋盘伪影参数也更少。复现时不要把上采样写在网络外面否则梯度回传路径不完整。判别器则是一个 VGG 风格的二分类网络输出一个 0 到 1 之间的真实性概率训练时用它逼生成器产出更细的纹理。我在工程里会把尺寸参数量化为scale生成器尾部根据int(math.log2(scale))决定堆几个 PixelShuffle 块。x2 放 1 个x4 放 2 个x8 放 3 个。这样一套模型结构能训三档倍率避免三份代码三份维护。判别器我一般把尾部线性层换成AdaptiveAvgPool2d(1)加 1x1 卷积让判别器能接受不同输入尺寸这样 x2/x4/x8 训练时无需重写判别器。BatchNorm 保留在残差块里但测试时必须把模型切成 eval 模式否则 running_mean 不更新输出会出现紫边和噪点。这个问题后面会单独讲。2.2 损失函数设计感知损失才是 SRGAN 的灵魂SRGAN 原文的生成器损失由三部分组成像素空间 MSE、VGG 特征空间感知损失、GAN 对抗损失。像素损失用的是生成图与真实 HR 的 MSE它保证低频结构不崩感知损失取 VGG19 的relu5_4层输出做 L2 距离作用是让生成图在高层语义特征上和真实图更接近对抗损失让生成器学会“制造判别器信以为真的纹理”。复现时最容易犯的错误是把 MSE 权重调得过大。感受一下如果像素损失权重是 1感知损失权重也是 1对抗损失权重 1e-3那么生成器会优先保像素精度纹理细节被平均掉GAN 等于没开。常见做法是感知损失权重 1MSE 权重 1e-3对抗损失权重 1e-3。注意感知损失不是直接在 RGB 上算而是把生成图和 HR 一起过 VGG取中间特征做距离。因此需要固定 VGG 权重且对输入做 VGG 自己的归一化通常是减均值除方差Pytorch 里torchvision.models.vgg19(pretrainedTrue)自带 preprocessing 逻辑复现时别漏。2.3 训练策略先回归后对抗两阶段避免崩盘SRGAN 最稳的训练路径是两阶段这也是复现工程和论文实验的常见差异。论文里 SRResNet 用 MSE 预训练后作为生成器初始化然后才进入 GAN 对抗训练。我实际跑下来直接上 GAN 十次有八次判别器很快把生成器压死生成图全是灰色色块。先只用 MSE 训生成器几十个 epoch让网络先学会一个相对稳定的超分映射再打开对抗损失微调收敛概率高非常多。在对抗阶段判别器和生成器不要用同一个学习率。常见配置是生成器 1e-4判别器 5e-5判别器太强就减半。每多少个 iteration 保存一次验证集 PSNR同时绘制 G loss、D loss、PSNR 三条曲线观察 D loss 是否长期不下降。如果 D loss 稳定在 0.1 以下说明生成器已经被压制需要调低判别器学习率或临时关闭对抗损失继续训。2.4 评估口径PSNR 必须在 Y 通道、去边界、对齐裁剪算PSNR 这个指标在 SRGAN 复现里容易造假。很多人直接在 RGB 三通道上算或者用skimage.metrics.peak_signal_noise_ratio默认参数结果和论文差距巨大。标准做法是先把 RGB 转到 YCbCr只取 Y 通道因为人眼对亮度最敏感超分评估也以亮度为主。其次要去掉图像边缘的 shave 像素SRGAN 生成图像边缘有卷积 padding 造成的暗边不去掉会把 PSNR 拉低很多但去多了又虚高。测试集上原论文用 Set5、Set14、BSD100、Urban100 四个数据集。复现时至少准备 Set5 和 Set14这两个集图片少、下载方便、结果好对比。评估前把所有测试图统一裁剪成scale的整数倍不然生成图和 HR 尺寸对不上。写评估脚本时把 LR 的生成结果和 HR 转 Y 通道后先取公共区域再算 MSE最后10 * log10(255^2 / MSE)。这套口径下同一模型和网上公开数值对比才有意义。3. 工程目录与核心代码从数据加载到一份结构兼容 x2、x4、x83.1 环境准备和目录结构复现这套代码我建议先建一个干净的 Pytorch 环境。Anaconda 里执行conda create -n srgan python3.8然后激活环境装 pytorch。安装时根据本机 CUDA 版本选择对应版本用conda install pytorch torchvision cudatoolkit或者 pip 装都行CPU 版也能跑通流程只是训练慢很多。工程目录按职责拆开别把训练、测试、画图都塞在一个文件里。我常用的结构如下SRGAN-PyTorch/ ├── dataset.py # 数据加载、随机裁剪、LR/HR 配对 ├── model.py # Generator、Discriminator、VGG 特征提取 ├── loss.py # 感知损失、对抗损失、像素损失 ├── train.py # 两阶段训练主循环 ├── test.py # 测试权重、输出超分图和 PSNR ├── utils.py # PSNR、YCbCr 转换、训练曲线绘制 ├── weights/ │ ├── x2/ # 最优 PSNR 权重 │ ├── x4/ │ └── x8/ └── samples/ # 测试输入图像数据目录里放 HR 原图即可不提前生成 LR 图因为训练时每次随机裁剪增强鲁棒性。如果你的数据是从视频抽帧来的先做一次基础筛选去掉模糊和纯黑帧否则这些坏样本会把训练 loss 拉高。3.2 dataset.py让 LR 和 HR 严格对齐的裁剪逻辑超分数据加载最容易出错的地方是 LR 和 HR 不对齐。常见错误是先把整张图缩小再从缩小图上随机裁剪但裁剪位置没有映射回 HR 坐标。正确做法是先裁 HR patch再将同一个 patch 用Image.BICUBIC缩小成 LR保证内容一一对应。下面是一个可直接用的数据类片段import random import torch from torch.utils.data import Dataset from PIL import Image def random_crop_aligned(hr_img, scale, lr_size): 先裁 HR 再缩成 LR保证位置严格对齐。 lr_size 是 (w, h)HR 裁剪尺寸 lr_size * scale。 lr_w, lr_h lr_size hr_w, hr_h lr_w * scale, lr_h * scale if hr_img.width hr_w or hr_img.height hr_h: raise ValueError(输入 HR 图太小无法完成该尺寸裁剪) left random.randint(0, hr_img.width - hr_w) top random.randint(0, hr_img.height - hr_h) hr_crop hr_img.crop((left, top, left hr_w, top hr_h)) lr_crop hr_crop.resize((lr_w, lr_h), Image.BICUBIC) return lr_crop, hr_crop class SRDataset(Dataset): def __init__(self, hr_dir, scale4, lr_size(48, 48)): self.files [f for f in hr_dir.iterdir() if f.suffix in (.png, .jpg, .jpeg)] self.scale scale self.lr_size lr_size def __len__(self): return len(self.files) def __getitem__(self, idx): hr Image.open(self.files[idx]).convert(RGB) lr, hr random_crop_aligned(hr, self.scale, self.lr_size) # 数据增强LR 和 HR 必须做相同的翻转 if random.random() 0.5: lr lr.transpose(Image.FLIP_LEFT_RIGHT) hr hr.transpose(Image.FLIP_LEFT_RIGHT) lr_t torch.from_numpy(np.array(lr)).permute(2, 0, 1).float() / 127.5 - 1.0 hr_t torch.from_numpy(np.array(hr)).permute(2, 0, 1).float() / 127.5 - 1.0 return lr_t, hr_t这个类里有几个参数值得注意。lr_size我通常设成 48x48这样 x4 时 HR 是 192x192x8 时 HR 是 384x384显存压力随倍率上涨明显。如果你的显卡只有 8G 显存x8 训练建议把 lr_size 降到 32或者缩小 batch。归一化到[-1, 1]而不是[0, 1]是为了配合生成器末尾 Tanh 输出这也是 SRGAN 复现里容易被忽略的细节。如果加载后 HR 图很小建议在__getitem__里跳过或重新采样但要注意别让某个 epoch 因为跳过太多而训练不充分。3.3 model.py一份生成器兼容三个倍率判别器兼容任意输入生成器的残差块和 SRResNet 原版保持一致。重点看尾部上采样部分倍率变化只影响上采样块数量。判别器我做了尺寸兼容改造这样换倍率不用重写结构。下面只贴核心结构代码import math import torch import torch.nn as nn class ResidualBlock(nn.Module): def __init__(self, channels64): super().__init__() self.conv1 nn.Conv2d(channels, channels, 3, 1, 1) self.bn1 nn.BatchNorm2d(channels) self.relu nn.ReLU(inplaceTrue) self.conv2 nn.Conv2d(channels, channels, 3, 1, 1) self.bn2 nn.BatchNorm2d(channels) def forward(self, x): out self.conv1(x) out self.bn1(out) out self.relu(out) out self.conv2(out) out self.bn2(out) return self.relu(out x) class Generator(nn.Module): scale 传入 2、4、8生成器会自动堆对应数量的上采样块。 输入输出都是 RGB范围 [-1, 1]。 def __init__(self, scale4, base64, num_blocks16): super().__init__() self.scale scale head [nn.Conv2d(3, base, 9, 1, 4), nn.PReLU()] body [ResidualBlock(base) for _ in range(num_blocks)] tail [nn.Conv2d(base, base, 3, 1, 1), nn.BatchNorm2d(base)] upsample [] for _ in range(int(math.log2(scale))): upsample.append(nn.Conv2d(base, base * 4, 3, 1, 1)) upsample.append(nn.PixelShuffle(2)) upsample.append(nn.PReLU()) last [nn.Conv2d(base, 3, 3, 1, 1), nn.Tanh()] self.net nn.Sequential( *head, *body, *tail, *upsample, *last ) def forward(self, x): return self.net(x) class Discriminator(nn.Module): def __init__(self, input_ch3, base64): super().__init__() layers [] current input_ch for ch, repeat in [(base, 1), (base * 2, 2), (base * 4, 2), (base * 8, 2)]: for _ in range(repeat): layers.append(nn.Conv2d(current, ch, 3, 1, 1)) layers.append(nn.BatchNorm2d(ch)) layers.append(nn.LeakyReLU(0.2, inplaceTrue)) current ch layers.append(nn.Conv2d(current, ch, 3, 2, 1)) layers.append(nn.LeakyReLU(0.2, inplaceTrue)) current ch # 用全局池化兼容不同输入尺寸 self.features nn.Sequential(*layers) self.head nn.Sequential( nn.Conv2d(current, 1, 3, 1, 1), nn.AdaptiveAvgPool2d(1) ) def forward(self, x): return torch.sigmoid(self.head(self.features(x)).view(x.size(0), -1))生成器里math.log2(scale)决定 PixelShuffle 的数量x2 是 1x4 是 2x8 是 3。每个 PixelShuffle 前都先升通道到base * 4这是保证重排后通道数回到base是 PixelShuffle 的标准用法。判别器尾部用 AdaptiveAvgPool 替代全连接好处是 HR patch 尺寸变化也能跑代价是局部细节的判别能力略有下降实际训练中影响不大。注意判别器输出用了 sigmoid后面损失计算用BCELoss会更直观如果用BCEWithLogitsLoss记得把这里return的 sigmoid 去掉。4. 训练循环与曲线绘制按 PSNR 峰值保存三档权重4.1 两阶段训练循环先 MSE 预训练再打开 GAN 微调训练脚本的核心是两套逻辑切分。第一个阶段生成器只用像素损失优化器只管生成器第二个阶段生成器损失换成感知加对抗加像素判别器开始参与。下面是训练循环的骨架阶段切换用phase控制# 伪代码骨架只保留关键逻辑 for step, (lr, hr) in enumerate(train_loader): lr, hr lr.cuda(), hr.cuda() if phase srresnet: # 阶段一只优化生成器 sr netG(lr) loss criterion_pixel(sr, hr) # nn.MSELoss() optimizerG.zero_grad() loss.backward() torch.nn.utils.clip_grad_norm_(netG.parameters(), 10.0) optimizerG.step() schedulerG.step() elif phase gan: # 阶段二先更判别器再更生成器 sr netG(lr) real_pred netD(hr) fake_pred netD(sr.detach()) loss_D criterion_gan(fake_pred, torch.zeros_like(fake_pred)) loss_D criterion_gan(real_pred, torch.ones_like(real_pred)) optimizerD.zero_grad() loss_D.backward() optimizerD.step() sr netG(lr) fake_pred netD(sr) loss_percep criterion_feature(sr, hr) # VGG relu5_4 loss_adv criterion_gan(fake_pred, torch.ones_like(fake_pred)) loss_pixel criterion_pixel(sr, hr) loss_G loss_percep 1e-3 * loss_adv 1e-3 * loss_pixel optimizerG.zero_grad() loss_G.backward() torch.nn.utils.clip_grad_norm_(netG.parameters(), 10.0) optimizerG.step() schedulerG.step()判别器损失用真假标签做二分类真实 HR 标签为 1生成结果标签为 0。生成器损失里对抗部分的目标是让判别器把生成结果判为 1所以loss_adv用的是ones_like。这个阶段生成器损失中感知权重为 1像素和对抗都为 1e-3。你可能会问为什么保留像素损失因为完全去掉像素约束生成器容易产生高频伪影尤其训练早期。clip_grad_norm_很重要GAN 训练中生成器梯度爆炸是家常便饭限制到 10 能避免 loss 突然变 NaN。学习率我一般按 iteration 而不是 epoch 来调度。生成器和判别器都从 1e-4 起步在预训练阶段跑到总步数一半时降到 5e-5。进入 GAN 阶段后判别器学习率固定为 1e-4和生成器一样如果 D loss 一直很低再把 discriminator 的 lr 调成 5e-5。batch size 默认 16显存不够就降到 8但感知损失和 BN 对 batch size 都敏感尽量别低于 4。4.2 训练曲线图绘制四条线看透训练状态训练曲线对排查问题非常有价值比盯着终端 loss 数字直观很多。我一般每个 validation step 记录四个量iteration、G loss、D loss、验证集 PSNR存成 JSON 或直接 append 到 csv训练结束后用 matplotlib 画成 2x2 的四宫格。下面是一个最小绘制脚本import json import matplotlib.pyplot as plt def plot_curves(log_file, output_file): with open(log_file) as f: logs json.load(f) fig, axes plt.subplots(2, 2, figsize(12, 8)) ax1 axes[0, 0] ax1.plot(logs[steps], logs[loss_G], labelG loss) ax1.set_title(Generator Loss) ax1.set_xlabel(iteration) ax1.legend() ax2 axes[0, 1] ax2.plot(logs[steps], logs[loss_D], labelD loss) ax2.set_title(Discriminator Loss) ax2.set_xlabel(iteration) ax2.legend() ax3 axes[1, 0] ax3.plot(logs[steps], logs[psnr], colorgreen, labelPSNR) ax3.set_title(Validation PSNR) ax3.set_xlabel(iteration) ax3.legend() ax4 axes[1, 1] ax4.plot(logs[steps], logs[lr_G], colorred, labellr) ax4.set_title(Generator Learning Rate) ax4.set_xlabel(iteration) ax4.legend() fig.suptitle(SRGAN Training Curves) plt.tight_layout() plt.savefig(output_file, dpi150)PSNR 曲线能直接看出两个阶段的分界MSE 预训练阶段 PSNR 稳定爬升进入 GAN 阶段后通常先掉 1 到 2 个 dB然后缓慢回升。如果 PSNR 掉下去后 10 个 epoch 都没回来说明对抗损失太强或者判别器学习率过高这时候要早点干预而不是等训练结束换一组 seed 重来。四种曲线一起看还有一个好处能识别判别器是否过拟合D loss 一直下降但 G loss 不降说明生成器已经学不到有效梯度此时减判别器 lr 或者每 3 步只更一次判别器。4.3 最优 PSNR 权重保存策略别等最后一轮才存盘标题里明确要求“最优 PSNR 的模型权重文件”这意味着训练过程里必须每若干 iteration 做一次验证并且只在验证 PSNR 超过历史最高时才保存。我建议保存三份权重而不是一份srresnet_best_psnr.pth、srgan_best_psnr.pth、srgan_last.pth。前两份分别保存预训练阶段和 GAN 阶段的最优 PSNR 模型第三份保存最后状态。GAN 阶段训练结束后你会发现srgan_best_psnr的 PSNR 往往没有srresnet_best_psnr高这是正常的因为生成器开始牺牲像素相似度换取感知纹理。但srgan_last有时反而在主观效果上更好。测试时先对比三份权重在验证集上的 PSNR再看几张主观图再决定用哪个。对于 x2、x4、x8 三个倍率建议分开训练三次共用一份数据加载代码只改scale参数。训练顺序建议先 x4再 x2最后 x8。x4 是原论文主打倍率训练方法和参数最稳x2 相对简单但学习率可以调大x8 需要更长的预训练阶段不然进入 GAN 阶段容易直接崩。权重文件名里带上倍率和 PSNR例如best_psnr_x4_29.83.pth方便后面对比。5. SRGAN复现避坑5 个让新手翻车的真实场景5.1 GAN 微调后 PSNR 反而下降不是 bug现象MSE 预训练阶段 PSNR 能到 29.5进入 GAN 阶段后掉到 28.2而且怎么调都回不去。很多人这时候会怀疑代码写错实际上这是 SRGAN 的典型行为。原因是对抗损失推着生成器去生成看起来更真实的纹理而真实纹理和原始像素值之间不一定能同时满足 MSE 最小化。解决不要把 PSNR 作为 GAN 阶段的唯一指标重点看感知效果和纹理自然度。保存好预训练权重的峰值文件后续如果 GAN 阶段彻底不收敛还能回退到预训练权重继续调。5.2 测试时忘了切换 eval 模式图像出现紫边现象训练时效果不错测试保存出来的图片色调偏紫或者放大后有奇怪噪声条纹。原因生成器里的 BatchNorm 在训练模式下使用当前 batch 的均值方差测试时如果不调model.eval()BN 层会继续用 batch 统计量而不是保存的 running_mean导致输出不稳定。解决测试脚本里必须有netG.eval()和torch.no_grad()而且在加载权重后先跑一次 dummy input让 BN 的 running 统计量稳定下来再输出图片。5.3 PSNR 计算玄学色空间和边界没对齐现象自己算的 PSNR 比论文高 3 个 dB或者同一个模型在不同脚本里算出来差很大。原因一是在 RGB 上直接算二是没有去边界三是 HR 和生成图尺寸不一致。解决严格按前面说的 YCbCr Y 通道、去边缘像素、公共区域对齐三个步骤来算。Set5 这类测试集图片小时长差几个像素都会影响结果。写一个rgb_to_ycbcr工具函数所有验证只用这个函数避免测试代码各写一套。5.4 训练中段 loss 突然变成 NaN现象训练 30 多个 epoch 后loss 从正常值直接变成 nan后续步数全部 nan。原因最常见的是生成器梯度爆炸其次是数据集中混入了损坏图片或者是学习率过大导致优化器更新步长越界。解决在 backward 后加clip_grad_norm_把所有加载的图片统一验证能否用 PIL 正常打开过滤损坏文件。如果已经 nan不要硬调直接 load 最近一个正常保存的 checkpoint 重来。保留周期性 checkpoint 就是防这种事情的后悔药。5.5 x8 不要一次上采样模型会崩现象用同一套生成器把 scale 设成 8训练后生成图有明显的重复纹理和块状伪影PSNR 远低于 x4。原因一次放大 8 倍像素重排和卷积需要生成的信息量太大生成器难以稳定拟合。解决常见做法是拆成多级比如先训 x2再把 x2 结果作为输入训下一个 x2形成级联。如果坚持用单模型 x8需要增加残差块数量同时把预训练阶段拉长到总步数的三分之二。标题里 x8 权重文件能提供的前提是采用合理训练策略否则很难复现出可用效果。6. 拿到权重之后怎么验证一个测试脚本看透模型成色6.1 单图测试脚本命令行输入、PSNR 输出、结果存盘验证权重能不能用最直接的方式是准备几张标准测试图跑一遍前向输出 PSNR 和图片。下面是一个最小测试脚本可以直接扩展到批量测试import torch import numpy as np from PIL import Image from model import Generator def load_model(weight_path, scale): net Generator(scalescale) state torch.load(weight_path, map_locationcpu) net.load_state_dict(state[model]) return net.eval() def rgb_to_y(img): return (0.257 * img[..., 0] 0.564 * img[..., 1] 0.098 * img[..., 2] 16.0) def psnr_y(img1, img2): mse np.mean((img1 - img2) ** 2) return 10 * np.log10(255.0 ** 2 / mse) net load_model(weights/x4/best_psnr_x4.pth, scale4) lr Image.open(samples/butterfly.png).convert(RGB) # 输入尺寸自动对齐宽高取 scale 的整数倍 w, h lr.size w, h w - w % 4, h - h % 4 lr lr.crop((0, 0, w, h)) lr_t torch.from_numpy(np.array(lr)).permute(2, 0, 1).float() / 127.5 - 1.0 sr_t net(lr_t.unsqueeze(0)) sr ((sr_t[0].permute(1, 2, 0).detach().numpy() 1) * 127.5).clip(0, 255) Image.fromarray(sr.astype(np.uint8)).save(output_x4.png)这个脚本里map_locationcpu保证没有 GPU 也能加载权重。权重文件里不仅存 model state_dict还建议存scale和psnr两个 meta 字段load 时直接读取避免手滑拿 x2 权重当 x4 用。如果测试输出有明显色偏先检查是不是解码时 RGB 和 BGR 通道混了PIL 默认 RGB而某些训练脚本用 OpenCV 读图通道顺序不一致会让结果看起来像调色盘。6.2 判断权重值不值得继续投入的三个主观指标客观 PSNR 只能说明一部分问题我判断一个超分权重是否靠谱通常看三处细节。第一放大到 400% 后文字边缘是否有锯齿或重影SRGAN 生成结果应当是清晰但不生硬的边缘。第二平坦区域有没有水渍感和不规则亮斑G 网络如果过拟合纹理会在墙面上生成假细节。第三输入压缩痕迹明显的图像输出是否合理修复而不是凭空臆造纹理。这三点比 PSNR 高 0.1 更能说明模型可用性。还有一个实际操作上的习惯新权重拿到后不要在单张图上自嗨把测试脚本跑在 10 张以上不同场景的图上统计平均 PSNR 和主观评分。单张高 PSNR 很可能是运气批量结果才稳定。如果批量主观效果不稳定优先回退到 MSE 预训练权重SRGAN 在精细调参前并不总是能赢过单纯的 SRResNet。复现这套代码真正的收获不只是三份权重文件而是你掌握了如何用训练曲线判断模型状态、用统一口径评估效果、以及在 GAN 训练翻车时快速定位问题的能力。我当年第一次训 SRGAN 就折戟在 BN 模式上后来才把验证流程固化下来。希望帮到你少走这段弯路。本文还有配套的精品资源点击获取