☰
基于Vision Transformer的图像去雾:Uformer架构与NH-HAZE训练实践
2026/10/26 16:07:07 网站建设 项目流程

简介:这份资源面向从事图像去雾、低层视觉或Transformer模型应用研究的开发者与学生,以Vision Transformer为核心,提供一套完整的图像去雾算法实现流程。资源包共338个文件,压缩后约156.36MB,主体由204个Python脚本构成,覆盖数据切分、训练、推理等模块,并搭配16个YAML配置、12个CSV训练数据、9个Jupyter Notebook演示及图片、动图、说明文档等辅助材料,目录结构清晰,便于按模块定位代码与实验记录。已有291人学习浏览,适合具备Python与深度学习基础、希望快速搭建去雾模型实验环境的进阶用户。通过阅读源码和文档说明,可了解从NH-HAZE等数据集预处理、训练超参数设定到模型评测的完整思路,并基于现有工程进行结构改进或超参数调优。

1. 基于Vision Transformer的图像去雾:从NH-HAZE数据到Uformer训练

拿到“基于Vision Transformer的图像去雾算法研究与实现源码+文档说明”这份资源,多数人是冲着“高分项目”来的,但真正值得看的是里面从数据预处理到270轮训练再到loss landscape分析的一整条链路。这个项目不是玩具demo:它用Uformer这种按Transformer思路做图像复原的U型网络,在NH-HAZE真实雾天数据集上训练,输入patch为128×128,轮数开到270,还附带了CIFAR-100上ViT-Ti与ResNet-50的损失地形对比CSV。适合两类人:一是要做图像去雾相关毕设或小论文的,二是想把深度学习算法从“调包”推进到“调参数”的。如果你只想快速出一张效果图,这份资源偏重;但如果你想复现一套能写进论文的实验体系,它比多数README详尽得多。

2. 为什么是Transformer:Uformer架构与去雾任务的选型逻辑

2.1 雾图成像模型与去雾任务的病态性

去雾在底层视觉里属于图像复原(restoration)任务,核心是求解雾天成像模型:

I(x) = J(x)t(x) + A(1 − t(x))

其中I是观测到的雾图,J是清晰场景辐射,t是透射率(范围0到1,越接近1说明越清晰),A是全局大气光。这个公式本身不复杂,难在它是一个病态方程:一个像素点,三个未知量J、t、A,没有任何附加约束是解不出来的。传统方法像暗通道先验(DCP)假设无雾区域局部至少一个通道亮度接近0,用统计先验把t估计出来;后来的CNN方案如DehazeNet、AOD-Net则直接端到端回归。

但真到NH-HAZE这种真实雾图场景,DCP会在天空区域翻车,因为天空不满足“暗通道为0”的假设。我自己复现过AOD-Net,发现卷积的感受野限制导致它对全局大气光A的估计总是偏:远处雾浓的地方暗部细节保得住,天空亮度却容易过曝。说到底,问题出在卷积只能在局部范围内建立特征交互,全局信息要靠层数硬堆。Transformer解决这个问题的思路很直接:自注意力把每个像素和图上所有其他像素的相似度都算一遍,全局上下文是显式的而非隐式的。透射率t的估计本质上需要“看远处”,因为雾浓度在空间上是平滑变化的,远处像素的信息对当前点透射率估计有直接帮助。这就是在去雾任务里选Transformer路线的第一层理由。

2.2 Uformer的LeWin块与U型结构,以及它和原始ViT的差别

如果你直接把ViT-Ti搬到去雾任务上,patch=16意味着一个token对应16×16的区域,复原任务需要像素级细节,这种粗粒度会在输出端产生明显的块状伪影。Uformer没有这样做。它保留了ViT最核心的自注意力机制,但做了三个关键改动,这三点也是理解这份源码中Uformer实现的关键。

