简介:一份面向深度学习与图像分类实践者的CNN-SVM融合实现资源,聚焦如何将卷积神经网络自动提取的特征输入支持向量机完成分类,以提升模型精度,适合图像识别及遥感地物分类等空间特征明显的任务。资源包共8个文件,压缩后约8KB,包含6个Python脚本、1个说明文档和1个Markdown文件;其中Python脚本完整覆盖了CNN模型训练、深度特征提取、SVM训练与预测,并附有t-SNE可视化脚本用于检验特征分布,说明文档和README则梳理了从Keras构建网络到scikit-learn接入SVM的衔接思路。目前已有2086人学习/下载。读者通过这份资源能拿到可直接运行的融合流程示例,既能掌握用CNN最后一层激活输出作为SVM输入的核心做法,也能参考现有代码调整网络层数、核函数与惩罚参数,快速迁移到自身图像分类任务中。
1. CNN-SVM 这个组合:卷积特征交给支持向量机,为什么在小样本图像上更管用
看过不少深度学习做图像分类的项目,最后的分类器几乎清一色是 Softmax,很少有人会回头再用一个经典 SVM。这份源码包恰好是反着来的:先用卷积神经网络把图像转成特征向量,再交给支持向量机划分类别。路线听起来传统,但我在小样本、类别不平衡的遥感图像分类场景里实际跑过,这个组合往往比单独调 CNN 更容易出效果,尤其当你的训练样本只有几百张、想靠数据增强硬撑 Softmax 又撑不动的时候。
这份资源是一个完整的 Python 实现,工程目录叫 Land_Use_CNN-master,核心脚本包括训练 CNN、提取特征、训练 SVM、预测和 t-SNE 可视化,一条链路走到底。适合样本量不大、想快速拿到可解释决策边界的从业者,也适合想把深度学习和 SVM 衔接机制看明白的初学者。解压后按顺序跑,一小时内能看到分类报告和可视化输出。
2. 源码包结构与执行顺序:六个 Python 脚本各管哪一段
2.1 文件清单与依赖关系
拿到压缩包先别急着跑,先看一遍文件列表,弄清楚每条脚本的上下游关系。包里除了说明文档,核心是 6 个脚本和 Land_Use_CNN-master 目录:
| 文件 | 职责 | 产出物 |
|---|---|---|
| train.py | 训练 CNN 分类网络,保存权重 | cnn_model.h5 |
| extract_features.py | 加载 CNN,截取倒数第二层输出作为特征 | features.npy、labels.npy |
| train_svm.py | 用特征训练 SVM 分类器 | svm_model.pkl、scaler.pkl |
| predict.py | 用 CNN 模型预测单张图 | 分类结果 |
| svm_predict.py | 加载 SVM 模型预测,输出分类报告 | 评估指标 |
| t_sne.py | 对特征做 t-SNE 可视化 | 二维散点图 |
执行链是严格单向的:train.py → extract_features.py → train_svm.py → svm_predict.py。t_sne.py 可以插在提取特征之后、训练 SVM 之前,用来验证 CNN 提取的特征是否真的分得开。
提示:跳过 train.py 直接跑 extract_features.py,会在 load_model 那一步直接报错,因为没有权重文件。常见做法是先把 CNN 训到收敛,哪怕分类准确率一般,特征也比随机初始化强很多。
2.2 环境安装与版本选择
这个项目用的是 Keras 构建 CNN、Scikit-learn 训练 SVM,两者都需要 python 环境。版本搭配我踩过一轮坑,最后固定下来的组合是这样:
conda create -n cnn_svm python=3.8 conda activate cnn_svm pip install tensorflow==2.10.0 pip install scikit-learn==1.2.2 pip install numpy==1.23.5 pandas matplotlib joblib为什么要锁这几个版本:TensorFlow 2.10 是最后一个能在 Windows 原生环境稳定安装的版本,2.11 以上对 Windows 的 CUDA 支持方式变了,容易在编译环节翻车。scikit-learn 1.2.2 配 numpy 1.23.5 是官方测试过的组合,直接跑 t_sne.py 不会因为版本不匹配报错。如果你用的是 macOS 或 Linux,TensorFlow 版本可以放宽,但 numpy 和 scikit-learn 的建议不要动。
装完后建议跑一条冒烟命令:python -c "import tensorflow, sklearn, numpy; print(tensorflow.__version__, sklearn.__version__, numpy.__version__)",三个版本号都打印出来再继续,能省掉后面一半的报错排查时间。这一步看起来多余,实际作用是把环境问题从项目问题里隔离出来。
2.3 train.py 大致做了什么
train.py 的任务是训练一个能对图像分类的 CNN,但它的产出不只是给 Softmax 分类用的,更多是为后续特征提取服务。典型的模型结构是这样:
# train.py 中的核心模型结构,适用于 64x64 的 RGB 图像 from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Conv2D, MaxPooling2D, Flatten, Dense, Dropout model = Sequential([ Conv2D(32, (3, 3), activation="relu", padding="same", input_shape=(64, 64, 3)), MaxPooling2D(pool_size=(2, 2)), Conv2D(64, (3, 3), activation="relu", padding="same"), MaxPooling2D(pool_size=(2, 2)), Flatten(), Dense(128, activation="relu"), # 这一层的输出后面会被 SVM 当特征用 Dropout(0.5), Dense(n_class, activation="softmax") # n_class 按你的数据集类别数改 ]) model.compile(optimizer="adam", loss="categorical_crossentropy", metrics=["accuracy"])参数说明:input_shape 按工程默认的 64×64 写,如果你的 Land Use 图像是 128×128,把第一个卷积层的 input_shape 同步改掉即可。padding="same" 是让卷积不缩小特征图尺寸,对浅层网络来说信息保留更完整。Dropout 0.5 加在全连接层之后,防止特征维度不高时过拟合。
训练完成后,准确率只是参考指标。即使 CNN 自己分类只有 80% 出头,只要特征不是完全乱成一团,后续 SVM 都可能拉到更高。这也是这个组合最反直觉的地方:实验里 CNN+Softmax 到 85% 就上不去了,同样的特征交给 SVM 能到 90%。原因在于 Softmax 的决策边界是线性的,而 SVM 用核函数在高维空间里找的边界更贴合小样本的分布。
3. 特征提取层:为什么截倒数第二层而不是直接用预测结果
3.1 像素直接喂给 SVM 的问题
有人会问,既然 SVM 能分类,为什么不让它直接吃原始图像?理论上可以,64×64×3=12288 维像素特征,RBF 核的 SVM 能算,但有两个实际问题:一是矩阵规模,样本量到了几千,SVM 要计算样本两两之间的核矩阵,内存和时间都扛不住;二是像素级特征对光照、平移、缩放太敏感,SVM 需要大量样本才能把这些干扰项学进边界里。
用 CNN 卷积层做特征提取,本质上是一种有监督的降维。卷积层一层层把局部纹理和边缘组合成语义信息,最终在全连接层形成 128 维或 512 维的浓缩特征向量。这一步把原始图像的空间冗余压缩掉了,SVM 再在这份特征上做分类,代价就小得多。
3.2 该取哪一层的激活值
不是每一层都值得拿来做 SVM 输入,选错层效果甚至会比直接跑 Softmax 还差。各层特征的性质差异很大:
| 层位置 | 维度示例 | 特征性质 | 适合做 SVM 输入吗 |
|---|---|---|---|
| 第一个卷积层输出 | 32×64×64 | 边缘、色块等低级纹理 | 不适合,维度太高且语义弱 |
| 最后一个卷积层输出 | 64×8×8 | 局部模式组合 | 可以,但需要先展平并降维 |
| 倒数第二个全连接层 | 128 | 全局语义特征 | 最常用,信息浓缩且维度可控 |
| Softmax 输出层 | n_class | 归一化概率 | 不适合,概率丢失方差,边界信息少 |
我一般取倒数第二个全连接层的激活值,也就是 Softmax 之前那层 Dense 的输出。这个位置的特征已经融合了全局信息,又没被归一化,SVM 能更好地利用特征分布的相对差异。
3.3 extract_features.py 的实现逻辑
extract_features.py 的写法核心是用 Keras 的 Model 接口截断网络:
# extract_features.py:加载训练好的 CNN,截取特征层输出 from tensorflow.keras.models import Sequential, Model, load_model import numpy as np model = load_model("cnn_model.h5") # 从后往前找最后一个 Dense 层,取它的输入作为特征 for i in range(len(model.layers) - 1, -1, -1): if "dense" in model.layers[i].name: feat_layer_index = i break feature_model = Model(inputs=model.input, outputs=model.layers[feat_layer_index].output) features = feature_model.predict(x_data, batch_size=32, verbose=1) np.save("features.npy", features) np.save("labels.npy", y_data)这里的逻辑说明:不用写死 model.layers[-2],因为网络最后可能还有 Dropout 或 BatchNormalization,写死索引容易在改结构之后取错层。用循环从尾部找到第一个名字含 dense 的层,取它上一层的输出,语义上正好是激活后的特征。
batch_size=32 控制显存和内存占用,显存不够就降到 16。这一步的产出 features.npy 是二维数组,每一行对应一张图的 128 维特征向量;labels.npy 是和它行数对齐的标签数组。这两个文件必须一起保存、一起 shuffle,后面 SVM 训练阶段只认这两个文件,不做任何跨文件对齐。
4. 训练 SVM:C 值、核函数与特征标准化的组合
4.1 train_svm.py 拆解
拿到特征向量之后,SVM 部分的代码比 CNN 简单一个量级,但参数影响反而更玄学。train_svm.py 的核心流程分为三步:标准化特征、训练 SVC、保存模型和标准化器:
# train_svm.py:特征标准化 + RBF 核 SVM 训练 import numpy as np import joblib from sklearn.svm import SVC from sklearn.preprocessing import StandardScaler from sklearn.metrics import classification_report features = np.load("features.npy") labels = np.load("labels.npy") scaler = StandardScaler() X_scaled = scaler.fit_transform(features) clf = SVC(C=1.0, kernel="rbf", gamma="scale", class_weight="balanced", probability=True, random_state=42) clf.fit(X_scaled, labels) joblib.dump(clf, "svm_model.pkl") joblib.dump(scaler, "scaler.pkl")参数说明:StandardScaler 先把每个特征维度减去均值、除以标准差,这一步对 SVM 至关重要。CNN 提取的特征不同维度量纲不一致,有的维数值域在 -10 到 10,有的在 0 到 1,SVM 计算距离时会把量纲大的维度主导掉。class_weight="balanced" 是在类别分布不均时自动给少数类加权,Land Use 这类遥感图像标注经常有类别比例悬殊,这个参数几乎每次都要开。probability=True 是为了后续能输出概率值,代价是训练时间增加,不需要概率可以关掉。
4.2 核函数和 C、gamma 怎么调
SVM 的核函数选择直接决定决策边界的形态。对 CNN 提取的高维特征,三种核通常这样选:
| 核函数 | 适用场景 | 参数注意点 |
|---|---|---|
| linear | 特征已经足够抽象,线性可分 | 基本只需调 C,训练最快 |
| rbf | 特征分布重叠、非线性强 | 需要同时调 C 和 gamma,RBF 是默认首选 |
| poly | 特征之间存在低阶多项式关系 | 阶数不好估,容易过拟合,少用 |
我一般先用 RBF 配合默认的 gamma="scale",它按 1 / (特征维度 × 特征方差) 自动估计 gamma,比手动给定一个固定值更稳。C 从 1 开始试,小样本下 C 太大容易过拟合训练集,太小的 C 又会让边界过于宽松。特征维度在 128 上下时,C 在 1 到 100 之间的效果区别不大,真正敏感的是 gamma 和是否做标准化。
4.3 网格搜索替代手试
手动调参容易漏,最省事的方案是直接网格搜索,把 C 和 gamma 的候选值丢给 GridSearchCV,用交叉验证选出最优组合:
# 网格搜索找 C 和 gamma 的最优组合 from sklearn.model_selection import GridSearchCV param_grid = { "C": [0.1, 1, 10, 100], "gamma": [0.001, 0.01, 0.1, "scale"] } grid = GridSearchCV(SVC(kernel="rbf", class_weight="balanced", probability=True), param_grid, cv=5, scoring="f1_macro", n_jobs=-1) grid.fit(X_scaled, labels) print("best params:", grid.best_params_) print("best cv score:", grid.best_score_)这里的逻辑说明:scoring 用 f1_macro 而不是 accuracy,因为类别不平衡时 accuracy 会虚高。cv=5 表示五折交叉验证,样本量少可以减少折叠数。n_jobs=-1 让所有 CPU 核并行搜索,能快不少,但数据量特别大时会吃满内存。
网格搜索跑完,把 grid.best_params_ 填回 SVC 再全量训练一次,得到的模型才是最终上线的版本。注意 GridSearchCV 里面已经做过一次标准化吗?没有,标准化必须在搜索之外单独做,GridSearchCV 只管分类器参数。如果你把 StandardScaler 放进 Pipeline,搜索时交叉验证的每一折都只用了训练部分的特征统计量,这样更规范,但在特征维度不算太高时效果差异不大。
5. 常见问题与排查:五个容易翻车的地方
5.1 训练报告很漂亮,预测时全归到某一类
现象:svm_predict.py 输出的分类报告里,某一类 precision 和 recall 接近 1,其他类别几乎全为 0,预测结果清一色是同一个类别。
原因:CNN 提取的特征在小样本下本身有偏,加上 SVM 默认不考虑类别不平衡,决策边界整体偏移到样本量大的那一类。这类问题在 RBF 核上尤其明显。
解决:训练 SVC 时带上 class_weight="balanced",让少数类的错误代价放大。如果加了还是压不住,对少数类做简单过采样,把样本数少的类别重复采样到次小类别的数量,再做标准化和训练。
5.2 SVM 效果比 CNN 自带的 Softmax 还差
现象:提取特征后 SVM 分类准确率反而低于 train.py 里的 CNN 准确率,差 5 到 10 个点。
原因:取错了特征层。常见的是直接把 Softmax 输出喂给 SVM,或者取到了第一个卷积层的展平输出。Softmax 输出经过归一化,概率值之间相互制约,丢失了特征分布的细节;浅层卷积特征又没有语义浓缩,维度还高,SVM 学不到有效边界。
解决:按照第 3 章的循环找最后一个 Dense 层的输入作为特征。如果工程里给的特征提取脚本是直接取 model.layers[-2].output,先打印各层名字核对一遍,确认取的是 Dense 层输入而不是 Dropout 层输入。
5.3 RBF 核训练时间过长,内存占用异常
现象:特征维度在 128 左右、样本量上万时,GridSearchCV 一次搜索跑了十分钟还没完,内存也跟着涨。
原因:RBF 核要计算样本两两之间的核矩阵,复杂度接近样本量的平方。样本上万、候选参数几十组时,总计算量很可观。特征维度高时,即使样本量不大,kernel 计算也会慢。
解决:先对特征做 PCA 降到 64 到 128 维,再喂 SVM。降维损失一点特征细节,但 SVM 的分类准确率通常不会降太多,训练时间能缩短一个数量级。另一个做法是先把 RBF 换成 linear 试试,如果 linear 效果已经够用,就没有必要执着于 RBF。
5.4 训练时准确率高,线下评估却对不上
现象:train_svm.py 里的交叉验证 score 很高,svm_predict.py 单独跑另一批数据的准确率明显偏低。
原因:很大的可能性是特征和标签的对应关系在 shuffle 时错位了。extract_features.py 保存的 features.npy 和 labels.npy 如果各自 shuffle,或者后续载入时用了不同的索引顺序,SVM 学到的是错位的标签分布。这个问题很隐蔽,代码不会报错,但结果完全不可信。
解决:提取特征时同时输出一个长度为样本数的索引数组,训练和预测都按这个索引取特征和标签,保证两批数据严格对齐。判断是否错位的最快方法是对比 features.npy 的行数与 labels.npy 的长度,不一致一定是提取阶段就有问题。
5.5 numpy 或 scikit-learn 版本冲突导致运行崩溃
现象:t_sne.py 或 svm_predict.py 运行时报错,类似 sklearn 内部抛 numpy 的 AttributeError,或者两个包相互提示接口不兼容。
原因:scikit-learn 新版本依赖较新 numpy,而 TensorFlow 2.10 对 numpy 有上限要求,pip 升级某个包时会把另一个包破坏。
解决:严格按第 2 章的版本组合安装,scikit-learn 1.2.2、numpy 1.23.5、TensorFlow 2.10.0。装完之后不要单独升级任何一个包。已经升级过导致报错的,直接用 conda 重建环境,比降级来回折腾快得多。
6. 训练 SVM 之前,先用 t-SNE 看一眼特征好不好
6.1 t_sne.py 的使用方式
SVM 调参调得再认真,如果 CNN 提取的特征本身分不开,后面全是白费。所以我排执行顺序的时候,习惯把 t_sne.py 放在 extract_features.py 之后、train_svm.py 之前,先用可视化确认特征结构。t_sne.py 的核心就几行:
# t_sne.py:将特征降到二维,按标签着色观察聚类情况 import numpy as np import matplotlib.pyplot as plt from sklearn.manifold import TSNE from sklearn.decomposition import PCA features = np.load("features.npy") labels = np.load("labels.npy") feat_reduced = PCA(n_components=50).fit_transform(features) tsne = TSNE(n_components=2, perplexity=30, random_state=42) feat_2d = tsne.fit_transform(feat_reduced) plt.figure(figsize=(8, 8)) plt.scatter(feat_2d[:, 0], feat_2d[:, 1], c=labels, cmap="tab10", s=5, alpha=0.8) plt.colorbar() plt.savefig("feature_tsne.png", dpi=200)参数说明:先做 PCA 降到 50 维再接 t-SNE,是常见做法。PCA 去掉噪声并大幅缩短 t-SNE 的计算时间,同时保留样本间的全局结构。perplexity=30 是 t-SNE 里最常用的值,样本几千到几万之间都能用;如果你的样本量特别小,比如不到 200,降到 10 到 15 效果更好。random_state=42 固定随机种子,保证每次跑出的图一致,方便对比不同训练轮次的特征变化。
6.2 怎么看这张图
合格的 t-SNE 图应该呈现同一类别聚成一块、不同类别之间有可见间隔的形态。边缘有一两个点混到别的类别里是正常现象,但如果出现两种极端情况就要回头处理:一是所有点混成一个圆团,说明 CNN 还没学到有效特征,回去调大 train.py 的训练轮数,或者加深模型;二是出现多条细长的线状簇,说明特征被某一维度主导,检查一下是否忘了标准化。这两种情况即使 SVM 交叉验证分数不低,也是假象,换到新数据大概率撑不住。
有了这张图做索引,后面调 C 和 gamma 就更有方向感。特征聚得紧凑时,C 不用开太大,gamma 用默认即可;特征叠得厉害时,优先考虑加特征维度和训练轮数,而不是死磕 SVM 参数。
6.3 一个经常被忽略的细节
跑了多组网格搜索之后,我把最优 SVM 模型和之前在 CNN 随手训出的 Softmax 模型做了对比,发现 SVM 确实略胜一筹,但优势没有想象中大。后来把那次的特征重新跑了一遍 t-SNE 才意识到,特征是单次训练的中间产物,训练过程本身波动很大,同一份数据换一个随机种子训出来的 CNN,特征分布也会有差异,后接的 SVM 自然跟着上下浮动。从那以后,我每次提取完特征都会固定随机种子重训两遍 CNN,比较两遍特征的 t-SNE 轮廓,差异明显才去调 SVM 参数,避免在偶然波动上花太多时间。希望帮到你。
本文还有配套的精品资源,点击获取