ARTICLE DETAIL

资讯详情

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

GEDI与Sentinel-2结合随机森林:森林地上生物量密度制图全流程

GEDI与Sentinel-2结合随机森林:森林地上生物量密度制图全流程 简介针对遥感与机器学习交叉应用场景这份PDF指南演示了如何整合GEDI、Sentinel-2与随机森林模型实现地上生物量密度AGBD建模。以Mafungautsi森林保护区为测试区内容覆盖Google Earth Engine账户初始化与认证、Sentinel-2合成影像创建、光谱指数计算、SRTM高程和坡度数据加载、训练/测试样本准备、随机森林回归训练、模型性能检查以及最后的结果预测与可视化。除了逐步代码实现还系统讲解了GEDI L4A数据集的生成原理、RH指标含义及各步骤背后的技术逻辑并专门讨论过拟合现象和提升泛化能力的策略。适合具备一定遥感与Python基础、关注森林碳储量和生产力评估的研究人员与工程师。压缩包共1个文件为PDF文档大小141KB结构完整可边读边练。目前已有101人学习下载是快速上手多源遥感数据生物量建模的简洁参考。1. GEDI、Sentinel-2与随机森林把森林碳储变成一块能“查”的密度图把 GEDI、Sentinel-2 和随机森林这三个词放在一起通常是这样一个场景手里有 GEDI 激光雷达点测的冠层高度波形有 Sentinel-2 光学影像的连续覆盖却缺一张逐像元的地上生物量密度AGBD单位 Mg/ha分布图。GEDI 的脚印直径约 25 米沿轨道采样密度高但不成面Sentinel-2 有 10 米到 20 米的空间分辨率但光学信号在茂密森林里容易饱和。随机森林在这里扮演的是“融合器”把 GEDI 的垂直结构、Sentinel-2 的光谱与纹理、地形辅助数据一起回归到生物量密度再用训练好的模型外推到整个影像覆盖区。这套流程适合做碳汇监测、林业二类调查预判、生态学研究生实验也适合给没有机载 LiDAR 的团队提供一条低成本替代路径。本文用 Python 从数据准备讲到最后出图并把你最可能翻车的几个点一次说透。2. 数据准备让 GEDI 足迹和 Sentinel-2 像元对齐一步一坑2.1 用合成样本先跑通结构与真实 GEDI 对齐的最小数据集新手拿到 GEDI L2A 的真实 HDF5 文件后第一关就卡在“数据怎么读、单位是什么、字段怎么对”。我的建议是先别碰真实数据用合成数据把“足迹级特征 → 随机森林 → 密度图”这条链路跑通确认代码逻辑没问题再回头处理真实文件的格式问题。下面的脚本合成 2000 个 GEDI 足迹级别的样本包含冠层高度 rh98、坡度、Sentinel-2 波段特征以及作为训练标签的 AGBD。字段命名和真实数据保持一致方便之后直接替换数据源。import numpy as np import pandas as pd rng np.random.default_rng(42) n 2000 # GEDI L2A 的 rh98冠层高度第98百分位数单位米真实范围约 0~45 rh98 rng.beta(2, 5, n) * 45 # 模拟 Sentinel-2 20m 格网上提取的波段均值 # 冠层越密红光越低近红外越高这是植被光谱的基本趋势 b4 0.04 - 0.0002 * rh98 rng.normal(0, 0.005, n) b8 0.30 0.001 * rh98 rng.normal(0, 0.01, n) ndvi (b8 - b4) / (b8 b4 1e-6) # GEDI 脚印范围内的平均坡度单位度 slope rng.uniform(0, 35, n) # 地上生物量密度标签单位 Mg/ha参考值域约 20~350 agbd 11.5 * rh98**0.62 0.4 * slope rng.normal(0, 8, n) # 模拟轨道编号每 400 个足迹属于同一条轨道后面 GroupKFold 要用 track_id np.arange(n) // 400 df pd.DataFrame({ rh98: rh98, b4: b4, b8: b8, ndvi: ndvi, slope: slope, agbd: agbd, track_id: track_id, }) print(df.describe())代码里的rh98**0.62是故意模拟的生物量异速生长关系冠层高度与生物量是亚线性关系30 米高的林子对应约 100~120 Mg/ha这与温带森林的常见量级一致。b4和b8的合成公式用了“冠层越密、红光吸收越强、近红外反射越高”的物理趋势这样随机森林能从中学到真实光谱与结构的相关性而不是纯噪声。需要注意真实 GEDI 数据里一个足迹对应一条记录Sentinel-2 特征是从足迹所在像元及其邻域聚合出来的而不是单像素取值。合成数据阶段可以不管空间关系但真实数据处理时这一点是必修课后面 2.3 节专门讲。2.2 真实 GEDI L2A 读取HDF5 字段、单位与质量筛选真实 GEDI L2A 产品是 HDF5 格式打开后你会看到BEAM0000到BEAM1011这一组 beam 组名。注意并非所有 beam 都有有效数据还需要通过质量标志筛选。import h5py with h5py.File(GEDI02_A_20200723154000_XXXXX_XXXXX_XXXX_XX002000T.H5, r) as f: beams [k for k in f.keys() if k.startswith(BEAM)] print(beams)筛选之后逐个 beam 读取。以下脚本读取经纬度、rh98、质量标志和灵敏度返回一个干净的 DataFramedef read_gedi_beam(path, beamBEAM0000): with h5py.File(path, r) as f: g f[beam] # latitude_ms / longitude_ms 单位是 10 毫角秒并不是毫秒 # 换算1 度 3,600,000 毫角秒 lat g[geolocation/latitude_ms][:] / 3600000.0 lon g[geolocation/longitude_ms][:] / 3600000.0 # rh 字段形状为 (shot_count, 100)第 99 个槽位对应 rh98 # 不要取 rh100波形尾部噪声会把高度抬得很高 rh98 g[rh][..., 98] qf g[quality_flag][:] degrade g[degrade_flag][:] sens g[sensitivity][:] df pd.DataFrame({ lon: lon, lat: lat, rh98: rh98, quality_flag: qf, degrade_flag: degrade, sensitivity: sens, }) # 质量筛选flag1, 非降级, 灵敏度高于 0.95 df df[(df[quality_flag] 1) (df[degrade_flag] 0) (df[sensitivity] 0.95)] return df.drop(columns[quality_flag, degrade_flag, sensitivity])这里有几个字段容易读错。rh是形状为(shot, 100)的波形分位数数组rh[..., 98]是第 98 个分位数的冠层高度通常作为森林冠层高度的代表值而不是直接用rh[..., 100]或rh[..., -1]。latitude_ms的ms是 milliarcsecond不是 millisecond单位换算错误会让坐标偏出几百度。这些字段的取值和单位在 GEDI L2A 用户指南里都有明确说明处理前最好对照一遍。另外GEDI 原始坐标是 WGS84 经纬度。之后和 Sentinel-2 对齐时最好把足迹投影到与影像一致的 UTM 坐标系不要在经纬度下直接做缓冲区分析否则不同纬度上的距离变形会直接影响 3×3 窗口的聚合结果。2.3 Sentinel-2 预处理20m 重采样、云掩膜与特征聚合Sentinel-2 的 10 米波段B2、B3、B4、B8和 20 米波段B5、B6、B7、B8A、B11、B12分辨率不同。GEDI 脚印直径约 25 米和 20 米像元尺度更匹配所以常见的做法是把所有波段统一重采样到 20 米。下面这段代码用 rasterio 把 10 米波段降采样成 20 米import rasterio from rasterio.enums import Resampling src_path S2_B8_10m.tif dst_path S2_B8_20m.tif with rasterio.open(src_path) as src: data src.read( 1, out_shape(int(src.height // 2), int(src.width // 2)), resamplingResampling.average, ) profile src.profile profile.update( heightdata.shape[0], widthdata.shape[1], transformsrc.transform * src.transform.scale(2, 2), ) with rasterio.open(dst_path, w, **profile) as dst: dst.write(data, 1)out_shape的高宽除以 2对应 10 米到 20 米的降采样倍数transform.scale(2, 2)同时把像元尺寸变为原来的两倍。重采样方法选average因为生物量密度是连续量取均值比最近邻更平滑也比双线性插值更抗噪声。如果源数据是 10 米和 20 米混合输入建议先把所有波段统一到 20 米再合成特征栈避免 RF 在特征间学到分辨率差异造成的伪影。云掩膜是整个流程里最容易被低估的一步。我常用的做法是基于 Scene Classification LayerSCL做白名单过滤只保留 SCL 值等于 4植被和 5非植被覆盖的像元。这个规则偏保守但能最大程度避免云、云影和薄雾污染训练样本。掩膜之后的统计数据可以作为特征矩阵的一部分比如“足迹周围 3×3 窗口内有效像元比例”低于 70% 的足迹直接丢弃。足迹与影像对齐时建议先建特征栈再按坐标提取。以下代码演示真实场景下的提取逻辑import geopandas as gpd import rasterio stack_path s2_20m_stack.tif gdf gpd.read_file(gedi_footprints.gpkg) with rasterio.open(stack_path) as src: gdf gdf.to_crs(src.crs) rows, cols rasterio.transform.rowcol( src.transform, gdf.geometry.x.values, gdf.geometry.y.values, ) band_data src.read() # 假设波段顺序是 B4, B8, B11, NDVI b4_vals band_data[0, rows, cols] b8_vals band_data[1, rows, cols]这里rowcol返回的是整数行列号对应每一个足迹中心所在的像元。如果要做 3×3 邻域聚合需要把rows、cols扩展成邻域坐标再取均值实际项目中特征向量通常包含“中心像元值 邻域均值 邻域标准差”三组这样既有空间代表性又不会让特征维度爆炸。聚合时重点关注窗口是否越界靠影像边缘的足迹要么补边缘值要么直接剔除否则模型在边缘附近会出现条带预测。3. 随机森林建模从光谱饱和到垂直结构特征怎么设计才靠谱3.1 特征设计光学波段在茂密森林里为什么会饱和Sentinel-2 的 NDVI 在低生物量区域50 Mg/ha与 AGBD 有较好的线性关系但冠层覆盖度接近 100% 之后NDVI 的变化开始趋平这就是常说的“光学遥感饱和问题”。这时候 GEDI 的垂直结构指标反而成为主导变量rh98 直接描述冠层高度rh50、rh75 等分位数描述冠层内部垂直分布这些信息与生物量的机械关系比光谱更强。因此特征矩阵至少要包含三类特征类别具体字段作用GEDI 垂直结构rh50、rh75、rh98打破光学饱和提供冠层高度与垂直分布Sentinel-2 光谱B4、B8、B11、B12 均值与标准差提供树种、郁闭度、水分差异衍生指数与地形NDVI、NDMI、坡度、坡向校正地形阴影和水分对反射率的影响特征不是越多越好。遥感随机森林模型里特征数翻倍往往带来的是训练时间翻倍和泛化能力下降而不是精度提升。我一般控制在 10~15 个特征以内。地形数据如果没有现成的 DEM用elevation的坡度提取替代不要为了凑特征硬加相关性低的纹理变量。在特征重要性解释上随机森林给出的feature_importances_有一定随机性不要把它当成绝对结论。如果你发现某一次运行 B4 的重要性突然超过 rh98先检查是不是云掩膜没做干净而不是急着调整模型。3.2 随机森林回归的三个必调参数从默认参数到可用模型随机森林回归算法在 scikit-learnsklearn里的实现非常成熟默认参数能跑通但直接用在遥感生物量建模上往往会出现“测试集 R² 高、实际出图花斑”的问题。需要重点调三个参数from sklearn.ensemble import RandomForestRegressor from sklearn.model_selection import train_test_split features [rh98, rh50, b4, b8, b11, ndvi, slope] X df[features] y df[agbd] X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42 ) rf RandomForestRegressor( n_estimators500, max_depth25, min_samples_leaf2, oob_scoreTrue, n_jobs-1, random_state42, ) rf.fit(X_train, y_train) print(train R2:, rf.score(X_train, y_train)) print(test R2:, rf.score(X_test, y_test)) print(OOB R2:, rf.oob_score_)n_estimators取 500 是性价比拐点400 棵以下时精度随树数增加明显800 棵以上的提升通常不足 0.01。max_depth设为 25 左右限制单棵树深度可以防止树对空间噪声过拟合纯遥感数据上 15~30 都合理具体取决于样本量。min_samples_leaf这个参数最容易被忽略它控制叶子节点最少样本数设为 2~5 可以显著减少预测图中的盐椒噪声。如果预测值经常出现极端值优先加大min_samples_leaf而不是调低。oob_score打开后可以拿到袋外估计 R²它比测试集 R² 更接近真实泛化表现因为每个样本的预测来自没参与该样本训练的树。输出里 train R² 接近 1 是正常的不用紧张真正要看的是 test R² 和 OOB R² 之间的落差。如果 test 0.85、OOB 0.45说明数据划分有问题或者特征里有和标签直接相关的泄漏字段。3.3 交叉验证不要随机打乱按轨道分组才是真实预测评估这是遥感随机森林中最容易被忽视、也最容易让人产生“模型很准”错觉的环节。GEDI 数据沿轨道采集同一轨道上相邻足迹之间的生物量高度自相关。如果直接train_test_split随机划分训练集和测试集里各有一半来自同一个轨道模型实际上在预测“隔壁邻居”R² 虚高。正确的做法是用GroupKFold按轨道分组让同一条轨道的足迹只出现在同一折里from sklearn.model_selection import GroupKFold, cross_val_score # 用 2.1 节合成的 track_id 作为分组依据 groups df[track_id] cv GroupKFold(n_splits5) scores cross_val_score(rf, X, y, cvcv, groupsgroups, scoringr2) print(GroupKFold R2: %.3f /- %.3f % (scores.mean(), scores.std()))真实数据里如果拿不到track_id可以用shot_number // 400近似因为一条轨道通常有几百个连续 shot。实际踩坑经验是随机打乱的 5 折交叉验证 R² 可能到 0.83而按轨道分组后直接掉到 0.57这个落差才是模型真实泛化水平的镜面。遥感机器学习入门者最容易在这里踩坑——不是模型不行而是评估方式给了你虚假的信心。评估时还要注意scoringr2对不同生态区的适用性。生物量密度范围越宽R² 天然越高范围窄的老龄林即便模型预测误差很小R² 也可能只有 0.3这时候补充 RMSE 和 MAE 一起看。如果分组后 RMSE 仍然小于 30 Mg/ha模型在实际应用中是有价值的不必只盯 R²。4. 避坑GEDI 和 Sentinel-2 生物量建模的 5 个翻车现场4.1 翻车rh98 和 AGBD 单位没换算预测值直接到五位数现象训练时各项指标正常出图后预测值动不动上万 Mg/ha明显超出物理可能。原因GEDI 的rh98单位是米Sentinel-2 地表反射率有的产品是 0~10000 的整型值。某个步骤漏了反射率除以 10000或者把 100 个波形分位数当成一个特征向量直接喂进去RF 就会从错误的量纲关系里学到虚假的放大系数。解决建模前先打印df.describe()看各字段值域。Sentinel-2 的反射率如果是uint16范围 0~10000统一除以 10000AGBD 标签要确认单位是 Mg/ha 而不是 g/m²。预测完成后对比rf.predict(X_train)的最大值与y.max()如果预测上限超出训练标签 50% 以上回去查特征值域。4.2 翻车云没掩膜干净云影区域被认成低生物量沙漠现象密度图上大面积出现零值空洞或条纹状低值且位置与影像上的云影位置重合。原因SCL 白名单设置太宽松把 SCL8、9 的云像元混进了特征矩阵或者薄云下垫面的反射率被当成真实地表信号。RF 学到“高亮度像元 → 低生物量”的假规则。解决SCL 只保留 4 和 5云影检测再加一道 NDVI 下限过滤NDVI 低于 0.15 的植被足迹直接剔除。更稳的做法是统计足迹 3×3 窗口内的有效像元比例低于 70% 就丢弃。别心疼样本量被云污染的特征对模型的伤害远大于少几百个训练样本。4.3 翻车交叉验证 R² 0.85换一个地区直接变负数现象同一景影像内部验证精度很好模型搬到相邻区域或另一个生态区后 R² 直接为负。原因训练和验证足迹来自同一轨道空间自相关导致信息泄漏同时随机森林外推能力很差新区域的特征值落在训练范围外时预测会收敛到树节点均值附近而不是合理外推。解决交叉验证用GroupKFold按轨道分组别用随机KFold。模型外推前先检查目标区域每个特征的取值范围是否落在训练集范围内尤其是rh98和波段反射率。如果目标区存在训练集没见过的高度等级或坡度建议补充采样再训练或者至少在成果图上标注模型的适用范围。4.4 翻车预测图出现负生物量现象青壮林区域预测为 -30 Mg/ha 或更低明显违反物理常识。原因min_samples_leaf太小某些叶子节点的样本均值被极端低值样本拉为负值。当rh98很小且坡度很大时树拟合出的叶节点预测值可能低于零。这属于过拟合的显性表现。解决先加一道“后悔药”式的预测裁剪import numpy as np pred np.clip(rf.predict(X_all), 0, 500)但裁剪只是止血。检查验证集中负值样本比例如果超过 1%把min_samples_leaf从 1 调到 3 或 5 重新训练再配合n_estimators500一起观察 OOB 误差。负值比例降到 0.1% 以下才算正常。4.5 翻车按图幅分块建模拼接处出现明显接缝现象输出大面积密度图时按 1000×1000 像元分块训练各块单独训练模型拼接后块与块之间出现台阶状突变。原因每块的数据分布、样本量不同随机森林学到了不同的特征-标签关系更隐蔽的一个原因是特征聚合时窗口跨越了分块边界边界像元的邻域统计量比内部像元少特征分布不一致。解决整个研究区只训练一个全局模型。推理时按块加载预测没问题但训练、特征聚合必须全局统一。如果内存确实是瓶颈合理的顺序是全量采样 → 训练保存 → 分块预测 → 拼接写盘。分块预测的每个块都要使用同一套特征提取函数尤其是 3×3 邻域窗口在块边界处的填充方式要保持一致。5. 验证与落地用蒙特卡洛方差输出一张带不确定性的 AGBD 图模型训练完成之后最常被问的问题是这张密度图的可靠性是不是只能用一个 R² 表达其实随机森林自带一个轻量化的不确定性估计工具——森林内部分歧。每棵树的预测结果相当于从不同子空间采样的估计预测值之间的标准差可以当作逐像元的不确定性指标。from joblib import load import numpy as np import rasterio model load(agbd_rf.joblib) with rasterio.open(s2_20m_stack.tif) as src: cube src.read() h, w src.height, src.width profile src.profile X_tile cube.reshape(cube.shape[0], -1).T # 取前 50 棵树做不确定性估计不需要全部 500 棵 tree_preds np.stack([ t.predict(X_tile) for t in model.estimators_[:50] ], axis0) pred_mean np.clip(tree_preds.mean(axis0), 0, 500) pred_std np.clip(tree_preds.std(axis0), 0, 120) profile.update(count1, dtypefloat32, compresslzw) with rasterio.open(agbd_mean.tif, w, **profile) as dst: dst.write(pred_mean.reshape(h, w), 1) with rasterio.open(agbd_std.tif, w, **profile) as dst: dst.write(pred_std.reshape(h, w), 1)pred_std不是严格意义上的统计置信区间它反映的是模型内部对同一输入的不同判断。标准差高的区域通常对应训练样本稀疏、地形复杂或光谱与结构模式不明确的地方。抽查这些高不确定区域比单纯报一个总体 R² 更能说明模型的适用边界。整幅影像直接reshape预测只适合内存充足的场景。我处理的第一个真实项目是 1 亿像元的全幅数据这样直接展开就把 32GB 内存吃满了后来改成按block_windows逐块读取、先写大数组再落盘。另一个习惯是出图后不要只看均值图把pred_std和云掩膜叠加检查一遍高不确定性区域正好落在云影或地块边界上说明特征聚合窗口有偏差回去修正比以后再补一次标签省事得多。这套流程从合成数据到真实数据、从单点预测到全图出值每一步都留了校验口希望帮到你。本文还有配套的精品资源点击获取
返回列表