
简介面向深度学习去噪研究的TEM-NLnet项目Demo定位为论文复现辅助资源适合需要理解或应用深度去噪网络的研究者与开发者。压缩包共11个文件以10个Python脚本为主另含1个Markdown说明文档整体仅14KB代码精简且结构清晰。脚本覆盖数据生成、预处理、模型构建、噪声学习与去噪推理等环节并包含utils与model基本模块配合README可快速掌握TEM-NLnet的设计思路与运行流程。已有135人下载学习适合正在开展图像去噪、信号恢复或时间序列分析等相关实验的读者参考。通过该Demo可系统梳理去噪网络的典型数据流水线、核心网络模块与训练策略便于在此基础上复现论文结果、调整参数或扩展自身实验场景。1. 这个项目是为了论文TEM-NLnet给低剂量电镜图像留细节的去噪网络做透射电镜图像去噪的人都卡在同一个问题上低剂量成像信噪比太低高剂量又会烧伤样品而且生物样品和纳米材料对电子束剂量特别敏感。传统去噪手法要么把噪声磨平要么把真实结构也当噪声一起抹掉。TEM-NLnet 不是一个花架子网络它把全图建模的思想用非局部机制落到电镜去噪上在抑制噪声的同时保住晶格条纹、纳米颗粒边界这些关键细节对论文里的定量分析格外有用。不管你是要复现论文做对比实验还是自己的数据集在 TEM 去噪上效果不理想这个方向都值得深挖一遍。2. 网络怎么拆非局部机制为什么适合 TEM 噪声2.1 先弄清 TEM 噪声是什么再谈网络结构很多人在 TEM 去噪上翻车是因为把噪声当成普通高斯白噪声去处理。TEM 图像的噪声来源很杂电子枪发射的电子到达探测器是一个随机计数过程低剂量时更接近泊松分布探测器读出过程叠加了高斯噪声样品本身在电子束下产生的漂移和充电效应又带来非平稳伪影。低剂量模式下泊松分量占主导换句话说噪声的方差和信号强度成正比——亮区噪声大暗区噪声小。而这种信号相关噪声用固定强度的高斯噪声模型去拟合天然就不准。我看过不少复现类似网络的代码训练时加的是固定 sigma 的高斯噪声拿到真实 TEM 数据上一测PSNR 看着还行但是结构细节全糊了。原因很简单网络学会了如何去掉均匀方差的噪声而真实数据的噪声在亮区和暗区强度不同网络把亮区的信号细节当成噪声抹掉了。所以做 TEM 去噪的网络输入层面就得考虑噪声强度估计或者在数据增广里混合泊松和高斯噪声否则模型在真实数据上的泛化能力会很有限。TEM-NLnet 的思路是先用一个浅层卷积模块提取局部特征然后把特征图交给非局部模块去建模全局依赖最后通过解码器逐步恢复分辨率。这个设计背后的逻辑是电镜图像里的结构往往在空间上是重复出现的比如周期性的晶格条纹、纳米颗粒的规则排列局部的卷积核感受野有限学习不到这种长距离依赖而非局部机制可以跨越整个空间范围寻找相似特征进行加权融合本质上是深度版本的近邻平均思想。2.2 从 UNet 骨架到非局部注意力组件之间怎么配合常见做法是网络主体采用 UNet 结构或者 ResNet 结构做编码解码。编码器下采样扩大感受野解码器上采样恢复细节跳跃连接把浅层纹理信息绕过后面的卷积送到深层。TEM-NLnet 选用 UNet 做骨架一个重要原因是电镜图像的分辨率普遍偏高一张 2048×2048 的 TEM 图直接送进全注意力网络显存会直接爆炸而 UNet 可以先把图像缩到合适尺寸再做全局建模显存压力小很多。非局部模块的实现并不复杂核心操作是计算特征图上任意两点之间的相关性用这个相关性做加权求和输出和输入保持同样的形状。它的本质可以用一个公式概括输出 注意力权重矩阵 × 输入特征。注意力权重来自输入特征自身的相似度矩阵经过 softmax 归一化。配合一个残差连接让网络可以在原始特征和非局部增强特征之间做权衡训练初期不会因为注意力权重不稳定而崩掉。网络的下采样阶段依次做两次步长为 2 的卷积把分辨率压缩到原来的四分之一在这个尺度上加非局部模块计算量是可控的。再下采样到八分之一后可能再加一个非局部模块作为深层全局建模。上采样用最近邻插值或者转置卷积恢复尺寸每个尺度都和编码器的对应层做拼接弥补下采样丢掉的空间细节。2.3 损失函数怎么定L2 与感知损失的配合要格外小心电镜去噪的损失函数选型直接决定了输出图像是锐利还是糊。常见的做法是 L2 损失均方误差因为它最大化 PSNR但是 L2 损失对高频细节非常宽容模型输出所有像素的均值解也能得到很小的 L2 误差代价是细节被平均掉了。所以在 TEM 去噪上我更建议 L1 损失为主、L2 损失为辅或者直接用 L1 加上一个小权重的频域损失比如让输出图像和干净图像在傅里叶域的振幅差异也进到损失里这样能强制模型保住晶格条纹对应的频率成分。还有一个容易踩的坑是感知损失。感知损失用预训练分类网络的特征匹配做监督对自然图像效果很好但是 TEM 图像的统计特性和自然图像相差太远预训练特征未必能捕捉到有用的结构信息有时反而会引入纹理伪影。我一般只在最后微调阶段加入感知损失权重设置在 0.01 到 0.05 之间训练初期不用它。3. 复现落地数据准备、训练脚本与推理入口3.1 标配目录结构与训练流程拿到一个论文相关项目包第一件事不是看代码而是理清目录和数据的组织方式。通常这类项目会包含训练脚本、网络结构定义、数据集构建和推理评估四个部分。如果包里的代码组织方式比较乱我建议自己建一个标准目录把数据、日志、权重分开后面做对比实验和调参会顺手很多。一个精简的目录结构如下TEM-NLnet/ ├── data/ │ ├── clean/ # 干净的高信噪比 TEM 图像 │ └── noisy/ # 对应的含噪图像真实采集或仿真生成 ├── logs/ # TensorBoard 日志 ├── checkpoints/ # 模型权重 ├── models/ │ └── nlnet.py # 网络结构定义 ├── datasets/ │ └── tem_dataset.py # 数据加载与增广 └── train.py # 训练入口上述目录结构是这类去噪项目最常见的组织方式分包里的代码未必完全一致但按这个思路整理过一遍后训练、评估、调参的入口就清晰了。如果包内已经自带了 README 或者 requirements 文件先把依赖装齐确认 PyTorch 版本和 CUDA 版本匹配再动训练。数据加载器的实现是第一个要留心的部分。TEM 图像通常是单通道灰度图直接读成 uint16 或者 uint32 的格式很常见因为电镜相机输出的是 16bit 甚至更高位深的原始数据。训练时统一把数据转成 float32做归一化到 [0,1] 范围再送进网络。如果数据本身就是 uint8 的导出图片精度会损失不少这一点对最终效果影响很大。3.2 训练脚本关键参数如何设置以下是一个基于 PyTorch 的训练脚本骨架展示了非局部去噪网络训练的关键步骤import torch import torch.nn as nn from torch.utils.data import DataLoader from datasets.tem_dataset import TEMDataset # 关键参数设置 batch_size 8 patch_size 128 # 训练时裁剪的方形块大小 init_lr 2e-4 # 初始学习率 num_epochs 200 mixed_noise True # 是否混合高斯泊松噪声做增广 train_dataset TEMDataset( clean_dirdata/clean, noisy_dirdata/noisy, patch_sizepatch_size, noise_typepoissongaussian if mixed_noise else gaussian, ) train_loader DataLoader( train_dataset, batch_sizebatch_size, shuffleTrue, num_workers4, pin_memoryTrue, ) # 损失L1 为主可选加频域损失 def loss_fn(pred, target): l1 nn.functional.l1_loss(pred, target) # 频域振幅一致性损失 pred_fft torch.fft.rfft2(pred) target_fft torch.fft.rfft2(target) freq_loss nn.functional.mse_loss( torch.abs(pred_fft), torch.abs(target_fft) ) return l1 0.05 * freq_loss # 训练循环简写 net NLNet(in_channels1, out_channels1) optimizer torch.optim.AdamW(net.parameters(), lrinit_lr, weight_decay1e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR( optimizer, T_maxnum_epochs, eta_min1e-6 ) for epoch in range(num_epochs): net.train() for noisy, clean in train_loader: noisy, clean noisy.cuda(), clean.cuda() pred net(noisy) loss loss_fn(pred, clean) optimizer.zero_grad() loss.backward() # 梯度裁剪非局部模块训练早期容易梯度爆炸 torch.nn.utils.clip_grad_norm_(net.parameters(), max_norm1.0) optimizer.step() scheduler.step()这个脚本的核心设计有两点一是损失函数里加了一个轻量级的频域振幅约束权重只给 0.05不至于喧宾夺主但能明显改善晶格条纹这类周期性结构的还原度二是梯度裁剪设置了 max_norm1.0非局部模块在训练初期相关性矩阵数值不稳定容易出现梯度暴涨这个裁剪能避免训练直接发散。学习率用余弦退火调度初始学习率 2e-4 适合大多数基于 UNet 骨架的去噪网络。如果你的显卡显存只有 8GB把 patch_size 降到 96batch_size 降到 4训练依然能收敛但要注意 patch_size 不能低于 64否则非局部模块能看到的空间范围太小全局建模的优势就发挥不出来了。3.3 推理与可视化评估训练收敛后推理阶段和训练阶段有一些细节不同。首先网络要切到 eval 模式把 dropout 和 batch norm 的统计量固定下来。其次输入图片不一定要整张塞进网络对于大尺寸 TEM 图可以先用带重叠的滑窗切块推理再用加权平均融合重叠区域避免拼接边界出现明显的分界线。推理脚本中的核心步骤是滑窗去噪因为电镜图像动辄几千乘几千像素全图推理常常超出显存而直接切块不重叠又会有边界效应。常用的做法是设定一个重叠区域比如 32 像素的重叠让每个位置的预测结果被多次覆盖最后对覆盖区域取平均值。这种做法对 TEM 去噪效果提升非常明显不会在图像上留下棋盘格状的拼接痕迹。评估时除了 PSNR 和 SSIM我建议把去噪前后的残差图拿来看把输入去噪结果和真实干净图做差观察残差里还有没有结构信息。如果残差图里出现了明显的颗粒或条纹轮廓说明网络把真实信号也削弱了一部分如果残差只是一片均匀的随机噪点说明模型学得比较干净。这一步比盯着 PSNR 数字更重要因为在论文的实际使用场景里下游分析关心的是结构信息是否完整而不仅仅是数值指标。4. 数据与实验策略patch 大小、噪声建模与训练细节如何影响效果4.1 patch 大小与非局部建模的有效范围训练时 patch 大小决定了非局部模块能够看到的最大空间范围。如果 patch 是 64×64非局部模块内部计算的特征图在四分之一尺度下变成 16×16也就是最多只能看到原始图像 64 像素范围内的相似性。对于晶格条纹这类周期性强的结构64 像素可能够用但如果要去除大尺度背景不均匀性或者恢复跨越大区域的低对比度结构patch 就得加大到 128 或者更高。一个容易忽略的问题是加大 patch_size 并不只是增加显存占用还会显著增加非局部模块的计算量因为非局部注意力矩阵的大小是空间像素数的平方。从一个 128×128 的特征图变成 32×32四分之一下采样后注意力矩阵是 1024×1024约一百万量级还在可控范围。如果 patch 加到 256下采样后是 64×64注意力矩阵变成 4096×4096显存和算力开销就不是线性增长了。我一般会根据显卡显存去选12GB 显存用 128 patch8GB 显存用 96 或者 64。如果数据集的图像细节非常丰富宁可减少 batch size 也要保证 patch size因为非局部机制吃的是空间范围batch size 小一点最多影响 batch norm 统计量的稳定性空间范围不够就直接限制网络能力上限了。4.2 噪声建模真实噪声与合成噪声的对齐训练数据的噪声来源有两种一种是直接用低剂量 TEM 采集的真实含噪图配对一个高剂量/长曝光干净图另一种是用噪声模型在干净图上合成噪声。真实配对数据最理想但是电镜采集成本很高而且高剂量采集本身可能改变样品结构配对的干净图未必是真正的 ground truth。合成噪声的优势是数据量不受限制关键是噪声模型要和真实采集情况足够接近。TEM 噪声的常见合成方式是按信号强度生成泊松噪声再做归一化最后叠加一个较小的高斯项。泊松噪声的生成需要把图像像素值当作期望值从泊松分布中采样这一步如果用 Python 循环逐像素做会非常慢正确做法是用 numpy 的向量化采样接口。生成的代码如下import numpy as np def add_poisson_gaussian_noise(clean, peak100.0, sigma5.0): # 输入 clean 范围 [0, 1]peak 控制泊松噪声强度 scaled clean * peak noisy np.random.poisson(scaled).astype(np.float32) noisy noisy np.random.normal(0, sigma, sizenoisy.shape) noisy noisy / peak return np.clip(noisy, 0, 1)上面的函数里peak 越大表示泊松噪声越弱因为同样的信号强度映射到更大的计数范围相对涨落变小peak 近似等于预期电子计数sigma 是读噪声的标准差。在训练时对每张图随机取 peak 在 10 到 200 之间sigma 在 0 到 10 之间覆盖不同噪声水平模型泛化能力会好很多。这里有一个容易踩的细节泊松噪声和图像内容相关的特性是在采样阶段体现的如果先用某个固定噪声强度合成一批数据再用这批数据直接训练模型只会记住这个特定噪声水平下的映射。更好的做法是训练过程中每轮读取干净图时实时生成不同强度的噪声让数据增广更加丰富。因为实时生成噪声不会增加磁盘占用也不需要预生成大量样本代价只是每轮训练多了几毫秒的采样计算完全可以接受。4.3 增广策略旋转变换在电镜数据上的特殊价值电镜图像和自然图像有一个很大区别自然图像有明确的上下方向旋转 90 度会把内容搞乱但 TEM 图像里的纳米结构和晶格条纹本身没有方向性旋转和翻转不会破坏语义反而能极大扩充训练数据的多样性。常见的增广包括随机旋转 90 度的倍数、水平垂直翻转、随机裁剪。注意这里不建议做随机旋转任意角度因为电镜图像的周期性条纹在非 90 度旋转后角度信息会被插值破坏网络学到的条纹方向特征就乱了。另一个重要增广是强度扰动TEM 图像的亮度和对比度在不同采集批次之间会有差异训练时对图像亮度做一个轻微的随机缩放和偏置是有帮助的。例如把图像乘以 0.9 到 1.1 之间的随机系数再加上一个 -0.02 到 0.02 的随机偏置。这样可以防止网络过拟合到固定的亮度范围特别是在训练样本来自单一采集条件的情况下。训练数据切块时我建议用随机裁剪而不是固定网格裁剪。固定网格每次把图切成固定的位置网络会记住每个位置的空间先验推理时对没见过的空间布局表现不自然。随机裁剪配合随机翻转能有效避免这个问题而且实现起来就一行代码。实际训练中每个 epoch 重采样的数据块都不相同相当于隐式做了无限数据扩增。4.4 超参数调节非局部模块的位置和数量非局部模块放在网络哪个位置、放几个是 TEM-NLnet 结构中影响最大的超参。放太浅层特征图分辨率高注意力矩阵计算量爆炸式增长而且浅层特征还只是边缘和纹理的局部组合全局相关性不够语义化放太深层分辨率已经压缩得很低非局部建模的空间精度有限。最稳妥的设置在二分之一或者四分之一下采样后的特征图上加非局部模块这个尺度既保留了足够的空间分辨率特征图尺寸又足够小计算量可控。模块数量上常见做法是放 1 到 3 个。只加一个模块在四分之一尺度适用于训练数据量不大、图像内容相对单一的情况加两个模块分别在四分之一和八分之一尺度适合结构复杂度较高的数据。超过三个模块带来的收益会递减因为深层特征图尺寸太小非局部注意力能区分的空间位置太少作用不大只会白白增加训练时间。5. 复现 TEM-NLnet 的避坑清单从显存溢出到指标虚高5.1 PSNR 高但结构细节呈糊状现象验证集 PSNR 比基准方法高出 2dB 以上但把去噪图和干净图放一起对比晶格条纹的边缘变模糊了细小的纳米颗粒边界也不清晰。原因这是 L2 损失的经典问题。L2 损失鼓励像素均值近似模型预测多条可能解的平均值虽然平均像素误差小但结构信息被抹平了。另一个原因是训练时噪声模型过于简单固定 sigma 的高斯噪声掩盖了真实噪声的信号相关特性模型在均匀噪声假设下过度平滑。解决把损失换成 L1 和 L2 混合L1 对异常值不过度惩罚能保留更多边缘锐度。同时检查训练数据噪声生成是否用了泊松模型没用的话重新生成。评估时把 PSNR 和 SSIM 分开看如果 PSNR 高但 SSIM 低基本可以断定结构保真出了问题要回头调损失函数。5.2 非局部模块导致显存溢出现象训练刚开始报 CUDA out of memory提示位置在非局部模块的前向计算有时连 batch size 设置为 1 都放不下。原因注意力矩阵的计算和存储复杂度是空间尺寸的平方。如果非局部模块加在网络较浅层特征图是 256×256注意力矩阵就是 65536×65536 的浮点矩阵单个矩阵就要 16GB 显存这种配置放在任何消费级显卡上都跑不动。解决把非局部模块往深层移动放到特征图尺寸不超过 64×64 的位置。另外可以限制训练时 patch_size从 128 降到 96 或 64显存占用会显著下降。还有一个技巧是使用分块计算注意力矩阵把注意力权重按行分成块逐步计算和反传避免一次性存储整个矩阵代码复杂度会增加一些但显存限制可以放宽不少。5.3 训练 loss 下降但验证集指标不涨现象训练集上的 loss 一直稳步下降PSNR 也在提升但验证集 PSNR 到了某个水平后不再上升甚至开始回落输出图像出现轻度虚假纹理。原因模型过拟合了训练数据的噪声分布。特别是用固定噪声强度合成的训练数据模型很快记住了这种噪声模式下怎么去噪一旦验证集和训练集的噪声强度或噪声类型有出入表现就不稳定这其实是在拟合噪声分布而非学习图像先验。解决增加噪声增广的随机范围把泊松噪声的 peak 范围调大sigma 也随机化。验证集也不要只用单一噪声水平最好准备一组覆盖不同噪声强度的验证集看模型在不同强度下的表现是否稳定。另外可以适当增加数据增广的翻转旋转让模型对图像内容的不变性更强减少对特定噪声分布的过拟合。5.4 复现论文指标对不上差异在数据预处理现象训练流程看起来完全一样但复现出来的验证指标和论文里报告的差 1dB 以上找不出代码层面的明显错误。原因数据预处理细节不一致。一个是归一化方式不同有的按最大值归一化有的按均值和标准差归一化另一个是评估时的像素值范围不同有的在 [0,1] 上计算 PSNR有的在 [0,255] 上计算差了约 24dB。还有一个隐蔽差异是训练时是否做了随机翻转和旋转这会小幅改变模型的隐藏层分布间接影响最终指标的波动。解决先检查评估脚本的 PSNR 计算方式和论文是否一致确认像素值范围和是剪裁到 [0,1] 还是 [-1,1] 之间。然后和论文描述的预处理步骤逐条对齐特别留意数据是否经过了中心裁剪或者亮度归一化。如果这些都对上了指标还有差异再检查随机种子是否固定PyTorch 的卷积在 cuDNN 下的非确定性有时候会让两次训练结果有 0.3dB 左右的波动。5.5 滑窗推理输出出现明显的网格状拼接伪影现象推理大图后保存的结果肉眼能看到方块状的接缝尤其在噪声较强、平坦区域明显的边界上去噪后的图像像被切成了一个个训练时大小的补丁。原因滑窗推理时窗口之间没有重叠或者窗口重叠区域采用了简单的直接覆盖而不是加权融合。训练时网络对固定尺寸的输入形成了隐式偏好推理时滑动窗口中心位置的预测往往比边缘位置更稳定接缝处就是不同窗口预测结果不连续的直接体现。解决滑窗设置重叠像素重叠宽度至少为 patch 的四分之一。融合时用线性权重窗口中心位置的权重高边缘位置的权重低这样可以平滑过渡。推理完成后把输出图像做一次轻微的边缘去除裁掉最外围一圈预测像素因为边缘区域受到卷积 padding 的影响最大预测可信度最低。6. 用残差分析和频率响应验证网络一个能带走的调参技巧前面提到过残差图分析法这里展开讲怎么定量使用它让它成为你调参的常规动作。准备好三张图干净图、去噪图、含噪图计算两个残差干净图减去去噪图得到信号损失残差含噪图减去去噪图得到去噪残差。信号损失残差反映了被网络当噪声消除的真实结构去噪残差是网络没有除掉的剩余噪声。理想状态是前者接近零且不包含任何结构轮廓后者是均匀随机分布的白噪声。具体观察方法是在图像中心取一个平坦区域计算去噪残差的标准差和高斯噪声的标准差做比较。如果去噪残差的标准差明显低于输入噪声标准差说明噪声已经被压制到较低水平。再看信号损失残差里有没有周期性条纹或者颗粒状图案如果存在这种结构说明部分信号被牺牲掉了。对周期性结构的频率响应分析也同样重要可以沿晶格条纹的方向取一条像素线做一维傅里叶变换对比去噪前后频谱中对应条纹频率的峰值是否保留。如果峰值明显变矮甚至消失说明网络把周期性信号削弱了这时候需要提高频域损失权重调整非局部模块的位置让网络更关注全局周期结构。我个人的一个习惯是每次调参后先把这三项记录到表里再决定下一步动什么参数残差里有结构轮廓就加频域损失权重、换损失函数噪声残留太重就加深网络或者加长训练轮次两者平衡已经很好但视觉效果还是不够清透就去检查数据预处理是不是亮度压缩过窄了。这样一个个环节验证下来很多调参的玄学问题都能落到具体原因上不用漫无目的地乱试。这个项目的代码框架和思路在论文中体现的核心价值就是非局部机制带来的全局感知能力把它按上述步骤一步步调通之后后续换到自己的 TEM 数据集上跑迁移成本会低很多。希望这些踩坑经验能帮到你让你在电镜图像去噪这条路上少走几趟弯路。本文还有配套的精品资源点击获取