深度多尺度生成对抗网络图像压缩感知重建原理与工程实践
2026/9/23 22:42:10 网站建设 项目流程

简介:这套源码是吉林大学学报(工学版)相关论文的配套实现,面向图像处理、深度学习和信号处理领域的研究者,使用多尺度生成对抗网络完成图像压缩感知重建,可在远低于奈奎斯特采样定律要求的条件下从稀疏观测中恢复高质量图像,并重点保护边缘与纹理细节。压缩包共二十八个文件,包含 Python 源码、JPEG 示例图片、CSV 训练统计、pyc 编译文件及项目说明,整体约 151KB;模型构建、训练、数据预处理和性能评估等环节均提供对应脚本,目录结构清晰,便于按需调用与二次开发。目前已有三百二十三人浏览学习;通过实际操作,可深入掌握对抗训练过程、多尺度特征提取策略与 SSIM 等指标计算,CSV 中的损失和评分记录还能辅助结果分析,适合在此基础上扩展超分辨率、图像修复等应用。

1. 从一张模糊的压缩感知重建图说起:深度多尺度生成对抗网络在什么场景下值得动手做

把一张自然图像压到 10% 的采样率,再送进传统的压缩感知重建算法,比如 TV 或 DCT 字典方法,跑出来的结果往往边缘糊成一片,纹理区域像被水洗过。如果你做的任务是监控视频、医学影像或遥感图像的重建,这种细节丢失并不是"再调两个参数"就能解决的,而是重建模型本身没有利用图像的多尺度结构信息。我在帮一个医学影像团队做加速采样评估时,同样的测量数据和采样率,换成基于深度多尺度生成对抗网络的图像压缩感知重建源码后,细节恢复明显上了一个台阶,训练和推理链路也都是标准 PyTorch 能跑通的东西。

这篇文章就把这类源码从原理讲到落地:压缩感知的测量建模、多尺度生成器的结构选择、生成对抗网络的损失配置,以及训练时最容易翻车的几个坑。适合手里已经有一批图像数据、想把采样率降下来但又不愿意牺牲重建质量的工程师和研究生。

2. 重建任务的三个核心组成:从压缩感知测量到生成对抗网络能补充的信息

2.1 压缩感知重建为什么难:测量方程、病态性和先验的作用

压缩感知的基本设定很简单:原始信号 x ∈ R^n,用测量矩阵 Φ ∈ R^{m×n} 得到测量值 y = Φx,其中 m << n。采样率定义为 m/n,通常取 5% 到 30%。重建就是解一个病态反问题:已知 y 和 Φ,反求 x。因为方程数少于未知数,解的集合是无穷的,必须加先验约束。

传统方法里,先验是稀疏性假设:x 在某个变换域 W 下是稀疏的,于是求解 min ||Wx||_1,同时要求 Φx = y。在图像上这可以工作,但有明显的结构性弱点。自然图像不是全局稀疏信号,边缘和纹理分布具有局部、多尺度的特点,一个固定字典或小波基很难同时刻画平滑区域和强纹理区域。所以在采样率低于 20% 时,稀疏性先验会使重建从全局来看变得过于平滑,高频纹理被解释为噪声而丢失。

深度学习方法改变的是先验的来源。深度神经网络不再显式定义一个变换域,而是从大量(x, y)配对数据中隐式学习图像的先验分布。换句话说,网络学习的是"看起来像自然图像"的集合。这个能力正是生成对抗网络的强项。

2.2 多尺度架构在重建中的价值:浅层纹理与深层结构不能用一个尺度的卷积处理

在图像重建任务中,单尺度卷积网络有一个典型的失效模式:如果你用一个单纯的 U-Net(编码器逐层下采样,解码器逐步上采样)处理,小尺寸纹理细节(例如皮肤毛孔、树叶脉络)在经过两三次下采样后信息基本被丢弃,重建结果往往平滑但缺少真实质感。反过来,如果只用不带下采样的全卷积,感受野太小,结构信息又抓不住。

