☰
深度多尺度生成对抗网络:图像压缩感知重建实战指南
2026/10/1 5:30:36 网站建设 项目流程

简介:本资源为吉林大学学报(工学版)投稿论文《深度多尺度生成对抗网络下图像压缩感知重建》的配套源码,面向图像处理、信号处理方向的研究生与算法开发者,帮助理解并复现基于GAN的压缩感知重建流程。压缩包共28个文件、约151KB,以8个Python源文件为核心,涵盖模型构建、训练、数据处理与性能评估等模块,另含6张JPEG图片作为训练验证数据、3个CSV记录训练与测试指标、4个pyc编译文件及LICENSE授权协议和readme说明文档,目录按data、train、valid、test等分层组织,结构清晰。已有324人学习下载。读者可据此掌握多尺度生成器与判别器的对抗训练思路,参考SSIM等定量指标评估重建质量,并在此基础上开展图像超分辨率、图像修复等方向的改进与创新。

1. 从一张 256×256 的模糊图说起:深度多尺度生成对抗网络到底在重建什么

一张 256×256 的灰度图,如果按 25% 的采样率做压缩感知观测,你手里只剩 16384 个测量值,原始像素是 65536 个。传统做法是拿 TV 正则或者 BM3D 去迭代求解,跑几十秒能出一张边缘发糊、纹理被抹平的结果。换成基于深度多尺度生成对抗网络的图像压缩感知重建,同样的测量值,前向推理几百毫秒,纹理和边缘能拉回来一大截。这就是这个方向真正吸引人的地方:它不是把重建当纯优化问题,而是把「图像长什么样」这个先验塞进了一个多尺度生成器里,再用判别器逼着输出往真实图像分布上靠。

这套方案适合谁?如果你在做图像压缩感知重建、稀疏视角 CT、单像素成像,或者任何「观测少、要还原」的任务,并且手里有 GPU、有配对或非配对的训练数据,那它值得投入。源码层面,这类项目通常包含生成器、判别器、多尺度重建模块、训练脚本和推理脚本五块,核心难点不在网络堆叠,而在多尺度特征怎么融合、对抗损失和重建损失怎么配比、采样矩阵怎么进网络。下面按「原理选型 → 网络与数据 → 训练与调参 → 避坑 → 进阶验证」的顺序,把能复现的细节讲透。

2. 多尺度生成对抗重建的原理与选型:为什么不是单纯堆一个 U-Net

2.1 压缩感知重建的三种技术路线对比

在动手写代码前,先想清楚你站在哪条路线上。压缩感知重建大致分三类:传统迭代优化、纯 CNN 回归、生成对抗重建。它们的差别直接决定你后面调参的方向。

路线代表思路重建质量推理速度对训练数据依赖典型问题
传统迭代TV、BM3D、D-AMP中等,纹理丢失慢,秒级到分钟级无参数难调,块效应
纯 CNN 回归ReconNet、ISTA-Net较好,偏平滑快,毫秒级中结果过度平滑
生成对抗重建多尺度 GAN纹理锐利快,百毫秒级高训练不稳,易伪影

选多尺度 GAN 的理由很直接:压缩感知的逆问题是不适定的,同一个测量向量对应无穷多张可能的原图。纯回归网络学的是条件均值,输出必然偏糊;GAN 通过判别器引入分布约束,能把解拉向真实图像流形,纹理细节就回来了。而「多尺度」是为了解决单一尺度 GAN 在低采样率下高频信息丢失的问题——粗尺度先恢复结构,细尺度再补纹理。

提示:如果你的采样率高于 40%,纯 CNN 回归可能已经够用,上 GAN 反而增加训练成本。采样率低于 25% 时,多尺度 GAN 的优势才明显。

2.2 多尺度生成器的结构设计:粗到细的三级重建

多尺度生成器的常见做法是三级金字塔:先把测量值 y 通过一个线性映射升维成初始重建,再在 1/4、1/2、全分辨率三个尺度上逐级细化。每一级接收上一级的上采样结果和原始测量值的重新投影,输出该尺度的残差。

