
简介面向使用PyTorch处理高光谱图像HSI的开发者与研究者这套代码包完整演示了DataLoader加载高光谱数据的标准流程。资源覆盖自定义Dataset类、光谱归一化、批次大小设置、多线程读取、随机打乱及数据增强策略并针对高光谱大内存占用问题给出pin_memory与自定义collate_fn的优化方案可直接迁移至IndianPines等公开数据集的训练任务。压缩包共7个文件包括3个Python脚本、2个已编译的pyc文件以及2个MAT格式数据文件总大小5.69MB脚本涵盖dataloader构建与train主流程目录结构简洁便于快速定位关键代码。目前已有611人学习通过阅读源码可掌握高光谱批次加载的完整写法理解如何避免内存溢出并提升训练效率适合中高级Python开发者结合深度学习项目参考实践。1. 高光谱数据加载为什么绕不开 PyTorch DataLoader一台无人机搭载的高光谱相机一次飞行就能产出 1024×1024 像素、224 个波段的立方体单帧 16bit 存储时体积轻松超过 400MB。很多用 PyTorch 做高光谱分类的人第一步就卡在数据加载上直接拿 cv2.imread 读图的老路子彻底失效整个立方体一次性装进显存又立刻爆掉。PyTorch 的 DataLoader 在这里不只是「一个数据迭代器」它决定了你的 GPU 利用率、训练速度和内存峰值。这篇文章不聊空洞的框架概念直接从高光谱数据最常见的 ENVI 格式和 GeoTIFF 格式入手手写一个能接进 DataLoader 的 Dataset 类把 patch 采样、反射率转换、标准化、并行加载参数逐个讲清楚最后把我踩过的五个典型坑按「现象→原因→解决」列出来。适合正在做高光谱分类、异常检测或者语义分割的从业者也适合刚装好 PyTorch、被三维数据搞懵的新手。你能直接照着代码改。2. 先把数据弄明白高光谱立方体的存储与读取姿势高光谱数据加载的第一个瓶颈不是 PyTorch而是数据本身。普通 RGB 图像是二维矩阵加三个通道高光谱是三维立方体波段数从几十到几百不等。你得先搞清楚文件里存的是什么、怎么排列的否则后面所有代码都是在错误的数据上跑。这一章先把 ENVI 和 GeoTIFF 两种最主流的格式讲透再解决高光谱如何转反射率这个躲不开的预处理问题。2.1 ENVI 格式被 .hdr 和 .dat 绕晕的人不在少数高光谱数据最常见的存储格式是 ENVI 标准格式包含一个 .hdr 头文件和一个 .dat 数据文件。头文件里写死了关键信息samples列数、lines行数、bands波段数、data type1 是 8bit 无符号、4 是 32bit 浮点、12 是 16bit 无符号、interleave数据排列方式BSQ、BIL、BIP。这三个 interleave 是最大的坑BSQ先存波段 1 的全部像素再存波段 2依次类推BIL按行交替先存第一行的所有波段再存第二行BIP按像素交替每个像素的所有波段紧密排列读取时如果搞错 interleave数据集形状会完全错乱。我用 spectral 库读最省事它自动解析 .hdr 里的排列方式import spectral.io.envi as envi # 读取 ENVI 格式高光谱数据 img envi.open(scene.hdr, scene.dat) # 返回 SpectralLibrary 对象转成 numpy 数组 data img.load() print(data.shape) # (lines, samples, bands)例如 (512, 512, 128)spectral.io.envi.open会自动解析 .hdr 里的 interleave 信息不需要手动处理。但要注意load()返回的数组 shape 永远是 (lines, samples, bands)不管文件里是 BSQ 还是 BIP。如果你的环境装不了 spectral也可以用 rasterio 读 GeoTIFF或者用 numpy 的 fromfile 配合 memmap 读裸数据。对于超过 1GB 的大立方体我一般用 numpy.memmap 做零拷贝读取避免一次性把整个立方体读进内存导致 16GB 内存都不够用import numpy as np # 以 BSQ 排列为例手动 memmap 读取 dtype np.uint16 # data type 12 对应 16bit 无符号 lines, samples, bands 512, 512, 128 offset 0 # 文件通常带 128 字节头需要按实际情况调整 mm np.memmap(scene.dat, dtypedtype, moder, offsetoffset, shape(bands, lines, samples)) # BSQ: band 优先 data np.asarray(mm).transpose(1, 2, 0) # 转成 (lines, samples, bands)这里的关键参数是offset——很多 ENVI 文件带 128 字节的文件头不跳过会把前 64 个像素读成乱码。shape按 BSQ 排列应该是 (bands, lines, samples)最后 transpose 一下统一成 (lines, samples, bands)。如果你读出来数据错位优先检查 interleave 和 offset 这两个参数。另外要注意memmap 读完的数组是只读的之后要做反射率转换先 np.asarray 变成可写副本。2.2 从 DN 值到反射率这一行矩阵运算不能省高光谱相机记录的原始值是 DNDigital Number它受光照条件、相机响应和大气影响不能直接作为训练特征。我见过不少人拿着 DN 值直接训分类网络结果换一台相机或者换一个采集时间模型准确率掉 20 个点。「高光谱如何转反射率」在代码里就是一条矩阵运算链路。一是辐射定标加大气校正用 ENVI 的 FLAASH 或者开源的 6S 模型这条链路比较复杂适合有地面控制点的严谨场景。二是用传感器厂家提供的定标系数做快速转换反射率 (DN - 暗电流) × 增益# 伪代码传感器定标系数通常是逐波段的 dark_current np.load(dark.npy) # shape: (bands,) gain np.load(gain.npy) # shape: (bands,) reflectance (data - dark_current) * gain # data: (lines, samples, bands) reflectance np.clip(reflectance, 0, 1) # 反射率定义域是 [0, 1]注意两个细节暗电流和增益都是逐波段的要沿着最后一个维度广播clip 到 [0,1] 不是可选项我见过不 clip 的数据出现 1.0 以上的异常值BatchNorm 直接算出 NaN。如果你的数据是机载高光谱通常厂家会提供一条标定曲线转换成反射率之后再做大气校正才是完整流程。但作为 DataLoader 的输入反射率转换到这里已经够用了。另一个容易被忽略的点是反射率转换必须在标准化之前做顺序反了暗电流和增益就白乘了。2.3 标准化策略逐波段 z-score 比全局 min-max 稳反射率数据转换完之后还需要标准化。高光谱数据的每个波段动态范围不一样——近红外波段的反射率可能到 0.6水吸收波段可能只有 0.02如果直接喂给神经网络高动态范围的波段会主导梯度。我常用的策略是逐波段 z-score# 逐波段标准化每个波段独立计算均值和标准差 mean data.mean(axis(0, 1)) # shape: (bands,) std data.std(axis(0, 1)) data_norm (data - mean) / (std 1e-6)这里axis(0, 1)是关键对空间维求均值、保留波段维。std 1e-6防止某个波段全部都是常数导致除零。这个 mean 和 std 必须在训练集上计算然后保存下来验证和测试的时候用同一组统计量不能各自算。我见过有人对训练集和测试集分别做 z-score相当于把测试集的分布信息泄漏进来了精度虚高。如果你用 min-max 标准化注意要逐波段做别用全立方体的全局最大最小值。高光谱数据偶尔会产生坏像元值可能是 65535 或者 NaN全局 min-max 遇到这种像素所有正常波段都会被压缩到几乎不可分辨。所以我一般建议用 z-score 配合异常值裁剪。统计量的计算放在 Dataset 的构造函数里只跑一次不要让__getitem__每次重复计算——高光谱的统计量计算要遍历整张图放在__getitem__里会让每个样本都白做一遍全图计算训练慢到怀疑人生。3. 手写高光谱 Dataset让 DataLoader 能读懂三维立方体PyTorch 的 DataLoader 本身不认识高光谱数据它唯一的要求是你提供一个可索引的对象实现了__len__和__getitem__。高光谱 Dataset 的核心职责有两个把三维立方体切成带标签的 patch以及控制每次采样时返回的数据类型和内存占用。这一章从 Dataset 的骨架写起再把「高光谱立方体怎么制作」这个问题落到代码上。3.1 Dataset 三件套init、len、getitem下面是完整实现。我把注释写在代码里每行代码背后的逻辑在代码块后面解释import torch import numpy as np from torch.utils.data import Dataset class HyperspectralDataset(Dataset): def __init__(self, data, labels, patch_size11, transformNone): data: (height, width, bands) 的 numpy 数组已完成反射率转换和标准化 labels: (height, width) 的类别标签图0 表示背景/无标签 patch_size: 以目标像素为中心裁剪的窗口大小必须是奇数 transform: 可选的数据增强 super().__init__() self.data data self.labels labels self.patch_size patch_size self.half_size patch_size // 2 self.transform transform # 只收集有标签的像素坐标跳过背景 labeled_rows, labeled_cols np.where(labels 0) self.samples list(zip(labeled_rows, labeled_cols)) def __len__(self): return len(self.samples) def __getitem__(self, idx): row, col self.samples[idx] # 边缘补齐用 reflect 填充而不是 zero padding pad self.half_size padded np.pad(self.data, ((pad, pad), (pad, pad), (0, 0)), modereflect) patch padded[row: row 2 * pad 1, col: col 2 * pad 1, :] # 转成 (bands, patch, patch) 的 PyTorch 格式 patch np.transpose(patch, (2, 0, 1)).astype(np.float32) label int(self.labels[row, col]) - 1 # 类别从 0 开始 if self.transform: patch self.transform(patch) return torch.from_numpy(patch.copy()), torch.tensor(label, dtypetorch.long)__getitem__返回一个 patch 和它的中心像素标签。patch_size必须是奇数这样窗口中心正好落在目标像素上。modereflect是对高光谱数据最友好的填充方式——镜像反射边缘像素不会像 zero padding 那样在边缘引入大量无意义的零值这对边缘像素的分类精度影响很大。这里有个性能细节np.pad在__getitem__里反复调用每次都会创建一个新的填充数组非常浪费。数据立方体大的时候这个操作会拖慢整个 DataLoader。我的改进做法是在__init__里只填充一次把self.data存成填充后的版本裁剪的时候直接用padded[row:row2*pad1, col:col2*pad1]免去每次采样都 pad。但注意填充后的数组只在__getitem__里被切片读取不影响原始坐标计算。还有一个内存细节torch.from_numpy(patch.copy())里的.copy()不能省。np.transpose返回的是视图内存不连续PyTorch 转 tensor 的时候会额外做一次复制如果不 copy会出现奇怪的显存碎片和加载错误。我在这个坑上花过半天时间排查。3.2 高光谱立方体怎么制作标签图与数据立方体的配对你可能注意到__init__里把(height, width)的标签图转换成了样本坐标列表。这里有一个高光谱特有的坑标签图通常是人工标注的类别图比如 Indian Pines 数据集的标签只有大约 10000 个像素有类别其余全是 0背景。如果你把 1024×1024100 万个像素全部作为样本等于让模型去学 99% 的背景训练效率极低。正确做法是只采集有标签的像素也就是用np.where(labels 0)。「高光谱立方体怎么制作」这个问题在实践中的答案就是把三维反射率数据和一个二维标注图组合成(height, width, bands)(height, width)的配对。如果你的任务不是分类而是目标检测标签图可以换成边界框列表或者分割掩膜Dataset 的结构不变只需要改__getitem__里的返回内容。对于 Indian Pines 这类经典数据集需要特别处理的点是训练集和测试集的划分方式。标准做法是按像素随机划分但很多论文按区域划分——Pavia University 数据集就常用这种策略。按区域划分时同一个区域的像素高度相关如果按像素随机划分验证集和训练集来自同一片区域模型很容易过拟合到空间纹理上指标虚高。复现高光谱分类论文时先确认对方的划分策略再动手否则复现结果和论文差距很大这不是训练问题是数据划分问题。3.3 样本不平衡WeightedRandomSampler 和 loss weight 双管齐下高光谱分类数据集的不平衡问题非常严重。以 Indian Pines 为例大豆类和玉米类可能有几千个样本而燕麦类只有几十个样本。直接用默认的 DataLoader 做随机采样模型会学到「永远预测大类」的策略。我常用的方案有两个第一个是类别权重采样from torch.utils.data import WeightedRandomSampler # 统计每类样本数量计算权重 class_counts np.bincount([label for _, label in dataset_samples]) weights 1.0 / class_counts.astype(np.float64) sample_weights [weights[label] for _, label in dataset_samples] sampler WeightedRandomSampler(sample_weights, num_sampleslen(sample_weights), replacementTrue) loader DataLoader(dataset, batch_size32, samplersampler)WeightedRandomSampler的核心参数是sample_weights和num_samples。replacementTrue意味着每一轮训练会按权重有放回地采样让小类样本被重复抽中的概率更高。注意sampler和shuffle不能同时用——DataLoader 只要指定了 sampler它的shuffle参数就会被忽略。第二个方案是用 CrossEntropyLoss 的weight参数给每个类别一个 loss 权重。这个效果更直接但不改变每个 epoch 实际看到的样本分布。我一般两个都做先用 WeightedRandomSampler 保证小类样本出现频率再用 loss weight 强化梯度双管齐下。小类样本的权重用 w 1 / 类别占比 作为起点然后 cap 在 10 以内防止模型过拟合到少数几个样本上。类别占比直接用class_counts / class_counts.sum()算不要把背景类算进去。4. DataLoader 参数调优batch_size、num_workers 与内存的三角关系高光谱 patch 的特点是通道数极大——一个 11×11 的 patch224 个波段展平后有 27104 个特征。这决定了 batch_size 不能像普通 RGB 图像那样动辄 128 或者 256。真正吃内存的不是 patch 本身而是采样和预处理过程中的中间变量。这一章把四个关键参数的取值逻辑和实测表现讲清楚。4.1 四个直接影响高光谱训练的参数参数推荐值说明batch_size16~64patch 通道多64 以上显存压力陡增num_workers4~8高光谱单样本读取快瓶颈在内存复制prefetch_factor2~4每个 worker 预取多少个 batchpin_memoryTrueGPU 训练必开跳过 CPU→GPU 的分页复制batch_size的物理意义在高光谱场景下很直接一个 batch 的 tensor 大小是 (batch_size, 224, 11, 11)展平后每个样本 27104 个 float32 元素。batch_size64 时每 batch 约 6.9MB看起来不大但高光谱网络的第一个全连接层或卷积层会把通道数膨胀好几倍显存消耗从第一个 batch 就开始叠加。我的经验是 32 起步显存不够降到 16不要硬开 128。pin_memoryTrue的作用是锁定 CPU 内存页让 GPU 通过 DMA 直接读取省掉了 CPU→GPU 传输前的分页复制。这个参数在高光谱场景下收益更大因为每 batch 的数据量是普通图像的几十倍传输耗时的占比更高。开了 pin_memory 之后数据从 CPU 到 GPU 的拷贝时间可以缩短 30% 到 50%。4.2 num_workers 与内存峰值fork 陷阱实测高光谱数据加载有个隐蔽问题Dataset 构造函数把整个立方体常驻在进程内存里。当 num_workers 0 时PyTorch 会用 fork 的方式创建多个 worker 进程每个进程都会复制一份父进程的内存空间。如果你的立方体是 2GBnum_workers8那么实际内存占用接近 2GB × 8 16GB还不算训练本身的开销——这是高光谱 DataLoader 翻车的头号原因。from torch.utils.data import DataLoader # 推荐配置高光谱数据 loader DataLoader( dataset, batch_size32, shuffleTrue, num_workers4, # 不要超过 CPU 物理核心数的一半 pin_memoryTrue, drop_lastTrue, # 防止最后一批 batch 太小导致 BatchNorm 统计量抖动 persistent_workersTrue # 避免每个 epoch 重新创建 worker )persistent_workersTrue是我强烈建议的配置。默认情况下每个 epoch 结束worker 进程会被销毁重建重建时又要复制一次那份巨大的立方体内存。persistent_workersTrue 让 worker 跨 epoch 复用内存峰值能降 40% 左右。drop_lastTrue对高光谱是有实际意义的——如果样本数不能被 batch_size 整除最后一个 batch 可能只有 2~3 个样本如果用了 BatchNorm这些样本算出的均值和方差抖动很大会导致训练震荡。如果你的机器内存紧张还有两个可选的降内存手段一是把立方体数据转成 float16 存储读取后再转 float32内存直接减半二是把整张图拆成若干块分片加载但这会破坏空间连续性patch 采样会出错不建议用。我实测下来64GB 内存的机器跑 2GB 立方体、num_workers4、batch_size32峰值内存约 12GB训练过程中 GPU 利用率稳定在 90% 以上这是比较舒服的配置区间。如果在 Windows 上跑你会发现 num_workers 0 时程序卡死或者报错。这是因为 Windows 没有 fork用的是 spawn 模式需要把 DataLoader 的创建放到if __name__ __main__:的保护里面。这个坑我在下一章的避坑部分详细展开。5. 避坑高光谱 DataLoader 的五个典型翻车现场这一章全部是我自己踩过的坑按「现象→原因→解决」的结构写。每条都对应高光谱数据特有的问题不是通用的 PyTorch 报错。你照着排查基本能覆盖加载链路里九成的异常情况。5.1 加载出来的 shape 不对ENVI interleave 搞混了现象用 spectral 库读出来的数据 shape 是 (bands, lines, samples)或者数据内容是花的、像噪声一样。打印前几个像素的值发现数值完全不连续。原因ENVI 文件的 interleave 是 BIP 或 BIL 排列而手动用 memmap 读的时候默认按 BSQ 排列解析。spectral 库本身会处理 interleave但如果你用了 numpy 的 fromfile就必须自己看 .hdr 里的 interleave 字段。问题是 .hdr 里的字段名是interleave bip或bil对应 BIP 的 memmap shape 是 (lines, samples, bands)BIL 的 shape 是 (lines, bands, samples)很容易搞混。解决读 .hdr 里的 interleave 字段然后按对应关系设置 memmap 的 shape。我的习惯是不再手写解析直接用 spectral 库的envi.open(...).load()它会自动把排列统一成 (lines, samples, bands)。如果一定要手写先在文件里打印前 10 个字节看一眼BIP 和 BSQ 的字节序差异一眼就能看出来。5.2 反射率转换之后出现 NaN训练直接崩现象训练第一个 epoch 就出现 NaN loss或者模型输出全部是 NaN。检查数据发现某些波段的数值是 NaN。原因反射率转换时某些波段的暗电流等于 DN 值反射率计算结果为 0 或者负值除以一个接近 0 的标准差后产生无穷大。另一个常见原因是坏像元——高光谱传感器偶尔会有死像素记录值 0在反射率转换后变成异常值。解决转换后立即 clip 到 [0, 1]外加 z-score 时在分母加1e-6。我还习惯在标准化之后做一次np.isnan(data).any()检查如果存在 NaN直接定位到具体波段用中值滤波修复坏像元。不要用均值填充均值会把坏像元信息扩散到整个波段。如果 NaN 只存在个别像素我的处理是把这个像素直接从样本坐标列表里剔除比修复更干净。5.3 Windows 下 num_workers 0 直接卡死现象在 Windows 上创建 DataLoader 后程序卡住不动或者报BrokenPipeError。偶尔能看到重复执行多次__main__的打印输出。原因Windows 不支持 forkDataLoader 的 worker 进程用 spawn 方式创建会重新导入主模块。如果 DataLoader 创建在模块级别spawn 会递归执行导致死锁。PyTorch 官方文档里明确写了这一点但对高光谱数据来说因为立方体大、worker 复制内存多这个问题触发得更早、更频繁。解决把数据加载逻辑放进if __name__ __main__:保护块。代码写在一个单独的main()函数里只有最后调用main()的一行放在if __name__ __main__:下面。如果只是调试代码干脆 Windows 上先用num_workers0等逻辑通了再上 Linux 服务器跑正式训练。我在 Windows 上调试高光谱代码时一律设num_workers0, persistent_workersFalse只有上 Linux 才开并行。5.4 标准化统计量泄漏训练和验证各算各的现象验证集准确率高得离谱比测试集高 10 个点但测试集表现正常。换一个数据集复现论文时指标对不上。原因验证集的 mean/std 是单独从验证集计算的包含了验证集自身的分布信息等于特征分布被「偷偷告诉」了验证集的统计特性。这在高光谱上尤其严重因为同一个场景的不同区域光照条件相似但又不完全一样验证集统计量能间接暴露验证集的光谱范围。另外高光谱波段数量大逐波段统计量的差异更容易被模型利用。解决把 mean 和 std 存成 .npy 文件训练过程中固化下来验证和测试全部加载同一份统计量# 训练集计算后保存 np.save(mean.npy, train_mean) np.save(std.npy, train_std) # 验证/测试时加载同一份统计量 train_mean np.load(mean.npy) train_std np.load(std.npy) val_data_norm (val_data - train_mean) / (train_std 1e-6)我在代码里把标准化写成一个独立的类或函数传入统计量文件路径避免 Dataset 内部重复计算。检查方法跑验证时故意把统计量换掉准确率应该有明显变化如果没变化说明标准化根本没起作用。5.5 patch 边缘的标签错位reflect padding 的隐藏问题现象验证集边缘像素准确率异常低甚至出现预测类别集中在某几类的情况。单独测试全图中心的 patch 时准确率正常边缘 patch 明显不对。原因modereflect填充对数据是合理的但标签图的坐标基准可能是偏移的。很多数据集的标注图先做了裁剪或重采样标签坐标和数据坐标之间存在一个固定的偏移量。窗口跨过图像边界时reflect 会镜像填充边缘——对中心像素来说是合理的——但如果你在标签图上取坐标时用了没有裁剪的原始范围中心像素实际对应的数据内容已经错位了。解决在整个流程的最开始确认标签图和数据立方体是否在同一坐标系下。数据集说明里一般会写「标签图像已经对原始数据做重采样」这类信息。更稳妥的做法是在收集样本坐标时直接排除掉离边界小于 half_size 的像素根本不采样边缘区。这个排除逻辑写在np.where之后、生成样本列表之前不要在图构建之后补否则样本坐标列表和 patch 裁剪逻辑耦合在一起排查起来很痛苦。6. 进阶把 DataLoader 封装成可复用的高光谱加载模块当你把 Indian Pines、Pavia University、Houston 这几个经典数据集都跑一遍的时候会发现每个数据集的形状、类别数、波段数都不一样。每换一个数据集就重写一遍 Dataset 是纯体力活我这章的方案是写一个工厂函数根据数据集名返回对应的 Dataset 和 DataLoader 配置同时把数据划分和冒烟测试固化到流程里。6.1 多数据集切换与三套 loader 共用内存def build_hyperspectral_loader(data_path, label_path, dataset_name, patch_size11, batch_size32, num_workers4, seed42): if dataset_name indian_pines: img load_envi(data_path) # (145, 145, 200) labels np.load(label_path) # (145, 145) elif dataset_name pavia: img load_envi(data_path) # (610, 340, 103) labels np.load(label_path) # 统一预处理管道 img to_reflectance(img) # 反射率转换 img zscore_normalize(img) # 逐波段标准化统计量来自训练集 np.save(train_mean.npy, train_mean) np.save(train_std.npy, train_std) # 按 60% / 20% / 20% 划分像素 train_idx, val_idx, test_idx split_pixels(labels, seedseed) train_ds HyperspectralDataset(img, labels, train_idx, patch_size) val_ds HyperspectralDataset(img, labels, val_idx, patch_size) test_ds HyperspectralDataset(img, labels, test_idx, patch_size) train_loader DataLoader(train_ds, batch_sizebatch_size, shuffleTrue, num_workersnum_workers, pin_memoryTrue, drop_lastTrue) val_loader DataLoader(val_ds, batch_sizebatch_size, shuffleFalse, num_workersnum_workers, pin_memoryTrue) test_loader DataLoader(test_ds, batch_sizebatch_size, shuffleFalse, num_workersnum_workers, pin_memoryTrue) return train_loader, val_loader, test_loader这里把 Dataset 的构造从「全图都采」改成了「传入索引列表」。好处是训练、验证、测试三套 loader 共用同一份内存中的数据立方体不会各自复制三份。split_pixels用固定的随机种子划分保证实验可复现。seed42是参数化的换种子能直接对比不同划分下的泛化能力——这在高光谱小样本场景下是个很实用的实验手段。训练前你最好打印三个 loader 的样本数检查划分比例和类别分布。数据量小的时候60/20/20 的划分可能让测试集某些类别只有几个样本这时候测试指标的置信度很低。我的做法是把 seed 固定但多跑几个不同 seed报告均值和方差而不是依赖单次划分的结果。6.2 验证加载正确性第一个 batch 的冒烟测试封装完之后最好先做一个冒烟测试确认 DataLoader 返回的数据是对的。我每次都会跑一遍检查脚本看三件事shape、数值范围、标签分布。for batch_data, batch_labels in train_loader: print(fbatch shape: {batch_data.shape}) # 期望 (32, 200, 11, 11) print(flabel shape: {batch_labels.shape}) # 期望 (32,) print(fvalue range: [{batch_data.min():.3f}, {batch_data.max():.3f}]) print(flabel classes: {torch.unique(batch_labels)}) break # 只检查第一个 batch如果 shape 错了说明 transpose 或者 padding 逻辑有 bug如果 value range 超出 [-3, 3] 这种范围说明标准化路径不对如果 label classes 的类别数不是预期的类别数说明标签映射有偏差。我遇到过标签从 0 开始还是从 1 开始的问题——很多公开数据集把 0 留给背景类从 1 开始编号所以我在 Dataset 里做了label - 1的映射。还有一种情况是torch.unique(batch_labels)打印出的类别不是连续整数说明标签图里有某些类别根本没有被采样到要去检查np.where(labels 0)的过滤条件。这些检查只需要几十秒但能省去整个训练后的排查时间。从那以后我每次换新数据集第一件事就是跑一遍这个冒烟脚本确认 Dataset、DataLoader、标准化这三个环节没有一处翻车再开始调模型。高光谱数据加载的坑基本都是数据格式和统计量的问题不是网络结构的问题把数据链路打磨稳了训练起来就顺了。希望帮到你。本文还有配套的精品资源点击获取