多尺度设计的常见做法是在生成器内部并联多个不同感受野的分支,同时保留跳连接。一个分支用 1×1 卷积看逐像素特征,一个用 3×3 且带膨胀率的分支看局部纹理,一个用 5×5 或更大膨胀率的分支看全局结构。三个分支的特征拼接后经过 1×1 卷积融合。这样的设计保证了同一层特征图里既包含精细纹理响应,也包含大范围结构响应,重建图像不容易在纹理区域出现"平板化"。

另一个常见多尺度做法是把图像分块做金字塔输入:原图、1/2 分辨率、1/4 分辨率各输入一个分支,然后在解码端融合。这种方案的缺点是显存占用成倍增加,对批量大小不友好。相比之下,同一个尺度下并联多感受野的分支,增加的参数和计算量更可控,是工程落地更常见的选择。

2.3 生成对抗网络在这里的角色:对抗损失解决"模糊但 PSNR 高"的问题

用纯 L2 损失训练重建网络,得到的结果有一个非常反直觉的现象:PSNR 很高,但图像看起来很糊。原因是 L2 损失对逐像素误差取平均,模型在多个可能重建结果的"中间地带"取均值,得到的像素值在数值上接近真实值,但形成了过度平滑的视觉假象。这是图像重建任务中一个被讨论很多的问题:内容损失和感知质量并不是一回事。

生成对抗网络在这里解决的是分布匹配问题。网络不再只做逐像素误差最小化,而是加一个判别器,让重建图像的分布去逼近真实图像的分布。判别器看到一批重建图和一批原图,输出"真实程度"的分数,生成器则试图让这些分数尽量接近"真实"。这样生成器被迫产生更锐利的边缘和更真实的纹理。

在这类源码里,常见的判别器设计有两种。一种是传统的 PatchGAN,输出一个 N×N 的得分图,每个得分对应原图的一个局部区域,能更好地约束局部纹理。另一种是全局+局部双判别器,专门处理重建结果中局部过平滑的问题。"条件生成对抗网络"这个思路在这里也很自然——把测量值 y 或者初始重建图作为条件输入判别器,让判别器在判断真伪时不仅看纹理,也看结构是否与测量信息一致。

3. 从测量矩阵到多尺度生成器:把图像压缩感知重建源码拆成三段直接训练

3.1 测量矩阵与数据管线:固定 Φ 还是动态生成 Φ

压缩感知的测量矩阵在训练阶段和推测阶段必须保持一致,这是源码设计里最容易忽略的部分。常见做法有固定的高斯随机矩阵和训练中动态生成的随机矩阵。固定矩阵的优点是训练和评估的环境完全一致,网络能针对这个矩阵收敛得更彻底;缺点是换到另一个矩阵要重新训练。动态矩阵增强泛化性,但收敛更慢,损失曲线更容易波动。

下面是一个生成固定高斯测量矩阵并做块压缩的代码段,按照图像块(patch)级别的测量方式来设计:

import torch import torch.nn as nn import numpy as np def create_gaussian_measurement(patch_size, channels, sampling_rate, seed=0): """ 生成固定高斯测量矩阵 Phi n: 单个图像块展平后的维度 m: 测量值维度 """ n = channels * patch_size * patch_size m = int(np.floor(n * sampling_rate)) torch.manual_seed(seed) # 标准差取 1/sqrt(m),保证测量值能量与输入信号在同一量级 phi = torch.randn(m, n) / np.sqrt(m) return phi def compress_batch(x, phi): """ x: (B, C, H, W) 图像块,值域建议在 [0, 1] phi: (m, C*H*W) 返回 y: (B, m) """ B = x.shape[0] x_flat = x.view(B, -1) y = x_flat @ phi.T return y

逻辑说明:图像块展平后与测量矩阵相乘,得到测量向量 y。标准差按 1/sqrt(m) 归一化,是压缩感知常用的做法,可以避免 m 增大时测量值幅度随维度增长而发散。训练时把这个 phi 作为 buffer 注册到生成器模型里,而不是作为普通 Tensor 放在全局变量中,可以保证模型保存和加载时测量矩阵被完整带走。

测量矩阵的数值分布在实际中非常重要。如果你用标准差为 1 的矩阵,测量值 y 的幅度会随 patch 尺寸急剧增大,后续全连接重建层需要花更多迭代去适应这个分布,收敛速度明显下降,甚至会出现梯度爆炸。