第一是U型编解码结构。编码器逐步下采样得到多尺度特征,解码器逐步上采样恢复分辨率,中间用类似UNet的跳跃连接把编码器特征拼到解码器上。去雾任务里多尺度信息很重要:大雾区域依赖全局语义判断,边缘纹理依赖高分辨率特征,U型结构让这两类信息在训练中自然融合。第二是窗口自注意力。全局自注意力在256×256输入下会产生65536个token,两两计算复杂度是O(n²),显存直接爆掉。Uformer在特征图上按窗口划分,窗口内做自注意力,通过移位窗口实现跨窗交互,思路类似Swin Transformer,但针对复原任务做了适配:窗口大小固定,注意力头数控制在4到8之间,前馈层改成卷积而不是MLP。LeWin名字里的“Locally-enhanced”说的就是这一步——注意力擅长建模长程依赖,但会损失局部性,卷积前馈把丢失的细节补回来。第三是深度监督。常见做法是在解码器多个尺度上分别计算输出,和ground truth算L1或Charbonnier损失。My_train.py里如果没开多尺度监督,那至少也应有基本L1损失,你训练前先看一眼loss定义那一行,这个选择对收敛速度影响很大。

2.3 项目里的CIFAR对比CSV:ViT-Ti和ResNet-50的损失地形在说什么

这份资源里有一组看似和去雾无关的文件:cifar100_resnet_dnn_50_losslandscape.csv、cifar100_vit_ti_losslandscape.csv、cifar100_vit_ti_9857b21357_x1_losslandscape.csv,还有cifar10_alexnet_dnn_corrupted.csv和cifar100_alexnet_dnn_corrupted.csv。如果你只盯着去雾训练,这组文件确实鸡肋;但放到“算法研究与实现”这个项目语境里,它们是作者为了论证“Transformer在视觉任务上的优势”而补的对比实验。

loss landscape(损失地形)的做法来自2018年那篇Visualizing the Loss Landscape of Neural Nets:训练完模型w后,随机生成两个与w同维度的方向向量d1、d2,按w + α·d1 + β·d2在二维平面上采样,记录每个(α,β)对应的loss,画成曲面图。平坦的地形意味着模型对权重扰动不敏感,泛化边界更宽;尖峭的地形则和过拟合强相关。CIFAR-100上ViT-Ti的地形通常比同量级的ResNet-50更平滑,这个对比成了作者支撑“Transformer泛化性优于CNN”的证据。9857b21357_x1这个文件名里的哈希串,是某个固定初始化或随机种子的标识,x1我理解是方向向量归一化系数取1的标记。后面第6章我会讲到怎么把这组CSV画回曲面图。

3. 数据预处理:把NH-HAZE原图切分并生成128分辨率训练patch

3.1 数据集目录与路径规划

资源文档里写明了数据集位置:/home/dell/桌面/TPAMI2022/Dehazing/#dataset/NH_haze/。注意这个路径里的“#”号不是笔误,是原作者目录命名的一部分。NH-HAZE数据集结构比较直观,train和test两个大文件夹,里面按场景组织,每张雾图对应一张清晰图。拿到手别急着跑脚本,我一般先做两步:第一步,用du检查train目录总大小。NH-HAZE原始图像是几千万像素的大图,patch切分会把数据量放大数倍,确保磁盘剩余空间至少是数据集的两倍。第二步,在项目根目录下建一个datasets目录,用软链接而不是复制:

mkdir -p /home/dell/桌面/2022毕业设计/Datasets/NH-HAZE ln -s /home/dell/桌面/TPAMI2022/Dehazing/#dataset/NH_haze/train /home/dell/桌面/2022毕业设计/Datasets/NH-HAZE/train_patches_src

这段bash的意思是:把原始train目录用软链接挂到项目期望的位置,而不必把几十GB文件复制一遍。软链接的好处是脚本里的绝对路径不变,磁盘占用为零,换机器时只需要重建链接关系。注意ln -s第一个参数是源路径,第二个是目标路径,写反了会生成断链,这是我在多台机器上踩过的低级坑。

3.2 generate_patches_SIDD.py 的运行与参数解析

patch生成脚本叫generate_patches_SIDD.py,名字里的SIDD虽然是智能手机图像去噪数据集,但脚本逻辑是通用的:遍历src_dir下所有图像,按固定尺寸切分,输出到tar_dir。摘要里给的生产命令是:

python3 generate_patches_SIDD.py --src_dir /home/dell/桌面/TPAMI2022/Dehazing/#dataset/NH_haze/train --tar_dir /home/dell/桌面/2022毕业设计/Datasets/NH-HAZE/train_patches

