简介:这份资源聚焦CNN与SVM的融合图像分类方案,面向对深度学习和机器学习感兴趣、希望在Python中实现特征提取与分类器结合的开发者。压缩包共8个文件、仅8KB,以6个Python脚本为核心,涵盖CNN训练、特征提取、SVM训练与预测、t-SNE可视化等环节,并附有说明文档与README,便于快速理解代码结构并复现实验。当前已有2085人学习下载,适合作为课程设计、毕业设计或算法对比实验的参考。读者可获得从Keras构建CNN、提取全连接层特征到利用scikit-learn训练SVM的完整流程,以及配套的模型评估与可视化思路,帮助在实际图像分类任务中提升精度并拓展调优方向。
1. 以 CNN-SVM 混合模型作为图像分类实战起点:这条路为何值得走
做过图像分类的都知道,纯 CNN 跑到后面会陷入一种尴尬:训练集准确率漂亮,验证集却来回震荡。换更深的网络,显存先扛不住;换更简单的全连接层,特征又不够抽象。我拆过不少课程设计的源码包,CNN-SVM 这种组合是出现频率最高的一个套路——把 CNN 当成自动特征提取器,把最后一层全连接换成 SVM 分类器。这个思路听起来简单,实际落地时涉及特征怎么导出来、SVM 的 C 和 gamma 怎么设、维度爆炸怎么处理等一系列问题。这份基于 Python 的 Land_Use_CNN 项目恰恰把这些步骤拆成了独立的脚本。本文就把整套流程掰开揉碎,从文件结构讲到参数调优,再讲几个我实际复现时踩过的坑,最后落到怎么用 t-SNE 验证自己提取的特征到底像不像样。适合刚跑通深度学习基础教程、想拿一个完整图像分类项目练手的人。
2. 项目文件拆解:train.py 到 svm_predict.py 各管哪一段
2.1 这个源码包的文件布局与数据流方向
解压之后能看到 Land_Use_CNN-master 目录下躺着六个文件加一个说明文档。train.py、extract_features.py、train_svm.py、svm_predict.py、t_sne.py 这五个 Python 脚本构成了完整流水线,README.md 是项目作者的说明,另外还有一个 CNN-SVM 说明.txt。从命名就能看出这个项目的设计思路:把深度学习特征提取和传统机器学习分类彻底解耦,每个阶段都能单独跑、单独验证。
数据流是单向的,而且非常清晰。train.py 负责训练 CNN 基座模型,把学好的权重存下来。extract_features.py 加载这个权重,在训练集和测试集上分别跑一遍,把全连接层之前的激活输出存成特征矩阵。train_svm.py 拿训练集的特征矩阵去训练 SVM。svm_predict.py 加载训练好的 SVM 模型,对测试集特征做预测。t_sne.py 则是把高维特征降到二维做可视化,让你直观看到两类样本在特征空间里是否真的分得开。这种一个阶段一个脚本的结构,对初学者非常友好,改任何一环都不会牵动其他代码。
这里有个关键点,项目用的是「先冻结 CNN、再单独训练 SVM」的两阶段方案,而不是端到端的联合训练。常见的做法是先把 CNN 训好,训练过程中全连接层承担分类任务;然后丢掉最后的 softmax 层,把前面所有层输出的特征向量交给 SVM。这样做的好处是你可以随时替换分类器,比如把线性 SVM 换成 RBF 核,甚至换成随机森林,而不需要重新训练那个耗时最长的卷积网络。
2.2 各文件的依赖关系与运行顺序
运行顺序有强依赖,不能乱。train.py 必须最先跑,因为它产出的是 extract_features.py 需要的模型权重文件。extract_features.py 必须在 train_svm.py 之前跑,因为后者依赖它导出的 .npy 特征文件。svm_predict.py 是最后一个环节,它需要同时加载 CNN 权重和 SVM 模型。t_sne.py 的位置稍微灵活一些,它只需要特征文件,所以可以在 extract_features.py 之后随时执行。
我把这份依赖关系列成一张表,方便你对照检查:
| 脚本文件 | 前置依赖 | 输出产物 | 运行耗时参考 |
|---|---|---|---|
| train.py | 原始图像数据集 | CNN 权重文件 | 最长,取决于 epoch 数 |
| extract_features.py | CNN 权重文件 | 训练/测试特征 .npy | 中等,纯前向推理 |
| train_svm.py | 训练特征 .npy | SVM 模型文件 | 快,秒级到分钟级 |
| svm_predict.py | 特征+SVM 模型 | 分类准确率/预测标签 | 极快 |
| t_sne.py | 特征 .npy | 二维可视化图 | 快 |
在实际操作中,很多人会把 extract_features.py 的输出特征也存一份到磁盘。这样做有个好处,SVM 调参的时候不需要反复跑 CNN 前向推理,特征矩阵加载到内存就行。我一般会把特征文件命名成 train_features.npy 和 test_features.npy,后面在 train_svm.py 里直接 np.load,省掉不少 IO 时间。
3. 训练 CNN 基座:train.py 的架构设计与特征提取边界
3.1 CNN 部分到底在学什么
CNN 的前半段由卷积层和池化层堆叠而成,后半段是展平后的全连接层。项目里 train.py 构建的模型大致是这个结构:输入层接两个卷积块,每个卷积块包含 Conv2D、ReLU 激活、MaxPooling2D,然后接一个 Dropout 做正则化,最后展平后接 Dense 层和 softmax 输出。这个结构不算深,但对于土地利用分类这种中等难度的数据集,已经足够学到有区分度的纹理和形状特征。
卷积层的作用是提取局部特征,第一层卷积学到的往往是边缘、颜色块这样的低级特征,第二层卷积开始组合出纹理模式。池化层在这里面承担两个任务:降维减少计算量,以及提供一定的平移不变性。关键的一点是,CNN 的特征提取能力和分类能力是耦合在同一个网络里的,我们之所以要截断它,是因为全连接层+softmax 学到的决策边界是线性的,而 SVM 配合核函数可以在特征空间中构造更复杂的非线性边界。
我把 train.py 里的模型结构整理成了参数表,方便你对照理解每一层的作用:
| 层类型 | 输出尺寸 | 参数数量 | 作用 |
|---|---|---|---|
| Conv2D(32, 3x3) | 224x224x32 | 约 900 | 提取低级边缘特征 |
| MaxPooling2D(2x2) | 112x112x32 | 0 | 降采样,增强平移不变性 |
| Conv2D(64, 3x3) | 112x112x64 | 约 18496 | 提取中级纹理特征 |
| MaxPooling2D(2x2) | 56x56x64 | 0 | 进一步降维 |
| Flatten | 200704 | 0 | 展平为特征向量 |
| Dropout(0.5) | 200704 | 0 | 防止过拟合(靠后才会用到) |
| Dense(128) | 128 | 约 2560 万 | 组合全局特征 |
| Softmax(N类) | N | 128*N | 输出类别概率 |
注意 Flatten 之后那个 200704 维的向量,这个维度直接决定了后面 SVM 的输入规模,也是我们提取特征时最关心的位置。
3.2 应该从哪一层截断来提取特征
这是整个项目里最值得琢磨的问题。截断位置不同,特征的性质完全不同。如果从最后一个卷积层之后截断,得到的是 56x56x64 的空间特征图,展平后维度极高,SVM 在这种维度上训练会非常慢,而且容易过拟合。如果从第一个全连接层之后截断,得到的是 128 维的紧凑向量,SVM 训练速度快,但可能丢掉部分细粒度信息。
从项目实际代码来看,extract_features.py 的做法是取全连接层之前的激活输出。我一般建议用 Dense(128) 这层的输出,而不是卷积层的输出。理由有三个:第一,128 维对 SVM 来说是非常友好的输入规模,RBF 核的 Gram 矩阵计算量可控;第二,全连接层已经把前面的局部特征做了全局组合,信息密度高;第三,维度低意味着你不需要额外做 PCA 降维,省掉一个环节就少一个坑。如果你发现 128 维特征在 SVM 上表现不佳,再回头尝试更大维度的中间层,这个排查方向是成本最低的。
特征提取的核心代码如下:
from keras.models import Model from keras.layers import Input def build_feature_extractor(model, layer_index=-3): """ 从已训练好的 CNN 模型中截断,返回特征提取器。 layer_index=-3 表示取倒数第三层,即 Dense(128) 的激活输出。 """ inputs = model.input features_layer = model.layers[layer_index].output extractor = Model(inputs=inputs, outputs=features_layer) return extractor这段代码用的是 Keras 函数式 API,先拿到原始模型的输入张量,再通过 layers 索引定位到目标层,最后用 Model 类把输入和该层输出重新包装成一个独立模型。这里 layer_index=-3 是经验值,不同模型的层列表长度不一样,取负索引是从尾部往前数,更稳。你先用model.summary()打印所有层,确认倒数第三层确实是 Dense(128),再跑这段代码,不要盲信索引。
3.3 训练参数怎么定:epoch、batch_size 与学习率
train.py 的训练参数直接决定了 CNN 基座的质量,也间接影响 SVM 的上限。如果 CNN 本身没训好,提取的特征就是垃圾,SVM 再厉害也救不回来。我拿到这个项目后,第一件事就是看它的默认参数,然后根据数据集规模做调整。
常见做法是 batch_size 设 32 或 64,epoch 设 30 到 50,优化器用 Adam,初始学习率 1e-3。数据量小的时候,epoch 数不要太大,配合 EarlyStopping 回调,监控验证集准确率,连续 5 个 epoch 不提升就停止。项目里如果没写 EarlyStopping,我建议自己加上,因为 CNN 在这个环节很容易过拟合。
这里有一个容易被忽略的细节:训练 CNN 时用的数据增强策略。如果你的项目里包含 ImageDataGenerator,shift_range 和 zoom_range 的值别设太大。土地利用图像如果放大 20% 以上,地物语义可能就变了,反而给模型灌入错误信息。我一般控制在 0.1 以内。
4. 特征提取与 SVM 训练:extract_features.py 到 train_svm.py 的完整链路
4.1 extract_features.py 到底导出什么
extract_features.py 做的事情,是把训练好的 CNN 模型在数据集上跑一遍前向推理,把 Dense(128) 层的输出保存为 numpy 数组。这个数组的 shape 是 (样本数, 128),每一行代表一张图的特征向量。
这里有一个比想象中更重要的步骤:特征是否需要归一化。SVM 对特征的尺度非常敏感,尤其是使用 RBF 核时,它计算的是样本间的欧氏距离,如果某个特征的数值范围是 [0, 255],另一个是 [0, 1],大数值范围的特征会完全主导距离计算。常见的做法是用 StandardScaler 做标准化,让每个维度均值 0、方差 1。代码如下:
import numpy as np from sklearn.preprocessing import StandardScaler def load_and_scale_features(feature_path): """加载特征矩阵并做标准化。返回标准化后的特征和 scaler 对象。""" features = np.load(feature_path) # shape: (n_samples, n_features) scaler = StandardScaler() features_scaled = scaler.fit_transform(features) return features_scaled, scalerStandardScaler 使用的是训练集的均值和方差,测试集必须用同一个 scaler 做 transform,不能在测试集上重新 fit。这个错误我见过太多次,测试集单独 fit 会导致数据泄露,SVM 的评估结果虚高。如果你在 svm_predict.py 里加载测试特征后直接丢给 SVM,而没有先经过 train_svm.py 里保存下来的 scaler,那结果就是错的。
4.2 train_svm.py 的参数设置:C 与 gamma 的选择逻辑
SVM 的核心参数有两个:正则化系数 C 和 RBF 核的 gamma。C 控制误分类的惩罚力度,C 越大,模型越倾向于把所有训练样本分对,但也更容易过拟合;C 越小,决策边界越平滑,泛化能力可能更好。gamma 控制 RBF 核的宽度,gamma 越大,每个支持向量的影响范围越小,决策边界越复杂;gamma 越小,边界越平滑。
我在实际跑这个项目时,最省心的方式是先用默认参数跑一遍基线,再用网格搜索 GridSearchCV 在几个数量级上搜索。搜索范围一般这样定:C 从 0.1 到 100 取对数间隔,gamma 从 0.001 到 1 取对数间隔。代码实现如下:
from sklearn.svm import SVC from sklearn.model_selection import GridSearchCV def train_svm_with_search(train_features, train_labels): """ 对 SVM 做网格搜索,返回最优模型。用 5 折交叉验证评估。 """ param_grid = { 'C': [0.1, 1, 10, 100], 'gamma': [0.001, 0.01, 0.1, 1], 'kernel': ['rbf'] } svm = SVC(probability=True, random_state=42) grid = GridSearchCV(svm, param_grid, cv=5, scoring='accuracy', n_jobs=-1) grid.fit(train_features, train_labels) return grid.best_estimator_, grid.best_params_这里有个参数值得单独说:probability=True。项目里 svm_predict.py 如果要输出置信度,就需要这个参数。但它有代价,SVM 的概率估计需要额外做一次 Platt scaling 交叉验证,训练时间会明显变长。如果你的应用只需要类别标签、不需要概率,建议把这个参数关掉,能省不少时间。
网格搜索的输出你会看到一个有意思的现象:最优的 C 往往不是最大的那个,最优的 gamma 也很少是 0.001 这种极小值。这说明特征空间里各类别并非线性可分,但也没有复杂到需要极端精细的边界。这个观察反过来印证了 CNN 特征提取的有效性。
4.3 svm_predict.py 的评估流程:保存模型,别把 scaler 忘了
训练完 SVM 之后,需要把模型和 scaler 都保存下来。很多初学者只保存了 model.pkl,测试的时候直接加载预测,结果准确率比训练时低一截,就是因为忘了保存 scaler。我习惯把两个文件放在同一个目录,命名上带上数据集信息,比如 svm_model_land_use.pkl 和 scaler_land_use.pkl。
预测阶段的代码大概是这个样子:
import joblib import numpy as np def predict_with_svm(features_path, svm_path, scaler_path): """ 加载测试特征、SVM 模型和 scaler,输出预测类别。 """ features = np.load(features_path) scaler = joblib.load(scaler_path) svm = joblib.load(svm_path) features_scaled = scaler.transform(features) # 注意用 transform,不是 fit_transform predictions = svm.predict(features_scaled) return predictions关注scaler.transform这一行,前面强调过,测试集上只能用 transform。如果误用了 fit_transform,scaler 会用测试集的统计量重新标准化,整个特征分布偏移,预测结果毫无参考价值。这个问题在深度学习项目里不太常见,因为 BatchNorm 层的参数在训练时就已经固定了,但在 sklearn 的流水线里是高频翻车点。
5. 避坑指南:特征层面的六个典型问题和排查思路
5.1 第一个坑:transform 和 fit_transform 混用
现象:测试集的准确率比训练集低 15% 以上,而且每次跑结果还不一样。
原因:测试特征在 svm_predict.py 里先调用了 fit_transform,scaler 重新计算了均值和方差,特征分布被改写了。这属于数据泄露的一种,模型的评估分数失去了意义。
解决:在 train_svm.py 里训练完 scaler 后立即 joblib.dump 保存,svm_predict.py 里只允许出现 scaler.transform。写代码时就养成习惯,fit 只出现一次,transform 可以出现无数次。
5.2 第二个坑:特征维度爆炸,SVM 训练卡死
现象:train_svm.py 跑了几十分钟还没出结果,CPU 风扇狂转,内存占用飙升。
原因:如果你从 Flatten 层或最后一个卷积层提取特征,维度可能是几万甚至几十万维。RBF 核 SVM 需要计算所有样本两两之间的核矩阵,复杂度是 O(n²·d),n 是样本数,d 是维度。几万维特征加上几千个样本,矩阵就爆炸了。
解决:改从 Dense(128) 层截断,把特征维度控制在百级。如果必须用高维特征,先做 PCA 降到 200 维以下再喂给 SVM。PCA 同样要保存降维器,测试集上做同样处理。
5.3 第三个坑:类别不均衡,准确率虚高
现象:整体准确率 92%,但查看混淆矩阵发现某一类几乎是零命中。
原因:土地利用数据里,林地面积常常远大于水体面积。CNN 训练时已经受到不均衡影响,SVM 在这个特征空间里也会倾向于把不确定样本分到多数类。
解决:SVM 里有一个 class_weight='balanced' 参数,它会根据样本数量自动调整类别权重。这个参数不加白不加,加了之后少数类的召回率往往有明显提升,多数类的准确率下降有限。效果不够再考虑对少数类做上采样。
5.4 第四个坑:SVM 没做交叉验证,被随机种子坑了
现象:同一个代码跑两次,准确率波动 2%~3%。
原因:SVM 的训练本身是确定性的,但如果你用 train_test_split 划分训练集和测试集,随机种子不同,数据分布就不同。这个波动其实是数据划分带来的,与模型无关。
解决:train_svm.py 里用 StratifiedKFold 做 5 折交叉验证,取平均准确率作为模型评估指标。最终模型用全量数据重新训练,这样既保证了评估稳定性,又利用了全部数据。
5.5 第五个坑:CNN 没训好就着急导特征
现象:SVM 准确率只有 50%,跟随机猜差不多。
原因:CNN 的训练 epoch 数不够,或者学习率设置不当,模型还在欠拟合状态就停了。特征是 CNN 前向推理输出的,基座没学好,特征自然是无效的。
解决:回过去看 train.py 的训练曲线,确认训练集准确率至少到了 95% 以上,再考虑导特征。如果训练集都上不去,调 CNN 的结构和参数;如果训练集高但验证集低,先解决过拟合。基座不稳,SVM 就是空中楼阁。
5.6 第六个坑:t_sne.py 画图结果完全分不开
现象:t-SNE 可视化图里两类样本完全混杂在一起,看不出任何聚类结构。
原因:t-SNE 对高维空间的距离感知非常敏感,如果不同类别的特征分布是渐变的,比如某两类地物的纹理相似,它们在特征空间里可能真的靠近。也可能是 perplexity 参数设置不当。
解决:检查用的是第几层输出,如果用的是最后的 softmax 层之前的位置,特征应该已经比较抽象了。如果用的是第一个卷积层,那信息确实不够。perplexity 设为 30 是常规值,样本太少就降到 5 到 10。
6. 把特征工程收个尾:t-SNE 可视化与交叉验证组合拳
说到验证整个 CNN-SVM 链路是否真的有效,我最看重的两个工具就是 t-SNE 可视化和分层交叉验证。前者用肉眼看特征可分性,后者用数字告诉你泛化能力,两个一起用,比只看准确率踏实得多。
t-SNE 的用法其实很简单,extract_features.py 导出 128 维特征之后,t_sne.py 把它降维到二维并画散点图。代码如下:
from sklearn.manifold import TSNE import matplotlib.pyplot as plt def visualize_features(features_path, labels_path, n_classes=6): """ 用 t-SNE 把高维特征降到二维,按标签着色可视化。 perplexity 设为 30,迭代次数设大一点保证收敛。 """ features = np.load(features_path) labels = np.load(labels_path) tsne = TSNE(n_components=2, perplexity=30, n_iter=1000, random_state=42) features_2d = tsne.fit_transform(features) plt.figure(figsize=(10, 8)) scatter = plt.scatter(features_2d[:, 0], features_2d[:, 1], c=labels, cmap='tab10', s=10) plt.colorbar(scatter) plt.title('t-SNE Visualization of CNN Features') plt.show()这段代码里要注意 n_iter 参数,老版本 sklearn 还叫 n_iter,新版本改成了 max_iter,不兼容会直接报错。如果遇到这个问题,把参数名改成 max_iter 就行。t-SNE 的随机性很强,random_state 固定下来,方便多次运行结果对比。
t-SNE 图应该怎么看?理想状态是不同类别呈现明显的团簇,每个簇内部紧凑,簇与簇之间有清晰的间隙。如果类别之间相互交叠严重,说明 CNN 提取的特征本身就没有把这两类区分开来。这时候你有两个选择:加深 CNN 网络结构,或者接受这个分类上限。一个有意思的观察是,如果某两类在 t-SNE 里交叠,它们在 SVM 上的混淆矩阵里也大概率是主要错误来源,两者高度对应。
交叉验证这一头,train_svm.py 里用交叉验证与 t-SNE 形成互补。t-SNE 告诉你特征在全局分布上是否可分,交叉验证告诉你在这个特征空间里 SVM 的泛化表现。我会打印每一折的准确率,如果五折结果标准差超过 1.5%,说明某些类别在特定划分下很难学,这时候需要回看类别样本数量和分布。
从那以后,我每次跑这个项目的完整流程,都会强制走一遍「先看 t-SNE 图,再跑交叉验证」的组合拳,最后才看测试集准确率。如果 t-SNE 图已经乱成一团,后面两步就直接跳过,省下的是大量调参时间。希望这份拆解笔记能帮你在自己的数据集上少走几步弯路。
本文还有配套的精品资源,点击获取