3.2 多尺度生成器的结构:从测量值到初始特征图再到重建

生成器通常分两个阶段。第一阶段是把测量向量 y 映射回一个与原始图像尺寸相同的初始特征图,这一步本质上是做一个线性逆变换或者简单的非线性映射;第二阶段是把这个初始特征图送入多尺度卷积网络进行精细重建。

初始映射部分,常见做法是直接用一个全连接层,输入维度 m,输出维度 C×H×W。全连接层的参数初始化为测量矩阵的转置近似(伪逆),这样模型从"测量值直接投影回图像域"开始学,而不是从随机初始化开始。伪逆初始化通常能比随机初始化在训练前期的重建质量高出好几个 dB。

class MultiScaleGenerator(nn.Module): def __init__(self, in_ch=3, patch_size=64, sampling_rate=0.1, base_dim=64): super().__init__() n = in_ch * patch_size * patch_size m = int(np.floor(n * sampling_rate)) # 第一阶段:测量值到初始重建 self.fc_init = nn.Linear(m, n) self._init_fc_with_pinv() # 伪逆初始化,细节见下方说明 # 第二阶段:多尺度残差块 self.head = nn.Sequential( nn.Conv2d(in_ch, base_dim, 3, padding=1), nn.ReLU(inplace=True) ) self.down1 = nn.Sequential( nn.Conv2d(base_dim, base_dim * 2, 4, stride=2, padding=1), nn.ReLU(inplace=True) ) self.down2 = nn.Sequential( nn.Conv2d(base_dim * 2, base_dim * 4, 4, stride=2, padding=1), nn.ReLU(inplace=True) ) # 多尺度残差块:三条不同膨胀率的分支 self.ms_res = MSResBlock(base_dim * 4, base_dim * 4) self.up1 = nn.Sequential( nn.Upsample(scale_factor=2, mode='bilinear', align_corners=False), nn.Conv2d(base_dim * 4, base_dim * 2, 3, padding=1), nn.ReLU(inplace=True) ) self.up2 = nn.Sequential( nn.Upsample(scale_factor=2, mode='bilinear', align_corners=False), nn.Conv2d(base_dim * 2, base_dim, 3, padding=1), nn.ReLU(inplace=True) ) self.tail = nn.Conv2d(base_dim, in_ch, 3, padding=1) def forward(self, y, patch_hw): """ y: (B, m) patch_hw: (H, W) 原始 patch 尺寸 """ B = y.shape[0] init = self.fc_init(y) # (B, n) -> reshape 成初始特征图 x = init.view(B, patch_hw[0], patch_hw[1], -1) x = x.permute(0, 3, 1, 2).contiguous() # (B, C, H, W) f0 = self.head(x) f1 = self.down1(f0) # 1/2 尺度 f2 = self.down2(f1) # 1/4 尺度 f2 = self.ms_res(f2) # 多尺度特征增强 f1_up = self.up1(f2) + f1 # 跳连接 f0_up = self.up2(f1_up) + f0 out = self.tail(f0_up) return out

伪逆初始化的思路很简单:在init里先生成与输入测量矩阵对应的批量数据,计算最小二乘解,把解矩阵填入 fc_init 的权重。这样网络一开始输出的就是伪逆重建图,后续卷积只负责修正细节。

需要说明两个关键点。一是跳连接采用相加而不是通道拼接,能减少显存占用且不损失太多信息,在 patch 较大的场景更合适。二是上采样用 Upsample + 卷积而不是转置卷积,后面避坑章节我会展开说明,这直接关系到棋盘伪影问题。

3.3 判别器与损失函数:PatchGAN、内容损失和感知损失的组合

判别器的作用区域如果覆盖太广,容易忽略局部纹理;如果覆盖太小,又容易对整体结构失去判断。实践中最稳的是 PatchGAN 判别器,输入整张图,但输出是若干个 patch 级别的得分,每个得分只对图上一个小区域负责。这样既保留了对细节的敏感,又不至于陷入纯逐像素判断。