这个命令做了两件事:--src_dir指定原始雾图所在train目录,--tar_dir指定切分后的patch输出目录。脚本内部常见实现是遍历所有子目录,每张图先转成RGB数组,再按patch_size和stride做滑窗切分。按摘要里“切成256×256小图”的描述,patch_size应为256,stride如果默认等于patch_size就是无重叠切分。全量跑之前,我习惯先用小数据集试跑:

python3 generate_patches_SIDD.py --src_dir ./sample_test --tar_dir ./sample_patches --patch_size 256 --stride 128

这段命令对参数做了减法:patch_size控制切出来小图的边长,stride控制滑窗步长。当stride大于patch_size时相邻patch有间隙,数据量减少;当stride小于patch_size时patch有重叠,数据量成倍扩大。去雾任务里我建议stride取patch_size的一半,也就是256切分、128步长,一张大图能产出近万个patch,数据增强效果显著。切patch这个动作本质上是为训练做数据增强:同一张雾图的不同区域,雾浓度差异很大,近处薄雾、远处浓雾、天空全雾,切成小patch后每个样本变成一个局部区域,模型从patch分布里学习不同雾浓度的统计特性,而不是被迫从整张图里记忆固定布局。

3.3 train_ps=128:训练输入与切分尺寸的差异

这里有个容易让新手疑惑的点:预处理切成256×256,但训练命令里--train_ps是128,这俩不是冲突,是两阶段设计。训练脚本My_train.py加载patch后,会再做一次随机裁剪,把256的patch裁到train_ps指定尺寸。train_ps=128意味着实际输入网络的分辨率是128×128×3。这么设计有三个好处:第一,随机裁剪等价于在线数据增强,模型每次看到的截取位置都不一样;第二,128是窗口注意力的友好尺寸,Uformer输入被划分成8×8或16×16的窗口,128恰好能被整除,避免padding带来的边界计算浪费;第三,显存占用和分辨率平方挂钩,128比256的占用只有四分之一。

如果想把train_ps改大,比如256,需要同步调整batch_size。一块24GB显存的GPU上,batch_size=32配train_ps=128能跑,改成256后batch_size通常要降到4到6。这里没有标准答案,资源给定的是精度和显存平衡好的默认值,先按默认跑通再改。另外,预处理切分尺寸和训练输入尺寸不一致时,一定要注意generate_patches脚本输出的目录名和My_train.py里读取的目录名是否匹配,很多人第一次跑报“找不到图片”就是因为两边路径前缀不一致。

4. 训练主流程:My_train.py的270轮设计与参数实测

4.1 训练命令与参数速查

训练是单条命令启动的:

python3 ./My_train.py --arch Uformer --nepoch 270 --batch_size 32 --env My_Infor_CR --gpu '1' --train_ps 128 --train_dir /media/dell/fd6f6662-7e3...

这一行里每个参数都值得拆开看:

参数值含义与注意事项
--archUformer模型架构选择;Uformer的模型导入通常在model模块里
--nepoch270总训练轮数;资源作者的默认收敛值
--batch_size32单步样本数;显存不足时优先降到16或8
--envMy_Infor_CR实验环境名,作为日志/checkpoint前缀
--gpu'1'指定第二张GPU;单卡机器必须改成'0'
--train_ps128训练输入分辨率,和预处理切分尺寸无关
--train_dir/media/dell/fd6f6662-7e3...训练patch目录的绝对路径,必须改成本机路径

270轮不是随便拍的。复原任务里Uformer的典型训练曲线在200轮后才进入平台期,270轮能保证loss和PSNR都收敛完。实测下来,在V100或3090上跑完这270轮大约需要两到三天,消费级卡时间还会拉长。如果你跑不了这么长时间,可以先降到50轮验证链路通不通,再决定是否开完整训练。

4.2 batch_size、显存与训练时长的权衡

batch_size=32在分布式环境下很常见,但单卡跑的时候要先确认显存。128×128×3输入、Uformer的tiny配置下,batch_size=32大约吃掉12到16GB显存,如果显卡只有8GB,第一反应不该是换模型,而是把batch_size降到8或16,同时把学习率按比例调小。PyTorch里动batch_size不是改个数字那么简单,它和优化器行为绑定。常见做法是batch_size减半、学习率也减半,否则梯度噪声变大,损失曲线会非常毛糙。270轮里如果发现验证PSNR较早就停滞,先别急着加轮数,看看是不是batch_size改了但学习率没动。

