
简介面向森林生态遥感与机器学习研究人员的一份Python入门指南聚焦利用GEDI L4A、Sentinel-2、SRTM海拔与坡度以及随机森林方法实现地上生物量密度AGBD的建模与预测。以Mafungautsi森林保护区为测试地点文档完整覆盖Google Earth Engine账户初始化、Sentinel-2合成影像构建、植被光谱指数计算、多源遥感数据整合、训练与测试数据集准备、随机森林模型训练、精度评估以及AGBD结果预测与可视化等环节同时结合GEDI L4A的RH波形指标和模型原理解释每一步背后的依据并对过拟合现象及参数调整思路作了探讨帮助读者理解多源遥感协同建模的完整逻辑。资源共1个PDF文件、约141KBPDF格式便于阅读与打印适合有一定遥感与机器学习基础、希望提升森林碳储量评估能力的研究人员或工程师参考学习。目前已有100人学习使用可作为入门到实践的快速起点。 做森林碳汇和遥感建模这摊事的人估计都经历过拿到GEDI数据后对着HDF5文件发呆的阶段。波形参数、footprint编号、质量标识一堆字段堆在眼前另一边是下载好的Sentinel-2影像有波段有指数但两者怎么对齐、怎么变成训练样本网上教程讲得稀碎。我自己在这条路上绕了不少弯从数据下载、特征提取到模型调参每一步都踩过坑。这篇就把基于GEDI、Sentinel-2和随机森林的地上生物量密度AGBD建模完整流程梳理一遍附可直接运行的Python代码给你一条能少走弯路的实操路线。这套技术路线适合正在做森林碳汇估算、林业资源调查相关课题的研究生和从业者也适合想了解激光雷达与光学影像如何结合的机器学习入门者参考。1. 为什么偏偏是GEDISentinel-2这组搭档1.1 GEDI到底给了我们什么别家给不了的信息GEDI全称Global Ecosystem Dynamics Investigation是美国国家航空航天局2018年底安装到国际空间站上的全波形激光雷达系统。它不像传统光学卫星那样被动接收地表反射的太阳光而是主动向地面发射激光脉冲记录激光穿过冠层、打到地面再反射回来的完整波形。这个波形里藏着森林的垂直结构信息冠层高度、冠层剖面、地表地形全部都能反演出来。为什么AGBD建模要依赖GEDI而不是直接用光学影像道理很简单光学遥感看到的是二维平面信息一个像素里绿色的浓淡、近红外的反射强度这些确实和植被茂密程度相关但同一高度的密灌丛和稀疏乔木林在影像上可能长得差不多而它们的生物量差异巨大。GEDI的波形能直接告诉你这25米直径的圆形区域里树冠顶部在哪、树冠底部在哪、冠层有多厚这些垂直方向的结构参数和生物量的关系远比二维光谱反射率强得多。目前常用的GEDI数据产品有四个等级我用一张表给你理清关系产品代号全称核心参数对我的建模有什么用L1BGeolocated Waveforms地理位置、全波形数据做波形处理算法研究用一般不直接用于AGBD建模L2AElevation and Height Metrics冠层顶部高度rh100、地形高程、相对高程度量提取垂直结构特征变量的主要来源L2BCanopy Structure Metrics叶面积指数、植被面积指数、冠层覆盖度、剖面特征补充结构特征选做特征变量L4AFootprint AGBD每束激光footprint尺度上的AGBD估计值及其不确定度直接当训练标签用或者和L2A结合1.2 Sentinel-2负责从点到面的桥接GEDI有个天然的短板它只在星下点采样激光脚印在轨道之间是不连续的。你可以把GEDI理解成一台在大片区域里随机钻孔取样的钻机每钻下去一个孔就知道那个点的地下情况但两孔之间是什么不知道。Sentinel-2正好补上这个窟窿。作为欧洲航天局哥白尼计划的多光谱成像卫星它提供10米空间分辨率的多光谱影像而且有专门针对植被监测设计的红边波段。这意味着Sentinel-2能在整个研究区范围内提供连续、平整的光学特征面把GEDI稀疏的点插值成一张完整的面。这套组合的技术逻辑非常清晰GEDI给出稀疏但高精度的生物量真值样本Sentinel-2给出密集但间接的光谱和结构特征。两者之间建立一个映射关系就能把GEDI的精度外推到整个研究区。机器学习模型在这里扮演的角色就是去拟合这个从光学特征到生物量的复杂非线性映射。1.3 机器学习建模的选型逻辑在模型选型上随机森林是这类遥感建模任务的稳妥选择。它有三个其他算法不好替代的优势一是能处理高维特征而不需要做复杂的特征标准化光谱波段、植被指数、地形因子直接堆进去就能跑二是对噪声和异常值有不错的鲁棒性遥感数据里难免有云雾残留、阴影、水体和传感器的异常值随机森林通过多棵树的投票机制能够降低个别样本的干扰三是训练结束后可以输出特征重要性这对做生态学解释非常关键你能直观看到哪个波段或指数对生物量预测贡献最大而不是面对一个黑箱模型干瞪眼。我试过用XGBoost和神经网络来对比这个任务前者的调参复杂度明显更高——你要同时处理学习率、树深度、正则化系数、子采样比例参数之间相互制约稍不注意就过拟合后者则需要精心设计网络结构和训练策略数据量不够时表现还不如传统树模型。随机森林在这方面是性价比最高的起点训练速度快、默认参数下精度就说得过去再加上我后面要说到的调参空间足够覆盖绝大多数区域尺度的AGBD建模需求。2. 环境搭建与数据准备这步卡住了后面全是白干2.1 推荐的工具链遥感数据处理有个特点光栅数据和矢量数据混着用格式转换频繁经常要在不同的库之间来回倒腾。我推荐的Python技术栈是这样的xarray / rioxarray —— 读取和处理带地理坐标的NetCDF/GeoTIFF数据 rasterio —— Sentinel-2光栅影像读写、重投影、重采样 geopandas —— 处理GEDI footprint矢量点、缓冲区构建 h5py —— 直接读取GEDI原始HDF5文件 pyproj —— 坐标系转换统一CRS numpy / pandas —— 基础矩阵运算和表格数据操作 scikit-learn —— 随机森林回归实现、交叉验证、超参数搜索 matplotlib / seaborn —— 结果可视化 tqdm —— 批量处理时的进度条显示安装直接用conda创建一个虚拟环境一次到位conda create -n agbd python3.10 -y conda activate agbd pip install xarray rioxarray rasterio geopandas h5py pyproj numpy pandas scikit-learn matplotlib seaborn tqdm2.2 两份核心数据的来源GEDI数据从美国国家航空航天局地球科学数据系统Earthdata网站申请账号后下载每个文件是HDF5格式文件体积不大几百MB就能覆盖一个研究区。能直接用的产品主要是L2A和L4AL2A提供冠层高度和地形相关指标L4A提供footprint尺度上的AGBD估计值。两个产品可以按beam和shot_number进行关联匹配。Sentinel-2 L2A级地表反射率产品可以从欧盟哥白尼数据空间生态系统下载也可以谷歌地球引擎里按需导出。需要注意统一地面采样距离10米波段直接用20米波段需要重采样到10米。2.3 下载和预处理中的两个深坑第一个坑是坐标参考系统不统一。GEDI原始数据里给的是经纬度坐标EPSG:4326而Sentinel-2影像是UTM投影EPSG:326xx。不统一坐标系就直接做空间运算会产生严重偏差。必须先给GEDI的足迹点设置WGS84坐标系再投影到和Sentinel-2影像相同的UTM分区。第二个坑是GEDI产品质量标识的使用。我在早期建模时图省事把所有footprint全部喂进模型结果精度惨不忍睹。后来核对发现L2A产品里每个footprint都带有一组质量标识字段quality_flag是数据质量总开关degrade_flag标记激光是否有退化sensitivity是地表探测灵敏度。标准的筛选条件是# quality_flag 1且 degrade_flag 0且 sensitivity 0.95 # 同时排除坡度过大一般取坡度 30度 的剔除 # 排除覆盖水体、建成区等非植被地类的footprint这套筛选逻辑执行完后大约30%-50%的footprint会被保留下来别心疼留下干净样本的模型才是真准。3. 样本构建与特征提取模型好坏这一步定了七八成3.1 从GEDI HDF5中提取建模数据以L2A数据为例用h5py读取HDF5文件。GEDI的组织方式是光束组内嵌套shot每个shot对应一束激光footprint。核心提取代码如下import h5py import numpy as np import pandas as pd def extract_gedi_l2a(filepath, beamBEAM0101): with h5py.File(filepath, r) as f: beam_group f[beam] lon beam_group[geolocation/longitude][:] lat beam_group[geolocation/latitude][:] # 冠层高度特征rh0-rh100各百分位高度 rh_metrics {} for i in range(0, 101, 5): rh_metrics[frh{i}] beam_group[frh_metrics/rh{i}][:] # 质量标识 quality beam_group[quality_flag][:] degrade beam_group[degrade_flag][:] sensitivity beam_group[sensitivity][:] # 地表坡度 slope beam_group[geolocation/slope][:] # 光束灵敏度也是重要筛选条件 beam_sensitivity beam_group[geolocation/beam_sensitivity][:] df pd.DataFrame({ longitude: lon, latitude: lat, **rh_metrics, quality_flag: quality, degrade_flag: degrade, sensitivity: sensitivity, slope: slope }) return df # 循环读取研究区所有GEDI文件然后拼接 all_dfs [] for f in gedi_files: shot_df extract_gedi_l2a(f) all_dfs.append(shot_df) df pd.concat(all_dfs, ignore_indexTrue) # 严格筛选 df df[(df[quality_flag] 1) (df[degrade_flag] 0)] df df[df[sensitivity] 0.95] df df[df[slope] 30]L4A的AGBD估计值提取方法类似把文件里的agbd字段选出来。但要注意L4A和L2A的shot编号在光束内是一一对应的要按beam和shot_number做内连接匹配这一步不要弄错否则特征和标签对不上号模型学出来的关系全是错的。3.2 从Sentinel-2影像提取特征拿到GEDI筛选后的footprint坐标之后就要从Sentinel-2影像里提取特征。我这里用的是以footprint为中心做缓冲区统计的方法因为GEDI footprint的真实覆盖范围是直径25米的圆形区域而一个Sentinel-2像素是10米x10米两者不是完全重合的关系直接取单点像素的反射率会损失信息。更合理的做法是对每个footprint中心点构建半径12.5米的圆形缓冲区然后统计这个缓冲区覆盖到的Sentinel-2像素的均值、中位数、标准差作为该footprint的光谱特征向量。import geopandas as gpd from shapely.geometry import Point import rasterio from rasterio.mask import mask # 坐标投影统一 gdf gpd.GeoDataFrame(df, geometrygpd.points_from_xy(df.longitude, df.latitude)) gdf.set_crs(epsg4326, inplaceTrue) gdf gdf.to_crs(epsg32650) # 按研究区UTM分区调整 # 构建缓冲区 gdf[buffer] gdf.geometry.buffer(12.5) gdf_buffer gdf.set_geometry(buffer) # 读取Sentinel-2的波段数据 with rasterio.open(sentinel2_10m.tif) as src: band_names [B2, B3, B4, B8, B5, B6, B7, B8A, B11, B12] features [] for _, row in gdf_buffer.iterrows(): geom [row[buffer]] out_image, out_transform mask(src, geom, cropTrue) feat_dict {shot_id: row[shot_number]} for i, band in enumerate(band_names): band_data out_image[i] valid_data band_data[band_data 0] # 剔除nodata if len(valid_data) 0: feat_dict[f{band}_mean] np.mean(valid_data) feat_dict[f{band}_std] np.std(valid_data) else: feat_dict[f{band}_mean] np.nan feat_dict[f{band}_std] np.nan features.append(feat_dict) feature_df pd.DataFrame(features)这里面有个性能问题如果研究区里有几千甚至上万个footprint逐个做掩膜裁剪会很慢。我的习惯是先做一次空间连接把footprint覆盖到的所有像素一次性提取出来再分组聚合速度能提升十几倍。代码写起来长一些但处理规模化数据时非常值得。3.3 衍生特征与样本去重除了原始波段的反射率均值我在实际项目中还会加入一系列衍生特征这些特征对模型精度的提升帮助非常明显植被指数NDVI、EVI、SAVI、NDVIre红边NDVI、MCARI叶绿素相关指数地形因子从数字高程模型提取的高程、坡度如果GEDI自带的有就直接用纹理特征基于灰度共生矩阵计算的对比度、均匀度能捕获冠层的纹理粗糙度这些特征和AGBD的物理关联都很直接。以NDVIre为例红边区域是植被反射率从红光低值到近红外高值之间的陡峭过渡区它比传统的NDVI对植被叶绿素含量和冠层结构变化更敏感在高生物量区域不容易饱和。样本建好后还有一个容易被忽视的坑空间重叠。GEDI的footprint之间偶尔会有重叠或者同一个地理位置在多个时间段被多次观测这会导致训练集和验证集之间出现空间相关的样本验证精度虚高。处理方式是做一个简单的空间去重对任意两个footprint如果中心点距离小于25米只保留其中一个。3.4 时间对齐别让不同年份的数据混在一起GEDI任务在2019年启动Sentinel-2A/B双星则持续提供影像同一个footprint位置可能有好几期的Sentinel-2观测。AGBD的日变化虽然不明显但季节性落叶林、农田等人为管理地类的生物量变化很大最稳妥的做法是限定一个月的时间窗口让GEDI的观测时间和Sentinel-2影像的获取时间尽量接近。我在处理热带雨林和温带常绿林时会把时间窗口放宽到正负60天效果还可以接受。但如果研究区里有明显的落叶阔叶林建议把窗口压缩到30天以内否则模型会把季节性的光谱变化当作生物量差异去学习结果一到做年度制图时就会出问题。4. 随机森林建模与超参数调优别一上来就GridSearch4.1 数据处理和训练集划分样本整理好后建模这块我习惯分四步走清洗、切分、训练、调优。清洗阶段检查每列特征是否有缺失值出现缺失值就填充该列中位数检查方差为零的常数列直接删除对特征做简单的相关性分析相关系数超过0.95的保留一个这一步能有效减少冗余特征对模型可解释性的干扰。切分阶段千万不能简单用train_test_split随机划分这会因为空间自相关导致验证集和训练集之间的样本距离很近模型几乎是在背答案。我的做法是按1km x 1km的网格对研究区做空间分块把网格划分到训练集或验证集再提取网格内的样本。这样一来训练集和验证集在空间上完全分离得到的精度指标才是真实水平的估计。from sklearn.ensemble import RandomForestRegressor from sklearn.metrics import r2_score, mean_squared_error, mean_absolute_error from sklearn.model_selection import RandomizedSearchCV import numpy as np # X为特征矩阵y为AGBD标签 X feature_df.drop(columns[agbd]).values y feature_df[agbd].values # 这里假设已经生成了空间分块后的索引 train_idx 和 val_idx X_train, X_val X[train_idx], X[val_idx] y_train, y_val y[train_idx], y[val_idx]4.2 随机森林核心参数理解随机森林有四个核心超参数需要理解透彻分别对应着建多少棵树每棵树能长多深每棵树用多少特征每个节点最少留多少样本。n_estimators是森林里决策树的数量数量太少模型欠拟合数量太多训练时间线性上升但精度提升会进入平台期。我通常从200开始试如果验证集精度持续上升就加到500如果200到500之间精度变化小于1%就停在200。max_depth控制树的生长深度过深会让每棵树记住训练集的细节而不是学习规律过浅则模型能力不足。默认值None表示树一直长到所有叶子节点都是纯的在特征维度高时极易过拟合。我一般从10开始搜索逐步试到30。min_samples_leaf限制叶子节点的最少样本数这是控制过拟合最直接的工具。它的工作原理是即使当前节点还能继续分裂如果分裂后某个子节点样本数少于阈值就禁止分裂。阈值提高后每棵树变得更粗糙泛化能力反而上升。max_features是每个分裂节点随机选取的特征数量。默认值是特征总数的三分之一针对回归任务偏低会让每棵树的多样性增加但单棵树的预测能力下降偏高则多样性不足。常见做法是在0.3到0.7之间搜索。4.3 用随机搜索代替全网格搜索网格搜索把所有参数的组合全部跑一遍理论上能找到最优组合但参数一多计算量指数级增长。随机搜索在参数空间中随机采样固定数量的组合虽然不能保证找到全局最优但能以远低于网格搜索的计算量找到足够好的结果。我用的是RandomizedSearchCV参数空间设置如下param_dist { n_estimators: [200, 300, 400, 500], max_depth: [10, 15, 20, 25, 30], min_samples_leaf: [1, 2, 4, 6, 8], max_features: [0.3, 0.4, 0.5, 0.6, 0.7], bootstrap: [True, False] } rf_base RandomForestRegressor(random_state42, n_jobs-1) random_search RandomizedSearchCV( rf_base, param_dist, n_iter50, cv5, scoringneg_root_mean_squared_error, random_state42, n_jobs-1 ) random_search.fit(X_train, y_train) best_rf random_search.best_estimator_我建议在做五折交叉验证时用K折要结合前面讲的空间分块逻辑RandomizedSearchCV默认的KFold是随机划分如果不改成空间分块调参阶段就可能因为数据泄漏选出一组看起来很好但实际泛化能力差的参数。训练完成后查看验证集上的表现y_pred best_rf.predict(X_val) print(fR2: {r2_score(y_val, y_pred):.3f}) print(fRMSE: {np.sqrt(mean_squared_error(y_val, y_pred)):.3f} Mg/ha) print(fMAE: {mean_absolute_error(y_val, y_pred):.3f} Mg/ha)4.4 特征重要性的解读与方法选择scikit-learn里RandomForestRegressor的特征重要性默认是杂质减少量它在每次节点分裂时累加该特征带来的杂质方差下降总量。这个指标计算效率高但有个已知缺陷对高基数特征和彼此相关的特征存在偏见。比如两个强相关特征重要性会被它们平分导致每个看起来都没那么重要。想得到更可靠的重要性排序可以用排列重要性permutation importance对验证集样本随机打乱某个特征列的取值观察模型误差上升多少。误差上升越多说明模型对这个特征越依赖。这个方法能真实反映特征在验证集上的贡献程度。from sklearn.inspection import permutation_importance result permutation_importance( best_rf, X_val, y_val, n_repeats10, random_state42, n_jobs-1 ) perm_importance pd.Series( result.importances_mean, indexfeature_df.columns ).sort_values(ascendingFalse)我在项目中做过一个有趣的观察当我把20米分辨率的红边波段重采样到10米并加入特征后模型精度的提升幅度比增加任何单一波段都要大。红边区域705-740nm是植被反射率陡变区它对叶绿素含量和冠层结构的极其敏感这种对生物量变化的高灵敏度正好是随机森林这种非线性模型最擅长利用的信号。所以特征选择时不要只死死抱住10米波段不放红边波段哪怕是20米重采样的和短波红外波段的价值都很值得挖掘。5. 精度验证与空间制图R²不是终点空间分布才是交付物5.1 验证指标的综合解读模型跑完后R²、RMSE、MAE是常见的三个指标但单看任何一个都容易误判。R²容易被极端值拉高RMSE对大误差敏感MAE更稳健但不能反映方差。我习惯把三个指标放在一起看并额外关注偏差bias偏差接近0说明模型在整个生物量范围内没有系统性高估或低估偏差明显为正说明模型输出系统性偏高偏差明显为负说明系统偏低。验证散点图要画出1:1参考线如果散点在低生物量区聚集在1:1线上方、高生物量区聚集在1:1线下方这就是典型的回归压缩效应说明模型对高值区存在饱和需要考虑能否加入更敏感的冠层结构特征来缓解。5.2 生成空间分布图模型验证通过后下一步就是制图也就是对整个研究区的每个10米像素做逐像元预测。做法是准备一张完整的Sentinel-2特征图让它通过随机森林模型的predict方法输出AGBD空间分布图。import numpy as np import rasterio # 假设已经构建了研究区全范围的特征图 feat_stack # feat_stack 的shape为 (n_features, height, width) height, width feat_stack.shape[1], feat_stack.shape[2] feat_flat feat_stack.reshape(feat_stack.shape[0], -1).T # 逐块预测避免内存溢出 block_size 10000 pred_flat np.zeros(feat_flat.shape[0]) for i in range(0, feat_flat.shape[0], block_size): block feat_flat[i:iblock_size] # 处理可能存在的nodata值 valid np.all(np.isfinite(block), axis1) pred_block np.full(block.shape[0], np.nan) if np.any(valid): pred_block[valid] best_rf.predict(block[valid]) pred_flat[i:iblock_size] pred_block agbd_map pred_flat.reshape(height, width) # 写GeoTIFF with rasterio.open(sentinel2_10m.tif) as src: profile src.profile.copy() profile.update(dtyperasterio.float32, count1, nodatanp.nan) with rasterio.open(agbd_map.tif, w, **profile) as dst: dst.write(agbd_map, 1)逐块预测这一步别省。整幅影像的特征矩阵很容易超过几百万行一次性全部灌进内存很容易把电脑跑死分块处理更稳妥。预测完成后用matplotlib或qgis打开生成的agbd_map.tif叠加行政边界和地形晕渲一起看。5.3 空间制图后的质量检查做完整幅AGBD制图后我强烈建议做一个残差检验把你没参与建模的实测样地数据叠加到预测图上在每个样地点位上提取预测值和实测值做精度对比。这一步能暴露模型在空间外推时存在的问题纯粹用交叉验证指标是看不出来的。具体操作不复杂用样地坐标构建点矢量用rasterio的sample方法提取对应位置的像元值和实测AGBD做R²和RMSE计算。如果结果比验证集差很多大概率是研究区内部存在某种环境梯度比如干湿季节差异、土壤类型差异没有被训练样本覆盖到。5.4 从AGBD到碳储量的延伸拿到AGBD空间分布图后就可以按公式换算成碳储量和二氧化碳当量。国内外通用的做法是生物量乘以0.47的含碳率得到碳储量再乘44/12的分子量比换算成二氧化碳当量。agbd_map rasterio.open(agbd_map.tif).read(1) carbon_map agbd_map * 0.47 # 碳储量 Mg C/ha co2_map carbon_map * 44 / 12 # 二氧化碳当量 Mg CO2/ha # 统计研究区总碳储量 cell_area 10 * 10 / 10000 # 每个像素面积公顷 total_carbon np.nansum(carbon_map) * cell_area print(f研究区总碳储量: {total_carbon:.1f} Mg C)这个输出结果可以直接对标IPCC森林温室气体清单的方法学要求。做森林碳汇项目的朋友走到这一步基本就能交付了。6. 我在实际项目中踩过的三个坑先说时间匹配的坑。有一回我图省事把研究区所有时间的GEDI数据和单期Sentinel-2影像直接配对建模验证集精度R²能到0.78但做出来的制图结果在落叶林区域完全不能用明显是高估了冬季落叶期的生物量。后来排查才发现夏季影像对应秋季的GEDI观测光谱信号和真值之间的时间差带来的季节性差异全被模型学习进去了。从那以后时间窗口这道工序我一直严格守着。再说空间交叉验证的坑。这是我最想提醒各位的用默认的随机KFold交叉验证做调参R²能到0.75但换成1km空间分块验证R²直接掉到0.45。这个差距就是空间自相关带来的数据泄漏。原因是森林在空间上表现出强烈的连续性随机切分时训练集和验证集靠得很近的样本光谱特征几乎一样模型背题背得太轻松。换成空间分块后训练集和验证集在研究区上完全分离验证精度才是真实外推水平。最后说GEDI数据筛选的坑。我在早期建模时没有加degrade_flag筛选结果模型在个别高生物量区域出现极其离谱的负值预测。翻看数据才发现是激光器退化后波形质量变差导致rh100冠层高度被严重低估。GEDI处理手册里明确要求筛选掉degrade状态的数据这个标识不是摆设能省掉后面大量排查时间。这套流程走通之后往后换研究区、换传感器都是照着管道填新数据的事。你也可以在这个基础上把随机森林换成其他回归模型但核心思路——激光雷达提供真值样本、光学影像提供连续特征、机器学习建模桥接——不会变。做遥感碳汇项目这套组合是目前区域尺度上可靠性和可落地性的平衡点。本文还有配套的精品资源点击获取