ARTICLE DETAIL

资讯详情

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

遥感图像识别四模型串联实战:kNN+SVM+CNN+LSTM分层架构

遥感图像识别四模型串联实战:kNN+SVM+CNN+LSTM分层架构 简介本资源是一套完整的遥感图像识别高分课程设计项目面向人工智能、通信工程、自动化等专业的在校学生与初阶开发者聚焦机器学习在遥感领域的多模型融合实践。内容涵盖kNN、SVM、CNN与LSTM四类主流算法的实现与对比分析配套详细技术文档、可视化结果图如分类热力图、模型训练曲线及可直接运行的Python代码支持快速复现与二次开发。压缩包共33个文件含6个核心py脚本、4个Jupyter Notebook实验记录、5份Markdown说明文档、13张算法效果PNG图及2个C辅助工具整体仅1.17MB轻量易部署。已有57人下载学习资源经导师评审获95分高分代码全部实测通过目录按预处理→kNN→SVM→CNN→LSTM逐层组织结构清晰适合作为毕设选题参考、课程设计范例或机器学习进阶实战入口。1. 遥感图像识别不是调个 pretrain model 就完事kNNSVMCNNLSTM 四模型串联实战真能跑通、可答辩、能改、能交差你手头有一堆 Sentinel-2 或 GF-2 的遥感图想自动区分水体、农田、建筑、林地——但直接扔进 ResNet50准确率卡在 78% 上不去换数据增强加注意力调参三天后发现 validation loss 突然崩塌loss 曲线像心电图一样乱跳。这不是模型不行是遥感图像的“坑”根本没被系统性填平光谱维度高、空间纹理杂、样本不均衡、标注成本贵。这个资源包里塞的不是单个模型脚本而是一套分层递进、可解释、可替换、可复现的四段式识别链先用 kNN 做粗筛快、稳、不训练再用 SVM 做中粒度判别对小样本友好接着 CNN 提取局部空间特征卷积核尺寸、padding 方式全写死在 config.py 里最后 LSTM 捕捉多时相序列依赖比如同一地块春夏秋冬四期影像的时间演化。所有代码跑通在 Python 3.8 PyTorch 1.12 scikit-learn 1.0.2 环境下实测在 UC Merced Land Use 数据集上达到 94.2% OAOverall Accuracy比单模型提升 6.7%。适合正在赶毕设 deadline 的本科生、需要快速出 demo 的课程设计组、以及想搞懂“为什么遥感识别非要拼模型”而不是“怎么拼”的一线算法工程师。2. 四模型串联不是炫技为什么必须 kNN→SVM→CNN→LSTM 这个顺序2.1 为什么第一关非得是 kNN——它扛住了遥感数据最致命的“冷启动”问题遥感项目最常翻车的不是模型不准而是第一张图就加载失败。这个包里的0_kNN模块不是简单调sklearn.neighbors.KNeighborsClassifier而是做了三件事用rasterio读取.tif时强制指定nodata0并做 band-wise 归一化不是全局 min-max而是每波段独立除以该波段 98% 分位数特征向量不是 raw pixel而是先用cv2.calcHist提取 3 通道灰度直方图bins16再拼接成 48 维向量k 值不硬编码而是通过k_range range(1, 11)在验证集上暴力搜索最优 k并把结果存进knn_params.json。# 0_kNN/knn_main.py 关键片段 import rasterio import cv2 import numpy as np def load_and_preprocess_tif(tif_path): with rasterio.open(tif_path) as src: img src.read() # shape: (bands, height, width) # 每波段独立归一化避免近红外波段数值过大淹没可见光 for b in range(img.shape[0]): p98 np.percentile(img[b], 98) if p98 0: img[b] np.clip(img[b] / p98, 0, 1) return img.transpose(1, 2, 0) # (h,w,b) def extract_hist_features(img_rgb): # 强制转灰度再提直方图——遥感图 RGB 不是真彩色灰度统计更鲁棒 gray cv2.cvtColor((img_rgb * 255).astype(np.uint8), cv2.COLOR_RGB2GRAY) hist cv2.calcHist([gray], [0], None, [16], [0, 256]) return hist.flatten() / hist.sum() # 归一化为概率分布提示这段代码里cv2.cvtColor(..., cv2.COLOR_RGB2GRAY)是玄学关键——遥感图的 R/G/B 波段实际对应的是近红外/红/绿直接算 RGB 直方图会丢失光谱意义转灰度后反而抓住了反射率强度分布kNN 准确率提升 12%。2.2 SVM 为什么选 RBF 核而不是线性核——它专治遥感图的“类内离散”遥感图像里同一类地物比如“农田”可能包含水稻田、旱地、休耕地光谱差异比“农田 vs 建筑”还大。线性 SVM 在这种场景下决策边界太僵硬。包里1_SVM模块用GridSearchCV在{C: [0.1,1,10], gamma: [scale,auto,0.001,0.01]}空间搜索但不是搜完就完事——它把每个 class 的 support vector 数量、平均 margin 距离都写进svm_report.txt方便你判断哪类最难分。# 1_SVM/svm_train.py 关键参数 from sklearn.svm import SVC from sklearn.model_selection import GridSearchCV param_grid { C: [0.1, 1, 10], gamma: [scale, auto, 0.001, 0.01], # 注意scale 是 sklearn 默认但遥感数据常需手动调小 kernel: [rbf] # 线性核在本项目中被实测淘汰OA 下降 5.2% } grid GridSearchCV(SVC(probabilityTrue), param_grid, cv3, scoringaccuracy) grid.fit(X_train, y_train) print(fBest params: {grid.best_params_}) # 输出如 {C: 1, gamma: 0.01, kernel: rbf}参数说明gamma0.01比默认scale更小意味着 RBF 核的“影响半径”更大能更好覆盖农田内部的光谱离散性C1是平衡过拟合与欠拟合的甜点C 太大会让 SVM 过度关注少数难分样本比如混有建筑碎片的农田边缘。2.3 CNN 为什么只用 3 层卷积——不是层数越多越好而是要匹配遥感图的“有效感受野”遥感图分辨率高常为 2m/0.5m但目标尺度固定一栋楼约 20×20 像素一块田约 100×100。包里2_CNN的网络结构是Conv1: 323×3, stride1, padding1 → ReLU → MaxPool2d(2)Conv2: 643×3, stride1, padding1 → ReLU → MaxPool2d(2)Conv3: 1283×3, stride1, padding1 → ReLU → AdaptiveAvgPool2d(4)FC: 128 → num_classes为什么不用 ResNet因为 ResNet 的 shortcut 会让梯度绕过浅层特征而遥感图的纹理信息如田埂走向、屋顶排列恰恰藏在浅层。AdaptiveAvgPool2d(4)强制把 feature map 压到 4×4既保留空间结构又避免全连接层参数爆炸。# 2_CNN/models.py 定义 import torch.nn as nn class RemoteSensingCNN(nn.Module): def __init__(self, num_classes21): # UC Merced 有 21 类 super().__init__() self.conv1 nn.Conv2d(3, 32, 3, padding1) # 输入是 RGB 三波段 self.conv2 nn.Conv2d(32, 64, 3, padding1) self.conv3 nn.Conv2d(64, 128, 3, padding1) self.pool nn.MaxPool2d(2) self.avgpool nn.AdaptiveAvgPool2d(4) # 关键不是 1×1是 4×4 self.fc nn.Linear(128*4*4, num_classes) def forward(self, x): x self.pool(F.relu(self.conv1(x))) x self.pool(F.relu(self.conv2(x))) x F.relu(self.conv3(x)) # 第三层不池化保留细节 x self.avgpool(x) # 压缩到 4×4 x x.view(x.size(0), -1) # flatten return self.fc(x)逻辑说明第三层卷积后不接池化是为了让AdaptiveAvgPool2d(4)能采样到更细粒度的纹理view(x.size(0), -1)中的-1自动计算展平后维度128×4×42048避免手算出错。2.4 LSTM 为什么只接 CNN 最后一层——它不是处理像素序列而是处理“时相特征序列”很多人误以为 LSTM 要喂原始图像序列比如 4 张图拼成 4×H×W这会导致显存爆炸。这个包的3_LSTM模块是先用 CNN 提取每张时相图的 128 维 embedding即fc层前的输出再把 N 期 embedding 拼成(N, 128)序列喂给 LSTM。这样既利用了 CNN 的空间建模能力又让 LSTM 专注时间演化。# 3_LSTM/lstm_main.py 关键流程 from torch.nn import LSTM, Linear class TemporalLSTM(nn.Module): def __init__(self, input_size128, hidden_size64, num_layers1, num_classes21): super().__init__() self.lstm LSTM(input_size, hidden_size, num_layers, batch_firstTrue) self.classifier Linear(hidden_size, num_classes) def forward(self, x): # x shape: (batch, seq_len, 128) ← 来自 CNN 的 embedding lstm_out, _ self.lstm(x) # lstm_out shape: (batch, seq_len, hidden_size) # 取最后一个时相的输出假设最后一个是最新时相 last_output lstm_out[:, -1, :] # (batch, hidden_size) return self.classifier(last_output)参数说明hidden_size64是经验值——比输入 128 小一半既能压缩时序信息又不至于丢失关键变化模式batch_firstTrue避免维度混乱这是 PyTorch LSTM 的经典坑。3. 数据预处理不是 copy-pastedata_preprocessing目录里藏着遥感专用的 5 个硬核操作3.1crop_to_label不是按固定尺寸切图而是按 ground truth mask 切遥感标注图.png里白色区域是目标黑色是背景。但直接cv2.resize会模糊边缘。包里用scipy.ndimage.label找连通域再对每个连通域做最小外接矩形cv2.boundingRect确保每张 crop 图都完整包裹一个地物实例且长宽比保持原图比例。# data_preprocessing/crop_to_label.py import cv2 import numpy as np from scipy import ndimage def crop_by_mask(image, mask): # mask 是单通道 binary 图 labeled, num_features ndimage.label(mask) crops [] for i in range(1, num_features 1): region (labeled i) coords np.argwhere(region) y_min, x_min coords.min(axis0) y_max, x_max coords.max(axis0) # 加 5 像素 padding 防止切掉边缘 y_min max(0, y_min - 5) x_min max(0, x_min - 5) y_max min(image.shape[0], y_max 5) x_max min(image.shape[1], x_max 5) crop image[y_min:y_max1, x_min:x_max1] crops.append(crop) return crops逻辑说明ndimage.label比 OpenCV 的findContours更稳——它能处理 mask 中的微小孔洞遥感标注常有噪点且返回的labeled数组可直接索引避免 contour 排序错乱。3.2balance_by_area按面积重采样不是按图片数量遥感数据天然不均衡建筑区小但密集林地区大但稀疏。如果按图数量采样模型会严重偏向小目标。balance_by_area.py计算每张图中各类别的像素面积占比再按面积加权抽样。例如一张图含 1000 像素建筑 5000 像素农田则它贡献 1000 份“建筑样本”和 5000 份“农田样本”。# data_preprocessing/balance_by_area.py def calculate_class_area(mask, num_classes21): area np.zeros(num_classes) for c in range(num_classes): area[c] np.sum(mask c) return area # 返回 21 维向量每维是该类像素数 # 在 DataLoader 中使用 weighted sampler weights [] for mask_path in mask_paths: area calculate_class_area(cv2.imread(mask_path, 0)) # 权重 总像素数 / 该图中最大类别面积防止单类主导 weights.append(mask_total_pixels / area.max()) sampler WeightedRandomSampler(weights, num_sampleslen(weights), replacementTrue)参数说明replacementTrue是必须的——因为面积小的类别如“足球场”需要被多次采样才能凑够 batch size。3.3spectral_normalize不是 min-max而是按波段统计特性归一化Sentinel-2 有 13 个波段但包里只用 B02/B03/B04/B08蓝/绿/红/近红外。spectral_normalize.py对每个波段单独做去均值减去该波段在整批训练集上的均值除标准差除以该波段在整批训练集上的标准差截断clip 到 [-3σ, 3σ] 区间遥感图常有云阴影导致极值# data_preprocessing/spectral_normalize.py def normalize_band(band_data, mean, std): normalized (band_data - mean) / std return np.clip(normalized, -3, 3) # 关键遥感图云阴影导致 σ 外异常值多 # 训练集统计运行一次结果存入 stats.npz means [] stds [] for band_idx in [1,2,3,7]: # B02/B03/B04/B08 对应索引 1,2,3,7 band_stack np.stack([load_band(p, band_idx) for p in train_paths]) means.append(np.mean(band_stack)) stds.append(np.std(band_stack)) np.savez(stats.npz, meansmeans, stdsstds)逻辑说明clip到 ±3σ 是遥感预处理铁律——云、雪、镜面反射会造成单像素值突变不 clip 会导致 BN 层失效。3.4augment_remote遥感专用增强禁用旋转/镜像普通 CV 增强如随机旋转会破坏遥感图的地理朝向北方向必须保持一致。augment_remote.py只启用RandomBrightnessContrast亮度对比度微调模拟不同光照GaussianBlur模拟传感器模糊kernel_size3CoarseDropout模拟云遮挡dropout 5% 像素块size(16,16)# data_preprocessing/augment_remote.py import albumentations as A remote_aug A.Compose([ A.RandomBrightnessContrast(p0.5, brightness_limit0.1, contrast_limit0.1), A.GaussianBlur(blur_limit(3,3), p0.3), A.CoarseDropout(max_holes1, max_height16, max_width16, fill_value0, p0.3), # 禁用Rotate, HorizontalFlip, VerticalFlip —— 地理方向不能变 ])注意fill_value0是关键——遥感图的 nodata 值是 0云遮挡区域也应填 0而非随机噪声。3.5split_by_region不是随机划分而是按地理区块划分防止训练集和测试集出现“同区域不同期”导致数据泄露。split_by_region.py读取影像元数据中的UL_LAT,UL_LON左上角经纬度将所有图按经纬度网格0.1°×0.1°分组再按组划分 train/val/test比例 7:1.5:1.5。# data_preprocessing/split_by_region.py def get_grid_id(lat, lon, grid_size0.1): # 向下取整到 grid_size 倍数 grid_lat int(lat / grid_size) * grid_size grid_lon int(lon / grid_size) * grid_size return f{grid_lat:.1f}_{grid_lon:.1f} # 读取 .xml 元数据获取坐标示例 import xml.etree.ElementTree as ET tree ET.parse(IMG_PHR1A_MS_202105121023121.XML) root tree.getroot() ul_lat float(root.find(.//UL_LAT).text) ul_lon float(root.find(.//UL_LON).text) grid_id get_grid_id(ul_lat, ul_lon)逻辑说明get_grid_id用int(lat / grid_size) * grid_size而不是round()确保同一网格内所有图 ID 严格一致避免浮点误差导致分组错乱。4. 避坑这 4 个血泪经验让我重装了 3 次 Ubuntu 才摸清4.1 现象kNN模块报错ValueError: Expected 2D array, got 1D array instead原因extract_hist_features()返回的是(16,)一维数组但KNeighborsClassifier.fit()要求(n_samples, n_features)二维输入。新手常直接knn.fit(hist, label)忘了 hist 是单样本。解决必须hist.reshape(1, -1)或np.array([hist])。包里0_kNN/knn_main.py第 47 行已修复但如果你自己改代码务必检查X_train形状是否为(N, 16)。4.2 现象SVM训练时内存爆满32GB卡死不动原因GridSearchCV默认n_jobs-1会开满 CPU 核心但 SVM 的 pairwise distance 计算内存复杂度是 O(n²)10000 张图直接需要 100GB 内存。解决在1_SVM/svm_train.py中强制设n_jobs1或把cv3改成cv2。实测n_jobs1时内存峰值从 32GB 降到 4.2GB总耗时只增加 18%。4.3 现象CNN训练 loss 不下降acc 停在 4.8%等于随机猜原因2_CNN/train.py里DataLoader的num_workers0与rasterio的 GDAL 线程冲突导致部分图读取为全黑nodata0 被当背景。解决把DataLoader(num_workers0)或在__main__开头加os.environ[GDAL_NUM_THREADS] 1。包里2_CNN/train.py第 12 行已加num_workers0注释。4.4 现象LSTM报错RuntimeError: input.size(-1) must be equal to input_size原因3_LSTM/lstm_main.py中input_size设为 128但你改了 CNN 的hidden_size比如改成 256却没同步改 LSTM 的input_size。解决打开3_LSTM/config.py确认CNN_EMBED_DIM 128与LSTM_INPUT_SIZE 128严格一致。包里config.py已用assert CNN_EMBED_DIM LSTM_INPUT_SIZE做校验。4.5 现象README.md说支持Python 3.8但pip install -r requirements.txt报torch 1.12无 wheel原因PyTorch 1.12 官方 wheel 只支持 CUDA 11.3/11.6而你的nvidia-smi显示 CUDA 12.1。解决不要pip install torch去 https://download.pytorch.org/whl/torch_stable.html 手动下载对应 CUDA 版本的 wheel例如pip install torch-1.12.1cu116-cp38-cp38-linux_x86_64.whl注意cp38对应 Python 3.8cu116对应 CUDA 11.6必须完全匹配否则import torch会 segmentation fault。5. 模型融合不是简单平均用ensemble.py实现带置信度加权的四模型投票5.1 为什么不用sklearn.ensemble.VotingClassifier——它无法处理 CNN/LSTM 的时序输出VotingClassifier要求所有模型输出predict_proba()但 LSTM 的forward()输出是 logits需手动 softmax且 kNN/SVM 的概率是predict_proba()CNN/LSTM 的概率是F.softmax(output, dim1)数值尺度不一致。包里ensemble.py用置信度归一化 动态权重# ensemble.py import torch.nn.functional as F def ensemble_predict(knn_prob, svm_prob, cnn_logits, lstm_logits, weights): # Step 1: 统一转概率 cnn_prob F.softmax(cnn_logits, dim1).cpu().numpy() lstm_prob F.softmax(lstm_logits, dim1).cpu().numpy() # Step 2: 置信度归一化让各模型输出在 [0,1] 同尺度 knn_conf knn_prob.max() # kNN 置信度 最大概率 svm_conf svm_prob.max() cnn_conf cnn_prob.max() lstm_conf lstm_prob.max() # Step 3: 加权平均权重 × 置信度 × 概率 weighted_sum ( weights[0] * knn_conf * knn_prob weights[1] * svm_conf * svm_prob weights[2] * cnn_conf * cnn_prob weights[3] * lstm_conf * lstm_prob ) return np.argmax(weighted_sum, axis1) # weights 示例[0.15, 0.25, 0.35, 0.25] —— CNN 权重最高因它空间特征最强逻辑说明knn_conf * knn_prob是核心——kNN 在简单场景如纯水体置信度接近 1.0此时它权重放大在复杂场景如城乡结合部置信度可能只有 0.4权重自动衰减。这比固定权重更鲁棒。5.2 如何确定weights——用验证集 grid search不是拍脑袋包里ensemble_tune.py在[0.05,0.1,0.15,...,0.4]范围内对 4 个权重做穷举共 125 种组合记录每种组合在验证集上的 OA。结果存在ensemble_weights.csvkNNSVMCNNLSTMOA (%)0.100.200.400.3093.80.150.250.350.2594.2← 最优0.200.300.300.2093.5参数说明CNN 权重 0.35 最高印证了空间特征对遥感识别的决定性作用LSTM 权重 0.25 低于 CNN说明时序信息是补充而非主导。5.3 验证 ensemble 效果用confusion_matrix_analysis.py看哪类提升最大单纯看 OA 会掩盖问题。confusion_matrix_analysis.py生成 per-class accuracy 表并标出提升最显著的 3 类ClassSingle Model Acc (%)Ensemble Acc (%)Δ (%)原因分析Bridge82.191.79.6kNN 粗筛 CNN 边缘检测联合发力Football Field76.389.212.9SVM 对规则几何形状判别强LSTM 补充草皮状态变化River89.593.13.6所有模型都强提升有限技巧运行python confusion_matrix_analysis.py --model ensemble会自动画出热力图红色越深表示该类被其他类误判越多——比如 “Industrial” 和 “Storage Tanks” 常混淆说明需在 CNN 中加 shape-aware loss。从那以后我每次部署遥感识别 pipeline都强制走一遍ensemble.py的置信度加权流程哪怕单模型 OA 已达 92%——因为真实场景里模型在“难样本”上的表现才是交付时甲方盯着看的指标。希望帮到你。本文还有配套的精品资源点击获取
返回列表