训练时长有个估算公式:单epoch时间 ≈ 总patch数 / (batch_size × 每秒迭代次数)。假设train_patches里有20万张patch,batch_size=32,每秒3个iteration,单epoch大约35分钟,270轮要160小时左右。资源作者给的270轮是个上限保险,实际训练我会加一个early stopping,验证PSNR连续20轮不涨就提前终止,能省三分之一时间。My_train.py如果没内置这个逻辑,你可以在每轮验证后自己判断,只保存历史最优权重。

4.3 loss landscape CSV是怎么被记录下来的

回到那串看起来像实验记录的文件名:cifar100_resnet_dnn_50_losslandscape.csv、cifar100_vit_ti_losslandscape.csv。这些不是训练过程中自动产出的,至少不是My_train.py的默认行为。常见做法是训练完成后另跑一个评估脚本,加载最优权重,沿两个随机方向做插值采样。采样核心逻辑如下:

import torch import numpy as np def calc_loss_landscape(model, loader, base_weight, direction1, direction2, steps=20): # 在二维平面上采样损失曲面,训练结束后做评估分析 surface = np.zeros((steps, steps)) alphas = np.linspace(-1, 1, steps) betas = np.linspace(-1, 1, steps) for i, alpha in enumerate(alphas): for j, beta in enumerate(betas): # 把两个方向向量按系数叠加到当前权重上 for (p, w) in zip(model.parameters(), base_weight): p.data = w + alpha * direction1[i] + beta * direction2[j] surface[i, j] = evaluate_loss(model, loader) return surface

这段代码的作用是:把训练好的模型参数当作基准点,沿着两个随机归一化方向扫描,每到一个(alpha, beta)坐标就算一次验证集loss,最后得到一张steps×steps的loss矩阵。direction1和direction2通常取与base_weight同shape的标准正态随机张量,再按范数归一化到和模型权重同一尺度。为什么要归一化?因为如果不做,方向向量的尺度会完全淹没权重的实际变化幅度,画出来的地形要么全平要么全尖,看不出任何信息。资源里那个9857b21357_x1文件名的x1,很可能就是归一化系数取1的标记。

5. 避坑与排查:从显存溢出到PSNR虚高的四个翻车记录

5.1 显存溢出:batch_size=32在消费级显卡上并不总能跑

现象:按README原命令跑My_train.py,刚开始迭代没几步就报torch.cuda.OutOfMemoryError,有时还伴随CUDA error: device-side assert triggered,程序直接崩掉。

原因:Uformer虽是窗口注意力设计,但编码器部分仍有多分辨率特征图同时驻留显存,batch_size=32在不同算力、不同显存的卡上占用差异很大。另外,如果预处理切分出来的patch分辨率不统一,比如某些图滑窗后产生了非128倍尺寸,数据加载器里的一次resize会把显存临时顶高。

解决:先降batch_size到16或8,不要动train_ps,128本身已经很小。改完batch_size后把学习率同步按0.5倍缩放。如果还爆,检查数据加载器的num_workers,过多worker会把数据复制到pin_memory,也占显存。还有一种容易被忽略的情况:训练循环里同时开了tensorboard日志和每轮验证评估,验证过程额外占显存,我的习惯是把验证频率从每1轮改成每5轮。

5.2 损失曲线迟迟不降:Transformer对学习率和热身期的敏感

现象:训练前100轮loss几乎是一条水平线,或者微降后又弹回原位,看起来模型压根没在学习。

原因:Uformer这类Transformer结构对学习率非常敏感,直接用一个固定学习率从头训很容易在初期震荡。另一个原因是Adam优化器在窗口注意力这种稀疏梯度分布下,如果不配warmup,梯度方差的初始估计会偏大,导致实际步长过小。

解决:在训练初始化阶段加一个线性warmup,前5个epoch把学习率从0升到目标值,之后再按余弦退火衰减。如果不想改训练脚本,就做小规模trial:--nepoch 5先跑一轮,观察loss是否在前几百步内有下降趋势,有就是正常的。注意这个测试必须开同样的batch_size,因为warmup时长和batch_size匹配,改了batch_size就得重新试。