class PatchDiscriminator(nn.Module): def __init__(self, in_ch=3, base_dim=64): super().__init__() self.net = nn.Sequential( nn.Conv2d(in_ch, base_dim, 4, stride=2, padding=1), nn.LeakyReLU(0.2, inplace=True), nn.Conv2d(base_dim, base_dim * 2, 4, stride=2, padding=1), nn.BatchNorm2d(base_dim * 2), nn.LeakyReLU(0.2, inplace=True), nn.Conv2d(base_dim * 2, base_dim * 4, 4, stride=2, padding=1), nn.BatchNorm2d(base_dim * 4), nn.LeakyReLU(0.2, inplace=True), nn.Conv2d(base_dim * 4, 1, 4, padding=1) ) def forward(self, x): # 输出形状 (B, 1, H', W'),每个位置对应输入图的一个局部 patch return self.net(x)

损失函数部分,我习惯拆成三项。第一项是内容损失,用 L1 或者 Charbonnier 损失,它的梯度比 L2 对离群值更鲁棒,重建出的边缘更干净。第二项是对抗损失,生成器希望判别器对重建图输出接近 1 的分数,计算时用 soft labels 而不是硬编码 0/1,能明显稳定训练。第三项是感知损失,取一个预训练 VGG 网络中间层的特征图,计算重建图和原图的 L2 距离,这个约束强迫网络在语义特征层面保持一致。

def generator_loss(fake, real, d_fake, vgg_feat_fake, vgg_feat_real, l1_weight=10.0, adv_weight=0.1, vgg_weight=0.05): l1_loss = torch.nn.functional.l1_loss(fake, real) adv_loss = torch.mean((d_fake - 1.0) ** 2) # LSGAN 形式 vgg_loss = torch.nn.functional.mse_loss(vgg_feat_fake, vgg_feat_real) return l1_weight * l1_loss + adv_weight * adv_loss + vgg_weight * vgg_loss

逻辑说明:LSGAN 的生成器损失用的是 "+1 目标",相比原始 GAN 的交叉熵损失,梯度在训练后期不会消失,生成的纹理更锐利。感知损失权重通常要调得比 L1 损失小一个数量级,否则特征匹配会主导训练,导致生成的图像与原图在低频上过度拟合,纹理上反而失去真实感。

4. 训练参数与损失权重:让对抗生成网络稳定收敛的五个关键设置

4.1 损失权重分配:先单独训练内容损失,再叠加对抗损失

训练过程里最需要避免的就是一开始就让对抗损失参与主梯度。常见做法是:前 20-30 个 epoch 只训练生成器的内容损失部分(L1 + VGG 感知损失),让生成器先学会一个大致的重建雏形;然后再打开对抗损失,把生成器和判别器交替训练。这种做法能避免生成器在还不会重建时就被判别器带偏,进入模式崩溃状态。

实践中的权重范围可以参考下面的经验值:L1 损失权重在 10 到 30 之间,对抗损失权重在 0.01 到 0.2 之间,VGG 感知损失权重在 0.01 到 0.1 之间。如果发现重建图像纹理过于平滑,可以提高对抗损失的权重;如果发现图像出现奇怪的伪影或色彩异常,优先把对抗损失的权重降回一半再观察。

一个更容易被忽略的参数是判别器的更新频率。每训练一个 batch 的生成器,判别器需要提前更新两次到五次。具体做法是:先训练判别器 k 步,再训练生成器 1 步。k 值推荐在 2 到 5 之间,如果判别器 loss 下降过快而生成器 loss 波动剧烈,就把 k 值调小。

4.2 优化器参数:Adam 的 beta1 设置会直接影响稳定性

对抗网络训练有个通病:如果优化器的动量参数取默认值,生成器的权重波动会非常大。原因在于生成器的梯度噪声比普通分类网络更大,判别器又不断改变损失景观,高动量会让权重更新方向被历史梯度主导,难以收敛到稳定的平衡点。

我使用的参数配置是:Adam 优化器,beta1 取 0.5,beta2 取 0.999,生成器和判别器分别使用独立的优化器。beta1 从默认的 0.9 降到 0.5,可以明显减少训练初期生成器在真实图像和重建图像之间来回震荡的问题。学习率方面,生成器和判别器起始均为 2e-4,每 30 个 epoch 乘 0.5 衰减。

