做遥感的人,早晚都要过监督分类这一关。Google Earth Engine(GEE)把这扇门开得比别人都大——官方示例里三四十行代码就能出一张分类图,看起来相当唬人。但真正上手你就会发现,同样一套流程,换一个区域、换一景影像、换一套样本,精度可能直接从80%跌到60%,然后你就开始怀疑是不是分类器选错了,或者某个参数没调好。其实大多数问题都出在更前面:对"监督分类到底在算什么"这件事没理解透,样本准备和特征工程做得太糙。
这篇我就把GEE里的监督分类(Classification)从头到尾拆开讲:影像数据怎么准备、训练样本怎么采、特征怎么构建、三类主流分类器怎么用、精度怎么验,最后把我实际跑过的坑一并交代清楚。适合刚接触GEE的初学者,也适合那些已经抄过示例、但精度始终提不上去的朋友。
1. 先把监督分类的底层逻辑理清楚:GEE里的分类到底在算什么
1.1 监督分类的数学本质:从样本到决策边界
一句话版本:监督分类就是拿"已知答案"的样本去训练一个模型,然后用这个模型去预测"未知答案"的像素。每个像素在算法眼里只是一串特征值——比如Sentinel-2的12个波段,就是一个12维向量。你提供的训练样本,就是一堆"带标签的向量"。分类器要做的,是在这个多维特征空间里切出几块区域,让每一块尽量只包含同一个类别的样本。
打个比方:你想教小孩分辨苹果和橙子,不用给他描述"苹果皮是红的、橙子皮是橙的"这种规则,只需要给他一堆切好的水果、挨个告诉他哪个是苹果、哪个是橙子。孩子自己会总结出颜色、大小、纹理的规律。分类器也一样,给它足够多、足够准的带标签样本,它自己学出一套规则。这套规则写出来可能很抽象,比如"当波段B4大于0.3且B11小于0.1且……",但这不重要,重要的是它能正确分类新像素。
理解这一点,你就能明白一个常被忽略的道理:分类器本身不是精度的决定因素,样本才是。样本里的错误和偏差,算法再怎么调参也救不回来。这就是典型的"Garbage in, garbage out"。我见过太多人在GEE里跑分类,一上来就盯着RandomForest的决策树数量、SVM的核函数调参,却不肯花时间好好整理样本,方向从一开始就跑偏了。
1.2 GEE分类的完整数据流水线
GEE里做一次监督分类,代码结构极其固定,几乎可以当成流水线模板:
// 1. 影像数据 var image = ...; // 2. 样本数据(带类别标签的FeatureCollection) var samples = ...; // 3. 在样本点位上提取影像特征 var training = image.sampleRegions({ collection: samples, properties: ['class'], scale: 10 }); // 4. 训练分类器 var classifier = ee.Classifier.smileRandomForest(100).train({ features: training, classProperty: 'class', inputProperties: image.bandNames() }); // 5. 预测全图 var classified = image.classify(classifier); // 6. 精度验证 var accuracy = training.classificationAccuracy();后面所有花样,都是在替换或丰富这六个环节中的某几个:影像怎么选、样本怎么采、特征加不加、分类器用哪个、验证怎么做。把这行模板刻进脑子里,学GEE分类就不乱了。
需要提醒的是,上面的training.classificationAccuracy()是拿训练集自己验证自己,属于"开卷考试",精度通常会虚高。正规做法是留出一部分样本不参与训练,专门用来验证。这块我放到第5节详细讲,算是很多人忽略的核心细节。
2. 训练样本的质量决定精度上限:样本采集与FeatureCollection构建
2.1 样本数量、纯度和均衡性,三条铁律
我自己的经验是,样本至少要满足三条:够多、够纯、够均衡。
够多,是指每一类最少要有几十个独立样本。别小看这个数字,GEE官方示例里经常用"30个点"这种演示数据,那是为了跑通流程,不是给你当精度标准的。我自己做一般每类100到300个样本起步。另外,"独立样本"很关键——相邻像素高度相关,你要是把一个大的水体面分割成几十个点,这些点的信息量其实就相当于几个点,训练和验证的时候都会造成精度虚高,看起来很美,实际一塌糊涂。
够纯,是说标注的像素必须"纯粹"。比如你要分水体,就找完全被水覆盖的像素,别找半水半岸的过渡带像素。分类器训练的是像素级特征,你给它一个五成水、五成岸的混合信号,它就只能学到一条模糊的决策边界,分类结果自然是一带糊一片。这个道理放之四海皆准:纯样本训练出来的分类器,边界清晰;混合样本训练出来的分类器,处处和稀泥。
够均衡,是指各类别样本量不要悬殊太大。做过二分类的人都有体会,负样本是正样本十倍,模型会本能地倾向把所有东西都判成负类。遥感分类一模一样:水体样本300个、建筑样本只有30个,最后建筑类别很可能被水体吃掉一截。这时候要么补建筑样本,要么做样本加权,要么分层抽样控制比例,总之不能让类别数量差距过于夸张。
2.2 从矢量到FeatureCollection:样本数据的准备细节
GEE里的训练样本本质是ee.FeatureCollection,每个Feature的属性里至少要有一个字段表示类别,比如class。值用整数(0、1、2……)还是字符串('water'、'forest')都行,但整数跑起来更省心,也更容易处理混淆矩阵的输出。
构建方式无非三种:
- 直接在GEE的Code Editor里用画图工具画点或画多边形,再把geometry转成FeatureCollection;
- 从本地上传Shapefile或GeoJSON到Assets,然后加载;
- 用已有的公开分类产品当参照,通过规则筛选和随机撒点生成样本。
第三种方式在实操中用得最多。比如我要在某个区域做高精度土地利用分类,会先加载一个公开的粗略分类产品,按类别随机撒点,再人工抽查一部分点位确认无误后保留,作为训练样本。这比纯手工画点效率高得多,尤其适合大范围制图。
从geometry转FeatureCollection的写法:
// 假设你已经用画图工具画了两个多边形:水体采样区、植被采样区 var water = ee.FeatureCollection([waterPolygon]); var forest = ee.FeatureCollection([forestPolygon]); // 给每个Feature打上类别标签 water = water.map(function(f) { return f.set('class', 1); }); forest = forest.map(function(f) { return f.set('class', 2); }); var samples = water.merge(forest); Map.addLayer(samples, {color: 'blue'}, 'samples');上传Shapefile的话,记得在导出时选WGS84坐标系,并带一个整型的类别字段。这个环节的坑很隐蔽:有人上传后忘记检查属性表,字段名带了个看不见的空格,训练时GEE直接报"Property does not exist",查了半天才发现是字段名编码问题。所以样本一加载进来,先print(samples.limit(5))看一眼属性表,这是最低成本的排错方式。
2.3 训练集/验证集划分的正确姿势
样本集建好以后,第一件事不是拿去做训练,而是先切成训练集和验证集。切的方式要随机,最好还能兼顾空间分布——别让验证点和训练点挤在同一个图斑里,否则验证等于开卷考试。
实操中大多数人用randomColumn()随机分,简单够用:
// 加一列随机数,用固定种子保证结果可复现 samples = samples.randomColumn('random', 7); var trainingSet = samples.filter(ee.Filter.lte('random', 0.7)); var validationSet = samples.filter(ee.Filter.gt('random', 0.7)); print('训练样本数:', trainingSet.size()); print('验证样本数:', validationSet.size());randomColumn的第二个参数是随机种子,固定它才能让每次运行的结果一致,否则你每次调参看到的精度波动都来自随机性,根本没法判断是参数变好了还是运气变好了。三七开、二八开都可以,关键是验证集从训练开始到精度评估结束都不能露面。
3. 特征工程:选波段、加指数、防过拟合
3.1 波段选择不是越多越好
很多新手拿到Sentinel-2,恨不得把二十多个波段全部塞进分类器。这么做不但精度未必提升,训练时间还翻了好几倍,甚至出现过拟合——分类器把训练样本里的噪声都背下来了,换一景影像就崩。
选波段的核心逻辑是:这些波段对你关心的类别有没有区分度。水体、植被、建筑、裸地这几个大类,用可见光加近红外基本就分得开了。水体在近红外波段吸收极强,植被在红边和近红外波段有典型高反射,建筑和裸地在短波红外上比较接近,需要靠指数或纹理辅助区分。
我的习惯是先做一个波段敏感性测试:每次只用一个波段训练一个快速分类器,看单波段精度,把精度明显偏低的波段踢掉。这比盲目堆波段科学得多,特别是在样本量不够大的时候,宁少勿多。
3.2 常用指数特征:NDVI、NDWI、NDBI
把波段组合成指数,本质上是对特征做了一次非线性变换,能放大类别之间的差异。水体和植被在原始波段上可能差别不算大,但NDWI(归一化水体指数)一算,水体的值显著高于其他地物,分类器一下子就有了"杀手锏"。
// 假设image是多光谱影像,B4红光、B8近红外、B3绿光、B11短波红外 var ndvi = image.normalizedDifference(['B8', 'B4']).rename('NDVI'); var ndwi = image.normalizedDifference(['B3', 'B8']).rename('NDWI'); var ndbi = image.normalizedDifference(['B11', 'B8']).rename('NDBI'); image = image.addBands([ndvi, ndwi, ndbi]);加上这三兄弟之后,用sampleRegions提取特征时,分类器能用的特征就多了三个。这是GEE分类里性价比最高的特征增强方案,几乎零成本,收益却很实在。
3.3 特征太多反而翻车:维度灾难与相关性
特征越多越好这个直觉,在机器学习里经常不成立。假设你有30个特征,每个特征都带一点噪声,样本量又不够大,分类器在高维空间里很容易找到一条恰好分开训练样本的复杂边界,但这条边界对未知数据毫无泛化能力——这就是维度灾难。简单说,特征多了以后,模型学会了"背答案",而不是"懂规律"。
实操经验:先用print(image.bandNames())看看当前影像里到底有哪些波段,再把强相关的波段酌情去掉一个,比如Sentinel-2的B8和B8A高度相关,留一个就行。PCA做主成分降维是更高级的做法,但对初学者我建议先别急着上,把指数加上、剔除强相关波段、跑通整个流程,等精度瓶颈卡住了再回头考虑PCA。
下面这个片段演示了如何把原始波段和指数一起定义成特征列表,后续分类和验证都用它,保证特征一致:
var bands = ['B2', 'B3', 'B4', 'B8', 'B11', 'NDVI', 'NDWI', 'NDBI']; // 提取训练特征时指定波段,而不是用image.bandNames()全选 var training = image.select(bands).sampleRegions({ collection: trainingSet, properties: ['class'], scale: 10 });这里有个关键细节:sampleRegions时用的scale要跟影像分辨率匹配,Sentinel-2用10米,Landsat用30米。有人图省事不写scale,GEE会用影像默认分辨率,问题不大;但如果影像做了重采样,这里就必须显式指定,否则特征值的空间范围是错位的。
4. 分类器实战:RandomForest、SVM、CART的代码实现与调参
4.1 RandomForest随机森林:上手就能打的默认王者
RandomForest是我在GEE里用得最多的分类器,没有之一。它抗过拟合能力强,对特征尺度不敏感,不需要归一化,默认参数就能跑出不错的精度,特别适合遥感这种"特征杂、样本分布不完美"的场景。
var classifier = ee.Classifier.smileRandomForest({ numberOfTrees: 100, variablesPerSplit: null, // 默认取特征数的平方根 minLeafPopulation: 1, // 每个叶子节点最少样本数 bagFraction: 0.5, // 每棵树用多少比例的样本 outOfBagMode: true // 开启袋外误差估计 }).train({ features: training, classProperty: 'class', inputProperties: bands }); var classified = image.select(bands).classify(classifier, 'classification'); Map.addLayer(classified, classPalette, 'RandomForest 分类结果');参数说明就两句:numberOfTrees从100加到500,精度提升通常很有限,训练时间却线性增长,默认100到250之间就够用了;minLeafPopulation如果结果图碎得像芝麻粒,可以调到5到10,让每个叶子节点多几个样本兜底,图面会干净不少。别迷信"参数越多越好",随机森林真正吃功夫的地方在样本和特征。
4.2 SVM支持向量机:小样本高维特征的利器,但小心尺度
SVM在GEE里对应ee.Classifier.libsvm,擅长处理小样本、高维特征的问题。但它有一个遥感新手经常踩的大坑:SVM对特征尺度极其敏感。如果你的一个波段值域是0到1,另一个波段值域是0到10000,SVM会默认后者更重要,前者的信息基本被忽略。
// 先对特征波段做归一化到0~1 var min = image.select(bands).reduceRegion({ reducer: ee.Reducer.min(), geometry: roi, scale: 10, maxPixels: 1e9 }); var max = image.select(bands).reduceRegion({ reducer: ee.Reducer.max(), geometry: roi, scale: 10, maxPixels: 1e9 }); var imageNorm = image.select(bands).unitScale(min, max); var svm = ee.Classifier.libsvm({ kernelType: 'RBF', gamma: 0.5, // RBF核的宽度参数 cost: 10 // 惩罚系数 }).train({ features: training, classProperty: 'class', inputProperties: bands }); var svmClassified = imageNorm.select(bands).classify(svm, 'classification');gamma和cost都要靠实验调。gamma太小,决策边界过于平滑;gamma太大,又会过拟合。cost是错分惩罚,越大越严格。说实话,SVM调参比随机森林麻烦,我不会一上来就推荐它,只有当样本量小、特征维度高、随机森林精度又不理想的时候才换SVM试。
4.3 CART决策树:快是快,但裸树容易过拟合
CART即分类与回归树,GEE里对应ee.Classifier.smileCart。它训练速度极快,结果可解释性最好,你能直接看到每层按哪个波段、哪个阈值切分。但裸决策树很容易过拟合,实际项目里很少单独用。
var cart = ee.Classifier.smileCart({ maxNodes: null, // 不限制节点数 minLeafPopulation: 5 // 叶子最小样本数,适当调大防止过拟合 }).train({ features: training, classProperty: 'class', inputProperties: bands });minLeafPopulation调大是防止过拟合最直接的手段。不过说实话,在GEE里用CART更多是为了理解数据和做快速试验,生产级别的分类图,我还是建议随机森林或者SVM。CART真正的价值是告诉你"哪个波段最重要"——GEE里可以打印变量重要性,这个后面讲。
4.4 三类分类器对比与选型建议
我整理了一张实测对比表(同一组Sentinel-2样本,四分类,验证集独立评估),方便你直观选型:
| 对比维度 | RandomForest | SVM | CART |
|---|---|---|---|
| 训练速度 | 中等 | 较慢(大数据集更明显) | 最快 |
| 对样本量要求 | 中等偏高 | 小样本也能打 | 中等 |
| 对特征尺度敏感度 | 不敏感 | 敏感,需归一化 | 不敏感 |
| 泛化能力 | 强 | 强(但依赖调参) | 较弱,易过拟合 |
| 变量重要性分析 | 支持 | 不支持 | 支持 |
| 适合场景 | 大多数遥感分类任务 | 小样本高维特征 | 快速试验、规则理解 |
我的默认建议:先用RandomForest跑通全流程,拿到一个基准精度;如果样本少、特征多,再试SVM;CART用来做探索性分析。这个策略省时间,也不容易踩坑。
变量重要性在GEE里可以这样看:
var importance = ee.Dictionary(classifier.explain()); print('变量重要性:', importance.get('importance'));classifier.explain()输出的是一个字典,里面的importance字段就是各个特征的重要性排序。用这个结果反过来做特征筛选,比拍脑袋选波段靠谱得多。
5. 精度验证与分类后处理:别把初分类图直接拿去用
5.1 混淆矩阵的正确读法
分类图出来以后,第一件事不是看它好不好看,而是用事先留出的验证集做精度评估。核心工具是混淆矩阵,GEE里用errorMatrix计算。
// 在验证样本点位提取预测类别 var valPred = classified.sampleRegions({ collection: validationSet, scale: 10 }); // 计算混淆矩阵 var cm = valPred.errorMatrix({ actual: 'class', predicted: 'classification' }); print('混淆矩阵:', cm); print('整体精度:', cm.accuracy()); print('Kappa系数:', cm.kappa());混淆矩阵的行是真实类别,列是预测类别,对角线是正确分类的个数。它透露的信息比一个整体精度数字多得多。举个例子,矩阵里"真实水体"那一行如果有很多样本被分到了"建筑"列,说明水体和建筑的光谱特征混叠严重,光调分类器没用,你得回去加特征或者补样本。
两个逐类别的指标必须会看:生产者精度(Producer's Accuracy)对应漏分误差,衡量"真实是A类的像素有多少被判对了";用户精度(Consumer's Accuracy)对应错分误差,衡量"被分成A类的像素里有多少真的是A类"。一个类别的生产者精度低,说明漏分多;用户精度低,说明错分多。两者要放在一起看,单看哪一个都会被带偏。
5.2 分类后处理:众数滤波、小图斑剔除与面积统计
初分类图通常是"椒盐状"的,单个像素这里冒一个、那里冒一个,这在制图上是不能接受的。最轻量的降噪手段是focalMode,也就是众数滤波,用周围邻域里出现次数最多的类别替代中心像素:
var filtered = classified.focalMode({ radius: 2, kernelType: 'circle', iterations: 1 }); Map.addLayer(filtered, classPalette, '滤波后分类结果');radius是滤波半径,单位是像素。半径太大,细小地物(比如窄河沟、小路)会被抹掉;半径太小,又压不住噪声。我一般从2开始试,结合你的研究区地物尺度调整。滤波前后对比一下,你会发现图面干净很多,精度通常也会有小幅提升。
面积统计是分类后最常见的应用,配合ee.Image.pixelArea()按类别汇总:
var areaImage = ee.Image.pixelArea().addBands(filtered); var areaStats = areaImage.reduceRegion({ reducer: ee.Reducer.sum().group({ groupField: 1, groupName: 'class' }), geometry: roi, scale: 10, maxPixels: 1e9 }); print('各类别面积:', areaStats);pixelArea()会生成一个表示每个像素实际面积(平方米)的波段,按类别分组求和就得到了各类面积。注意scale必须跟分类影像一致,否则面积数值会偏差。结果除以10000就是公顷,除以1e6就是平方公里。
5.3 结果导出与可视化
分类结果存到你自己的Google Drive,用Export.image.toDrive,记得设置影像类型和坐标系:
Map.addLayer(filtered, classPalette, '最终分类'); Export.image.toDrive({ image: filtered.int16(), // 转成整型省空间 description: 'landcover_result', scale: 10, region: roi, maxPixels: 1e9, crs: 'EPSG:32650', // 替换成你研究区所在的UTM分带 fileFormat: 'GeoTIFF' });classPalette是一个颜色列表,每个类别对应一种颜色。可视化调色板至少要以一区分,比如水体蓝色、植被绿色、建筑红色、裸地黄色,方便直观检查。
6. 踩过的坑与调优经验:这几个细节决定成败
6.1 样本不均衡:少数类被吃掉
我第一次做五分类的时候就吃过这个亏。当时建筑样本只标了不到30个,其他四类都上百,跑出来的结果显示建筑类别大面积缺失,绿地和水体向外扩张。看混淆矩阵才发现,真实建筑那一行的生产者精度只有45%,大部分被分到了裸地。后来我补标了80个建筑样本,重新训练,建筑的生产者精度立刻跳到82%。教训就一句话:先看混淆矩阵再归因,九成精度问题出自样本而非算法。
6.2 过拟合的典型症状与应对
如果你的训练精度高达98%,验证精度却只有65%,大概率过拟合了。这时候别急着换分类器,先做三件事:检查样本纯度,把跨类别边界上的模糊点删掉;减少冗余特征,用变量重要性挑出前五六个特征重新跑;把随机森林的minLeafPopulation调大,限制树的生长。我实测过,这三步组合拳通常能把验证精度拉回七八个点。
6.3 多时相合成的坑
做分类之前大部分人都会用median()合成一段时间的影像,比如夏季三个月的Sentinel-2中值合成。这个做法本身没问题,但农作物和落叶林在季节变化下光谱差异巨大——同一个农田像素,六月份是绿色作物,九月份已经收割变成裸土,中值合成可能得到一个"既不像作物也不像裸土"的光谱。这种像素不管分类器怎么调都很难分对。
我的建议是:分类之前先明确你的"分类时相"是什么。如果是夏季土地利用,就用夏季一景或较短窗口的合成;如果关心的是年内稳定地物(水体、不透水面),用全年合成反而更稳。时间窗口选得越贴合地物实际状态,分类精度越容易上去。
6.4 我给监督分类工作流的最终建议
如果你只记住一条工作流,我希望是这套:小区域快试、看混淆矩阵、补样本调特征、全图预测、滤波后处理、导出。先用一个几平方公里的子区域跑通全流程,精度达标了再扩大到整个研究区,这能省掉大量反复 Export 的时间。
我个人还有一个习惯:每跑一组实验,就在代码里固定随机种子、记录下样本数、特征列表和精度指标,形成一个简单的实验日志。别小看这个动作,调参最怕的不是参数不对,而是你不知道上一次的"好结果"是怎么跑出来的。
最后再分享一个小技巧:验证样本的采集,尽量采用随机分层抽样,保证每类都有足够验证点;分类完成后,把被错分的样本地块单独导出来快速看一眼影像对比,你能直观看到是哪几类在打架——这个可视化检查,往往比任何精度数字都更能告诉你下一步该往哪使劲。