5.3 PSNR虚高:定量指标好看,主观效果还是灰蒙蒙

现象:验证集PSNR刷到38dB以上,SSIM也不错,但把去雾结果图拿出来看,整体还有一层灰白色,尤其天空区域。

原因:PSNR对整体亮度偏移不敏感。模型可能在训练中学到“轻微提亮+保结构”的捷径解:它没有真正估计透射率t,而是把输出整体往亮处拉,暗部细节保住了,PSNR自然高,但雾的物理去除并没有完成。NH-HAZE测试集场景复杂,这种解很容易混过指标。

解决:看指标的同时,把原图、输出去雾图、ground truth并排可视化对比。更严格的做法是用NH-HAZE官方评估脚本,额外算ΔE色差和局部对比度。如果PSNR高但主观差,第二排查点是损失函数:只用L1 loss容易产生亮度偏置,建议给训练加感知损失或频率域损失。我的习惯是每周抽一批中间结果到固定目录肉眼过一遍,指标可以自动跑,主观质量必须人工判断。

5.4 路径硬编码:换台机器就“玄学报错”

现象:严格按文档跑,报FileNotFoundError,路径里拼出/home/dell、/media/dell这种明显带个人色彩的目录。

原因:这份资源的训练和预处理脚本里用了大量绝对路径,原作者机器上的目录结构和你本机完全不一致。文档里数据集路径是TPAMI2022/Dehazing/#dataset/NH_haze,训练路径是/media/dell/fd6f6662-7e3...,这些路径在作者机器上存在,在你机器上就是死路。

解决:拿到资源后第一件事不是解压跑训练,而是全局搜索“/home/dell”和“/media/dell”字符串,把出现位置全部替换成你的项目根目录。用sed批量替换效率高:

grep -rl "/home/dell\|/media/dell" . --include="*.py" | xargs sed -i 's|/home/dell/桌面|/data/dehazing|g'

这条命令做了两件事:先找出所有包含旧路径的Python文件,再把/home/dell/桌面前缀替换成/data/dehazing。替换完必须复查,因为有些路径写在配置json里而不仅是py文件里。彻底的做法是改成相对路径,把所有数据访问统一成基于项目根目录的os.path.join,这样换机器只需要改一个根目录变量。

6. 训练之外:用损失地形和corrupted数据验证模型质量

损失地形CSV读回到二维数组,画成曲面图,很快就能看出模型的泛化性格:

import numpy as np import matplotlib.pyplot as plt # 读取loss landscape CSV,网格是20x20,按行为alpha方向 surface = np.loadtxt('cifar100_vit_ti_losslandscape.csv', delimiter=',') fig = plt.figure(figsize=(8, 6)) ax = fig.add_subplot(111, projection='3d') X, Y = np.meshgrid(np.linspace(-1, 1, surface.shape[0]), np.linspace(-1, 1, surface.shape[1])) ax.plot_surface(X, Y, surface, cmap='viridis', alpha=0.8) ax.set_xlabel('alpha') ax.set_ylabel('beta') ax.set_zlabel('loss')

画出来的图里,ViT-Ti的曲面如果底部是缓坡,说明泛化边界宽;如果四周陡峭、底部窄,说明对权重扰动敏感、有过拟合倾向。地形平坦程度和测试集上PSNR的稳定性有相关性,这个观察可以直接写进实验分析章节。

corrupted数据那一组CSV,是作者在CIFAR-10-C和CIFAR-100-C上跑AlexNet的记录。你可以把CSV展开成一张“corruption类型 vs 准确率”的表格,看模型在噪声、模糊、天气扰动下的退化速度。这个测试和去雾任务其实是呼应的:去雾模型的编码器如果对corruption鲁棒,在真实复杂天气下更稳。把这套验证流程沉淀成习惯后,我现在拿到任何带训练源码的项目,都会强制走一遍:先搜硬编码路径,再用小epoch快跑验证链路通不通,最后才开完整训练。这让我躲过多次“跑了两晚才发现数据加载错位”的惨剧。希望帮到你。

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

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

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

立即咨询