ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

TOA反射率深度学习反演PM2.5:从MODIS L1到空间制图的完整实践

TOA反射率深度学习反演PM2.5:从MODIS L1到空间制图的完整实践 简介这是一套基于Python的遥感毕业设计项目以TOA大气表观反射率数据为输入通过深度学习模型反演PM2.5浓度面向计算机、人工智能、遥感等相关专业的在校生与教师也适合需要完成毕设、课设或项目立项演示的初学者。压缩包共6个文件包含4个Python脚本分别用于数据准备、索引提取、数值提取和模型训练另有1个Jupyter Notebook可逐步演示反演流程以及1个说明文档辅助理解整个资源仅12KB体量轻巧但模块划分清晰。已有244人学习下载代码均通过运行测试答辩评审平均分达94.5分可直接作为项目起步或二次开发的基础。使用者可从中学到TOA数据预处理、特征工程、深度学习建模与PM2.5反演的完整链路并借助脚本注释和文档快速掌握实现思路若基础较好还可在此基础上扩展其他气溶胶参数或空气质量反演任务。1. 基于 Python 遥感毕业设计用 TOA 深度学习反演 PM2.5值得做吗很多做遥感方向毕业设计的同学一上来就盯着论文里“AOD → PM2.5”那条成熟路线走结果等自己动手才发现AOD 产品反演算法复杂、暗地表和亮地表各搞一套、云遮挡还留下一大片空值预处理没做完就先想换题。而“TOA 深度学习反演 PM2.5”这条路线是把卫星传感器直接测到的大气层顶反射率作为输入让卷积网络自己去学 TOA 到地面 PM2.5 浓度之间的映射关系。它的核心优势很直白输入只要 L1 级遥感产品省掉 AOD 这个中间变量带来的误差累积空间覆盖也比 AOD 产品完整得多代价是网络要自己消化辐射传输里那一整套说不清的物理过程对样本量、数据清洗和划分策略要求更高。这个题目在毕设里属于既有创新点、又能跑通全流程的“安全牌”。整条链路包括 MODIS L1 数据处理、TOA 反射率计算、站点 PM2.5 标签匹配、模型设计、训练与空间反演成图每一块都有明确交付物答辩时讲得出完整技术故事。适合有 Python 和 PyTorch 基础、手头能拿到公开 Level-1 卫星数据和地面监测站点数据的人去复现。下面按“数据处理 → 标签构建 → 模型搭建 → 训练与反演”的顺序把每一步的关键参数和常见翻车点说清楚。2. 从 TOA 反射率开始数据下载与预处理到模型输入2.1 TOA 是什么为什么跳过 AOD 直接用 TOA 反演 PM2.5TOA 反射率Top of Atmosphere Reflectance是大气层顶的表观反射率也就是卫星传感器经过辐射定标后直接测到的那个数里面既包含地表反射信号也包含大气分子和气溶胶的散射、吸收贡献。气溶胶对太阳短波辐射有显著散射作用而 PM2.5 又是气溶胶质量浓度的主要组成部分所以 TOA 反射率和地面 PM2.5 之间天然存在一个高度非线性、受太阳角度、地表类型、气象条件共同影响的隐式映射——这正是深度学习可以学习的那层关系。传统高精度遥感反演 PM2.5 的常规路线是“TOA → AOD → PM2.5”先做气溶胶光学厚度反演再建立 AOD 与地面站点浓度的统计回归。这路线本身没错但作为毕设去复现时会发现几个让人头疼的地方AOD 反演算法Dark Target、Deep Blue 等在不同地表条件下要切换城市亮地表和浓密植被区的反演结果差异极大AOD 产品经常出现大范围空值而且 AOD 分辨率大多在 3km 甚至 10km 量级和站点尺度匹配起来很别扭。跳过 AOD 直接用 TOA 反射率做端到端回归本质上是让网络自己去学“大气校正 气溶胶反演 浓度估计”这三个环节的联合概率分布。这条路近年来已在一些环境遥感论文里反复出现把它放大成毕业设计题目逻辑完整且工作量可控。2.2 读 MODIS L1 产品转 TOA 反射率的最小 Python 实现MODIS 的 L1 产品MOD02 系列以 HDF 格式存储影像值本身是辐射定标后的 16 位整型 DN 值。转 TOA 反射率有两个路径一个是按辐亮度公式配合 ESUN 表、日地距离、太阳天顶角逐步计算另一个更省事直接利用产品自带的 reflectance_scales 和 reflectance_offsets 两个元数据做线性换算。我一般用后者因为不查表、不引外部参数误差完全由官方定标系数决定。from osgeo import gdal import numpy as np def read_toa_reflectance(hdf_path, band_idx1): 从 MODIS MOD02 1KM 产品读取指定波段并转为 TOA 反射率 band_idx: MODIS 波段号与子数据集名称中的 band 序号对应 ds gdal.Open(hdf_path, gdal.GA_ReadOnly) if ds is None: raise ValueError(无法打开 HDF 文件请检查 GDAL 是否编译了 HDF4 驱动) # 子数据集名称通常形如 HDF4_EOS:EOS_SWATH:...:EV_1KM_RefSB_band1 target None for name, desc in ds.GetSubDatasets(): if RefSB in desc and fband{band_idx} in desc.lower(): target name break if target is None: raise RuntimeError(f未找到 band{band_idx} 对应的子数据集请确认数据产品级别) band_ds gdal.Open(target, gdal.GA_ReadOnly) dn band_ds.ReadAsArray().astype(np.float64) # MOD02 标准元数据字段直接给出线性定标系数 scale band_ds.GetMetadataItem(reflectance_scales) offset band_ds.GetMetadataItem(reflectance_offsets) if not scale or not offset: raise RuntimeError(找不到反射率缩放系数可能不是 MOD02 L1 产品) toa (dn - float(offset)) * float(scale) # 数值防御正常 TOA 反射率范围大致在 [0, 1] 附近 # 跳出区间的像元多为无效像元先粗过滤避免污染后续归一化 toa np.clip(toa, -0.05, 1.2) return toa这段代码的核心逻辑是两层第一层按子数据集描述信息里的 band 序号定位避免硬编码文件内部路径第二层直接从 HDF 元数据里取定标系数把 DN 值线性映射到反射率。clip 这一步不是为了剔除物理异常而是防止后面统计归一化参数时被极端值带偏。需要提醒的是reflectance_scales 和 reflectance_offsets 只存在于 MOD02 L1 产品的反射率子数据集里如果你下载的是 L1A 或经过了自定义预处理这两个字段可能不存在这时候就要退回 ESUN 公式法。2.3 波段选择与云掩膜特征怎么筛MODIS 有 36 个波段不是全都要进模型。气溶胶对短波波段散射最强蓝光波段band 30.47μm对细粒子最敏感红光band 10.65μm和近红外band 20.86μm能反映部分地表特征帮助网络区分“气溶胶导致的变亮”和“地表本身就亮”短波红外band 6、7对粗粒子和大粒径气溶胶有响应。常规做法是选 band 1、3、4、6、7 五个波段统一到 1km 分辨率组成特征栈。band 1 和 band 2 是 250m其余是 1km所以需要把 250m 波段做空间聚合。import numpy as np def downscale_250_to_1000(arr_250m): 250m 影像块平均为 1km。 输入形状要求能被 4 整除输出为四分之一尺寸。 块平均不会引入插值伪影是速度最快的降尺度方式。 h, w arr_250m.shape h_trim h - (h % 4) w_trim w - (w % 4) arr arr_250m[:h_trim, :w_trim] return arr.reshape(h_trim // 4, 4, w_trim // 4, 4).mean(axis(1, 3))块平均比双线性插值更推荐因为插值会产生原本不存在的平滑过渡值而块平均保留的是真实像元统计特征。如果你更倾向用 GDAL 统一重采样也可以但要注意 GDAL 默认的重采样算法和块平均结果有细微差异训练和推理阶段必须保持一致否则精度会莫名下降。云掩膜是另一个必须处理的环节。云顶反射率极高特征分布和晴空像元差别太大如果混入训练样本网络会被迫用大量容量去拟合“云的特征”压缩了对真实气溶胶信号的建模能力。常见做法是用 MOD35 云掩膜产品做逐像元过滤也可以用简单阈值法TOA 反射率在蓝光波段超过 0.4 的像元大概率是云或高亮地表直接给 mask。def cloud_mask_from_toa(toa_blue, threshold0.4): 极简云掩膜蓝光波段反射率超过阈值的像元视为云/高亮目标 return toa_blue threshold2.4 样本块怎么构建站点为中心裁窗样本构建的逻辑是每个地面站点坐标对应影像上的一个像元但 PM2.5 不代表一个像元本身而是代表该点周边一定范围内的气溶胶柱浓度。站点经纬度与像元中心也存在最高半个像元的定位偏差。所以采样时不能只取单像元而要以站点像元为中心裁一个正方形窗口让网络自己决定空间邻域里哪些位置更关键。def sample_patch(feature_stack, row, col, window5): 从 H×W×C 的特征栈中裁取以 (row, col) 为中心的窗。 window 必须为奇数越界时采用补零而不是截断 保证所有样本输出尺寸一致。 h, w feature_stack.shape[:2] r window // 2 pad_stack np.pad( feature_stack, ((r, r), (r, r), (0, 0)), modeconstant, constant_values0.0, ) patch pad_stack[row:row window, col:col window, :] return patch.astype(np.float32)窗口大小是需要网格搜索的超参数。3×3 太小特征信息不够网络很容易过拟合到单像元的噪声上9×9 太大样本间重叠度高、训练样本冗余、显存占用也翻倍。我一般建议在 5×5 和 7×7 之间试多数场景 5×5 能在信息量和样本独立性之间取得平衡。补零策略在边界站点会引入伪特征但这部分样本占比不高且边界补零比直接丢弃站点更有利于保持站点空间分布完整。3. 标签构建与数据集划分精度最大的杠杆其实在这里3.1 PM2.5 观测数据与卫星过境时刻的匹配模型输入是卫星某一次过境时的 TOA 反射率标签就必须是同一时间尺度的地面 PM2.5 观测值。MODIS 上午星 Terra 过境时间大约在当地时间 10:30下午星 Aqua 大约在 13:30和站点小时数据对应起来常规做法是取过境时刻前后 1 小时的站点均值作为标签。这个窗口不是越小越好因为站点仪器本身有响应延迟大气混合也在持续变化30 分钟窗口可能匹配到瞬时波动90 分钟窗口又会引入污染物浓度演变误差。import pandas as pd def match_satellite_ground(station_hourly, overpass_time, window_min60): station_hourly 至少包含列station_id, time, pm25, lat, lon overpass_time 用 pandas.Timestamp 表示 left overpass_time - pd.Timedelta(minuteswindow_min) right overpass_time pd.Timedelta(minuteswindow_min) matched station_hourly[ (station_hourly[time] left) (station_hourly[time] right) ] # 同一站点在窗口内有多个小时记录时取均值 result matched.groupby(station_id).agg( pm25(pm25, mean), lat(lat, first), lon(lon, first), count(pm25, count), ).reset_index() # 窗口内至少要有 2 条记录才算有效避免单点偶然性 return result[result[count] 2]这里有个容易被忽略的细节站点发布的 PM2.5 时间戳多数是北京时间而 MODIS 产品内部使用 UTC 时间匹配前必须先把两个时间统一到同一时区否则所有样本都会产生系统性漂移。数据量小的时候这个误差会直接转化为精度上限。建议在数据处理流水线里把“过境时间统一为北京时间”作为单独一步写清楚并打印校验。3.2 标签清洗负值、零值和极端高值为什么必须处理环境监测站点数据并不总是干净的。仪器零点漂移会产生负值或接近 0 的异常值站点维护期间可能上报固定值偶发沙尘天气会产生超过 1000μg/m³ 的极端值。回归模型用的是连续型损失函数一个极端异常的标签会让模型花费大量容量去拟合错误样本直接拉高整体 MAE。常规做法是双重清洗先按实际物理意义给值域一个宽范围再用均值标准差做一次离群值过滤。def clean_pm25_label(df, min_val0, max_val800, z_thresh3.0): 按可行值域 3σ 双重清洗。 z_thresh 可根据标签分布情况调整沙尘样本少时建议收紧到 2.5。 df df[(df[pm25] min_val) (df[pm25] max_val)].copy() mean, std df[pm25].mean(), df[pm25].std() df df[(df[pm25] - mean).abs() z_thresh * std] return df.reset_index(dropTrue)注意 z_thresh 不能盲目用 3.0。PM2.5 分布本身是右偏的冬季北方站点可能出现大量高值用对称的 3σ 过滤会把一部分真实重污染样本砍掉。更稳妥的做法是按季节或按站点分组后再做 3σ或者用 IQR 方法替代。清洗完之后一定要输出清洗前后的样本量对比这一组数字在论文里是可以直接写进数据描述部分的。3.3 站点级划分随机划分是高精度幻觉的起点这是整条链路里最隐蔽的坑。PM2.5 浓度存在强空间自相关相邻站点的观测值高度相似如果随机划分训练集和测试集同一个站点或相距几十公里的站点数据会同时出现在两端网络在训练时已经“见过”测试站点附近区域的分布测试精度虚高。这种现象在论文里叫空间数据泄漏做答辩时被评委指出来是最致命的。正确的做法是站点级留出按站点 ID 分组保证训练集和测试集里的站点完全没有交集。from sklearn.model_selection import GroupShuffleSplit def site_level_split(df, test_ratio0.2, random_state42): 按站点分组做划分确保同一站点的所有样本只进入训练或测试一侧。 gss GroupShuffleSplit( n_splits1, test_sizetest_ratio, random_staterandom_state ) train_idx, test_idx next(gss.split(df, groupsdf[station_id])) train_df df.iloc[train_idx].reset_index(dropTrue) test_df df.iloc[test_idx].reset_index(dropTrue) return train_df, test_df如果站点数量太少站点级划分会导致训练集和测试集的浓度分布差异很大这时可以考虑时间留出方案用 2019 年以前的数据训练、2019 年之后的数据测试或者按季节交错划分。但无论哪种都不应该用随机划分。这一点写论文时一定要在实验设置里讲清楚。3.4 数据集落盘一份高效的文件组织样本构建完成后建议直接用 NumPy 的 .npz 格式落盘不要存成几百个单张 PNG也不要直接塞进 PyTorch 的 Dataset 里每次现裁。一个约 5 万样本、5 波段、5×5 窗口的数据集用 float32 存储大约是 5 万 × 5 × 5 × 5 × 4 字节约 250MB完全可以用单文件装下。落盘时把特征、标签、站点经纬度、日期一起存方便后续调试时回溯样本来源。np.savez_compressed( dataset.npz, Xtrain_features, # shape: (N, C, H, W) ytrain_labels, # shape: (N,) station_idtrain_station, # shape: (N,) lattrain_lat, lontrain_lon, datetrain_date, )通道维放在第二维是为了迁就 PyTorch 的 Conv2d 输入格式省去每次 DataLoader 里做 transpose 的开销。压缩选项建议保留虽然读写慢一点但能省一半磁盘空间对这个量级的数据完全可接受。4. 用 PyTorch 搭一个能跑的 PM2.5 反演网络4.1 模型结构CNN 特征提取 MLP 回归别一上来就上 TransformerPM2.5 反演本质是回归任务输入是 TOA 反射率的小块输出是标量浓度值。它跟遥感图像语义分割不一样U-Net、SegFormer 这类语义分割网络的核心卖点是逐像素分类搬过来做回归只会让模型变得笨重且难训练。我一般推荐用 CNN 做空间特征提取、MLP 做浓度回归的轻量结构参数量小、训练快也足够表达气溶胶和反射率之间的非线性关系。import torch.nn as nn class PM25Regressor(nn.Module): 输入: (B, C, H, W) 的 TOA 反射率特征块 输出: (B, 1) 的 PM2.5 浓度 def __init__(self, in_channels5, window5): super().__init__() self.features nn.Sequential( nn.Conv2d(in_channels, 32, kernel_size3, padding1), nn.BatchNorm2d(32), nn.ReLU(inplaceTrue), nn.Conv2d(32, 64, kernel_size3, padding1), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.AdaptiveAvgPool2d(2), ) self.regressor nn.Sequential( nn.Linear(64 * 2 * 2, 32), nn.ReLU(inplaceTrue), nn.Dropout(0.2), nn.Linear(32, 1), ) def forward(self, x): feat self.features(x) return self.regressor(feat.view(feat.size(0), -1))两层 Conv2d 就够。第一层卷积主要提取边缘和云边界等低层特征第二层卷积组合出与气溶胶散射相关的空间模式。AdaptiveAvgPool2d(2) 的作用是把任意窗口尺寸统一成 2×2 特征图这样换窗口大小时不用改网络结构。Dropout 放在 MLP 入口处只作用于最后的回归层比放在卷积层后面更有效——卷积层的空间信息冗余本来就大Dropout 在那边只会拖慢收敛。4.2 训练主循环与数据组织训练主循环本身不复杂但对数据组织和设备搬运要严谨。先把样本按 site_id 分组做站点级划分再为每个样本找到空间坐标对应的窗口。用 DataLoader 的默认 sampler 就行但 shuffle 只能在 epoch 内做不能发生在划分环节。为了节省显存每次只把当前 batch 的样本从内存搬进 GPU。from torch.utils.data import TensorDataset, DataLoader train_dataset TensorDataset( torch.from_numpy(train_X), # (N, C, H, W) float32 torch.from_numpy(train_y), # (N,) float32 ) train_loader DataLoader( train_dataset, batch_size128, shuffleTrue, num_workers2, drop_lastFalse ) model PM25Regressor(in_channels5, window5) optimizer torch.optim.Adam(model.parameters(), lr1e-3, weight_decay1e-4) scheduler torch.optim.lr_scheduler.StepLR(optimizer, step_size15, gamma0.5)batch_size 的建议范围是 64 到 256 之间。太小的话 BatchNorm 的统计量不稳定太大则内存占用高但收益不明显。学习率 1e-3 这个起点只适用于 5 层以内的小网络如果你的模型加深了记得退回到 1e-4 重新调。StepLR 每 15 个 epoch 把学习率减半这种简单调度策略在回归任务上比 ReduceLROnPlateau 更稳定因为 RMSE 的波动本身比较大按 epoch 衰减不会被单次波动误导。4.3 损失函数MAE 训练、RMSE 报告损失函数的选择直接决定模型优化方向。均方误差MSE对大误差样本施加二次惩罚训练过程中容易被重污染事件主导平均绝对误差MAE对所有样本的惩罚是线性的受离群点影响更小。对 PM2.5 这个场景训练损失建议用 MAE或者用 Huber 损失折中一下——在小误差区间表现为 L2、在大误差区间表现为 L1。def huber_loss(pred, target, delta1.0): Huber 损失|err| delta 时走 L2其余走 L1抗离群点 abs_err (pred - target).abs() quadratic 0.5 * abs_err.pow(2) linear delta * (abs_err - 0.5 * delta) return torch.where(abs_err delta, quadratic, linear).mean()delta 的取值和标签的量纲有关。如果 PM2.5 浓度范围集中在 0 到 100μg/m³delta1 会显得很小几乎所有误差都落在线性区间Huber 就退化成了 MAE建议把标签先做一次标准化减均值除标准差再用 Huber或者直接把 delta 调到 10 左右。选择 MAE 训练还有一个好处验证时报告 RMSE 和 R²如果训练和验证的表现趋势一致说明优化方向是对的。4.4 早停、BatchNorm、Dropout 三个组件的取舍早停是必须加的否则只会得到一个在训练集上收敛完美、在测试集上惨不忍睹的模型。BatchNorm 在样本量低于几千时反而有害因为小 batch 内的统计量波动大会让训练震荡加剧样本量大时它能加速收敛也是我一直保留它的原因。Dropout 只保留一个轻量 0.2 即可——模型主体是卷积层本身就有参数共享带来的正则化效果过度依赖 Dropout 会拖慢收敛。best_val_loss float(inf) patience, bad_epochs 12, 0 for epoch in range(100): train_loss run_one_epoch(model, train_loader, optimizer, modetrain) val_loss run_one_epoch(model, val_loader, optimizer, modeval) scheduler.step() if val_loss best_val_loss: best_val_loss val_loss torch.save(model.state_dict(), best_pm25_model.pt) bad_epochs 0 else: bad_epochs 1 if bad_epochs patience: print(fepoch {epoch} 早停最优验证损失 {best_val_loss:.4f}) breakpatience 取值 10 到 15 是合适的。PM2.5 回归任务的验证损失曲线波动往往比较大patience 太小容易在第二个局部谷底被误杀太大则训练时间白白浪费。5. 训练与验证避坑记录R² 负数、反射率越界、站点泄露5.1 现象训练 R² 在 0.9 以上测试 R² 却接近 0这是我在这个方向上见到的最高频问题。原因几乎都是数据划分方式不对——用随机划分把同一站点的样本同时放进了训练集和测试集或者划分时没有按时间隔离导致站点数据和卫星数据的对应关系被间接记忆。解决方法是回到第 3.3 节用站点级留出重建划分并在论文里明确写清楚“测试站点的样本在训练期间完全不可见”。如果按站点划分后测试 R² 仍然很低那可能是样本量本身不够站点泛化需要补充更多不同地理区域的站点数据。5.2 现象TOA 反射率出现负值或者大于 1反射率转出来是负值基本是定标系数用错了。MOD02 产品里 reflectance_scales 和 reflectance_offsets 只适用于 EV_RefSB 子数据集如果你读的是 EV_Band 辐亮度数据集乘出来的结果自然是错的。大于 1 的像元则很可能对应云顶或耀斑区。解决办法是预处理过程中对每个波段的反射率做区间检查把超出 [-0.05, 1.2] 的像元直接标记为无效并在样本构建时丢弃不要依赖 clip 硬截断——clip 只保证数值范围不保证物理合理性。5.3 现象损失曲线像高原一样迟迟降不下去这通常是学习率设置过大优化过程在局部震荡或者 BatchNorm 在小 batch 上统计不稳定。排查方法是先打印每个 batch 的 loss 均值如果出现周期性跳变大概率是学习率问题如果 loss 整体平滑但不下降则把学习率调到 1e-4 后重跑。另一个容易被忽略的原因是特征没有标准化。TOA 反射率虽然是反射率但不同波段的动态范围差异很大蓝光波段动态范围窄短波红外动态范围宽直接进网络会让梯度被大动态范围的波段主导。建议按波段做标准化后再输入。5.4 现象空间反演成图时出现斑块状空白站点样本对应的特征块是稀疏的训练时只用了站点周边小窗成图时需要用训练好的模型对全图逐像元滑窗推理。很多人直接写双重 for 循环把全图所有窗口一次性进 GPU显存溢出后只看得到部分结果看起来就像斑块状空白。解决方法是分块推理按 512×512 或 256×256 的 tile 切图每个 tile 内做滑窗边界部分多取一个 window 半径的 overlap推理完再裁掉边缘拼回去。5.5 现象模型 R² 高到不真实超过 0.95回归任务 R² 能到 0.95 以上就值得警惕。先看测试站点和训练站点是否真的完全分开了再看有没有某个 batch 里混入了重复样本。有一种隐蔽情况是同一时刻的站点与站点之间浓度高度相似如果测试站点周围几公里内有训练站点模型仍能从空间邻域学到大量信息这是一种软性泄漏。严格做法是按地理区域划分比如把研究区切成东西两块用西边训练、东边测试这样空间自相关造成的信息泄漏被压缩到最低。6. 答辩用的三张图与精度验证的进阶技巧6.1 散点密度图预测与实测的 1:1 关系答辩时评委最关心的永远是一张预测值与实测值的散点图。不要用普通 scatter站点样本量大时会出现大量点重叠根本看不出分布密度。建议用二维直方图或 hexbin 图颜色深浅代表样本密度再叠加一条 1:1 参考线和线性拟合线。关键参数是 bins 不要太大也不要太小200 到 300 之间比较合适。拟合线斜率接近 1、截距接近 0R² 才有说服力。图上直接标注 MAE、RMSE、R²要打印清楚是用哪个样本集算的。import matplotlib.pyplot as plt import numpy as np def plot_density(y_true, y_pred, titleSite-level Test Set): 用二维直方图画密度散点避免高密度区域点重叠看不清 plt.figure(figsize(6, 6)) plt.hexbin(y_true, y_pred, gridsize200, cmapYlOrRd, mincnt1) max_val max(y_true.max(), y_pred.max()) plt.plot([0, max_val], [0, max_val], k--, lw1.5, label1:1) z np.polyfit(y_true, y_pred, 1) x_line np.linspace(0, max_val, 100) plt.plot(x_line, np.polyval(z, x_line), b-, lw1.2, labelOLS fit) plt.xlabel(Observed PM2.5 (μg/m³)) plt.ylabel(Predicted PM2.5 (μg/m³)) plt.legend() plt.title(title) plt.tight_layout()6.2 时间序列对比图选一个站点做逐日对比散点图证明整体相关性时间序列图证明时间趋势捕捉能力。取测试集中一个典型站点按日期画出观测值与预测值的折线特别注意要展示冬季重污染和夏季清洁两个极端时期。这能直观看出模型在高浓度段是否系统偏低——这是 PM2.5 反演模型最常见的问题因为重污染样本少、模型倾向于回归到均值附近。如果发现高值段被压缩可以在损失函数里给高浓度样本加权或者对标签做 log1p 变换后再训练但这不是必须的只要在论文里如实剖析就好。6.3 空间外推验证从站点到全区域是最后一公里模型在站点位置表现好不代表全图反演结果可靠。空间外推验证的常规做法是用一部分站点训练、在完全没参与训练的另一批站点上测试已通过站点级划分完成。全图反演时更要注意与训练数据的分布差异——研究区内山区和城市地表反射特性差异巨大如果训练站点集中在城区山区反演结果的可信度要单独说明。最后输出一张研究区 PM2.5 空间分布图用连续色带而不是离散分级加经纬度坐标和比例尺这张图是答辩时“工作量”的直接证据。我自己的习惯是把上述三张图固定成一套验证模板每次调完参数就完整跑一遍散点密度图管整体精度时间序列图管时间动态空间分布图管外推能力。哪个图出了问题就沿着对应的链路去排查。很多时候调了一周模型效果不好最后发现是站点匹配时间戳差了一个小时这类问题在这个方向上反复出现处理数据时的每一处小疏忽最终都会在验证图上暴露出来。希望这篇笔记能帮你少走这些弯路把精力花在真正重要的模型和实验设计上。本文还有配套的精品资源点击获取
返回列表