还有一个细节:生成器和判别器的学习率不要设成一样。判别器往往比生成器收敛快,如果把两者学习率都设成同样大小,判别器会迅速变得太强,导致生成器梯度消失。推荐判别器学习率是生成器的二分之一到三分之一,也就是生成器 2e-4 时,判别器取 1e-4。

4.3 batch size 与归一化选择:小 batch 方案下的稳定性预案

图像压缩感知重建通常处理的是 64×64 或 128×128 的 patch,如果原图太大就得裁剪。batch size 在 GPU 显存允许下尽量取大,推荐 32 以上。但当 batch size 只有 4 或 8 时,BatchNorm 的均值和方差估计非常不稳,生成器的输出会随 batch 内容波动,判别器也会因为统计量不稳定而丧失判别能力。

遇到小 batch size 的情况,我通常有两种替代方案。一是把生成器和判别器里的 BatchNorm 全部换成 InstanceNorm,每个样本单独归一化,不受 batch 内其他样本影响。二是使用 GroupNorm,把通道分组做归一化,它对小 batch 的鲁棒性比 BatchNorm 好得多。

归一化层的选择也会影响重建细节。BatchNorm 在纹理类数据上会模糊高频信息,因为它强制每个通道在 batch 内对齐分布。InstanceNorm 不依赖 batch 内统计,对保留单张图像的纹理特征更友好。如果你的任务里纹理是核心指标,尽量用 InstanceNorm。

5. 训练落地必避的五个坑:模式崩溃、棋盘伪影与测量矩阵翻车排查手册

5.1 重建图像一片模糊且判别器 loss 无法下降

现象:生成器输出的图像在结构上正确,但整体像蒙了一层雾,纹理区域完全丢失;判别器 loss 停在一个高位不上不下。

原因:对抗损失权重太低,同时内容损失中的 L1 权重过高。L1 损失是绝对的逐像素平均,它把网络朝"均值解"方向拉,纹理区域因为像素值方差大,被平均后自然变平滑。对抗损失不足以压制这个趋势时,模糊就成了局部最优点。

解决方法:把对抗损失权重调高 3 到 5 倍,并把 L1 权重下调 20% 左右。还有一种更实用的办法是用 Charbonnier 损失替代 L1,它的损失函数带一个常数项 epsilon,对大梯度误差的惩罚小于 L1,能稍微降低模型对纹理区域大梯度的"恐惧",保留更多锐利边缘。

5.2 重建图出现周期性网格状伪影(棋盘伪影)

现象:重建图放大后,在边缘附近出现类似棋盘格子的交替明暗点阵,尤其在高频纹理区域最明显。

原因:这是深度重建任务中最常见的翻车点。生成器如果使用了转置卷积做上采样,且 kernel size 不能被 stride 整除,输出特征图会叠加周期性波动,形成棋盘伪影。问题不是出在测量矩阵或损失函数,而是上采样方式的固有缺陷。

解决方法:用双线性 Upsample 或 PixelShuffle 替代普通转置卷积。双线性上采样加普通卷积是我最常用的方案,上采样本身没有可学习参数,不会引入周期性模式。PixelShuffle 也很好,但需要调整通道数策略,对初学者不太直观。改完结构后,即使不做其他调整,伪影一般会大幅度减轻。

5.3 测试时换了测量矩阵,重建质量突然崩盘

现象:训练阶段 PSNR 正常,但把模型用在一批新的测试数据上,重建质量比训练时掉 5 到 10 dB,像是模型忽然失效。

原因:测量矩阵和训练时的数据分布不匹配。深度重建网络本质上把测量矩阵的具体形式也学进去了,如果测试时的矩阵分布不同(例如随机种子不同导致具体数值不同、采样率不同、或矩阵生成方式改变),生成器的输入分布与训练分布完全不同,输出自然崩盘。

