Google Earth Engine监督分类实操:从样本到分类器调参
干了这么多年遥感接触Google Earth Engine也有段时间了如果非要挑一个GEE里最常用、也最容易被低估的功能我会选监督分类Classification。很多刚入门的同学总觉得分类就是点几个样本、跑一个分类器、出图这么简单但实际做下来会发现从样本选取到分类器调参每一步都藏着坑。这篇内容不端架子直接把我用GEE做监督分类的完整思路、代码解析、踩坑记录一次讲透适合刚接触GEE的初学者也适合做土地利用/覆被分类做到头疼的同行。1. 监督分类的整体逻辑GEE为什么适合干这事1.1 监督分类的本质教会计算机认地物先摆一个底层逻辑监督分类不是让计算机凭空识别地物而是用已知类别的样本去训练一个分类器再用训练好的分类器去预测未知区域。这个过程和人认东西很像——你告诉一个小孩带四条腿、会喵喵叫的是猫他见过几次之后再看到猫就能认出来。GEE里的监督分类也一样你在影像上标出水体、林地、建筑的样本点/多边形每个样本点上都有对应的光谱值比如Landsat的蓝、绿、红、近红外波段分类器学习这些光谱特征与类别标签之间的关系最后把整个影像的每个像元都代入这个关系给它打一个类别标签。这套逻辑在GEE里跑起来特别顺因为GEE天然处理的是影像集合 矢量样本的组合你不用像传统桌面软件那样先把影像裁到本地、再把样本转成特定格式直接在云上操作即可。1.2 为什么选GEE而不选传统桌面软件我早期做分类用的是传统桌面遥感软件流程极其繁琐影像预处理、样本ROI导出、格式转换、分类参数调半天、最后还要重新导入验证。十次有八次时间花在数据搬家上真正花在分类思路上的时间很少。换到GEE之后流程被大幅压缩步骤传统桌面软件GEE数据获取下载、裁剪、辐射定标直接引用云上数据目录样本制作手动绘制ROI再导出直接在图上画点/多边形分类器训练参数面板反复试一行代码调用算法精度验证单独工具计算混淆矩阵内置confusionMatrix()结果分享导出栅格再发送直接生成地图链接/导出最关键的一点是可复现性传统软件里你点鼠标完成的每一步很难被记录成文本但GEE的代码本身就是完整的实验记录换一台电脑、换一个区域只要改一下影像ID和样本坐标同一个脚本就能重新跑一遍。这对科研和业务项目都很重要审稿人/甲方要你的处理流程直接甩代码链接就行。1.3 常见分类器选型别让默认配置害了你GEE的ee.Classifier下面有十几种分类器但我实际用下来出场率最高的就几类各有脾气CART决策树快非常快解释性强但单棵树容易过拟合精度一般。适合做快速预实验、看个大概趋势。RandomForest随机森林我默认首选。它通过多棵树投票抑制过拟合对光谱噪声不敏感参数少且稳定几轮实验下来准确率通常比单棵树高一大截。SVM支持向量机小样本高维特征下效果很能打但GEE里对大数据量训练比较吃内存参数调起来也更讲究核函数、惩罚系数。NaiveBayes朴素贝叶斯快但假设特征独立对多波段强相关的遥感数据不算友好用得少。我个人的建议是第一次跑通流程用CART正式实验用RandomForest如果样本量小且类别可分性强再考虑SVM。别一上来就追新算法GEE里算法只是工具关键是样本和特征。2. 训练样本准备这一步决定分类精度的80%2.1 样本怎么采点样本还是多边形样本GEE里做监督分类构建训练数据有两条路点样本用ee.FeatureCollection导入点坐标CSV/Shapefile每个点带一个类别属性。操作简单适合样本量大的场景。面样本在影像上手动绘制多边形每个多边形赋一个类别。表面上看只是画得多一点但好处是每个多边形能覆盖几十上百个像元等于一轮标注产生大量训练样本而且能更好覆盖地物光谱的变异性。我踩过的坑是只画点样本结果分类结果在边界区域像撒芝麻一检查发现是样本点太稀疏部分地类的光谱没被充分覆盖。后来改成面样本为主、点样本补充零星地类的做法精度提升非常明显。实操建议面样本多边形不要太大但每个类别至少画20-30个分散的多边形覆盖影像中的不同亮暗区域。如果一个类别的光谱变异性大比如农田里既有水田又有旱地就多画几个多边形把这些情况都包进去。2.2 样本质量自查清单这部分是很多人忽视的隐形门槛我直接列一份自查清单类别体系是否清晰先想清楚分几类。见过有人一口气分15类结果混淆矩阵一塌糊涂——类别太细、光谱重叠太严重分类器根本分不开。样本是否落在正确的位置用高分辨率影像叠加检查确保样本没画在边界混合像元上比如水陆交界处。样本是否覆盖整个研究区如果研究区很大样本必须散布在整个区域而不是只集中在局部。分类器只会学它见过的光谱没见过的区域就会乱分。类别光谱是否可分光谱可分性差的两个类别例如裸土和建筑中的部分像元分类器很难愉快地区分必要时需要合并类别或加入新的特征波段。2.3 特征波段选择不只是全选很多人做分类时直接把影像所有波段丢进去训练这其实是个坏习惯。多波段确实提供了更多信息但无关波段和相关波段会引入噪声、拖慢训练速度。我常用的特征构建思路基础光谱波段例如Landsat 8的蓝、绿、红、近红、短波红外它们直接反映地物反射特征。指数特征NDVI植被、NDWI水体、MNDWI建筑/水体分离、BSI裸土等这些指数把多波段信息压缩成单一指标能有效放大类别间差异。纹理/辅助特征如果分的是城市类别我还会加纹理特征如灰度共生矩阵或DEM高程如果做山区分类高程和坡度对区分林型、草地极其有用。一个补充经验GEE里有ee.Image.normalizedDifference和ee.Image.addBands可以很方便地生成指数并拼到原始影像上。实际操作时我会先做一个特征堆叠再通过分类结果反推哪些特征有用、哪些没用。后面章节的代码里会演示这个操作。3. 核心实操从零写好一份完整分类代码3.1 先跑通最小流程Landsat 8监督分类完整代码我以Landsat 8 Level-2地表反射率影像为例展示一个最小可用的监督分类流程。如果你是第一次写GEE分类代码先别急着上复杂功能把这段代码跑通、理解每一步在干什么再往下扩展。// 步骤一加载研究区与影像 // 用世界边界做示例实际使用时换成你自己的研究区 var roi ee.FeatureCollection(FAO/GAUL/2015/level0) .filter(ee.Filter.eq(ADM0_NAME, China)) .geometry(); // 加载Landsat 8 Level-2 SR数据按云量过滤取研究区范围内的影像并合成 var collection ee.ImageCollection(LANDSAT/LC08/C02/T1_L2) .filterBounds(roi) .filterDate(2020-01-01, 2020-12-31) .filter(ee.Filter.lt(CLOUD_COVER, 10)); // 取中位数合成减少云和阴影影响 var image collection.median().clip(roi); // 可视化看一下影像 Map.centerObject(roi, 7); Map.addLayer(image, {bands: [SR_B4, SR_B3, SR_B2], min: 0, max: 3000}, True color);注意这里有一个经常让新手困惑的点Landsat 8 Level-2产品的波段名带SR_前缀因为GEE的数据目录里是不同的处理级别。如果你用的是LANDSAT/LC08/C01/T1_SR旧版本波段名可能是B4等不带前缀但2023年后旧集合下线了现在建议直接用C02版本的T1_L2产品。// 步骤二构建特征波段 // 选择参与分类的光谱波段 var bands [SR_B2, SR_B3, SR_B4, SR_B5, SR_B6, SR_B7]; // 计算NDVI、NDWI、MNDWI等指数并添加到影像上 var ndvi image.normalizedDifference([SR_B5, SR_B4]).rename(NDVI); var ndwi image.normalizedDifference([SR_B3, SR_B5]).rename(NDWI); var mndwi image.normalizedDifference([SR_B3, SR_B6]).rename(MNDWI); // 特征堆叠把原始波段和指数合在一起形成一个多波段“特征影像” var featureImage image.select(bands) .addBands(ndvi) .addBands(ndwi) .addBands(mndwi); print(Feature image bands:, featureImage.bandNames());为什么选择SR_B2到SR_B7而不是全部波段因为B1海岸波段主要对气溶胶敏感、B8全色波段分辨率不同B9卷云波段和B10/B11热红外又各有各的用途强行堆在一起反而让分类器为难。六段光学波段三个指数的组合是性价比很高的默认方案。// 步骤三制作训练样本 // 这里演示用点样本。实际项目中我更推荐面样本但逻辑是一致的。 var trainingPoints ee.FeatureCollection([ // 示例点类别名称放在class属性里 ee.Feature(ee.Geometry.Point([116.35, 39.85]), {class: water}), ee.Feature(ee.Geometry.Point([116.37, 39.87]), {class: forest}), ee.Feature(ee.Geometry.Point([116.39, 39.84]), {class: urban}), // ...实际使用时样本肯定不止这么几个下面会说如何批量导入 ]); // 从影像上提取样本点的光谱特征 var training featureImage.sampleRegions({ collection: trainingPoints, properties: [class], scale: 30 }); print(Training data size:, training.size());关于sampleRegions的三个参数我重点解释一下properties指定从矢量中携带哪些属性到训练样本中这里就是把class带到每个点上scale是采样分辨率一般和影像分辨率一致Landsat30m别乱填小值否则耗时成倍增加默认tileScale等参数新手先不用管。如果你有自己的Shapefile点数据可以直接从Asset上传后导入替换掉trainingPoints那一段即可不用手动一个个写点坐标。// 步骤四训练分类器 // 方案ACART决策树 var classifierCart ee.Classifier.smileCart().train({ features: training, classProperty: class, inputProperties: featureImage.bandNames() }); // 方案B随机森林推荐 var classifierRF ee.Classifier.smileRandomForest({ numberOfTrees: 100, variablesPerSplit: 3 }).train({ features: training, classProperty: class, inputProperties: featureImage.bandNames() });这里有个小细节很多人没注意train()里指定inputProperties为featureImage.bandNames()意味着分类器是用featureImage的所有波段作为输入特征。如果你只想用其中一部分波段就传一个只包含目标波段的列表。波段越多不一定越好但至少代码层面要清楚自己在用什么特征。// 步骤五执行分类与可视化 var classifiedRF featureImage.classify(classifierRF); // 调色板顺序对应类别water/forest/urban var palette [blue, green, red]; Map.addLayer(classifiedRF, {min: 0, max: 2, palette: palette}, Classification RF);这里有一个非常关键的点分类出的图每个像素的值是整数类别编号其含义来自你训练样本属性的类型。如果你用字符串class属性water/forest/urbanGEE会自动将它们编码为0/1/2如果是整数属性1/2/3则直接沿用。可视化时min/max和调色板顺序要跟这些编码严格对应否则图就画反了。3.2 批量制作样本别一张张手画很多教程教你一个个用绘图工具添加点但如果研究区域大、类别多手画能把人画崩溃。推荐两种高效方式导入外部矢量在ArcGIS/QGIS里基于高分辨率影像数字化存成GeoJSON或Shapefile压缩成zip后上传到GEE Assets。注意上传之前确保属性表里有一个类别字段如class字段值统一可以是urban、forest这种字符串也可以是1、2、3这种整数。GEE内置绘图如果你只在GEE里快速试一下用地图左侧的几何图形工具画点/面画完通过geometry导入代码。但这种方式产生的要素没有类别属性你需要为每个几何体单独设置属性再合并。实际操作中我建议加一行代码统一处理var water ee.FeatureCollection(projects/your-project/assets/water_polygons); var urban ee.FeatureCollection(projects/your-project/assets/urban_polygons); // 给不同类别的几何统一打上类别标签再合并 var waterLabeled water.map(function(f) { return f.set({class: 0}); }); var urbanLabeled urban.map(function(f) { return f.set({class: 1}); }); var allSamples waterLabeled.merge(urbanLabeled);3.3 精度验证分类不评精度等于白做跑完分类结果、图很好看没做精度验证等于白做。GEE里做精度验证的正统姿势是把样本拆成训练集和验证集。我习惯用randomColumn函数把样本随机分成70%训练、30%验证// 给样本随机打一个分数再按分数拆分 var sampleAll featureImage.sampleRegions({ collection: allSamples, properties: [class], scale: 30 }).randomColumn(random); var training sampleAll.filter(ee.Filter.lte(random, 0.7)); var validation sampleAll.filter(ee.Filter.gt(random, 0.7)); // 用训练集训练随机森林 var classifier ee.Classifier.smileRandomForest(100).train({ features: training, classProperty: class, inputProperties: featureImage.bandNames() }); // 用验证集做精度验证 var validationClassified validation.classify(classifier); var testAccuracy validationClassified.errorMatrix({ actual: class, predicted: classification }); print(Validation error matrix:, testAccuracy); print(Overall accuracy:, testAccuracy.accuracy()); print(Kappa coefficient:, testAccuracy.kappa());errorMatrix生成的是一个混淆矩阵——矩阵的行代表真实类别列代表预测类别对角线是正确分类的数量。两个关键指标Overall accuracy总体精度所有被正确分类的样本数 / 总样本数。这个值越高越好但类别不平衡时它有欺骗性比如全是水体样本水体分类精度90%整体精度虽然高但林地分得稀烂。Kappa系数排除了随机偶然一致的干扰更严格。一般0.8以上算优秀0.6-0.8算可以0.6以下就得回头看看样本和特征了。3.4 导出分类结果分类结果不能只活在GEE地图里导出到本地/云端才能做进一步分析。常用两种导出方式// 导出为GeoTIFF到Google Drive Export.image.toDrive({ image: classifiedRF, description: landcover_classification, folder: GEE_Exports, fileNamePrefix: landcover_2020, region: roi, scale: 30, crs: EPSG:4326, maxPixels: 1e10 }); // 如果结果要转成矢量可以先转成单波段整数再做reduceToVectors var classifiedInt classifiedRF.toInt(); var vectors classifiedInt.reduceToVectors({ geometry: roi, scale: 30, geometryType: polygon, eightConnected: false });一个隐藏点maxPixels这个参数很多人忽略导出大区域时容易报用户内存限制或超过最大像素数错误直接设成1e10百亿像素量级会减少很多烦恼。分类结果在导出前最好通过set加几个影像属性类别说明、时间、分类器参数方便自己和别人后续使用。4. 高频踩坑从报错信息到结果异常4.1 新手最容易遇到的5个报错把我在各平台群聊里看到的高频问题整理成了一个速查表错误现象根本原因解决思路ImageCollection: No suitable images found影像筛选条件太严格或区域/时间不对把云量阈值放宽到30确认时间范围有数据Cannot compute a trained classifier with no training data训练样本为空或sampleRegions没采到值打印training.size()检查样本量确认点是否落在影像的有效范围内Layer error: Image.constant: Invalid image type分类图的像素值类型不对如浮点型分类结果用toInt()做可视化User memory limit exceeded计算的中间数据量太大加clip(roi)缩小范围用median()合成而不是所有影像逐像元计算Classifier is not trained调用classify()时分类器还没train保证分类器的train()已成功执行检查训练有没有报错4.2 分类结果椒盐现象严重怎么办分类图上出现密密麻麻的离散小斑块俗称椒盐现象主要是像元级分类天然会有的不确定性以及光谱噪声引起的。三个实用的缓解办法平滑后处理用focalMode()或reduceNeighborhood()做众数滤波。例如对分类结果做classifiedRF.focalMode(30)把周围30米内的众数类别赋给中心像元。注意这类滤波会损失细节不要反复用。var smoothed classifiedRF.focalMode(30); Map.addLayer(smoothed, {min: 0, max: 2, palette: palette}, Smoothed RF);使用面向对象分类先做影像分割ee.Algorithms.Image.Segmentation在对象上分类而不是在单个像元上分类。这样能显著减少椒盐现象但代码复杂度上一个台阶。提高样本质量和数量把椒盐归因于分类器没学好回头补样本、调特征才根治。4.3 精度上不去的4个常见原因如果总体精度一直卡在0.7以下先从这几个方向排查样本量太少每个类别至少几十个样本点换算成面样本最好每个类别上百个像元。样本量不足时分类器没见过世面。类别定义模糊比如你把草地和农田分两类但两者在影像上光谱非常接近这属于类别可分性问题可能需要合并类别或引入物候特征例如多时相NDVI曲线。特征波段不足只用Landsat光学波段对某些地类如区分常绿/落叶林不够考虑加SAR数据、纹理或地形特征。训练/验证样本来自同一区域这种同源验证会虚高。如果做跨区域分类一定要用不同时间/不同区域的独立样本验证。4.4 我的调优心得先看混淆矩阵再看图最后分享一个我自己的习惯跑完分类第一步永远不是盯着地图看颜色而是打印混淆矩阵。混淆矩阵能精确告诉你哪两个类别互相打架。举个例子有次我做城市分类建筑和裸土精度都只有70%左右从混淆矩阵看到大量建筑被分为裸土、裸土被分为建筑。原因很清楚——两个类别在Landsat影像上光谱非常接近。我的对策不是盲目加更多样本而是加入SAR后向散射特征和NDVI时序特征一下把两者精度拉到了85%以上。另外一个容易被忽略的点是样本的时空一致性。假设你在2024年的影像上取样本用的却是2020年的分类训练结果去分2024年的影像迁移效果通常不好因为地表在变化、影像辐射也有差异。同一年份的样本训练同一年份的分类是最低要求。5. 再往前走一步监督分类的扩展方向5.1 从单期分类到时序分类单期监督分类只能告诉当前这一刻的地表状态但很多场景需要知道变化过程——比如城市扩展监测、森林砍伐追踪。把单期分类扩展到时序的思路很直接对每年的影像分别做分类逐像元比较类别是否发生变化如果变化频繁或变化方向不合理很可能是分类误差而非真实变化。需要注意的是时序分类对分类器的稳定性要求更高因为每年影像的光照、物候条件都不同样本如果只在某一年采集其他年份分类精度可能会崩。这就要求你构建跨年份的训练样本集或者做样本迁移。5.2 从像元分类到面向对象GEE官方推出了ee.Algorithms.Image.Segmentation分割算法虽然不如专业面向对象软件功能多但胜在能和分类算法无缝衔接。面向对象分类的思路是先分割成对象再对对象提取光谱统计特征均值、标准差、形状特征、纹理特征最后在对象层面分类。这样做的好处是结果干净、地物边界完整尤其适合高分辨率影像分类。不过GEE上的分割算法比较基础参数需要反复试验。我的建议是如果只是做中等分辨率如Landsat 30m的宏观分类像元级随机森林中值滤波已经足够只有在高分影像如Sentinel-2 10m、甚至亚米级上才需要考虑面向对象。5.3 从单分类器到集成学习GEE里虽然没法直接调scikit-learn的堆叠/投票集成但可以用一个朴素集成思路分别用随机森林、CART、SVM跑分类最后对三个分类结果做逐像元投票。如果两个以上分类器把某个像元归为同一类就采信这个类别。这个方法在GEE里实现非常简单却能有效吸收不同算法的优势分类稳定性明显提升。// 训练三个分类器并分别分类 var classCart featureImage.classify(classifierCart); var classSVM featureImage.classify(classifierSVM); var classRF featureImage.classify(classifierRF); // 投票逻辑多数服从少数 var ensemble classCart.add(classSVM).add(classRF); // 如果类别是0/1/2则判断哪个类别出现次数最多 // 简化处理取三个分类结果的中位数 var ensembleResult ensemble.divide(3).round();6. 总结一份我的保姆级操作清单如果你今天就要跑自己的第一份监督分类直接照着这份清单做确定研究区和时间范围影像集合按区域和时间过滤云量阈值先设10%。构建特征影像选6个光学波段 计算3个指数NDVI、NDWI、MNDWI堆叠成一个影像。准备样本每类至少20-30个样本面样本优先点样本兜底类别标签统一为0/1/2整数。拆分训练与验证样本按0.7/0.3拆分验证样本绝对不能参与训练。训练分类器先跑CART快速验证流程再换随机森林正式出图。验证精度打印混淆矩阵、总体精度、Kappa系数。后处理与导出用focalMode()去椒盐用Export.image.toDrive()导出GeoTIFF。每次分类前我都会问自己三个问题我的样本真的能代表整个研究区的光谱变化吗待分的类别之间在光谱上能否被分开如果我换一个季节的影像这个分类器还能work吗这三个问题想清楚GEE监督分类就已经成功了七成。剩下的三成是随机的数据噪声和永远调不完的参数但至少你知道了该往哪个方向努力。