做高光谱相关任务的人,大概率都经历过这么一段“找数据、读数据、骂数据”的死循环。明明拿到手的是公开数据,下载地址也找到了,可是文件全是缩写,格式还是ENVI这种二进制老古董。等你好不容易把它们读进numpy,又开始头疼怎么才能变成PyTorch能直接吃的Dataset。这篇文章就把整套流程完整过一遍:从NASA DataPort(实际对应的下载入口是NASA Earthdata Search)检索并下载AVIRIS高光谱影像,到结合USGS光谱库做地物光谱标注,再到写一个能稳定跑起来的PyTorch Dataset类,全程带代码和避坑点。适合遥感、地学方向的研究生,以及刚接触高光谱影像的算法工程师。如果你手头正好有AVIRIS数据,但不知道怎么处理成训练集,这篇文章能帮你省下至少一晚上试错时间。
1. 为什么把AVIRIS影像和USGS光谱库绑在一起:高光谱数据的人工智能准备逻辑
1.1 高光谱数据为何让人又爱又恨
普通RGB图像每个像元只有3个通道,高光谱影像动辄上百个连续波段。AVIRIS(Airborne Visible/Infrared Imaging Spectrometer)全名是机载可见光/红外成像光谱仪,一次飞行能拿到224个波段的影像,覆盖380nm到2500nm,横跨可见光、近红外和短波红外。正因为波段足够密集,不同矿物的反射光谱特征才能被区分开,这也是遥感领域做矿物填图、植被胁迫监测、水体参数反演的基础。
但高光谱数据的麻烦在于:第一,数据体积大,一景AVIRIS影像经常几百MB甚至几个GB,用普通图片读取思维去load,内存直接爆;第二,没有现成的标签,不像ImageNet那样每个样本对应一个类别;第三,波段之间有很强的相关性,但又有少量波段是坏的,比如水汽吸收带附近信噪比极低,直接用会让模型学偏。这三个麻烦,就是在正式开始写Dataset之前必须解决的。
1.2 USGS光谱库在这里扮演什么角色
USGS光谱库(USGS Spectral Library)是地物光谱的“参考字典”,最新版本splib07a里收录了矿物、岩石、植被、人造材料、土壤、水体等上千条光谱曲线。它给每类地物提供了一个标准答案,告诉我们“赤铁矿的光谱长这样”“方解石的光谱长那样”。在做高光谱分类任务时,USGS光谱库有两种典型的用法:
- 第一种是监督分类:把光谱库里的参考曲线重采样到AVIRIS波段,直接当作训练集,再对影像上的每个像元做匹配或分类;
- 第二种是辅助标注:AVIRIS影像本身没有像元级标签,可以用光谱角匹配算法,把影像中的纯像元和光谱库里的参考曲线做对比,用最相似的光谱库类别给像元打标签,再拿去训练深度学习模型。
本文后面走的方案是第二种。对遥感项目来说,真实影像上“哪里有哪种矿物”这种现场勘探成本极高,用光谱库生成初始标签是性价比最高的做法。等模型在初始标签上跑通,再慢慢用人工标注或主动学习去修正,这是比较成熟的落地路线。
1.3 整条数据链路涉及的角色关系
- NASA DataPort(Earthdata Search):数据仓库,负责把AVIRIS影像从服务器拿到你电脑上;
- AVIRIS影像文件:原始素材,里面每个像元是一条224维光谱;
- USGS光谱库:标签依据,提供参考光谱形状;
- PyTorch Dataset:最终交付物,让模型可以用标准方式迭代样本。
这条链路中间随便哪一环出了问题,最终都会表现为“模型训练时loss乱跳”“准确率一直不动”,但根因往往不在模型,而在数据管线。所以,在建模型之前把数据处理干净,优先级远高于调参。
2. NASA DataPort到底怎么下载:入口、产品级别和文件清单避坑
2.1 先说清楚:标题里的NASA DataPort指的是哪个平台
如果你在浏览器里直接搜“NASA DataPort”,第一屏可能看到的是NASA的数据开放平台。但对遥感用户来说,真正下载AVIRIS数据的官方入口是NASA Earthdata Search(search.earthdata.nasa.gov),这就是大多数教程里被泛称为“NASA DataPort”的入口。打开之后需要先注册一个NASA Earthdata账号,下载数据前还要在个人profile里完成Earthdata Login授权绑定,否则点击下载时会一直被跳转到授权页面,折腾一圈才能放行。我第一次做的时候卡了将近半小时,所以单独拿出来提醒一句。
实际上下载AVIRIS数据的入口不止一个,NASA JPL的AVIRIS官网(aviris.jpl.nasa.gov)也提供历史数据下载入口。两者拿到的核心数据一样,但Earthdata Search的检索体验更好,可以按地理位置、采集时间、云量等条件过滤,还提供HDF5格式的便于直接使用的版本。HDF5通常已经做过辐射定标和部分大气校正,适合快速上手;而AVIRIS官网的历史数据很多是ENVI格式,能让你看到最原始的二进制数据长什么样,对理解格式本身更有帮助。如果你后续要自己写处理流程,建议从ENVI格式开始,这样能把底层逻辑吃透。
2.2 产品级别怎么选:L1、L2与表面反射率的差别
JPL和NASA分发AVIRIS数据的级别,常见的有下面几类:
| 产品级别 | 内容 | 典型用途 |
|---|---|---|
| L1A/L1B | 原始辐射值,做过传感器矫正但未做大气校正 | 研究传感器特性、自己写大气校正流程 |
| L2表面反射率 | 经过大气校正的反射率产品 | 地物分类、光谱匹配,最省事 |
| 几何矫正产品(orthorectified) | 校正了地形和飞行姿态的影像 | 与GIS结合做地图制图 |
我的建议是,做深度学习分类任务首选L2表面反射率产品。原因很简单:USGS光谱库里的参考光谱是地面实测的反射率,如果影像还是原始辐射值,两者做光谱匹配前还需要自己做大气校正,这条链路不仅长,而且大气校正参数怎么设置全是坑。不过也要注意,表面反射率产品在植被浓密区和山谷阴影地带经常会有异常值,需要在Dataset里做特殊处理,后面会讲到。
2.3 下载时的文件清单:别只盯着.img
一次完整的下载,你通常拿到的不只是一张图。以我自己常用的某景AVIRIS场景为例,这里用通用命名规则说明,常见文件包括:
- 主体影像文件:例如
f080614t01p00r07rdn_b.img,后缀是.img,这是二进制栅格主体,也是后面所有处理的原材料; - 头文件:同名但后缀为.hdr,记录行列数、波段数、数据类型、波段中心波长、interleave方式等关键信息,少了它,.img就只是一堆不知道形状的裸二进制;
- 几何校正文件:例如 .ort后缀的校正影像,以及记录每个像元经纬度的点位文件;
- 质量标识文件:例如 .qual,逐像元记录数据质量等级或掩膜,可以在Dataset里当作辅助mask使用;
- 波段中心波长信息通常直接内置在.hdr的wavelength字段里,不一定需要单独下载。
下载完别急着解压就完事,先检查文件清单是否完整。我遇到过只有.img没有.hdr的情况,后来读数据时各种不舒服,因为ENVI格式离开头文件就是一堆裸二进制,连行列数都不知道,更不要说波段顺序了。
3. AVIRIS数据不是普通图片:ENVI格式读取与通道排布的坑
3.1 .hdr头文件里藏着哪些关键信息
ENVI格式可以看作“二进制数组+ASCII头文件”的组合。头文件看起来是一堆键值对,但对正确读取数据来说,下面几个字段是必备的:
- samples:每行像元数,也就是列数;
- lines:影像行数;
- bands:波段数;
- data type:数值类型,1表示uint8,2表示int16,4表示float32,12表示uint16,13表示uint32等;
- interleave:数据排列方式,有bil、bip、bsq三种;
- byte order:0表示小端,1表示大端;
- wavelength:列表形式给出每个波段的中心波长,单位通常是nm。
如果打开一个AVIRIS场景时,所有波段并不是想象中的“第三维就是波段”,而是被拆分到不同位置,那么理解interleave就成了第一道门槛。很多人读出来的数组shape是错的,本质都是这里没搞清楚。
3.2 BIP、BIL、BSQ三种排列方式的差别
高光谱影像在磁盘上的排列方式有三种,理解它们比背概念更重要,因为这直接决定你用np.reshape时到底怎么写:
- BIP(Band Interleaved by Pixel):像元内部先连续放所有波段,一行就是 samples × bands,读取某个像元的全光谱时最方便,但按波段处理时跨步长访问;
- BIL(Band Interleaved by Line):先放第1行所有像元的波段1,再放第1行所有像元的波段2,依次类推,这是最常见的一种排列方式;
- BSQ(Band Sequential):先把波段1的整张影像放完,再放波段2,波段间完全独立,按波段做统计最方便。
对PyTorch训练而言,我们通常希望最后拿到(H, W, C)或(C, H, W)的数组,C放在最后一个维度。BIP格式天然接近(H, W, C),BIL和BSQ都需要转置或用库函数把波段轴换到正确位置。如果不懂排列方式,直接用np.fromfile读raw,数据形状会完全对不上,而且表现很诡异:图像变成条纹状、颜色错乱,模型训练自然一塌糊涂。
3.3 用spectral库最快地读取AVIRIS
读ENVI格式最省心的方式是使用spectral库。安装就一行命令:
pip install spectral读取代码很简单:
from spectral.io import envi # 打开时同时传入hdr和img img = envi.open('f080614t01p00r07rdn_b.hdr', 'f080614t01p00r07rdn_b.img') # 加载整景数据,返回ndarray data = img.asarray() print(data.shape) # (lines, samples, bands)spectral库会在打开时自动识别interleave,并且把数据整理成(lines, samples, bands)的排列,这是最推荐的方式。老版本里可能用img.load(),新版本推荐用asarray()。不过,当影像超过2GB,直接转成ndarray很容易触发MemoryError,或者让程序开始疯狂吃交换分区。这时建议改成memmap方式:
mmap_img = img.open_memmap()open_memmap()不会把数据一次性搬进内存,而是用numpy.memmap封装,按需读取某个像元或某个波段的切片,速度和内存利用率都比较理想。在Dataset的__getitem__里直接对这个memmap切片,是高光谱训练里很常见也够用的写法。
3.4 手工用numpy读取的兜底方案
如果环境里装不了spectral,或者你想完全搞清楚文件格式,手工读取也不复杂。以BIL排列、float32、samples=677、lines=2011、bands=224为例,可以这样读:
import numpy as np lines, samples, bands = 2011, 677, 224 dtype = np.float32 raw = np.memmap('f080614t01p00r07rdn_b.img', dtype=dtype, mode='r', offset=0) # BIL排列:第1行内先放波段1,再放波段2,因此reshape后是(lines, bands, samples) data = raw.reshape(lines, bands, samples) # 把波段轴放到最后,得到 (lines, samples, bands) data = np.transpose(data, (0, 2, 1))如果是BSQ格式,reshape成(bands, lines, samples)后按波段索引即可;如果是BIP格式,reshape成(lines, samples, bands)后不需要转置。这里最容易栽坑的是把BIL当成BSQ处理,导致波段之间错位,模型训练时特征全乱。我的建议是:拿到数据后先随便画一个波段的灰度图,确认图像内容看起来是连续地物而不是雪花噪声,再继续后续处理。这个小习惯能省下大量排查时间。
4. 从二维影像到光谱样本:提取、标注与USGS参考光谱对齐
4.1 三种标注方案的成本对比
高光谱影像的像元级标注是整个项目里最费人力的环节。目前我常接触的方案有三种:
- 全手工标注:在一个标注工具里看完224个波段合成的假彩色图,像画语义分割label那样逐个地物边界描出来。准确度最高,但一景大图可能要画好几天;
- 半自动标注:先挑出几个纯像元种子点,用区域生长或聚类方法扩展,再人工修正;
- 全自动初始标注:用USGS光谱库做光谱角匹配,给每个像元一个初始类别。
在还没有任何标签、想快速验证模型能不能跑通的时候,第三种方案性价比最高。本文后面的步骤就走这个方案,先把流程搭通,之后再慢慢补充人工精修标签即可。等项目管理规范之后,还可以引入主动学习,用模型挑选最不确定的样本让人工标注,效率会更高。
4.2 光谱角匹配(SAM)生成初始标签
光谱角匹配(Spectral Angle Mapper)的核心思想是:把像元光谱和参考光谱都看作高维空间的向量,计算两者之间的夹角。夹角越小,说明两条光谱的形状越接近。匹配结果对增益不敏感,因为归一化过程会去掉绝对反射率的差异,这对高光谱这种“形状特征远比强度重要”的数据非常友好。
简化版的计算代码如下:
import numpy as np def spectral_angle_mapping(pixel_spectrum, library_spectra, band_mask): """ pixel_spectrum: (B,) 像元光谱 library_spectra: (N_classes, B) 参考光谱 band_mask: (B,) 布尔数组,True表示保留该波段 """ p = pixel_spectrum[band_mask].astype(np.float64) p = p / np.linalg.norm(p) scores = [] for ref in library_spectra: r = ref[band_mask].astype(np.float64) r = r / np.linalg.norm(r) cos_val = np.clip(np.dot(p, r), -1.0, 1.0) scores.append(np.arccos(cos_val)) scores = np.array(scores) return scores.argmin(), scores.min()对整景影像做逐像元SAM计算,可以用矩阵乘法一次性算出所有像元的归一化光谱与参考光谱的夹角,比上面这版循环快很多。不过第一次调试时,逐像元循环能方便地打印中间结果,先跑通再优化不迟。
这里有个前提:library_spectra需要先重采样到AVIRIS的波段网格,否则维度对不上。USGS光谱库的splib07a通常提供2nm间隔的光谱,AVIRIS波段中心波长可以从.hdr的wavelength字段解析出来。重采样可以用scipy.interpolate:
from scipy.interpolate import interp1d def resample_to_aviris(usgs_wavelengths, usgs_reflectance, aviris_wavelengths): f = interp1d(usgs_wavelengths, usgs_reflectance, kind='linear', bounds_error=False, fill_value=np.nan) return f(aviris_wavelengths)需要注意:如果AVIRIS波段范围没有完全覆盖USGS光谱范围,重采样后的边缘位置会出现NaN,这些波段应该在后续步骤里一起过滤掉。每次重采样完,我都建议顺手把NaN的数量打出来看一眼,不能等到训练时才发现。
4.3 噪声波段与水汽吸收带的删除策略
AVIRIS数据224个波段里并不是都能用。水汽吸收带集中在1350~1420nm和1800~1950nm附近,这些波段的地表信号几乎被大气水汽吸收殆尽,信噪比极低。另外,传感器最边缘的几个波段也常常表现不稳定。实际处理时,我一般会维护一个band_mask,把这些波段置为False:
aviris_wavelengths = img.metadata.get('wavelength', []) try: wl = np.array([float(x) for x in aviris_wavelengths]) except Exception: # 某些版本的.hdr wavelength字段带逗号或引号,需额外清洗 wl = np.array([float(x.strip().replace(',', '')) for x in aviris_wavelengths]) band_mask = np.ones_like(wl, dtype=bool) band_mask[(wl >= 1350) & (wl <= 1420)] = False band_mask[(wl >= 1800) & (wl <= 1950)] = False # 边缘不稳定波段也直接去掉 band_mask[:5] = False band_mask[-5:] = False删完这批波段之后,224维光谱通常会降到150~180维左右。实践下来,把坏波段喂给模型不仅不能提升性能,反而会让loss曲线出现莫名其妙的抖动,因为模型在努力拟合噪声。这一步不能省,也不要心疼那几十个波段。
4.4 样本提取的两种粒度:逐像元和图像块
在决定Dataset怎么设计之前,先确定样本粒度。两种做法各有适用场景:
- 逐像元(pixel-wise):每个训练样本就是一条光谱曲线,形状为
(保留波段数,),配合SAM生成的像元级标签,训练一个MLP或1D-CNN做分类。优点是显存占用小、实现简单,适合验证流程;缺点是完全没有空间上下文,分类结果会有大量椒盐噪声; - 图像块(patch-wise):以每个像元为中心取一个3×3或5×5的小patch,输入模型时是
(bands, patch_size, patch_size)或把patch内的光谱平均后作为一维向量。空间上下文让分类更稳定,但Dataset的读取逻辑更复杂,需要考虑边界padding。
对于刚起步的项目,我建议先把逐像元版本跑通,再做patch版本。两个版本在Dataset类上的差异主要体现在__getitem__里是返回一条向量还是一个patch。第5节的代码以逐像元版本为主线,patch版本我会额外提注意事项。
5. PyTorch Dataset完整实现:代码骨架、归一化与缓存策略
5.1 Dataset骨架:三个必须实现的方法
PyTorch的Dataset约定非常固定,重写三个方法:
__init__:保存数据路径、标签、波段筛选索引、归一化参数等;__len__:返回样本总数;__getitem__:给定索引,返回(x, y)样本对。
结合前面的步骤,一个可用的逐像元Dataset实现如下:
import numpy as np import torch from torch.utils.data import Dataset from spectral.io import envi class AVIRISPixelDataset(Dataset): def __init__(self, img_path, hdr_path, samples, labels, bands_idx, mean=None, std=None): """ samples: List[(row, col)] labels: List[int] bands_idx: np.ndarray, 保留波段的索引 mean/std: 用于标准化的按波段统计量,可选 """ self.img = envi.open(hdr_path, img_path).open_memmap() self.samples = samples self.labels = labels self.bands_idx = bands_idx self.mean = mean self.std = std self.cache = {} def __len__(self): return len(self.samples) def __getitem__(self, idx): if idx not in self.cache: r, c = self.samples[idx] # memmap返回的是视图,这里转float32并拷贝,避免后续问题 spectrum = np.asarray(self.img[r, c], dtype=np.float32).copy() self.cache[idx] = spectrum x = self.cache[idx][self.bands_idx] if self.mean is not None: x = (x - self.mean) / (self.std + 1e-8) y = self.labels[idx] return torch.from_numpy(x), torch.tensor(y, dtype=torch.long)这个类里几个细节值得展开说。第一个是np.asarray(...).copy():open_memmap()返回的切片本质上是指向磁盘的视图,直接作为返回值时,DataLoader在多进程环境下有时会报错,或者因为非连续内存导致collate阶段意外失败。加上copy()之后这些问题基本都能避开。第二个是cache字典:它把最近取过的光谱缓存下来,避免每次__getitem__都去磁盘seek。
5.2 为什么用memmap而不是整景load进内存
高光谱影像一景有几千行、几百列、两百多个波段,float32条件下光一景就是几百MB甚至几个GB。如果整景load进内存,不仅内存占用巨大,而且Dataset每个epoch都要随机访问这些数据,内存完全没有必要常驻那么大。open_memmap()的优势是数据仍在磁盘上,需要哪个像元就读取哪个像元,访问模式非常契合Dataset的随机采样。再叠加一个cache字典,把最近访问过的光谱缓存几万条,能明显减少大文件反复seek的耗时。
cache的大小需要根据样本量控制。几万样本的规模下,cache对内存的压力不大;到百万级样本时,建议限制cache容量,或者改用functools.lru_cache。
5.3 归一化策略:全局统计量优先,不要逐样本min-max
很多初学者会直接在__getitem__里对每条光谱做min-max归一化,这其实是个坑。逐样本归一化会把每个像元的光谱强度拉成[0,1],导致不同亮度区域的相对强度信息被抹掉。在高光谱里,反射率绝对值本身是有含义的,暗色岩石和亮色岩石可能是完全不同的类别。因此更推荐的做法是使用全局统计量标准化:先在训练集上统计每个波段的均值和标准差,然后在__getitem__里按这些固定值标准化。
# 假设sample_spectra是形状 (N, B) 的训练样本光谱矩阵 per_band_mean = sample_spectra.mean(axis=0) per_band_std = sample_spectra.std(axis=0) + 1e-8 np.save('per_band_mean.npy', per_band_mean) np.save('per_band_std.npy', per_band_std)计算统计量时可以用训练集的样本光谱,也可以直接用整景影像做抽样统计,关键是mean和std都要按波段维度得到向量,而不是一个标量。统计量需要在构建Dataset之前就固定下来,训练集和验证集必须用同一份mean和std,否则验证集效果会忽高忽低。
5.4 配合DataLoader使用时的注意事项
Dataset写好后,配合DataLoader使用时有几个高频坑:
num_workers设太高时,每个worker都会持有一份memmap引用,文件句柄可能被占满。建议先从0或1开始,确认逻辑正确再逐步调大;- 如果
__getitem__返回的是memmap切片,DataLoader在collate时可能因为数组不是连续内存而报错,解决办法就是我代码里的.copy(); shuffle=True时,建议把samples和labels用同一个索引绑定,不要分别打乱。实现上用zip打包后一起shuffle最不容易出错。
初始化Dataset后,可以用几行代码快速测试:
dataset = AVIRISPixelDataset(img_path='data.img', hdr_path='data.hdr', samples=all_samples, labels=initial_labels, bands_idx=band_idx, mean=per_band_mean, std=per_band_std) dataloader = DataLoader(dataset, batch_size=64, shuffle=True, num_workers=0) x, y = next(iter(dataloader)) print(x.shape, y.shape) # 预期输出: (64, 保留波段数) (64,)这一步能跑通,数据管线的主干就通了。
6. 进DataLoader前必做的四项验证:形状、数值、光谱曲线与踩坑记录
6.1 验证形状和dtype:从打印shape开始
也许你写Dataset花了小半天,但调试时值得再花半小时做验证,因为数据层面的错误比模型代码更难发现。先从形状开始:
- 打开影像后,确认
print(data.shape)是(lines, samples, bands)而不是(bands, lines, samples); - 用
len(dataset)确认样本数,和samples列表长度一致; - 取单个item,确认x的维度是
(保留波段数,),dtype是float32,y是整数标量。
如果形状是反的,说明interleave处理有误。这时候用matplotlib画第50个波段的灰度图,看到连续的山脉、河流、道路轮廓,基本就能确认读取正确;看到竖向或横向条纹,就是BIL/BSQ解析错了,赶紧回去核对头文件里的interleave字段。
6.2 检查NaN、极端值和全零像元:脏数据比坏模型更可怕
AVIRIS表面反射率产品里经常会有NaN、全零像元(通常是掩膜区域或观测死角),以及异常大的负值。这些脏数据直接进模型,轻则让loss变成NaN,重则让模型早早收敛到无意义的解。我习惯在构建Dataset前先做一轮统计:对每个保留波段计算min、max、NaN比例,并统计全零像元数量。
如果是大面积全零像元,直接在samples列表里过滤掉;如果只是少量NaN,可以在归一化后对NaN填充为该波段均值。批量检查代码可以这样写:
# data_subset是抽样出的(样本数, 保留波段数)矩阵 valid_mask = np.isfinite(data_subset).all(axis=1) print('valid ratio:', valid_mask.mean()) print('sample min:', data_subset[valid_mask].min()) print('sample max:', data_subset[valid_mask].max())如果影像规模太大,可以先对所有像元抽稀,比如每隔100行取一行,快速估计数据质量。实际项目里,全零像元比NaN更常见,也更容易被忽略,因为常规的均值、方差统计不会直接报错,但模型训练出来的特征图上会出现大片黑色空洞。
6.3 把样本画成光谱曲线和USGS参考值对比:最有说服力的验证
数据验证最有说服力的一步,是随机抽取几个类别的样本,把光谱曲线画出来和USGS光谱库中对应类别的参考曲线叠加对比。曲线形状接近,说明重采样、波段筛选、标注全部正确;如果波峰波谷位置都对不上,那很可能是重采样时波长的单位搞错了(比如把微米当纳米),或者波段索引错位。
画图只需要matplotlib:
import matplotlib.pyplot as plt wl_kept = wl[band_idx] plt.figure(figsize=(10, 4)) for i in range(5): x = dataset[i][0].numpy() plt.plot(wl_kept, x, alpha=0.7, label=f'sample {i}') plt.plot(wl_kept, library_sample, 'k--', label='USGS reference') plt.xlabel('Wavelength (nm)') plt.ylabel('Reflectance') plt.legend() plt.show()这一步我自己吃过一次亏。有一回模型训练了20个epoch,精度停在60%上不去,后来画曲线才发现,重采样后的USGS参考光谱因为插值越界,尾部变成了NaN,训练时这些NaN被强制转成0,直接污染了损失函数。发现之后把无效波段剔除,重新训练,精度很快就上去了。
6.4 我在这条链路上踩过的几个高频坑
最后把高频坑集中列一下,当作清单参考:
- 水汽吸收带波段不删除,训练初期loss下降正常,后期振荡加剧。原因是模型在试图用噪声波段做特征,再好的网络结构也救不回来;
- 归一化统计量在构建Dataset之前没有固定,训练集和验证集用了不同版本的mean/std,导致验证集效果和训练集差距巨大。解决办法是把统计量保存成npy文件,训练时加载同一份;
- memmap模式下,如果文件被另一个进程占用,或者磁盘权限不足,Windows系统上读取时会抛PermissionError。解决办法是右键检查文件权限,或者复制到本地工作目录再打开;
- 直接把memmap切片作为返回值时,DataLoader可能报"cannot pickle numpy.memmap object"或数组不连续的错误,处理方式就是开头说的
.copy(); - 重采样后边缘波段出现NaN却忘了检查,进入归一化后NaN变成一个很大的负数或直接变成0,模型训练曲线非常奇怪。每次重采样之后都要强制检查一次NaN的数量。
如果上面4项验证都通过,你的Dataset就已经可以安心喂给模型了。后续无论是做分类、聚类还是降维可视化,数据基础都算扎实了。
最后再分享一个我个人长期保留的习惯:不管项目多赶,数据管线一定要单独写一个verify脚本,建好Dataset之后跑一遍全量统计,顺手把前几个batch的张量落盘保存。以后模型效果不对时,回来看数据总是最快的排障方式。高光谱项目里,模型往往不是瓶颈,数据才是。