解决方法:把测量矩阵作为模型的一部分保存。用 register_buffer 注册的 Phi 会随模型参数一起保存和加载,保证测试时使用同一矩阵。如果确实希望泛化到不同矩阵,就得在训练阶段动态生成矩阵,并保证每次训练数据经过的矩阵都重新采样,增加泛化性。但会牺牲一些在固定矩阵上的最佳性能。

5.4 感知损失报错或者训练显存溢出

现象:程序在计算 VGG 感知损失时报显存不足,或是运行到第几个 epoch 后 CUDA 内存持续增长直到溢出。

原因:感知损失用到的主干网络(一般是 VGG16 的某几个层)没有处于 eval 状态,导致网络内的 BatchNorm 参数也在训练中被更新且不断累积计算图;同时如果把整张图不裁剪送进 VGG 提取特征,中间层特征图的尺寸会很大,花费大量显存。

解决方法:把感知损失的主干网络冻结并设置成 eval 模式,在计算损失时使用 torch.no_grad() 包裹特征提取部分。具体实现里把 VGG 网络的 requires_grad 全部置为 False,并调用 eval() 方法;输入给 VGG 的 patch 尺寸最好在 64 到 128 之间,过大的图可以先裁剪或缩放后再提取特征。

5.5 生成器和判别器训练不平衡,loss 一高一低震荡

现象:训练日志里生成器 loss 和判别器 loss 像跷跷板,一个升另一个就降,但重建图像质量没有实质提升,生成器偶尔输出完全相同的两张图。

原因:这是对抗训练典型的模式崩溃前兆,出现频率不低。根本原因是判别器训练步数与生成器不匹配,或者两者学习率比例失衡。判别器太强,很快学会区分真假,生成器梯度方向失去指导意义;判别器太弱,又无法给生成器足够的约束。

解决方法:回退到更稳定的训练配置。把判别器的学习率降到生成器的三分之一,同时把判别器的更新频率降为每训练两次生成器更新一次。如果问题依然存在,优先检查损失权重中的对抗权重,通常下调对抗权重能暂时稳住训练;等生成器重建质量回升后,再逐步加回对抗损失权重。

6. 重建质量的验证习惯:PSNR、LPIPS 之外还要盯住的那张测试图

客观指标在压缩感知重建里并不够用,所以我的验证流程固定为两套并跑。第一套是数值指标,PSNR 和 SSIM;第二套是感知质量指标 LPIPS,用预训练网络提取深度特征后对比两张图的特征距离。LPIPS 对纹理与感知细节更敏感,是评估对抗网络重建结果的可靠指标。

后续跑测试时,在测试集里固定三张图,一张是有大量规则纹理的建筑外墙,一张是包含平滑渐变的人像皮肤,一张是树叶或草地这类高随机纹理。用固定的原图、固定的测量矩阵与随机种子,跑完测试后把输出图直接排在一起看。只看 PSNR 数值会上当:某些模型在 PSNR 上高 0.5 dB,纹理却明显更糊;但 LPIPS 和主观视觉能直接揭穿这个问题。我就在这个环节被坑过多次,所以现在把它写成固定检查项,而不是等业务方反馈后才补测。

训练过程中的监控同样重要。每 10 个 epoch 把重建结果保存成图片,对比训练早期和中后期的纹理变化。如果 PSNR 涨但 LPIPS 不降反升,说明模型正在往过度平滑方向优化,此时直接把对抗损失权重调高,而不是等着训练结束再看。这个习惯让我的重建模型在采样率 10% 到 20% 之间的表现稳定了很多,省去了不少反复重训的精力。

最后补充一个很实用的经验:多尺度生成对抗网络的性能并不是采样率越低越好。在采样率低于 5% 时,测量信息本身严重不足,对抗损失会把纹理往"看起来真实但不忠于原图"的方向拉,造成幻觉纹理。所以设计源码时,在生成器尾部加一个可选的 fidelity 约束分支:重建输出经过相同的测量矩阵后与实际测量值计算误差,如果误差超过阈值就加强 L1 损失权重。这个技巧能有效抑制低采样率下的幻觉纹理,让生成对抗网络既保留纹理能力又不至于放飞。希望这组方法和排查顺序能真正帮到你,至少在踩坑时有个可对照的方案。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询