CNN-SVM图像分类实战:特征提取与小样本遥感应用
发布时间:2026/10/1 17:35:29来源:尧图网络
简介面向深度学习图像分类场景的编程实现资源包重点演示卷积神经网络与支持向量机两种模型的结合方法。内容先介绍卷积网络通过卷积、池化及全连接层自动提取图像特征再将特征送入支持向量机进行分类的思路从而弥补单一模型在特定任务上的不足并提升小样本条件下的分类稳定性。压缩包共含8个文件包括6个功能脚本和2个文档脚本覆盖网络训练、特征提取、向量机训练与预测、特征降维可视化等环节文档则说明整体流程与参数含义整体体积仅8KB代码精简适合初学者快速理解并改造。已有2086人学习这份资源可帮助读者避开重复造轮子的过程直接获得一套可运行的联合分类示例作为项目基线或教学案例均较合适。1. 这个 CNN-SVM 项目到底在做什么不是二选一是接力赛做图像分类的同行看到CNN-SVM这个组合第一反应多半是早过时了。但翻完这份 Land_Use_CNN-master 源码我得说它在特定场景下依然能打CNN 负责把原始图像压缩成高层次的语义特征SVM 拿着这些特征做最后的分类决策两边各干各的拿手活。整个过程用 Python 串起来Keras 搭 CNNScikit-learn 建 SVM中间用特征文件接驳。对正在做土地利用分类、遥感影像小样本分类、或者被过拟合折磨的从业者这份代码给出了一条不需要重训大模型就能提点的路。项目里六个脚本分工非常清楚train.py 训 CNNextract_features.py 抽特征train_svm.py 训分类器svm_predict.py 做推断t_sne.py 做特征可视化。想搞懂这套组合拳怎么打照着跑一遍比看十篇综述都管用。2. 为什么 CNN 抽特征 SVM 分类全连接层的短板与决策边界2.1 CNN 是特征提取器SVM 是精分类器CNN 的卷积层和池化层本质上是把图像从像素空间映射到特征空间。这个映射过程是逐层抽象的浅层卷积核捕捉边缘、颜色块深层卷积核组合出纹理、形状甚至语义部件。真正做分类的是网络末尾的全连接层加 Softmax这一步本质是一个线性分类器套在非线性特征上。问题恰恰出在这里——Softmax 交叉熵训练出来的全连接层在类别间距离较近、样本量不足时决策边界容易画得糙。SVM 的优势是结构风险最小化它找的超平面不光要把训练样本分开还要让间隔最大。这个最大间隔的约束让 SVM 在特征维度高、样本量少的小样本分类任务里泛化能力往往比 Softmax 那一层更稳。把 CNN 卷积层输出的特征向量丢给 SVM相当于先用深度学习做了一次有监督的降维和特征抽象再用统计学习理论里最成熟的分类器收尾。这个组合在遥感土地利用分类这种类别多、每个类别样本几百张、类间差异有时只有纹理不同的任务里是能实打实看到精度提升的。2.2 特征怎么抽全连接层输出还是全局池化输出这也是这份代码包里 extract_features.py 最值得看的地方。抽取特征有两种常见做法一是取最后一个全连接层通常是 Dense 256 或 Dense 512的输出二是取最后一个卷积层后面接全局平均池化GlobalAveragePooling2D的输出。前者特征维度高、语义信息强但全连接层参数量大容易把小样本数据集的噪声也学进去后者没有额外参数相当于把每个特征图的空间信息压缩成一个值泛化更稳。我翻这个项目时注意到它偏向第一种方案取倒数第二层全连接层的激活值作为特征。这对后续 SVM 调参有一个直接影响全连接层输出是 ReLU 激活后的值分布形态是左边大量 0、右边长尾而 RBF 核 SVM 对特征尺度很敏感后面必须做标准化。如果你打算改成全局池化方案提取的特征分布会更接近高斯分布SVM 的 C 和 gamma 参数也能沿用一组默认值少调一轮。两种方案没有绝对优劣土地利用数据里类别纹理差异大的用池化方案类别外观相近的要留给全连接层更多语义抽象。2.3 代码包里的分工六个脚本各管一段整个工作流是串行的脚本之间的接驳全靠中间文件先弄明白谁产出什么、谁消费什么跑起来才不会手忙脚乱。train.py 负责训练 CNN 并保存模型权重文件extract_features.py 加载这个权重把训练集和测试集的图像分别过一遍网络在指定层截断输出导出成 npy 特征文件train_svm.py 读入训练集特征和标签用 Scikit-learn 训练 SVM 并序列化保存模型svm_predict.py 加载测试集特征和训练好的 SVM 模型输出预测结果和分类报告predict.py 是端到端的快速推断入口输入单张图片先过 CNN 再走 SVMt_sne.py 则是在训练 SVM 之前对特征做降维可视化帮你判断特征到底分没分开。提示这套设计里 CNN 只负责特征提取不承担最终分类因此训练 CNN 时用的损失函数和精度指标只是特征质量的间接度量。真正决定最终精度的是 SVM 那一层的参数。3. 跑通 CNN 特征提取train.py 与 extract_features.py 的正确用法3.1 环境与依赖配置代码基于 Python 3 编写核心依赖是 Keras或 TensorFlow 2 里的 tf.keras、Scikit-learn、NumPy、Matplotlib。我建议直接建一个干净的环境避免和其它项目的包版本打架。顺序上先装 TensorFlow再装 scikit-learn最后补 matplotlib 用于 t-SNE 出图。pip install tensorflow2.10.0 pip install scikit-learn1.1.2 pip install numpy1.23.5 matplotlib3.6.2这里有个版本陷阱TensorFlow 2.10 是最后一个在 Windows 上原生支持 GPU 的版本之后就要用 WSL 或者 Linux 环境了。如果你的机器没有 NVIDIA 显卡就用 CPU 版本训练慢一点但不会报错。NumPy 版本要卡在 1.23 附近太新的版本会碰到 Keras 内部 API 兼容问题。如果代码是用老版 Keras 写的注意看 train.py 头部有没有from keras.models import Sequential有的话建议改成from tensorflow.keras.models import Sequential避免加载到已经废弃的独立 Keras 包。3.2 训练 CNN 并保存权重train.py 的核心逻辑是构建一个适用于土地利用图像的卷积网络结构。它不是一个很深的大模型——图像分辨率通常压到 128×128不是 ImageNet 级别的任务深度控制在三个卷积块左右就够了。# train.py 核心结构 from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Conv2D, MaxPooling2D, Flatten, Dense, Dropout from tensorflow.keras.preprocessing.image import ImageDataGenerator IMG_WIDTH, IMG_HEIGHT 128, 128 BATCH_SIZE 32 NUM_CLASSES 6 # 用地类别数按你的数据实际情况改 model Sequential([ Conv2D(32, (3, 3), activationrelu, paddingsame, input_shape(IMG_WIDTH, IMG_HEIGHT, 3)), MaxPooling2D((2, 2)), Conv2D(64, (3, 3), activationrelu, paddingsame), MaxPooling2D((2, 2)), Conv2D(128, (3, 3), activationrelu, paddingsame), MaxPooling2D((2, 2)), Flatten(), Dense(256, activationrelu, namefeature_layer), Dropout(0.5), Dense(NUM_CLASSES, activationsoftmax) ]) model.compile(optimizeradam, losscategorical_crossentropy, metrics[accuracy]) model.summary() # 数据增强小样本里防过拟合的关键 datagen ImageDataGenerator( rotation_range20, width_shift_range0.2, height_shift_range0.2, horizontal_flipTrue, rescale1./255 ) history model.fit( datagen.flow(train_data, train_labels, batch_sizeBATCH_SIZE), epochs50, validation_data(val_data, val_labels) ) model.save(cnn_feature_extractor.h5)这段代码里最关键的是namefeature_layer这一行——extract_features.py 后续就要按这个名字定位截断位置。Dropout(0.5) 在全连接层之后是为了防止特征层过拟合训练集如果发现训练精度很高但验证精度跟不上把这个值从 0.5 往上调对某些场景反而有奇效。数据增强在这一步是必须的土地利用影像里同一类别在不同光照、不同季节下差异很大不加增强的 CNN 特征层会学到太多与类别无关的纹理噪声下游 SVM 的输入特征就脏了。3.3 提取特征并落盘extract_features.py 的参数含义CNN 训练完后extract_features.py 干的事是把训练好的模型在指定层截断让所有图像前向传播一次把那层输出的向量保存下来。# extract_features.py 核心逻辑 import numpy as np from tensorflow.keras.models import load_model, Model IMG_WIDTH, IMG_HEIGHT 128, 128 base_model load_model(cnn_feature_extractor.h5) # 关键取出 feature_layer 的输入作为特征映射输出 feature_model Model(inputsbase_model.input, outputsbase_model.get_layer(feature_layer).output) def extract_to_file(image_paths, labels, out_feat, out_label): features [] for path in image_paths: img load_and_preprocess(path, IMG_WIDTH, IMG_HEIGHT) feat feature_model.predict(np.expand_dims(img, axis0), verbose0) features.append(feat.flatten()) np.save(out_feat, np.array(features)) np.save(out_label, np.array(labels)) extract_to_file(train_paths, train_labels, train_features.npy, train_features_label.npy) extract_to_file(test_paths, test_labels, test_features.npy, test_features_label.npy)注意 load_and_preprocess 是封装函数常见做法是内部调用 OpenCV 或 PIL 读图做 BGR/RGB 转换、缩放到 128×128、除以 255 归一化。这里最容易踩坑的是训练集和测试集的特征必须分别提取绝对不能用训练集的 CNN 去跑测试集然后再混在一起也不能在提取特征之前做全局标准化再划分数据集——那属于数据泄漏SVM 训练出来的精度会虚高部署到新数据立刻现原形。特征落盘成 npy 格式后这份代码的中间产物就是两个文件特征向量矩阵和对应的标签数组SVM 训练阶段只需要读这两个文件不需要再回到图像数据。4. 训练 SVM 并预测从特征文件到分类结果4.1 加载特征与标签SVM 训练阶段的输入是上一步产出的 npy 文件。我建议在加载后先检查 shape确认特征维度和样本数和你预期一致。特征维度通常是 256对应全连接层神经元数样本数应该等于图像张数这一步不要凭记忆跳过。# 检查特征文件 import numpy as np X_train np.load(train_features.npy) y_train np.load(train_features_label.npy) X_test np.load(test_features.npy) y_test np.load(test_features_label.npy) print(训练特征:, X_train.shape, 测试特征:, X_test.shape) print(训练标签:, np.unique(y_train), 测试标签:, np.unique(y_test))这里如果发现 X_test 的样本数和标签数不匹配八成是 extract_features.py 里图像路径列表和标签列表不是一一对应拼接的回去检查路径和标签是不是同一个序。另一个常见情况是标签是字符串比如 urban、water 这类类别名SVM 需要整数编码常见做法是先用 LabelEncoder 做一次映射训练完成后保存编码器预测阶段还要用同一套映射把整数还原成语义标签。4.2 train_svm.py核函数、C 值怎么调拿到特征后SVM 的关键参数就三个核函数、C、gamma。这份代码里默认走高斯核RBF是大多数图像特征的正确起点。RBF 核能处理特征和标签之间的非线性关系而线性核在特征维度较高时可以更快但精度通常略逊。C 是误分类惩罚项C 越大模型越努力让训练集分类正确容易过拟合C 越小决策边界越平滑泛化好但容易欠拟合。gamma 控制单个样本的影响半径gamma 太大每个样本只影响很小范围决策边界支离破碎gamma 太小所有样本互相影响边界过于平滑几乎等于线性分类器。# train_svm.py 核心逻辑 import numpy as np from sklearn.svm import SVC from sklearn.preprocessing import StandardScaler from sklearn.model_selection import GridSearchCV from sklearn import joblib # 新版用 sklearn.externals 里已经没有这个模块了 # 1. 标准化 scaler StandardScaler() X_train_s scaler.fit_transform(X_train) X_test_s scaler.transform(X_test) # 2. 网格搜索找好参数范围 param_grid { C: [0.1, 1, 10, 100], gamma: [scale, 0.001, 0.01, 0.1], } svc SVC(kernelrbf, probabilityTrue, random_state42) grid GridSearchCV(svc, param_grid, cv3, scoringaccuracy, n_jobs-1) grid.fit(X_train_s, y_train) print(最佳参数:, grid.best_params_) print(交叉验证精度:, grid.best_score_) # 3. 用最佳参数重新训练并保存 best_clf grid.best_estimator_ joblib.dump(best_clf, svm_model.pkl) joblib.dump(scaler, scaler.pkl)标准化这一步不能省。CNN 的 ReLU 输出特征分布是左边截断的尺度在不同维度上可能差一个量级RBF 核的距离计算会完全被大尺度维度主导小尺度维度等于没参与分类。fit_transform 在训练特征上做transform 在测试特征上做顺序一定不能反。网格搜索这里 cv3 在小数据集上够用如果样本量充足可以加到 5但要注意别把训练时间拖太长。n_jobs-1 意思是利用全部 CPU 核心并行搜索只对你机器友好别在共享服务器上随手开满。4.3 svm_predict.py用训练好的模型做推断预测阶段是把图像路径 → CNN 特征 → 标准化 → SVM 决策函数 → 标签整条链路串起来。实际部署时不会有人把特征文件存下来再预测而是实时算特征。这份代码里 svm_predict.py 的逻辑是加载 pkl 模型和 scaler然后对新图像走一遍前向传播。# svm_predict.py 部署思路 import numpy as np from sklearn import joblib from tensorflow.keras.models import load_model, Model # 载入 CNN 截断模型、scaler、SVM cnn_model load_model(cnn_feature_extractor.h5) feature_model Model(inputscnn_model.input, outputscnn_model.get_layer(feature_layer).output) scaler joblib.load(scaler.pkl) clf joblib.load(svm_model.pkl) def predict_single_image(img_path, label_encoder): img load_and_preprocess(img_path, 128, 128) feat feature_model.predict(np.expand_dims(img, axis0), verbose0) feat_s scaler.transform(feat.reshape(1, -1)) label_id clf.predict(feat_s)[0] return label_encoder.inverse_transform([label_id])[0] # 示例对测试集批量预测并输出分类报告 from sklearn.metrics import classification_report, confusion_matrix y_pred clf.predict(X_test_s) print(classification_report(y_test, y_pred, target_namesclass_names)) print(confusion_matrix(y_test, y_pred))这里有个值得注意的细节CNN 的 Softmax 输出也叫 predict但与 SVM 的 predict 不是一回事。前者给的是各类别概率分布后者给的是最大间隔超平面下的硬分类结果。如果你在生产环境里需要概率输出SVM 要设置probabilityTrue且预测时用predict_proba而不是predict。开启 probability 后训练时间会明显变长因为要做 K 折交叉验证来校准概率但换来的是可以设置置信度阈值低于阈值的样本直接进入人工复审流程。对土地利用这种错了可能影响规划决策的任务这个概率输出很有价值。5. 避坑记录CNN-SVM 最常见的四个翻车点5.1 特征没标准化SVM 训练极慢且精度上不去现象train_svm.py 跑了一个小时还没出结果或者交叉验证精度停留在某个低水平上不去。原因CNN 全连接层输出经过 ReLU大量维度为 0非零维度数值跨度从 0 到几百RBF 核的距离计算被大数值维度主导同时不同维度的尺度差异导致 Hession 矩阵病态SMO 优化器收敛极慢。解决训练 SVM 前必须做 StandardScaler 标准化。我一般把scaler.fit 放在训练集上测试集只调用 transform不让测试集的信息参与拟合。标准化后 C 的搜索范围从 [0.1, 1, 10, 100] 开始调gamma 优先试scale自动计算值再根据效果微调。从那以后我每次训练 CNN-SVM 都强制把标准化写死在流程里而不是在要不要加之间犹豫。5.2 训练集和测试集特征混在一起提取数据泄漏导致精度虚高现象交叉验证和测试精度都是 98% 以上但部署到新采集的图像上精度掉到 70% 以下。原因extract_features.py 里把全部图像一次性过 CNN统一存成特征文件后再按比例切分训练测试集或者对全部特征做了全量标准化再划分。这样 SVM 在训练时已经间接看过测试集的数据分布信息对应的精度是作弊的。解决trichotomy 处理特征提取就先按目录划分好训练集和测试集分别提取、分别落盘标准化同样严格分开。判断有没有数据泄漏有个笨办法——看训练 SVM 的交叉验证分数和测试分数有多接近如果相差超過 3~5 个百分点先怀疑数据泄漏而不是模型能力强。5.3 t-SNE 图看着分得开SVM 却分错一大片现象t_sne.py 出了图各类别颜色在小范围里聚成一团一团肉眼看着分得很开但 SVM 在测试集上精度一般错的大多集中在某两个相似类别。原因t-SNE 是非线性降维它优化的是局部结构保持而不是全局距离保真。图上分得开只能说明每个类别在局部邻域内紧凑不能保证这些簇在原始高维空间里是线性可分的。SVM 虽然用了核技巧但 RBF 核不是万能的两个类别在特征空间里呈嵌套状分布时核函数也难画出一个干净的边界。解决t-SNE 图只能当作提交流程里的诊断工具不能当精度保障。见到图上分不开的类别应该回头检查这两个类别的混淆矩阵确认到底哪些样本被分错了去查这批样本的图像内容是不是确实存在歧义比如裸地和耕地在某些波段下很难区分。这类问题靠换 SVM 核函数解决不了更有效的办法是给这两个类别增加训练样本或者回到 CNN 训练阶段调高这两个类别的采样权重。5.4 类别不平衡导致 SVM 偏向多数类现象某土地利用类别在训练集里占 70%其他五个类加起来只占 30%SVM 训练完精度看着挺高但看混淆矩阵时发现少数类几乎全被预测成多数类。原因SVM 的优化目标是整体分类误差最小在类别不平衡时把少数类强行分成多数类造成的总损失最小这是数学上的必然不是参数调得不对。解决两种方案。一是调整样本权重在 SVC 里设置class_weightbalanced让少数类的错误惩罚更高二是做数据增强对少数类做更多的旋转、翻转、色彩抖动把样本量补齐。从实际效果看class_weightbalanced对 SVM 的效果不如它在深度学习里的同功能方案那么灵因为 SVM 依赖的是支持向量而不是样本分布少数类样本太少时支持向量根本不够用。我的习惯是先做轻度数据增强把少数类样本量提到多数类的 1/3 以上再配合 class_weight 调整。6. 进阶验证用 t_sne.py 检查特征可分性再决定要不要换核函数训练完 SVM 后别急着收工用 t_sne.py 做一次特征分布可视化你会发现它比一堆精度指标更有诊断价值。t-SNE 把高维的 CNN 特征降到二维平面每一个点是一张图像颜色固定为类别。如果图上同一类别的点聚成紧凑的团、不同类别的团之间有明显空隙说明 CNN 特征层输出的可分性不错SVM 在 RBF 核下应有的表现已经兑现到位。如果看到的是多个类别搅在一起分不出边界说明特征本身就没有把区分信息抽出来这时候去调 SVM 参数是浪费时间。运行 t_sne.py 时有两个参数值得关注。第一个是 perplexity默认 30它决定了每个点在降维时看到的邻居数量。在图像特征场景下样本量几百到一千时 perplexity 用 30 比较稳样本量超过两千建议调到 50否则图上的簇结构会碎成一块一块看不出整体分布。第二个是 n_iter建议设置不低于 1000迭代太少时目标函数没收敛图上会出现假簇给后续判断造成误导。# t_sne.py 可视化关键参数 from sklearn.manifold import TSNE import matplotlib.pyplot as plt tsne TSNE(n_components2, perplexity30, n_iter1000, random_state42) X_tsne tsne.fit_transform(X_train_s) # 用标准化后的特征 plt.figure(figsize(8, 8)) scatter plt.scatter(X_tsne[:, 0], X_tsne[:, 1], cy_train, cmaptab10, s8, alpha0.8) plt.colorbar(scatter) plt.title(CNN Feature t-SNE Visualization) plt.savefig(feature_tsne.png, dpi150)出图之后对照混淆矩阵看图上有哪几簇靠得近混淆矩阵里往往就对应哪几类互相分错。土地利用数据里常见的是灌木丛和低矮植被这类外观相近的类别聚在一起。看到这种局面最有效的动作不是换核函数而是回去检查 CNN 训练时的类别交叉熵损失有没有收敛到比较低的值。如果训练损失还在高位说明这个 CNN 根本没学会区分这些类别SVM 拿到的手艺本来就是残缺的。另一个实用技巧是用 SVM 的决策值做二次筛选。sklearn 的 SVC 在设置probabilityTrue后可以输出decision_function或predict_proba对每个测试样本如果最大概率值低于阈值比如 0.6就把它标注为待人工确认。这个方法在土地利用分类中尤其适用——本来遥感解译就有严格的质检流程机器负责把置信度高的大批样本分掉人只处理机器拿不准的少量样本项目整体交付效率能提升不少。这套 CNN SVM 的流程跑完我的习惯是留下一个特征归档目录每个版本的 CNN 权重、对应的特征文件、SVM 参数和 t-SNE 图放在一起标注当时的数据集版本。为什么因为 SVM 的参数调好之后一旦 CNN 重新训练过特征分布跟着变了原来那组 C 和 gamma 就不一定还有效。每个组合对应一组参数这就是能复现结果的前提。希望这份拆解能帮你在自己的数据上少走几趟弯路。本文还有配套的精品资源点击获取
网站建设高端定制企业官网