
简介11种卫星拍摄下的建筑图像分类数据集面向计算机视觉研究与工程实践提供约1100张已标注图片覆盖机场、大桥、教堂、森林、湖泊、体育场、公园等11个类别类别清单内置于资源中的json文件。数据已完成预处理并按7:3比例划分训练集与验证集同类图片存放于对应目录可直接作为分类网络输入使用。包内另附一个Python可视化脚本运行后可直观检查各类别样本便于在模型训练前快速验证数据质量。资源包共1168个文件以jpg图片为主体1166张另有py脚本与json配置各1个整体压缩包大小约40.4MB下载和部署成本较低。目前已有49人浏览学习适合作为图像分类、分割网络改进实验的基础数据集也便于入门者熟悉卫星遥感图像的整理与训练流程整体脉络清晰类别划分明确。1. 卫星建筑图像分类数据集一个 1100 张的 11 分类起步盘第一次拿到这份「11 种卫星拍摄下的建筑图像分类数据集」时我第一反应是疑惑约 1100 张数据、11 个类别平均每类一百来张容量不算大。但真正跑通流程后才发现卫星视角下的建筑分类跟常见的目标分类完全是两码事同一栋房子换个季节、换个拍摄角度纹理和阴影能差出两个类别的距离。这份资源的价值不在“量大管饱”而在它把遥感图像分类里最麻烦的类别边界、标注歧义和验证集切分问题都摆到了台面上。适合两类人一是刚接触遥感图像分类、想拿一份已标注数据把 ResNet/EfficientNet 全流程跑通的开发者二是需要评估“约 1100 张的小标注集到底能不能撑起一个建筑分类 Baseline”的从业者。先把家底摸清再谈训练下面就从目录结构和标注格式说起。2. 看清家底目录结构、标注格式与类别分布这一步的价值是避免返工。我拿到任何数据集都会做三次检查先看目录、再读标注、最后统计类别分布。遥感数据尤其要这样做因为它的组织方式和自然图像数据集差异很大。2.1 下载解压后先做什么目录解剖如果直接复制一份网上现成的训练代码开始跑很容易翻车因为这份资源可能有两种组织方式一种是 ImageFolder 风格训练和验证分别放在 train/类别名/xxx.jpg、val/类别名/xxx.jpg 下标签藏在文件夹名里另一种是 CSV/JSON 风格images 文件夹平铺所有图片labels.csv 里逐行列 filename、label甚至带 tile_id 或经纬度字段。这两种方式对模型代码影响不大但对你后面做按区域划分训练集影响非常大。所以先写好一个目录解剖脚本import os ROOT data def walk_dir(path, depth0, max_depth3): 递归打印目录树并在每个子目录后统计文件总数。 if depth max_depth: return for entry in sorted(os.listdir(path)): full os.path.join(path, entry) if os.path.isdir(full): n sum(len(files) for _, _, files in os.walk(full)) print( * depth f[{entry}] {n} 个文件) walk_dir(full, depth 1, max_depth) else: print( * depth f{entry}) if __name__ __main__: walk_dir(ROOT)逻辑说明walk_dir 用 os.listdir 按名字排序遇到目录就递归进去并用 os.walk 统计该目录下的文件总数遇到文件就只打印文件名。max_depth 参数控制递归深度像 train 这种包含大量子目录的层级把它设为 3 就不会刷屏。跑完你会得到一张清晰的“目录树 每目录文件数”不到一分钟就能确认图片根目录、标注文件、验证集位置。参数说明ROOT 指向解压后的根目录max_depth 默认 3如果数据集只有一层目录可以调到 4 看全目录特别深就调小到 2。这个脚本不依赖第三方库任何 Python 3 环境都能直接跑。拿到目录树后我一般还会顺手用 PIL 抽看几张图片的尺寸确认是不是同一分辨率。卫星数据里常见一种情况部分图像来自不同来源尺寸从 224×224 到几千像素不等。如果尺寸差异大后面的 Resize 策略就要提前想好是统一缩到小图还是按瓦片切割。2.2 标注到底长什么样CSV、文件夹名还是 JSON确认目录结构之后下一步是打开标注文件。如果是 ImageFolder 方式类别列表就是 train 下一级的文件夹名ls data/train输出里出现的每一行就是一个类别。常见的遥感建筑分类会把类别按建筑功能和形态区分比如住宅、商业、工业、学校、医院、体育场馆、仓库、停车场、在建工地等具体是哪 11 类以你解压后看到的 label_map 或类别文件夹为准。这里我要强调一件事把类别名单记下来和 labels.csv 中的 label 字段逐一比对确认没有拼写不一致。如果资源给的是 CSV通常长这样字段含义典型取值filename相对 images 根目录的图片路径0465_08.jpglabel类别名residentiallabel_id类别数字编号可能没有0 ~ 10tile_id来源瓦片或区域 id用于避免数据泄漏13742比对方法很简单用上一节的脚本打印 label 的唯一值再与文件夹名集合做差集。差集不为空说明标注文件与目录组织对不上这种脏数据要早发现否则训练时文件找不到报了 KeyError 你才回头查浪费的时间够写完两个数据集加载器。这里有个血泪经验不少 CSV 里 label 是英文小写而目录是首字母大写字符串比较时要注意 strip() 和 lower()。我一般会在读取 CSV 时统一做一次规整df[label] df[label].str.strip().str.lower()注意label 字段最怕混进不可见字符比如行尾的 \r。strip() 之后如果还发现问题打印 repr(df[label].unique()) 看原始值。另外类别定义本身就有主观性。同是“住宅”在北美卫星图上可能是独栋加草坪在亚洲则可能是密集小区绿地与阴影比例完全两样这会影响模型学到的纹理特征。1100 张的规模不可能覆盖所有地理风格所以要降低期望把目标定在“类内风格相对一致”这个前提上。2.3 统计类别分布并保存标签映射类别统计直接决定训练策略。遥感建筑数据天然不均衡有的类采集容易、有的类要专门飞一趟。若某类只有十几张你就得提前决定加样本增强、用类别权重还是干脆做类别合并。统计脚本如下import csv import os from collections import Counter ROOT data rows [] with open(os.path.join(ROOT, labels.csv), encodingutf-8) as f: reader csv.DictReader(f) for r in reader: rows.append(r) counter Counter(r[label] for r in rows) print(类别数量:, len(counter)) for label, count in counter.most_common(): print(f{label}: {count}) # 检查标注里的图片是否真实存在 missing [ r[filename] for r in rows if not os.path.exists(os.path.join(ROOT, images, r[filename])) ] print(标注了但缺失的图片:, len(missing))逻辑说明csv.DictReader 把每一行读成字典Counter 统计 label 字段的分布最后用 os.path.exists 校验图片路径避免“标注 1100 张、实际文件少 200 张”的情况。校验这步非常便宜却能让后面少摔一个大跟头。参数说明encodingutf-8 是必须的我遇到过 CSV 用 GBK 编码导致中文标签乱码改成 gbk 后正常如果 labels.csv 不在根目录把 ROOT 改成实际路径。missing 检查只做存在性判断不做文件完整性校验后者要逐字节读在 1100 张图上没必要。把统计结果画成条形图更有直觉常见做法是横向条形图类别多的在上面import matplotlib.pyplot as plt labels, counts zip(*counter.most_common()) plt.barh(range(len(labels)), counts) plt.yticks(range(len(labels)), labels) plt.xlabel(count) plt.tight_layout() plt.savefig(label_dist.png)逻辑说明barh 画的是横向条形图y 轴放类别名x 轴放样本数方便一眼看出少数类。保存成 png 而不是直接 plt.show()因为在远端服务器上跑训练时没有显示器。参数说明counts 的顺序来自 counter.most_common()已经按数量降序排好如果要在论文或报告里引用可以顺手再保存一份 label_map.json把类别名和数字编号对应关系固定下来后面训练、评估、可视化都从这份 json 读避免口径不一致。到这里数据集的“家底”就全部清楚了目录结构、标注格式、类别分布、图像分辨率。接下来可以放心做训练。3. 用预训练 ResNet 把 1100 张图跑出第一个 Baseline家底摸清之后就是训练。这一章给一条可复现的路径从模型选型、数据加载到训练脚本和结果分析全部落成代码。3.1 为什么是预训练 ResNet而不是 ViT 或从零训练在小数据集上做遥感图像分类选型的第一原则是“低层特征尽量复用”。ImageNet 上预训练出来的卷积网络前几层学到的是边缘、纹理、颜色块这些通用特征这对卫星建筑同样适用真正需要重新学习的只是“屋顶结构如何组合”这种高层语义。因此虽然 ViT 在很多榜单上更好但 1100 张数据不足以让 Transformer 的注意力机制稳定收敛常见做法还是用 ResNet18 或 ResNet50 做冻结迁移。选 ResNet18 还是 ResNet50我的习惯是先用 ResNet18 把流程跑通。理由很实际1100 张图用 ResNet50 训练一轮要长不少而你在 Baseline 阶段更关心的是数据有没有问题、loss 会不会降、混淆矩阵长什么样。等流程稳定了再升级到 ResNet50通常能再提 1~3 个点。3.2 数据加载把 CSV 和 ImageFolder 统一成同一个 Dataset为了避免纠结 CSV 还是文件夹我直接写一个兼容两者的自定义 Dataset。它的输入是一个 pandas DataFrame包含 filename 和 label_id 两列无论资源是哪种组织方式最后都转成这个 DataFrame然后喂给同一个数据集类。import os import pandas as pd import torch from torch.utils.data import Dataset from PIL import Image class SatelliteBuildingDataset(Dataset): 从 DataFrame 读取卫星建筑图像转成 RGB 并返回 (img, label_id)。 def __init__(self, df, img_root, transformNone): self.df df.reset_index(dropTrue) self.img_root img_root self.transform transform def __len__(self): return len(self.df) def __getitem__(self, idx): row self.df.loc[idx] path os.path.join(self.img_root, row[filename]) img Image.open(path).convert(RGB) # 统一转 RGB防止灰度图报错 if self.transform is not None: img self.transform(img) label int(row[label_id]) return img, label逻辑说明getitem是 PyTorch Dataset 的核心它做的事只有三件按行号找到图片路径、用 PIL 打开并强制转成 RGB、返回图像张量, 标签整数。convert(RGB) 很关键卫星影像偶尔会出现灰度图或 RGBA 图不主动转训练到一半可能因为通道数不一致报错。参数说明df 的 label_id 必须是整数CrossEntropyLoss 不接受字符串标签所以加载后要用 label_map 做一次编码再存回去transform 为空时返回的是 PIL 对象只有传入 transforms.Compose 才会变成 Tensor。把原始 CSV 转成带 label_id 的 DataFrame 的代码放在构建 Dataset 之前import pandas as pd df pd.read_csv(data/labels.csv) label_map {name: i for i, name in enumerate(sorted(set(df[label])))} df[label_id] df[label].map(label_map) print(label_map)这里 sorted(set(...)) 保证了类别编号稳定不会因为 CSV 行的顺序变化而改变。如果你发现资源里本身就有 label_id那就直接用不用再生成一遍。提示如果你更喜欢 ImageFolder 的写法torchvision.datasets.ImageFolder 可以直接读目录但它要求训练和验证目录结构完全一致且没有区域分组信息。这也是我更常用自定义 Dataset 的原因——后面做 GroupShuffleSplit 时DataFrame 操作比文件操作方便得多。3.3 训练主循环与参数设置数据加载准备好后核心训练脚本如下。我以 ResNet18 为例完整跑 30 轮import torch import torch.nn as nn from torch.utils.data import DataLoader from torchvision import models, transforms # 数据增强训练集做轻度扰动验证集只做 Resize train_tf transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomCrop(224), transforms.RandomHorizontalFlip(), transforms.RandomRotation(15), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.2), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]), ]) val_tf transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]), ]) # 加载数据假设 df 已准备好且按 8:2 分成 train_df / val_df train_ds SatelliteBuildingDataset(train_df, data/images, transformtrain_tf) val_ds SatelliteBuildingDataset(val_df, data/images, transformval_tf) train_loader DataLoader(train_ds, batch_size32, shuffleTrue, num_workers4) val_loader DataLoader(val_ds, batch_size32, shuffleFalse, num_workers4) # 模型把 ResNet18 的最后一层换成 11 类 model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) model.fc nn.Linear(model.fc.in_features, 11) device torch.device(cuda if torch.cuda.is_available() else cpu) model model.to(device) # 只微调最后两个阶段前几层冻结减少过拟合 for name, param in model.named_parameters(): if layer4 not in name and fc not in name: param.requires_grad False criterion nn.CrossEntropyLoss() optimizer torch.optim.AdamW(model.parameters(), lr1e-4, weight_decay1e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max30) def evaluate(model, loader, device): model.eval() correct 0 total 0 with torch.no_grad(): for x, y in loader: x, y x.to(device), y.to(device) pred model(x).argmax(dim1) correct (pred y).sum().item() total y.size(0) return correct / total epochs 30 for epoch in range(epochs): model.train() total_loss 0.0 for x, y in train_loader: x, y x.to(device), y.to(device) optimizer.zero_grad() out model(x) loss criterion(out, y) loss.backward() optimizer.step() total_loss loss.item() * x.size(0) scheduler.step() train_loss total_loss / len(train_ds) val_acc evaluate(model, val_loader, device) print(fepoch {epoch 1}/{epochs} loss{train_loss:.4f} val_acc{val_acc:.3f})逻辑说明resnet18(weights...) 会下载 ImageNet 预训练权重第一次运行需要联网之后有本地缓存。把 model.fc 换成 nn.Linear(..., 11)是因为这份数据集的类别数是 11如果你的资源实际类别数不同把 11 改成 len(label_map)。冻结策略是只让 layer4 和 fc 参与训练冻结的层 requires_gradFalse 后不会计算梯度省显存也降低过拟合。参数说明batch_size32 在 8GB 显存上够用如果你只有 4GB改成 16num_workers4 是 CPU 预取进程数Windows 上有时会报错调成 0 走主进程最保险lr1e-4 是微调常用值太大容易把预训练权重冲坏太小则训练很慢我一般在这个范围内试 5e-5 和 3e-4。CosineAnnealingLR 的 T_max 和 epochs 一致让学习率在 30 轮内从 1e-4 平滑降到接近 0。训练 30 轮在这个规模下通常只需要几分钟到十几分钟。如果 val_acc 一直不涨先别调模型回头检查第 2 章的类别分布和标签映射多半是数据问题。3.4 用混淆矩阵和分类报告找类别间的“混乱对”总体准确率是个毒药指标在类别不均衡时acc 90% 可能只是把多数类都猜对了。所以我跑完训练的第一件事是输出 sklearn 的分类报告和混淆矩阵from sklearn.metrics import classification_report, confusion_matrix model.eval() all_preds [] all_labels [] with torch.no_grad(): for x, y in val_loader: x, y x.to(device), y.to(device) all_preds.extend(model(x).argmax(dim1).cpu().tolist()) all_labels.extend(y.cpu().tolist()) print(classification_report(all_labels, all_preds, target_nameslist(label_map))) print(confusion_matrix(all_labels, all_preds))逻辑说明classification_report 给出每一类的 precision、recall、f1 和支持数比总体 acc 细得多。卫星建筑数据里常见的高混淆对是“住宅 vs 公寓”“仓库 vs 工业厂房”——它们在俯视图上都是矩形屋顶区别只在纹理和周围环境。混淆矩阵里非对角线的大数字就是你下一步应该重点收集样本的方向。参数说明target_names 必须和 label_map 的 key 顺序一致否则报告里的类别名对不上建议统一用 label_map 构造时 sorted 的顺序。到这里第一个 Baseline 就完整了接下来才是真正考验工程经验的部分。4. 避坑卫星建筑分类的五个常见问题与排查方法这部分全是实操中摔过的坑每条按“现象 → 原因 → 解决”的流程给出来方便你直接复制排查步骤。4.1 验证集精度 95%换一块新区域就翻车地理泄漏现象在随机划分的验证集上 acc 轻松 0.95你觉得模型已经好了结果把整张新卫星影像切成瓦片送进去准确率直接掉到 0.6 以下。原因随机划分时同一栋建筑、同一个屋顶的不同裁剪块一半进了训练集一半进了验证集。模型记住了具体屋顶的阴影和纹理分布等于开卷考试。卫星数据和自然图像最大的不同就是“空间自相关”相邻瓦片相关性极高不能按文件随机分。解决按瓦片或区域 id 分组拆分。CSV 里如果有 tile_id 就用它没有的话用文件名前缀分桶from sklearn.model_selection import GroupShuffleSplit df[group] df[filename].str.split(_).str[0] # 假设文件名前缀是区域 id gss GroupShuffleSplit(n_splits1, test_size0.2, random_state42) train_idx, val_idx next(gss.split(df, groupsdf[group])) train_df df.iloc[train_idx].copy() val_df df.iloc[val_idx].copy()逻辑说明GroupShuffleSplit 的 groups 参数保证同一个组的样本全部进同一边验证集看到的是模型从未见过的区域这才是有意义的泛化指标。random_state 固定为 42方便复现。注意如果文件名前缀不是区域 id而只是随机序号这个分组就失效了。我见过一些数据集把原始瓦片裁剪成瓦片块后重新编号这种情况下只能回源头找瓦片级标注或者用文件名里保留的坐标信息。4.2 损失函数降不下去或剧烈抖动类别不均衡与学习率过大现象前几轮 loss 从 2.4 降到 2.0 后就上蹿下跳val_acc 一直在 0.4 附近震荡。原因一是少数类样本太少模型干脆把它们全错分到多数类也能维持一个低的 loss二是 lr 设太大预训练权重被冲乱loss 在高点反复横跳。解决先用类别权重让少数类错分的代价更高import torch.nn as nn counts train_df[label_id].value_counts().sort_index().values weights 1.0 / torch.tensor(counts, dtypetorch.float32) weights weights / weights.sum() * len(weights) criterion nn.CrossEntropyLoss(weightweights.to(device))逻辑说明每个类别的权重是“样本数倒数再做归一化”样本越少的类权重越大。loss 公式里每个样本的贡献都会乘上对应类别的权重模型被逼着去学少数类。参数说明weights 要放到 device 上否则 CPU 张量和 CUDA 张量相加会报错如果你在训练过程中切了半精度把 weights 的 dtype 也转成 float16。学习率方面从 1e-4 起步如果 loss 还是一直抖降到 3e-5 再看三轮不要一步到位用 1e-3那是在烧预训练权重。4.3 增强过度卫星图被搞成抽象画现象同样一份增强配置用在 ImageNet 上效果很好但在这里训练完验证集精度不升反降而且模型对阴影非常敏感。原因卫星图像的拍摄条件固定——俯视、无视角变化、色调稳定。RandomAffine 的强旋转会让屋顶形状发生非线性扭曲ColorJitter 的强饱和度偏移会改变屋顶材质颜色这些都创造了训练集里不存在的分布。解决把增强参数限制在一个保守区间train_tf transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomCrop(224), transforms.RandomHorizontalFlip(), transforms.RandomVerticalFlip(), # 卫星图上下翻转不会改变语义 transforms.RandomRotation(10), # 小角度模拟航向偏差 transforms.ColorJitter(brightness0.1, contrast0.1, saturation0.1), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]), ])逻辑说明卫星图允许水平翻转也允许垂直翻转因为俯视图没有“正立”概念旋转角度从 15 度收到 10 度避免裁切后出现大面积插值伪影色彩抖动的三个系数都收到 0.1只模拟光照差异不改材料颜色。参数说明这里没有用 RandomAffine 和 RandomResizedCrop因为随机裁剪的比例变化在卫星图上会改变建筑的尺度感——同一个建筑在 0.5 倍裁剪下可能被误判成另一种类别。要做尺度增强建议在瓦片级裁剪时做而不是在训练时随机做。4.4 标注语义对不上同一种建筑在不同样本里叫法不同现象你抽查了 20 张训练图发现模型经常把“工业仓库”预测成“工业厂房”人工核对后发现这两类在该数据集里根本没有清晰边界有些标注员按屋顶颜色分有些按高度分。原因建筑分类的类别定义天然带主观性。尤其从卫星视角没有立面、没有入口信息单靠俯视图判断功能就是难。标注不一致会直接限制模型上限。解决正视标注歧义做“类别合并”。做法是画一个映射表把语义相近、混淆矩阵里经常互为误判的类合并成一个粗类merge_map { warehouse: industrial, factory: industrial, residential: residential, apartment: residential, } df[label] df[label].map(merge_map)逻辑说明这是一种降级——牺牲类别粒度换标注一致性。在 1100 张的小样本上把 11 类并成 8 类或 9 类通常比硬撑着 11 类训练效果更好。这个决定要以混淆矩阵为依据不要靠直觉拍脑袋。参数说明merge_map 的写法是把多个旧类名映射到同一个新类名映射完要重新生成 label_id 和 label_map否则旧的数字编号还残留着不连续的空档影响 CrossEntropyLoss 的输出维度。4.5 训练太慢、显存不够先跑通再跑大现象batch_size64 直接 OOM或者 CPU 训练一轮要十分钟你开始怀疑人生。原因1100 张图虽然不大但 Resize 到 256×256 后每张图是 500KB 级别的浮点张量batch 太大或者 CPU 解码瓶颈都会拖垮效率。解决先把 batch_size 降到 16把 num_workers 设为 0再把输入尺寸降到 160×160 做一次快速验证train_tf transforms.Compose([ transforms.Resize((160, 160)), transforms.RandomCrop(144), # 其余增强保持原样 ])逻辑说明在流程验证阶段144×144 的输入足够看出数据有没有问题。等流程验证没问题了再把尺寸恢复到 224×224 正式训练。这比一开始就上 224 然后被各种报错打断要省时间。参数说明如果显存还紧张可以顺手在训练循环里加一句 torch.cuda.empty_cache()并关掉梯度裁剪如果用的是多卡先别想 DDP单卡跑通是第一位。5. 进阶从分类走向区域级建筑判断Grad-CAM、TTA 与滑窗衔接Baseline 跑通后如果还想往下走我推荐做三件事先用 Grad-CAM 确认模型的判断依据再上 TTA 稳定预测最后把分类器接到检测或分割任务上让分类结果真正作用于大图。Grad-CAM 解决的是“模型到底在看什么”。ResNet18 的 layer4 是最后一个卷积阶段特征图还保留空间位置信息我们可以注册一个 forward hook 抓住它的输出再用分类得分的梯度做通道加权activation {} def hook_fn(module, input, output): activation[value] output.detach() handle model.layer4[-1].register_forward_hook(hook_fn) x val_img.unsqueeze(0).to(device) x.requires_grad_() out model(x) pred out.argmax(dim1) model.zero_grad() out[0, pred].backward()逻辑说明这一步的重点是思路——把梯度回传到最后一个卷积层得到每个通道对预测类别的贡献权重加权求和后就是热力图。实践里通常要同时挂 forward hook 和 backward hook取 layer4 的输出和对应梯度。热力图叠在原始图上如果高亮区域集中在屋顶边界而不是旁边停车场说明模型学到了建筑形态高亮区域飘在道路上就要怀疑数据有背景泄漏。TTA测试时增强是成本最低的精度提升手段。对一张验证图做水平翻转、垂直翻转、以及小角度旋转分别预测再平均def predict_tta(model, img, device, n_aug4): model.eval() preds [] tta_tf transforms.Compose([ transforms.RandomHorizontalFlip(p0.5), transforms.RandomVerticalFlip(p0.5), ]) for _ in range(n_aug): x tta_tf(img).unsqueeze(0).to(device) with torch.no_grad(): preds.append(torch.softmax(model(x), dim1)) return torch.mean(torch.stack(preds), dim0)逻辑说明每次增强都是同一个类别语义的不同视角softmax 后取平均相当于做了 4 次预测的集成。对本来置信度就高的图提升有限但对那些边界模糊的“住宅 vs 公寓”类通常能涨 1~3 个点代价只是推理时间翻倍。如果你的应用对延迟敏感可以不做 TTA只在离线批量评估时用。最后是和检测、分割任务的衔接。分类器可以直接当“区域筛选器”用把大图切成 224×224 的滑窗每个窗口过一遍分类器置信度低于阈值的窗口直接跳过不让它们进入后续更重的检测模型置信度高的窗口再去做建筑轮廓提取或目标检测。常见做法是步长设为 112即 50% 重叠保证建筑边界不会被窗口切碎。自从有一次在验证集上 acc 很高、换一块区域就崩掉之后我每次拿到新数据集都会强制走一遍第 2 章先统计类别和区域分布第 4 章第一件事用 GroupShuffleSplit 检查分组泄漏然后才敢开训。这份 1100 张数据虽然小但把这三个进阶动作走完你会发现它比很多“看起来更大”的数据集更能让你看清遥感分类的真实玩法。希望帮到你。本文还有配套的精品资源点击获取