多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

深度学习特征提取与SVM分类器融合:CNN-SVM图像分类实战

深度学习特征提取与SVM分类器融合:CNN-SVM图像分类实战 简介CNN-SVM融合模型Python实现源码包面向深度学习中图像分类任务的学习者与研究者旨在解决单独使用CNN或SVM时特征提取与分类边界构造的不足。包内共8个文件包括6个Python脚本、1个说明文档和1个Markdown笔记压缩包大小约8KB规模精简却覆盖从CNN训练、特征提取到SVM分类预测的完整流程。目前已有2086人浏览或学习适合具备一定Python与深度学习基础的入门和进阶用户。源码包提供训练、特征提取、SVM建模、预测与特征可视化等完整脚本便于理解特征分布与模型效果稍作修改即可迁移到其他图像数据集。通过这套代码读者可快速复现CNN提取特征SVM分类的实验流程并进一步调整网络结构或SVM参数优化性能。1. 当 CNN 遇上 SVM为什么深度学习特征还要配一个传统分类器做图像分类时很多人默认卷积神经网络CNN就是终点——卷积层提特征、全连接层做分类一气呵成。但我在处理土地利用类型识别这类遥感图像任务时发现CNN 最后一层输出的特征向量直接扔给 Softmax 分类未必是最优解。Softmax 是在特征分布上拟合一个线性边界而 SVM 天生就是找最大间隔超平面的尤其在高维特征空间里它比 Softmax 更能扛住小样本、类别不平衡的情况。这个资源就是把 Keras 训练好的 CNN 当成特征提取器把最后一层激活输出喂给 Scikit-learn 的 SVM 重新训练分类器。适合那些手里有图像分类需求、已经跑通 CNN 但精度卡住或者想理解特征迁移和分类器融合的从业者。整套代码跑下来你会发现这个组合在中小规模数据集上往往比纯 CNN 更稳。2. CNN-SVM 的融合原理为什么不直接用 Softmax特征又要从哪里取2.1 CNN 特征提取的本质卷积层在做什么CNN 的训练过程本质上是让卷积核在图像上滑动逐层抽象出从边缘、纹理到局部形状的语义信息。以这个项目为例输入图像经过若干卷积层和池化层后会被压平Flatten成一个一维向量这个向量就是模型对输入图像的“理解”。在 Keras 里这个过程的代码形态很直观from keras.models import Sequential from keras.layers import Conv2D, MaxPooling2D, Flatten, Dense model Sequential([ Conv2D(32, (3, 3), activationrelu, input_shape(64, 64, 3)), MaxPooling2D(pool_size(2, 2)), Conv2D(64, (3, 3), activationrelu), MaxPooling2D(pool_size(2, 2)), Flatten(), Dense(128, activationrelu), Dense(21, activationsoftmax) ])这段代码定义了一个两层卷积的 CNN。关键点在最后两行Dense(128)是特征向量所在层Dense(21)是 Softmax 分类层21 对应数据集里的类别数。取特征时我们要的是 128 维那个向量而不是 Softmax 输出的概率分布。这里有个常见的认知误区很多人觉得 Softmax 输出的概率值也是特征喂给 SVM 不也一样吗不是。Softmax 的输出做了归一化把特征压缩到了 0 到 1 的区间而且它本质上是对每个类别的条件概率估计已经丢失了特征空间里的几何结构。SVM 最擅长处理的恰恰是高维空间里的线性可分离性原始特征向量保留了更多的判别信息。另外一个工程上的考究为什么这个项目在Flatten()之后接了一个Dense(128)而不是直接把 Flatten 的结果作为特征因为 Flatten 之后的维度可能高达几十万维直接喂给 SVM 会导致核矩阵计算量大得离谱。中间缩小到 128 维既能保留主要判别信息又能让后续 SVM 训练在可接受的时间范围内完成。2.2 SVM 在高维空间里的优势最大间隔在分类中的实际意义SVM 的核心思想是找一个超平面让它到两类样本的最小距离最大化。这个“最大间隔”带来的泛化优势在小样本场景下特别明显。深度学习的全连接层在做分类时本质上是在学一个 Softmax 映射它受数据分布的影响较大而 SVM 只关心支持向量——那些离决策边界最近的样本。当 CNN 提取的特征已经让同类样本在特征空间里聚拢、异类样本疏离时SVM 在这个特征空间里画一条“最宽”的分界线往往比 Softmax 学到的边界更稳健。我个人的经验是在图像类别超过 10 类、每类样本量只有几百张的中小数据集上CNN-SVM 比纯 CNN 的准确率能高出 2 到 5 个百分点。这个提升不是玄学而是因为 Softmax 的 loss 是在整个训练集上做梯度下降类别不平衡时会偏向样本多的类SVM 的 hinge loss 则天然只关注边界附近的困难样本。Scikit-learn 里做这件事的代码很简洁from sklearn import svm from sklearn.metrics import accuracy_score clf svm.SVC(kernelrbf, C1.0, gammascale) clf.fit(train_features, train_labels) predictions clf.predict(test_features) print(Accuracy:, accuracy_score(test_labels, predictions))这里的kernelrbf是径向基核函数它能把特征映射到无穷维空间实际效果是在原特征空间里拟合出一个非线性决策边界。C是正则化系数惩罚错误分类的力度gamma控制单个样本的影响半径。这两个参数对最终精度影响很大后文避坑章节会详细展开。2.3 这套代码的目录结构每个文件该在哪个阶段用拿到资源包后先别急着跑代码花五分钟理清文件关系能省下很多试错时间。包里除了说明文档Python 脚本按执行顺序是这样排的文件执行阶段作用train.py第一步训练 CNN 基座模型保存权重extract_features.py第二步加载训练好的 CNN提取特征向量train_svm.py第三步用特征训练 SVM 分类器predict.py第四步单独用 CNN 做预测对比用svm_predict.py第四步用 CNNSVM 组合做预测t_sne.py可选t-SNE 可视化特征分布注意这里有个容易迷惑的点extract_features.py依赖train.py产出的权重文件train_svm.py又依赖extract_features.py产出的特征文件它们是链条关系不能跳步。很多新手习惯上来就跑train_svm.py结果报错说找不到.npy特征文件就是这个原因。3. 完整复现流程从零跑通 CNN-SVM 图像分类3.1 环境准备与数据摆放这个项目基于 KerasTensorFlow 后端和 Scikit-learnPython 3.6 以上就能跑。我建议用一个干净的虚拟环境避免版本冲突。安装命令pip install tensorflow2.4.1 keras2.4.3 scikit-learn0.24.2 numpy1.19.5 matplotlib3.3.4版本选型有个讲究TensorFlow 2.4 配 Keras 2.4.3 是一个非常稳定的组合太新的 TensorFlow 版本对旧代码的兼容性反而有风险。numpy必须锁 1.19.5因为 TF 2.4 以下版本对 numpy 1.20 的接口变动会直接报np.floatattribute 错误。数据目录的组织方式直接影响代码能不能跑通。Land_Use_CNN-master这个文件夹里训练和验证图片应该按类别分子目录存放像这样data/ ├── train/ │ ├── agricultural/ │ ├── forest/ │ ├── residential/ │ └── ... └── validation/ ├── agricultural/ ├── forest/ ├── residential/ └── ...Keras 的ImageDataGenerator会读取子目录名自动生成标签。如果代码里写死的路径和你实际的数据位置对不上要么改代码里的路径常量要么按它的约定摆放数据。我个人习惯是建一个data/软链接指向实际数据目录这样不动代码也能跑。3.2 训练 CNN 基座先把特征提取器调好train.py的核心是训练一个能完成分类的 CNN但这个模型的精度并不是最终目标——它存在的意义是让卷积层学到有效的特征表示。训练参数一般这样设置from keras.preprocessing.image import ImageDataGenerator train_datagen ImageDataGenerator(rescale1./255, rotation_range20, width_shift_range0.2, height_shift_range0.2, horizontal_flipTrue) train_generator train_datagen.flow_from_directory( data/train, target_size(64, 64), batch_size32, class_modecategorical ) model.compile(optimizeradam, losscategorical_crossentropy, metrics[accuracy]) history model.fit(train_generator, epochs30, validation_datavalidation_generator)参数说明rotation_range20做 20 度范围内的随机旋转width_shift_range和height_shift_range做平移增强horizontal_flipTrue做水平翻转。这些数据增强手段对小数据集特别重要能有效防止 CNN 过拟合。epochs我一般先设 30观察验证集 loss 下降曲线如果 30 轮还没收敛就加大到 50。这里有个技巧训练完不要直接进下一步先单独跑一下predict.py看看纯 CNN 的基线精度。这个基线值就是后面对比 CNN-SVM 提升幅度的参照物。如果纯 CNN 精度已经很高比如 95%说明这个数据集对 CNN 来说已经很“容易”SVM 带来的提升空间可能不大反之如果纯 CNN 精度在 85% 以下那 SVM 大概率有发挥空间。3.3 提取特征搞清楚输出层和输入 SVM 的数据格式extract_features.py是整个流程最关键的一步它决定了喂给 SVM 的数据长什么样。代码实现如下from keras.models import Model import numpy as np # 加载训练好的权重 model.load_weights(cnn_weights.h5) # 从模型中截取特征层去掉最后的 Softmax 输出层 feature_layer Model(inputsmodel.input, outputsmodel.get_layer(dense_128).output) def extract_features_from_generator(generator, sample_count): features [] labels [] for i in range(sample_count // generator.batch_size): batch_images, batch_labels next(generator) batch_features feature_layer.predict(batch_images) features.append(batch_features) labels.append(batch_labels.argmax(axis1)) return np.vstack(features), np.concatenate(labels) train_features, train_labels extract_features_from_generator(train_generator, 2000) np.save(train_features.npy, train_features) np.save(train_labels.npy, train_labels)注意get_layer(dense_128)这里的层名要和train.py里定义层时一致。如果你在train.py里改了Dense层比如加了一个 256 维的中间层层名就会变要用model.summary()先确认层的名字。sample_count要设成 batch_size 的整数倍否则最后一批数据不足 32 张next(generator)可能取不到干净的数据。特征提取完成后去检查一下train_features.npy的 shape。它应该是(样本数, 128)的二维数组train_labels.npy是(样本数,)的一维数组。这两个文件的维度匹配是 SVM 训练不出错的前提。3.4 训练 SVM 分类器参数选择与交叉验证train_svm.py里读取刚才保存的.npy文件然后用 Scikit-learn 训练 SVMfrom sklearn import svm from sklearn.model_selection import GridSearchCV import numpy as np train_features np.load(train_features.npy) train_labels np.load(train_labels.npy) # 先做标准化这一步容易被忽略但对 SVM 影响很大 from sklearn.preprocessing import StandardScaler scaler StandardScaler() train_features_scaled scaler.fit_transform(train_features) # 网格搜索找最优参数 param_grid {C: [0.1, 1, 10, 100], gamma: [0.001, 0.01, 0.1, 1]} grid GridSearchCV(svm.SVC(kernelrbf), param_grid, cv5) grid.fit(train_features_scaled, train_labels) print(Best params:, grid.best_params_) clf grid.best_estimator_为什么标准化这一步很关键SVM 的 RBF 核依赖样本间的高斯距离如果特征数值范围差异大比如某维特征在 0 附近另一维在 100 附近距离度量就会被数值大的维度主导SVM 学到的边界会失真。StandardScaler把每维特征变成零均值、单位方差保证各维特征对距离的贡献等价。这个细节不处理好后面调 C 和 gamma 基本是白调。网格搜索的cv5是五折交叉验证把训练数据分成 5 份轮流拿 1 份做验证、4 份做训练最终取平均精度最高的一组参数。参数范围我习惯先粗后细第一轮 C 和 gamma 各给三四个跨度大的候选值确定大概区间后再在小范围里精调。3.5 推理与精度对比CNN 单独跑还是 CNN-SVM 一起上最后一步是验证效果。svm_predict.py做的事情是先用 CNN 的 feature_layer 提取测试图片特征再做标准化然后交给训练好的 SVM 预测from sklearn.preprocessing import StandardScaler import numpy as np # 假设 test_features 是从测试集提取的特征 test_features_scaled scaler.transform(test_features) y_pred clf.predict(test_features_scaled) # 和纯 CNN 的预测结果做对比 from sklearn.metrics import classification_report print(classification_report(test_labels, y_pred))代码里的scaler.transform()要注意是 transform 而不是 fit_transform——标准化器的均值和方差是训练阶段在训练集上拟合好的测试阶段直接用同一套参数转换就够了重新 fit 会导致训练和测试数据的分布不一致。如果 CNN-SVM 的精度反而不如纯 CNN先不要怀疑这个组合无效。检查一下特征层是不是选错了常见的是误把 Flatten 层当成了 Dense 层另一个高频错误是 SVM 参数没调好C 太大导致过拟合训练集gamma 太大导致决策边界过于崎岖。4. 避坑指南我在这套流程里翻过的四次车4.1 报错np.floatattribute 不存在现象跑train.py时Keras 内部调用 numpy 报module numpy has no attribute float直接崩溃。原因numpy 1.24 及以上版本移除了np.float、np.int这些别名但旧版 TensorFlow/Keras 代码里还在引用这些过时接口。解决把 numpy 降级到 1.19.5 或 1.23.5 版本。如果因为其他依赖不能降级可以在导入环境变量里加一行兼容import numpy as np np.float np.float64 np.int np.int32这个兼容写法只适合在入口脚本顶部加不要乱改库代码。4.2 提取特征时get_layer报 KeyError现象model.get_layer(dense_128)抛异常提示找不到这个层。原因Keras 的层名是自动生成的Dense(128)在模型里实际叫dense_1、dense_2还是dense_3取决于你定义了几个全连接层以及顺序。如果你在 Conv2D 之后多加了层编号会平移写死的名字就对不上了。解决在train.py里加载完模型后先执行model.summary()打印出每一层的名字和输出 shape然后照着实际名字去get_layer()。我一般会把这个方法改成def get_feature_layer(model): for layer in model.layers: if dense in layer.name and layer.output_shape[-1] 128: return layer.name raise ValueError(No matching dense layer found)自动匹配层名避免手写编号。4.3 SVM 训练特别慢几个小时都不出结果现象train_svm.py跑GridSearchCV时一个参数组合要等十几分钟整体跑完遥遥无期。原因特征维度太高比如直接用 Flatten 层的输出做特征维度到了上万RBF 核计算两两样本的距离矩阵复杂度是 O(n²×d)样本量和维度一起加大后指数级膨胀。解决先检查特征维度如果超过 1000 就对特征做降维常见做法是 PCAfrom sklearn.decomposition import PCA pca PCA(n_components128) train_features_pca pca.fit_transform(train_features_scaled)另一个办法是把GridSearchCV改成随机搜索RandomizedSearchCV在参数空间中随机采样有限组组合能显著缩短时间。如果数据集超过一万张图SVM 的核矩阵就会变得非常大这时候优先考虑用线性核linear配合C参数调整速度能快一到两个数量级。4.4 CNN 在训练集上 98% 准确率SVM 却只有 80%现象单独跑 CNN 的验证精度有 90% 以上但把特征提取出来喂给 SVM 后精度反而掉到 80%。原因多数情况是特征提取时用错了数据。比如用训练集做了标准化拟合但提取特征时又对测试集重新做了一次标准化把两次特征分布搞乱了。另一个可能数据增强导致特征分布被“扭曲”训练时的图片是旋转翻转过的测试时是原图特征空间不一致。解决统一标准化流程训练和测试都用同一个StandardScaler实例做 transform。对于数据增强的影响我建议提取特征时关掉数据增强——用ImageDataGenerator(rescale1./255)就够了不要带旋转和平移。否则 CNN 看到的是增强后的特征分布SVM 拿这个学边界而测试特征来自未增强的图片分布对不上。5. 进阶技巧用 t-SNE 验证特征质量再手动微调 SVM 参数先讲一个快速判断 CNN 提特征质量的方法跑t_sne.py把特征向量降到二维平面上可视化。如果不同类别的点各自聚成团团与团之间有明显空隙说明特征空间区分度很好这时接 SVM 几乎必然有效如果各个类别的点混成一锅粥那问题不在 SVM而在 CNN 没有训好——需要回去加深网络、增大训练轮数或加数据增强。t-SNE 的代码核心就几行from sklearn.manifold import TSNE import matplotlib.pyplot as plt features_2d TSNE(n_components2, perplexity30, n_iter1000).fit_transform(train_features) plt.scatter(features_2d[:, 0], features_2d[:, 1], ctrain_labels, cmaptab20, s5) plt.savefig(tsne_features.png, dpi150)perplexity30是 t-SNE 的困惑度参数它控制每个点考虑多少近邻。样本数大几千张时 30 是安全值小样本数据集可以降到 15 左右。这个可视化适合在提取特征后、训练 SVM 前做一次能帮你提前判断组合方案有没有戏。SVM 参数微调上网格搜索找到的C和gamma是粗糙区间里的最优实际还能再进一步精调。我的做法是锁定一个参数画另一个参数的学习曲线。比如固定C10把gamma从 0.0001 到 1 按对数间隔取 10 个值画出交叉验证精度曲线看是单调上升还是过了峰值就掉。如果单调上升说明范围没取够继续加大如果过峰后下降说明已经过拟合了。另外有个偏差值得留意SVM 在训练集上精度极高接近 100%是正常的因为它只受支持向量影响不追求把所有训练点都完美分开。不要被这个数值迷惑判断模型好坏只用验证集或测试集精度。我在这套流程里反复用下来的习惯是先跑通默认参数拿到一个基准结果然后保存模型和预测输出再开始调参。每次只动一个变量记录它对最终精度的影响。从那以后我每次做 CNN-SVM 都强制走一遍“CNN 基线精度 → 特征可视化 → SVM 默认参数 → 单变量调参”这个顺序不确定就先画图再动参数。希望帮到你。本文还有配套的精品资源点击获取
返回列表