
简介这套源码是吉林大学学报工学版相关论文的配套实现面向图像处理、深度学习和信号处理领域的研究者使用多尺度生成对抗网络完成图像压缩感知重建可在远低于奈奎斯特采样定律要求的条件下从稀疏观测中恢复高质量图像并重点保护边缘与纹理细节。压缩包共二十八个文件包含 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, seed0): 生成固定高斯测量矩阵 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_ch3, patch_size64, sampling_rate0.1, base_dim64): 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, padding1), nn.ReLU(inplaceTrue) ) self.down1 nn.Sequential( nn.Conv2d(base_dim, base_dim * 2, 4, stride2, padding1), nn.ReLU(inplaceTrue) ) self.down2 nn.Sequential( nn.Conv2d(base_dim * 2, base_dim * 4, 4, stride2, padding1), nn.ReLU(inplaceTrue) ) # 多尺度残差块三条不同膨胀率的分支 self.ms_res MSResBlock(base_dim * 4, base_dim * 4) self.up1 nn.Sequential( nn.Upsample(scale_factor2, modebilinear, align_cornersFalse), nn.Conv2d(base_dim * 4, base_dim * 2, 3, padding1), nn.ReLU(inplaceTrue) ) self.up2 nn.Sequential( nn.Upsample(scale_factor2, modebilinear, align_cornersFalse), nn.Conv2d(base_dim * 2, base_dim, 3, padding1), nn.ReLU(inplaceTrue) ) self.tail nn.Conv2d(base_dim, in_ch, 3, padding1) 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_ch3, base_dim64): super().__init__() self.net nn.Sequential( nn.Conv2d(in_ch, base_dim, 4, stride2, padding1), nn.LeakyReLU(0.2, inplaceTrue), nn.Conv2d(base_dim, base_dim * 2, 4, stride2, padding1), nn.BatchNorm2d(base_dim * 2), nn.LeakyReLU(0.2, inplaceTrue), nn.Conv2d(base_dim * 2, base_dim * 4, 4, stride2, padding1), nn.BatchNorm2d(base_dim * 4), nn.LeakyReLU(0.2, inplaceTrue), nn.Conv2d(base_dim * 4, 1, 4, padding1) ) 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_weight10.0, adv_weight0.1, vgg_weight0.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.5beta2 取 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 损失权重。这个技巧能有效抑制低采样率下的幻觉纹理让生成对抗网络既保留纹理能力又不至于放飞。希望这组方法和排查顺序能真正帮到你至少在踩坑时有个可对照的方案。本文还有配套的精品资源点击获取