import torch import torch.nn as nn class MultiScaleGenerator(nn.Module): def __init__(self, measure_dim=16384, base_ch=64): super().__init__() # 测量值到初始特征图的线性映射,输出 1/4 分辨率 self.init_fc = nn.Linear(measure_dim, 64 * 64 * base_ch) self.base_ch = base_ch # 三个尺度的残差细化块,每个尺度两层卷积 self.refine_1 = nn.Sequential( nn.Conv2d(base_ch, base_ch, 3, padding=1), nn.ReLU(inplace=True), nn.Conv2d(base_ch, base_ch, 3, padding=1) ) self.refine_2 = nn.Sequential( nn.Conv2d(base_ch, base_ch, 3, padding=1), nn.ReLU(inplace=True), nn.Conv2d(base_ch, base_ch, 3, padding=1) ) self.refine_3 = nn.Sequential( nn.Conv2d(base_ch, base_ch, 3, padding=1), nn.ReLU(inplace=True), nn.Conv2d(base_ch, 1, 3, padding=1) ) self.up = nn.Upsample(scale_factor=2, mode='bilinear', align_corners=False) def forward(self, y): # y: [B, measure_dim] x = self.init_fc(y).view(-1, self.base_ch, 64, 64) # 1/4 尺度 x = x + self.refine_1(x) # 粗尺度残差 x = self.up(x) # 到 1/2 尺度 x = x + self.refine_2(x) # 中尺度残差 x = self.up(x) # 到全分辨率 x = self.refine_3(x) # 细尺度输出单通道 return x

这段代码里,init_fc把 16384 维测量值映射成 64×64×64 的特征图,对应 1/4 分辨率。三个refine块都是残差形式,x = x + refine(x)保证梯度能直通,避免深层退化。base_ch默认 64,显存紧张可以降到 32,但重建质量会掉约 0.5 dB PSNR。上采样用双线性而不是转置卷积,是为了减少棋盘伪影——这是血泪经验,转置卷积在低采样率下特别容易出网格状伪影。

2.3 判别器与损失函数:对抗损失和重建损失怎么配

判别器不需要太深,PatchGAN 就够用,它判断的是局部 patch 的真假,对纹理细节更敏感。损失函数是这套方案的核心调参点:重建损失(MSE 或 L1)保证像素级接近,对抗损失保证分布接近,感知损失可选。

