首页 / 资讯中心 / 文章详情

Keras实现SRGAN:用生成对抗网络做图像超分辨率重建

Keras实现SRGAN:用生成对抗网络做图像超分辨率重建 ★ FEATURED ARTICLE
把一张模糊的老照片放大当壁纸或者从视频监控里截取一帧人脸想看清楚这种事情做过的人应该都有体会用双三次插值一类传统算法放大之后边缘是变清晰了但整个画面假得厉害皮肤纹理像塑料边缘有光晕细节基本靠脑补。这两年我在图像重建方向折腾了不少时间SRGANSuper-Resolution Generative Adversarial Network这个模型是把超分辨率重建效果拉到看起来像真照片这条路线上最有代表性的一个方案而用Keras实现它是性价比最高的上手方式。这篇文章把我从数据准备、网络搭建、损失函数设计到训练排坑的完整过程都写出来代码思路基于Keras/TensorFlow 2.x适合有深度学习基础、想自己跑通一个GAN项目的人参考。先说结论SRGAN的核心贡献不是把PSNR刷得多高而是用感知损失骗过人眼。传统超分模型追求像素误差最小化结果产出的是平均脸——每个细节都是模糊的最优解。SRGAN引入生成对抗机制让生成器学会输出看起来真实的高频纹理而不是和原图逐像素对齐。理解了这个思路你在调参时才不会一头扎进loss数值里出不来。1. 超分任务为什么难从双三次插值到学习型重建1.1 超分辨率的病态本质图像超分辨率Super-ResolutionSR在数学上是一个典型的病态逆问题。一张高分辨率图像经过下采样、模糊、加噪声之后就变成了低分辨率图像但这个退化过程是不可逆的信息已经被丢弃了理论上存在无穷多张高分辨率图像可以退化出同一张低分辨率图像。传统方法比如双三次插值Bicubic、Lanczos等本质上只是在频域上猜测丢失的高频分量它们没有学习过任何图像先验所以放大倍数一高比如4倍边缘锯齿和振铃效应就压不住了。我在最早接触超分任务时第一反应是用普通的CNN做端到端回归输入低分辨率图输出高分辨率图用MSE作为损失函数。当时跑的模型训练了几天PSNR确实比插值高了不少但把结果放大看细节纹理区域一塌糊涂全是糊成一片的颜色。后来才明白MSE损失引导模型学到的是所有可能高分辨率图的期望而期望值在感知上往往就是模糊的。感知质量高的超分结果应该是一个具体且锐利的解而不是一个统计学上最稳妥的平均解。1.2 SRGAN改变了什么SRGAN的思路来自生成对抗网络GAN的启发原论文发表在CVPR 2017上叫Photo-Realistic Single Image Super-Resolution Using a Generative Adversarial Network。它把超分问题拆成两个网络的对弈生成器G负责把低分辨率图重建为高分辨率图判别器D负责区分输入图是真实的高分辨率原图还是生成器伪造的图。两个网络互相促进G想骗过D就不得不生成越来越逼真的高频细节D想识破G就得学会捕捉越来越细微的伪造痕迹。但单纯的对抗损失还是不够的SRGAN真正的杀手锏是感知损失。生成器输出的图像不再直接和原图比像素而是先把两者都送进一个预训练的VGG19网络在高层特征空间比较差异。这样的哲学是人眼对图像内容的感知主要由高层特征决定而不是每个像素的数值。保留VGG特征空间的相似性会让重建结果在内容上忠实于原图同时允许生成器在纹理上自由发挥把伪影降到最低。提示SRGAN不是第一个把GAN用到图像生成任务的模型但它首次把感知损失对抗损失这套组合完整地用于超分重建后续的ESRGAN、SRFeat等模型本质上都是在这个框架上演进的。1.3 选用Keras而非纯TensorFlow/PyTorch的理由我最终选择Keras来实现SRGAN有几个很实际的考量。Keras在模型层的抽象上做得极其干净生成器和判别器都可以用顺序模型加函数式API拼装不用自己手写前向传播和反向传播的细节。对初学者来说训练循环的细节已经够多了再用原生框架徒增心智负担。另一个好处是Keras对VGG19这类预训练模型有直接封装加载ImageNet权重一行代码的事这在实现感知损失时非常省力。当然Keras也有它的短板自定义训练循环时会有一些隐形的坑比如BatchNorm在对抗训练中的行为、梯度在多个loss之间如何加权等等。这些坑我后面专门开了一章节来讲。总体而言对于想快速完成SRGAN的可行性验证这个目标Keras是最短路径。2. SRGAN的博弈架构生成器、判别器和损失函数的三角关系2.1 生成器残差密集连接与子像素卷积SRGAN生成器的主干是一个由16个残差块Residual Block堆叠而成的深度网络残差块的内部结构是卷积层 → BatchNorm → PReLU → 卷积层 → BatchNorm然后和输入相加。残差连接解决的是深度网络退化问题让梯度能顺畅回传同时让生成器集中精力学习残余的高频细节而非整张图的完整映射。这两点对稳定训练很重要。上采样部分SRGAN没有直接用反卷积转置卷积而是用了子像素卷积Sub-pixel Convolution也叫PixelShuffle。它的做法是先通过卷积把通道数放大到目标倍数的平方例如4倍超分就是16倍通道数再重排成高分辨率图像的空间像素。这种方式的优势是避免了反卷积中常见的棋盘伪影因为可学习的卷积参数都在低分辨率空间里重排本身不引入重叠和空洞。NCHW格式下4倍超分时的子像素卷积实现思路大致是输入尺寸为[batch, h, w, c]经过卷积后变成[batch, h, w, c * 16]然后重排为[batch, h * 4, w * 4, c]。2.2 判别器VGG风格二分类网络判别器采用VGG风格的网络结构上是一连串的卷积批归一化LeakyReLU模块。和生成器里的PReLU不同判别器每一个卷积层后面接的是LeakyReLU负斜率为0.2。LeakyReLU在负区间保留了梯度避免判别器因输入分布偏移而梯度消失。判别器的输入是原图高分辨率图或生成器输出的图输出是一个标量表示该图是真实图的概率。Keras里用单个神经元加sigmoid激活即可。判别器不用太深因为它的任务是区分真图和假图太深的网络反而可能陷入过拟合导致训练不稳定。2.3 损失函数感知损失才是主角SRGAN的损失由两部分组成内容损失和对抗损失。内容损失不是MSE虽然原论文也做了MSE作为内容损失的消融实验而是基于VGG19的感知损失。具体实现把生成图和高分辨率原图分别送入VGG19提取relu5_4层的特征图然后计算两者的欧氏距离。用relu5_4这个较深的层意味着网络更在意语义内容是否一致而不是像素是否对齐这给生成器留下了足够的自由度去合成逼真纹理。对抗损失用的是标准GAN的二元交叉熵。生成器的对抗损失是让判别器把生成的图像判定为真实图像即最小化log(1 - D(G(z)))或最大化log(D(G(z)))实践中常把标签翻转来简化实现。总损失写作总损失 内容损失 1e-3 * 对抗损失对抗损失的权重系数在1e-3量级是经验值。太小则对抗效应不显著重建图趋向于保守的模糊太大则生成器被判别器带偏出现色彩失真、结构扭曲等伪影。3. 数据与预处理如何制作一张合格的低分辨率图片3.1 降采样方式的选择与影响很多人跑超分模型时图省事直接用某种固定插值把高分辨率图缩小再作为低分辨率输入。其实降采样细节对模型影响很大的问题在于模型学到的退化先验必须和测试时的退化方式匹配。如果你训练时都用双三次插值缩小测试时却用手机拍摄的模糊照片真实退化模型表现会急剧下降。最稳妥的做法是模拟多种退化方式双三次缩小、高斯模糊后缩小、加噪声后缩小等甚至可以在随机范围内组合这些退化参数提升模型的泛化能力。具体的数据准备流程我用这套方案从公开数据集如DIV2K、Flickr2K选取800张以上高清图裁成各类大小的子图每张子图随机做水平翻转、90度旋转、色彩扰动等增强按4倍因子用双三次插值缩小得到低分辨率图低分辨率图归一化到[-1, 1]高分辨率图同步归一化。3.2 训练数据组织的几个细节在组织batch时我建议固定裁切尺寸比如HR图裁成128x128对应LR图32x32。这样batch内所有样本形状统一训练效率高。如果内存够大batch size取16比较合适显存紧张时降到8也行但BatchNorm的行为会受到batch size影响这点在GAN训练里会被放大后面会细说。还要避免的一个坑是低分辨率输入必须和生成器输出尺寸对齐。生成器内部的上采样倍数必须精确等于LR到HR的缩放比例。如果一张LR图尺寸是32x32而生成器设计的是4倍放大输出应该是128x128。训练时如果数据读取出错用了不同尺寸的LR图network会直接报维度错误或者更隐蔽地用自动填充硬算导致生成结果错位这种bug排查起来非常费时。3.3 归一化的坑tanh输出与感知损失的配合SRGAN生成器最后一层用的激活函数通常是tanh所以我把HR图也归一化到[-1, 1]和生成器输出保持同一分布空间。归一化范围的选择在GAN训练中很容易被忽略但确实重要。如果HR图是[0, 1]范围而生成器输出是[-1, 1]内容损失一开始就会处于一个不合理的量级梯度更新方向也会不自然。这个坑我踩过前几次训练时loss下降曲线很怪推测就是归一化空间不匹配导致的。4. Keras实现与训练循环搭建从生成器预训练到对抗微调4.1 生成器与判别器的Keras实现生成器我用函数式API实现。残差块用一个自定义函数封装方便堆叠16次。子像素卷积在Keras里可以直接用tf.nn.depth_to_space也可以写一个Lambda层调用它。import tensorflow as tf from tensorflow.keras import layers, Model def residual_block(x, filters64): shortcut x x layers.Conv2D(filters, 3, paddingsame)(x) x layers.BatchNormalization()(x) x layers.PReLU(shared_axes[1, 2])(x) x layers.Conv2D(filters, 3, paddingsame)(x) x layers.BatchNormalization()(x) return layers.add([x, shortcut]) def build_generator(scale4): lr_input layers.Input(shape(None, None, 3)) x layers.Conv2D(64, 9, paddingsame)(lr_input) before_residuals layers.PReLU(shared_axes[1, 2])(x) x before_residuals for _ in range(16): x residual_block(x) x layers.Conv2D(64, 3, paddingsame)(x) x layers.BatchNormalization()(x) x layers.add([x, before_residuals]) # 4x upsampling, two stages of 2x for _ in range(scale // 2): x layers.Conv2D(256, 3, paddingsame)(x) x tf.nn.depth_to_space(x, 2) # sub-pixel conv x layers.PReLU(shared_axes[1, 2])(x) x layers.Conv2D(3, 9, paddingsame, activationtanh)(x) return Model(lr_input, x)判别器我定义成独立的模型输入是HR图像或生成图输出是sigmoid概率def conv_bn_lrelu(x, filters, kernel3, strides1, alpha0.2): x layers.Conv2D(filters, kernel, stridesstrides, paddingsame)(x) x layers.BatchNormalization()(x) x layers.LeakyReLU(alphaalpha)(x) return x def build_discriminator(): hr_input layers.Input(shape(128, 128, 3)) x layers.Conv2D(64, 3, strides1, paddingsame)(hr_input) x layers.LeakyReLU(alpha0.2)(x) x conv_bn_lrelu(x, 64, strides2) x conv_bn_lrelu(x, 128, strides1) x conv_bn_lrelu(x, 128, strides2) x conv_bn_lrelu(x, 256, strides1) x conv_bn_lrelu(x, 256, strides2) x layers.Flatten()(x) x layers.Dense(1024)(x) x layers.LeakyReLU(alpha0.2)(x) x layers.Dense(1, activationsigmoid)(x) return Model(hr_input, x)注意一个细节判别器的输入尺寸是固定的128x128但生成器可以用任意尺寸输入。在组合训练时我把生成器的输出直接接到判别器的输入所以Keras组合模型里它们的尺寸必须匹配。这也是训练时输入裁剪尺寸和放大倍数都要固定约束的原因。4.2 自定义VGG损失函数VGG19加载预训练权重提取block5_conv4也就是原论文中的relu5_4的特征图计算两张特征图的均方误差作为感知损失from tensorflow.keras.applications import VGG19 def build_vgg_loss_model(): vgg VGG19(include_topFalse, weightsimagenet, input_shape(128, 128, 3)) vgg.trainable False feature_extractor Model(vgg.input, vgg.get_layer(block5_conv4).output) return feature_extractor vgg_extractor build_vgg_loss_model() def perceptual_loss(y_true, y_pred): true_features vgg_extractor((y_true 1.0) * 127.5) # denormalize to 0-255 for VGG pred_features vgg_extractor((y_pred 1.0) * 127.5) return tf.reduce_mean(tf.square(true_features - pred_features))实现细节VGG19是在ImageNet上训练的输入范围是[0, 255]。所以送入VGG之前一定要做反归一化。这个坑我一开始没有注意直接用[-1, 1]的图送入VGGloss曲线一开始很大后来检查才发现是输入范围完全不对。4.3 两阶段训练法先MSE预训练再GAN微调直接端到端训练SRGAN生成器几乎必然崩溃。我在实践中用原论文推荐的两阶段训练法阶段一纯MSE预训练生成器只用生成器 MSE损失把低分辨率图直接映射到高分辨率图训练约50-100个epoch。这个阶段的目标是让生成器先具备基础的超分能力至少轮廓和颜色要对。预训练后的PSNR一般能到28-30dB4倍超分场景虽然视觉上偏糊但已经是可用的底子了。阶段二GAN微调冻结VGG特征提取器把生成器和判别器组合成GAN框架交替训练。此时生成器的总损失 感知损失 对抗损失。感知损失保留内容结构对抗损失逼出高频纹理。这个过程我通常训练150-200个epoch学习率从1e-4逐步下降到1e-5。4.4 组合模型的构建与交替训练在Keras中我构建了两个独立的训练步骤交替执行# 组合模型生成器 判别器训练生成器时冻结判别器 generator build_generator() discriminator build_discriminator() # 判别器单独优化 discriminator.compile(optimizertf.keras.optimizers.Adam(2e-4), lossbinary_crossentropy) # 组合模型 lr_input layers.Input(shape(32, 32, 3)) hr_fake generator(lr_input) validity discriminator(hr_fake) combined Model(lr_input, validity) combined.compile(optimizertf.keras.optimizers.Adam(1e-4), lossbinary_crossentropy)训练循环的关键点在于每个step里用一个batch的HR原图和LR图把LR图送入生成器得到HR_fake用HR_true和HR_fake训练判别器正样本是HR_truelabel1负样本是HR_fakelabel0用LR图训练组合模型目标是让判别器把HR_fake判为1同时传入感知损失辅助。这里我踩过一个影响很大的坑在训练组合模型时如果不显式设置VGG提取器的trainableFalseKeras会自动把VGG里的BN层等参数也纳入更新导致整个训练跑偏并且显存暴涨。组合模型的输入是LR图输出是判别器的sigmoid概率感知损失需要以额外的loss项加进去。Keras的Model.compile(loss[...])要传入两个loss一个对抗loss一个感知loss并配权重。我用的权重配置是combined.compile( optimizeropt, loss[binary_crossentropy, perceptual_loss], loss_weights[1e-3, 1.0] )也就是说对抗损失的权重是1e-3感知损失权重为1。如果顺序写反了训练会直接毁掉预训练好的生成器。5. 训练踩坑实录Loss不收敛、色彩偏移与棋盘伪影的排查5.1 预训练不充分就上对抗导致生成器崩溃我第一次训练SRGAN时急于求成直接把MSE预训练的epoch数从100砍到20结果一上对抗训练损失曲线像过山车一样震荡生成图像的色彩饱和度急剧下降然后出现大量绿色和紫色的斑点。排查后发现原因很明确生成器在MSE阶段的拟合能力不足给出的重建结果质量太差判别器轻松就能分辨出真伪随即给出极强的梯度信号把生成器推向一个只顾骗过判别器、不顾内容结构的极端解。这个现象在GAN里叫模式崩塌的变种生成器学会了用高频噪声欺骗判别器但画面本身没有意义。经验MSE预训练的epoch数宁可多不要少并把生成器的Val loss作为是否进入阶段二的判据。只有当验证集PSNR稳定在28dB以上再切换对抗训练的成功率会大幅提高。5.2 学习率的对称性与Adam参数GAN训练对学习率很敏感。SRGAN原论文里生成器和判别器的学习率并不相同。我的经验是判别器可以比生成器稍快因为判别器任务相对简单快速收敛到能有效辨别的状态反过来给生成器提供稳定的学习信号。但如果判别器太快它会把真假样本分得过于开生成器的梯度就会消失。实际操作里我用这样的配置判别器用1e-4生成器在对抗阶段用1e-4到5e-5。同时开启Adam的beta_10.9, beta_20.999。在训练中如果发现D的loss持续逼近0说明判别器太强了此时应降低D的学习率或提升生成器的频率。5.3 棋盘伪影的根源与排查训练初期我经常在生成的图片上看到像棋盘一样规律的网格纹理尤其在平滑区域和天空部分。排查后发现原因有两个反卷积转置卷积带来的重叠堆积是经典元凶。但SRGAN明明用了子像素卷积为什么还会出现棋盘原因是生成器最后一个卷积层的卷积核大小和步长配置不当在子像素重排之后仍有局部重叠更隐蔽的原因是训练数据裁剪时选了带重压缩痕迹的JPEG图片压缩块效应被模型学成了先验。换成无损PNG或者干净的高清源图后棋盘伪影明显减少。解决棋盘伪影我最终的组合方案是生成器最后一层卷积核大小调整为3x3、paddingsame上采样前在卷积之间加一层高斯模糊层用固定权重的卷积核做平滑这一招虽然看似笨拙但很有效。5.4 色彩偏移问题的定位与修复运行到后期我注意到部分输出图像的饱和度和原图不一致有的偏黄、有的偏蓝。这个问题排查起来比较费劲一度怀疑是生成器训练不充分。后来通过逐层检查logits分布发现是BatchNorm在batch size较小时导致的统计量漂移。对抗训练里batch size通常不能开太大尤其在生成高分辨率图像时显存吃紧如果batch size4甚至更小BatchNorm计算的均值方差不稳定给生成器注入了奇怪的噪声反映到输出就是色彩偏移。我采取的办法是在生成器中尽量用Instance Normalization代替BatchNorm或者在训练初期固定较大的batch size让BN统计量先稳定下来。ESRGAN后来把BN全部去掉改用其他归一化手段其实也是在规避这类问题。5.5 组合模型保存与恢复的坑训练到一半断电重启是家常便饭。但Keras的Model.save保存的是整个模型结构、优化器状态和模型权重加载后重新训练一般没问题。但在SRGAN这种多模型组合场景下我建议分别保存生成器、判别器和VGG提取器各自独立的权重文件而不是只保存组合模型。因为组合模型中包含VGG的权重和优化器状态占据了大量空间加载还容易混入不相关的参数。我用的保存策略是每隔20个epoch保存一次generator_weights.h5和discriminator_weights.h5然后附一个training_state.json记录当前epoch和学习率。恢复训练时手动重新构建模型并load权重比直接load整个组合模型省心得多。6. 效果评估与调优方向PSNR只是及格线感知质量才是目标6.1 指标的迷思PSNR高不等于好看测超分效果大家第一反应就是PSNR峰值信噪比和SSIM结构相似度。这两指标的优点是计算简单、可复现但它们和人的主观感知并无直接对应关系。我跑过一个对照实验同一个超分模型输出结果MSE比SRGAN低了30%PSNR高了1.2dB但做主观测试时所有人都认为SRGAN的结果更自然细节更接近真实照片。原因就在于MSE型的模型为了让所有像素误差都小会倾向于生成平滑、保守的结果而人类视觉系统真正关注的是边缘、纹理和结构的高级语义这些恰好是感知损失侧重的特征。所以在评估模型时我长期用一套PSNR SSIM 主观盲评的组合方案。盲评时找几个人把SRGAN结果、MSE模型结果和真实HR图打乱顺序让他们按像不像真照片打分。这种主观分往往比任何指标都更能反映模型的实用价值。6.2 从SRGAN到ESRGANBN层与感知损失的演进SRGAN跑通之后我接着调研了它的改进版本ESRGANEnhanced SRGAN。ESRGAN最核心的改动是去掉生成器所有的BatchNorm层改用残差密集块和更多层的残差连接感知损失改用VGG特征的相对对抗损失强调相对真实而不是绝对真实。从实际效果看ESRGAN的纹理比SRGAN更锐利抑制伪影的能力也更强。如果你在SRGAN上已经练熟了手再往前探索ESRGAN会很自然。我的体会是超分模型没有绝对正确的架构只有更适合当前退化场景和数据分布的方案。SRGAN是理解整个GAN超分家族的最佳起点因为它把对抗学习、感知损失、两阶段训练这些核心概念都集中在一个结构很清晰的框架里。7. 一个完整的实验配置参考把整体配置整理成一张表方便照抄配置项我的设置说明训练集DIV2K Flickr2KHR裁128x128LR双三次缩小32x32800张以上数据增强生成器16个残差块 2级子像素上采样基础SRGAN结构判别器VGG风格二分类LeakyReLU(0.2)轻量、稳定内容损失VGG19 block5_conv4特征MSE权重1.0对抗损失二元交叉熵权重1e-3阶段一优化器Adam(1e-4)MSE预训练约80 epochs目标PSNR≥28dB阶段二优化器G: Adam(1e-4)D: Adam(2e-4)150-200 epochsBatch size16低于8时容易出色彩偏移显存不够时减少HR尺寸而非batch数据归一化LR/HR都归一化到[-1, 1]送VGG前反归一化到[0, 255]两处都容易踩坑训练耗时单张主流显卡上阶段一约6小时阶段二约18小时总显存占用视HR尺寸而定。128x128的HR输出batch size16时显存约10GB。显存不够的读者可以把HR裁成96x96batch降到8效果损失的幅度完全在可接受范围内。最后再分享一个锦上添花的小技巧训练完成后跑推理时对输入LR图做self-ensemble自集成把图和它翻转、旋转后的版本都送入生成器得到多个HR图后再反变换回去求平均。这个无技术含量但有效的方法可以让最终的PSNR提升0.2-0.3dB纹理更稳定。代价只是推理时间变成原来的8倍按需取舍。我在实际做项目时96x96以上的LR图通常要跑很久所以只对关键帧做self-ensemble其他场景直接用单次推理结果兼顾速度和效果。
阅读完成 · 觉得有帮助?
咨询建站