class PatchDiscriminator(nn.Module): def __init__(self, in_ch=1, base_ch=64): super().__init__() self.net = nn.Sequential( nn.Conv2d(in_ch, base_ch, 4, 2, 1), nn.LeakyReLU(0.2, True), nn.Conv2d(base_ch, base_ch * 2, 4, 2, 1), nn.BatchNorm2d(base_ch * 2), nn.LeakyReLU(0.2, True), nn.Conv2d(base_ch * 2, base_ch * 4, 4, 2, 1), nn.BatchNorm2d(base_ch * 4), nn.LeakyReLU(0.2, True), nn.Conv2d(base_ch * 4, 1, 4, 1, 1) # 输出 patch 级真假图 ) def forward(self, x): return self.net(x) # 损失配比:重建损失权重 1.0,对抗损失权重 0.01,感知损失权重 0.1 def compute_loss(fake, real, disc, criterion_pixel, criterion_adv): loss_pixel = criterion_pixel(fake, real) # L1 重建损失 loss_adv = criterion_adv(disc(fake), torch.ones_like(disc(fake))) return loss_pixel + 0.01 * loss_adv

对抗损失权重设 0.01 是常见起点。设大了(比如 0.1)训练会崩,生成器只顾骗判别器,PSNR 掉得厉害;设小了(0.001)纹理恢复不明显,跟纯 CNN 差不多。感知损失用 VGG 特征算,权重 0.1 左右,能提升视觉质量但对 PSNR 帮助有限。我一般先用纯 L1 跑通,确认重建正常后再加对抗损失,这样出问题好定位。

3. 从测量值到重建图:数据管线、采样矩阵与训练脚本落地

3.1 压缩感知采样矩阵的生成与加载

压缩感知的观测过程是 y = Φx,Φ 是 M×N 的采样矩阵,M 是测量数,N 是像素数。常见做法是用高斯随机矩阵或部分哈达玛矩阵。高斯矩阵重建质量好但存储大,哈达玛矩阵快但需要 N 是 2 的幂。

import numpy as np def build_sampling_matrix(n=65536, m=16384, mode='gaussian', seed=42): rng = np.random.RandomState(seed) if mode == 'gaussian': phi = rng.randn(m, n) / np.sqrt(m) # 归一化,保证能量一致 elif mode == 'hadamard': from scipy.linalg import hadamard H = hadamard(n) / np.sqrt(n) idx = rng.choice(n, m, replace=False) phi = H[idx, :] return phi.astype(np.float32) # 观测:y = Phi @ x,x 是展平的图像 phi = build_sampling_matrix() x = np.random.randn(65536).astype(np.float32) y = phi @ x # 得到 16384 维测量值

seed固定是为了训练和测试用同一个采样矩阵,否则重建结果没法对比。/np.sqrt(m)归一化很关键,不归一化的话测量值量级随 M 变化,网络输入分布不稳定,训练容易发散。采样矩阵在训练前生成一次存成.npy,训练时直接加载,不要每个 batch 重新生成,否则随机性会干扰收敛。

3.2 数据加载与多尺度标签准备

训练数据用 BSDS500、DIV2K 或你自己的图像集都行。关键是每个样本要同时提供测量值 y 和多尺度标签。粗尺度标签用下采样得到,保证每个尺度都有监督信号。

from torch.utils.data import Dataset import torch.nn.functional as F class CSDataset(Dataset): def __init__(self, img_paths, phi, img_size=256): self.paths = img_paths self.phi = torch.from_numpy(phi) # [M, N] self.img_size = img_size def __len__(self): return len(self.paths) def __getitem__(self, idx): import cv2 img = cv2.imread(self.paths[idx], cv2.IMREAD_GRAYSCALE) img = cv2.resize(img, (self.img_size, self.img_size)).astype(np.float32) / 255.0 x = torch.from_numpy(img).view(-1) # [N] y = self.phi @ x # [M] 测量值 # 多尺度标签:1/4、1/2、全分辨率 x_quarter = F.interpolate(img[None, None], scale_factor=0.25, mode='bilinear')[0, 0] x_half = F.interpolate(img[None, None], scale_factor=0.5, mode='bilinear')[0, 0] return y, x, x_quarter, x_half

这里返回三个尺度的标签,训练时每个尺度的输出都算损失,粗尺度损失权重可以设小一点(0.5),细尺度设 1.0。img_size固定 256 是为了和采样矩阵维度对齐,如果你的图像尺寸不固定,要么统一 resize,要么用块状压缩感知分块处理。

3.3 训练循环与关键超参设置

训练循环里最容易翻车的是判别器和生成器的更新节奏。常见做法是判别器更新一次、生成器更新一次,但判别器太强时生成器梯度会消失。

import torch.optim as optim device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') G = MultiScaleGenerator().to(device) D = PatchDiscriminator().to(device) opt_G = optim.Adam(G.parameters(), lr=1e-4, betas=(0.5, 0.999)) opt_D = optim.Adam(D.parameters(), lr=1e-4, betas=(0.5, 0.999)) criterion_pixel = nn.L1Loss() criterion_adv = nn.BCEWithLogitsLoss() for epoch in range(200): for y, x, x_q, x_h in dataloader: y, x = y.to(device), x.to(device) # ---- 更新判别器 ---- fake = G(y).detach() real_img = x.view(-1, 1, 256, 256) loss_D = 0.5 * (criterion_adv(D(real_img), torch.ones_like(D(real_img))) + criterion_adv(D(fake), torch.zeros_like(D(fake)))) opt_D.zero_grad(); loss_D.backward(); opt_D.step() # ---- 更新生成器 ---- fake = G(y) loss_G = criterion_pixel(fake, real_img) + 0.01 * criterion_adv(D(fake), torch.ones_like(D(fake))) opt_G.zero_grad(); loss_G.backward(); opt_G.step()

学习率 1e-4、betas=(0.5, 0.999) 是 GAN 训练的经典配置,betas 的 0.5 比默认 0.9 更稳。判别器更新时对 fake 用.detach()切断梯度,这是必须的,忘了会导致生成器梯度被污染。如果训练中 loss_D 迅速降到 0,说明判别器太强,把它的学习率降到 5e-5 或者给判别器输入加噪声。如果 loss_G 震荡不降,先检查重建损失是不是在降,重建损失降了对抗损失震荡是正常的。

4. 训练不收敛、重建出伪影:多尺度 GAN 的 5 个高频坑

4.1 坑一:生成器输出全灰或全黑

现象:训练几个 epoch 后,生成器输出一张接近常数的灰图,PSNR 卡在 12 dB 左右不动。

原因:初始化不当或学习率过大,生成器一开始就输出饱和,判别器轻松区分真假,梯度消失。

解决:把init_fc的权重用nn.init.kaiming_normal_初始化,学习率从 1e-4 降到 5e-5,先只用 L1 损失训练 20 个 epoch 让生成器有基本重建能力,再加对抗损失。

4.2 坑二:重建图出现棋盘格伪影

现象:全分辨率输出上有规律的网格状纹理,放大看像马赛克。

原因:上采样用了转置卷积,或者多尺度融合时上采样和下采样不对齐。

解决:上采样统一用nn.Upsample(mode='bilinear', align_corners=False),不要用ConvTranspose2d。如果已经用了转置卷积,在它后面接一个 3×3 卷积平滑。多尺度融合时确保每一级的上采样倍数是整数倍,1/4 到 1/2 是 2 倍,1/2 到全分辨率是 2 倍,不要跨尺度跳跃。

4.3 坑三:判别器 loss 变成 0,生成器不再进步

现象:训练日志里 loss_D 很快降到 0.001 以下,loss_G 的对抗部分变成常数,重建质量停滞。

原因:判别器太强,生成器梯度消失。这是 GAN 训练的经典问题,在多尺度结构里因为粗尺度信息简单,判别器更容易赢。

解决:给判别器加谱归一化(nn.utils.spectral_norm),或者把判别器学习率降到生成器的 1/2。还可以用标签平滑,把真实样本的标签从 1.0 改成 0.9,让判别器不那么自信。

4.4 坑四:不同采样率下重建质量断崖式下跌

现象:25% 采样率下 PSNR 有 32 dB,降到 10% 直接掉到 22 dB,纹理全丢。

原因:测量值维度变化后,init_fc的输入维度没跟着改,或者网络容量对低采样率不够。

解决:采样率变化时重新生成采样矩阵并调整init_fc的输入维度。低采样率(<15%)下把base_ch从 64 加到 96,多尺度级数从 3 级加到 4 级,让粗尺度承担更多结构恢复任务。另外低采样率下对抗损失权重可以适当加大到 0.02,逼生成器补纹理。

4.5 坑五:训练集 PSNR 很高,测试集一塌糊涂

现象:训练集重建 PSNR 38 dB,换一张没见过的图只有 25 dB,泛化差。

原因:过拟合,或者训练集和测试集的采样矩阵不一致。

解决:检查采样矩阵是不是训练测试共用的同一个.npy文件。数据增强用随机裁剪和水平翻转,不要用颜色抖动(灰度图没用)。如果数据量小于 1000 张,加 dropout 或者 weight decay 1e-5。判别器的感受野不要太大,PatchGAN 的 70×70 感受野比全图判别器更不容易过拟合。

5. 进阶:用 SSIM 和对抗损失曲线判断重建是否真的收敛

训练到后期,光看 PSNR 不够,PSNR 高不代表视觉质量好。我一般同时盯三个指标:PSNR、SSIM 和判别器的输出均值。PSNR 到 30 dB 以上后,SSIM 更能反映结构恢复情况。判别器对生成样本的输出均值如果在 0.4~0.6 之间,说明对抗训练处于平衡态;如果接近 0 或 1,说明一方压倒另一方,该调权重了。

from skimage.metrics import structural_similarity as ssim import numpy as np def evaluate(G, dataloader, device): G.eval() psnr_list, ssim_list = [], [] with torch.no_grad(): for y, x, _, _ in dataloader: y = y.to(device) fake = G(y).cpu().numpy() real = x.view(-1, 1, 256, 256).numpy() for i in range(fake.shape[0]): f = np.clip(fake[i, 0], 0, 1) r = real[i, 0] mse = np.mean((f - r) ** 2) psnr_list.append(10 * np.log10(1.0 / (mse + 1e-8))) ssim_list.append(ssim(r, f, data_range=1.0)) return np.mean(psnr_list), np.mean(ssim_list)

这段评估代码里,np.clip把输出截到 [0,1] 是必须的,生成器可能输出超范围值,不截断算 PSNR 会偏。data_range=1.0对应归一化后的图像,如果你的图是 0~255 就改成 255。SSIM 的win_size默认 7,图像小于 7×7 会报错,256×256 没问题。

一个具体技巧:训练时每 10 个 epoch 存一次生成器的输出图,拼成网格看。有时候 PSNR 在涨但视觉上出现了局部伪影,网格图一眼就能看出来。我习惯把粗尺度、中尺度、细尺度的输出分别存,如果粗尺度结构就歪了,后面细尺度再补也没用,得回去查init_fc和采样矩阵。

最后说个我踩过的坑:多尺度 GAN 的源码里,很多人把三个尺度的损失直接相加,但粗尺度的 L1 损失量级比细尺度大(因为粗尺度像素少但误差被平均了),导致细尺度学不动。正确做法是每个尺度的损失除以该尺度的像素数,或者给粗尺度损失乘 0.5 的权重。这个细节不改,训练 100 个 epoch 细尺度纹理也出不来。希望帮到你。

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

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

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

立即咨询