ACASB-JavaSpringBoot+Python异构双引擎项目实战 - 2 - 一些问题
继上回,方案看似很好,所以我设计了一个19维度的参数,从归一化后(统一调整为400*400大小的图像),大概是不同颜色的占比、图像中的线条结构等信息,然后拿去训练了一个浅层的神经网络(128 64 32),先测试了一下简单的二分类,即把宫廷建筑和平民建筑分类,效果很好。我以为这就完了,结果按照预期方案进行四分类的时候出乱子了,发现训练效果非常容易就过拟合了

把所有样本映射到这样一个二维图上,问题很明显了,官员阶级的建筑风格混合了其他三个阶级的风格,导致神经网络很难学习到相关的信息,所以我开始考虑支持向量机来进行分类任务了。
支持向量机 SVM
借用知乎上的图,支持向量机的原理是通过找出一个最大间隔,将东西分类

而我们的输入数据有19维的数据,即所有样本在一个19维的超平面里,我们的目的是找到另外的超平面,将样本大致分为四类
当然,我们现有的MLP也没有落下,我重新设计了一个新的Hybrid MLP,他是把SVM中某个样本距离边界的距离再作为一个输入交给MLP,扩展了MLP的输入,初步看起来效果还挺好
| 图片 | 真实标签 | SVM预测 | Baseline MLP | Hybrid MLP |
| royal/1.jpg | 皇帝 | 皇帝 ✓ | 皇帝 ✓ | 皇帝 ✓ |
| prince/1.jpg | 亲王 | 亲王 ✓ | 官员 ✗ | 亲王 ✓ |
| official/1.jpg | 官员 | 平民 ✗ | 平民 ✗ | 平民 ✗ |
| civi/1.jpg | 平民 | 平民 ✓ | 平民 ✓ | 平民 ✓ |
| official/5.jpg | 官员 | 官员 ✓ | 官员 ✓ | 官员 ✓ |
| official/10.jpg | 官员 | 官员 ✓ | 官员 ✓ | 官员 ✓ |
在第二组测试里,SVM提供的新维度的输入正好纠正了原本MLP的预测数据,这样就成功的结合了支持向量机和MLP各自的优势
但是新的问题又出现了,我注意到所有的预测,信度都偏低,比如上述六个预测的信度分别为0.49 0.45 0.57 0.87 0.77 0.91,很多的信度明显偏低
我当时认为的主要原因应该是训练样本的相似度有些高(目前条件被迫限制了每类样本只有50个左右),19维特征太少,这是很典型的小样本训练。还有在第一组测试中,虽然hybrid MLP的信度为0.49,但SVM的信度高达0.8,还可能是SVM决策值和原始特征的融合方式有点问题
所以接下来的问题貌似很清楚了,增加MLP的复杂度,多训练几轮
按照这个方法来,信度明显上升了,但是准确度却发生了下降?
信度表
| 图片 | 真实标签 | 优化前Hybrid | 优化后Hybrid | 提升 |
| royal/1.jpg | 皇帝 | 0.4961 | 0.7968 | +60.6% |
| prince/1.jpg | 亲王 | 0.4552 | 0.6163 | +35.4% |
| official/1.jpg | 官员 | 0.5710 | 0.7844 | +37.4% |
| civi/1.jpg | 平民 | 0.8743 | 0.7936 | -9.2% |
准确度表
| 模型 | 优化前 | 优化后 | 变化 |
| SVM | 65.91% | 59.09% | -6.82% |
| Hybrid MLP | 63.64% | 59.09% | -4.55% |
| Baseline MLP | 56.82% | 47.73% | -9.09% |
接下来,我尝试获取到现在的神经网络在训练集上的表现怎么样,发现无论是SVM还是HybridMLP,准确度都大于90%,而原始的MLP准确度仅有60%
SVM 混淆矩阵 真实\预测 皇帝 亲王 官员 平民 皇帝 51 0 1 3 亲王 0 51 1 1 官员 1 1 48 1 平民 3 3 4 49 HybridMLP 混淆矩阵 真实\预测 皇帝 亲王 官员 平民 皇帝 50 0 1 4 亲王 1 50 1 1 官员 1 1 48 1 平民 1 2 5 51
看起来还不错,但作为建筑判断,我总觉得19维的参数不太够,于是又增加了很多新的参数,比如HSV偏度,LBP纹理等等,信度又明显提升了,准确度接近95%
正当我沉浸在胜利的快感中,我的直觉告诉我出大事了
过拟合:来了奥
我把测试集拿去进行测试,显然意外发生了
我的神经网络在测试集上的准确度表现低于40%,这意味着我的MLP完全是在瞎猜
而且我重复进行了五次测试,准确度都惊人的统一,这证明了我的神经网络完全记住了训练集里的答案,首先让我怀疑的是这接近50维的参数,可能是维度过大导致MLP什么都学不到,而且样本量偏少,导致了训练效果不好且容易过拟合,所以我进行了一些数据增强,比如对50%的样本进行水平翻转、小角度偏转、亮度偏移等等,把样本量扩大了一倍,然后将MLP中的神经元个数所哦见到原来的一半,开启早停防止过拟合
from torchvision import transforms数据增强变换
self.transform_augment = transforms.Compose([
transforms.RandomResizedCrop(224, scale=(0.8, 1.0)), # 随机裁剪
transforms.RandomHorizontalFlip(p=0.5), # 随机水平翻转
transforms.RandomRotation(degrees=15), # 随机旋转
transforms.ColorJitter( # 颜色抖动
brightness=0.2,
contrast=0.2,
saturation=0.2,
hue=0.1
),
transforms.ToTensor(),
transforms.Normalize(
mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225]
)
])
这一番操作下来是好了一些,但依然让我不满意

增加ResNet18
忽然一个想法从我脑袋里蹦出来,对于古建筑那种模糊的图像,简单的算法貌似根本无法提取深层的语义数据,可以尝试一下使用ResNet18 残差神经网络来对图像进行处理,直接把resnet的输出当作建筑的颜色细节结构信息等等
核心算法如下:
# ResNet部分
import torch
import torch.nn as nn
from torchvision import models, transforms
from PIL import Imageclass ResNet18FeatureExtractor:
def __init__(self):
self.device = torch.device('cpu')
self.model = self._load_model()
self.transform = self._get_transform()
def _load_model(self) -> nn.Module:
# 加载预训练ResNet18
model = models.resnet18(weights=models.ResNet18_Weights.DEFAULT)
# 冻结所有参数(不进行微调)
for param in model.parameters():
param.requires_grad = False
# 移除最后的全连接层,只保留特征提取部分
model = nn.Sequential(*list(model.children())[:-1])
model.eval()
model = model.to(self.device)
return model
def _get_transform(self) -> transforms.Compose:
return transforms.Compose([
transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize(
mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225]
)
])
def extract_features(self, image_path: str) -> np.ndarray:
image = Image.open(image_path).convert('RGB')
image_tensor = self.transform(image).unsqueeze(0).to(self.device)
with torch.no_grad():
features = self.model(image_tensor)
# 提取512维特征向量
features = features.squeeze().cpu().numpy()
return features
#Hybrid MLP部分
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
from sklearn.svm import SVC
from sklearn.neural_network import MLPClassifier
from sklearn.model_selection import StratifiedKFold
from sklearn.metrics import classification_report, confusion_matrix, accuracy_scoreclass HybridClassifier:
def __init__(self, n_classes: int = 4):
self.n_classes = n_classes
# 特征预处理
self.scaler = StandardScaler()
self.pca = PCA(n_components=0.95, random_state=42) # 保留95%方差
# SVM分类器(线性核,低C值以最大化间隔)
self.svm = SVC(
kernel='linear',
C=0.1,
probability=True,
random_state=42
)
# MLP分类器(极简结构,高正则化)
self.mlp = MLPClassifier(
hidden_layer_sizes=(32,),
activation='relu',
solver='adam',
alpha=0.01, # L2正则化
batch_size=16,
learning_rate='adaptive',
learning_rate_init=0.001,
max_iter=2000,
random_state=42,
early_stopping=True, # 早停
validation_fraction=0.1,
n_iter_no_change=50,
verbose=False
)
def fit(self, X: np.ndarray, y: np.ndarray):
# 标准化
X_scaled = self.scaler.fit_transform(X)
# PCA降维
X_pca = self.pca.fit_transform(X_scaled)
# 训练两个分类器
self.svm.fit(X_pca, y)
self.mlp.fit(X_pca, y)
def predict(self, X: np.ndarray) -> np.ndarray:
# 预处理
X_scaled = self.scaler.transform(X)
X_pca = self.pca.transform(X_scaled)
# 获取概率
svm_proba = self.svm.predict_proba(X_pca)
mlp_proba = self.mlp.predict_proba(X_pca)
# 软投票:平均概率
ensemble_proba = (svm_proba + mlp_proba) / 2
predictions = np.argmax(ensemble_proba, axis=1)
return predictions
我选择删去原版ResNet的最后一层FC Layer,这样我就获得了一个512维的关于这个建筑的详细参数,当然512维度依然太大了,所以我又考虑到了降维处理,解释95%的方差,把512维的数据压缩到20-40维左右,大致流程如下
输入图像 (400px)
↓
ResNet18特征提取(冻结参数)
↓
512维特征向量
↓
StandardScaler标准化
↓
PCA降维(95%方差)
↓
25维特征
↓
├─→ SVM (linear, C=0.1) ──┐
│ ├─→ 软投票 → 预测
└─→ MLP (32 hidden) ──────┘
这样测试数据的准确度来到了47左右,但依然偏低
所以我考虑是不是可以再略微提升一下输入维度,我将输入维度调整到35维,SVM的线性核改成了RBF核,提高了正则化强度,准确度提升到了50%左右,依然很差
self.svm = SVC(
kernel='rbf', # 从linear改为rbf
C=0.5, # 增加C值
gamma='scale',
probability=True,
random_state=42
)self.mlp = MLPClassifier(
hidden_layer_sizes=(64, 32), # 增加网络容量
activation='relu',
solver='adam',
alpha=0.05, # 从0.01改为0.05(更强的L2正则化)
batch_size=16,
learning_rate='adaptive',
learning_rate_init=0.001,
max_iter=2000,
random_state=42,
early_stopping=True,
validation_fraction=0.2, # 增加验证集比例
n_iter_no_change=30,
verbose=False
)
后来我又进行了样本增强,把样本扩大到了初始的3倍左右,再将PCA维度调整为40,测试集的准确度继续来到55%,当前的配置如下
特征提取: ResNet18 (预训练,冻结)
数据增强: 3x (随机裁剪、翻转、旋转、颜色抖动)
降维: PCA (40维,解释65.62%方差)
分类器A: SVM (RBF核, C=1.0)
分类器B: MLP (64→32隐藏层, alpha=0.05)
集成: 软投票(平均概率)
交叉验证: 5折分层
目前看来已经极限了,但我又想到Random Forest算法,想着它应当会有点作用罢,但是RF的加入导致准确度暴跌10%。
from sklearn.ensemble import RandomForestClassifierself.rf = RandomForestClassifier(
n_estimators=200, # 树的数量
max_depth=10, # 限制树深度
min_samples_split=5, # 最小分割样本数
min_samples_leaf=2, # 最小叶子样本数
random_state=42,
n_jobs=-1, # 并行
class_weight='balanced' # 类别平衡
)
我们目前的瓶颈准确度大约就在45%-55%的样子了,继续提高准确度就只能去扩大样本量了,而且我查看了测试集的准确度分析
类别 | 测试集召回率 | 主要问题 |
皇帝 | 90% | 轻微 |
亲王 | 30%-40% | 误判为1或4 |
| 官员 | 0% | 完全被误判到平民 |
平民 | 90% | 轻微 |
还记得一开始的这张图吗?

我们不看绿色的部分,会发现皇帝、亲王、平民的建筑有一个比较明显的分界线,但是官员阶级建筑的加入,直接造成平民、亲王阶级的建筑被混淆到一起。
总结一下我目前使用过的所有的版本,在目前这个受限的条件下选出了一个比较优秀的解,剩下的问题就靠数据集了
版本 | 样本数 | PCA维度 | 交叉验证 | 测试集 | 过拟合程度 |
传统特征 | 218 | NaN | 69.71% | 55.00% | 严重 |
ResNet18 (125维) | 218 | 125 | 69.71% | 55.00% | 严重 |
ResNet18 (25维) | 218 | 25 | 80.70% | 47.50% | 严重 |
ResNet18 (50维) | 218 | 50 | 77.07% | 50.00% | 严重 |
ResNet18 (35维, RBF) | 218 | 35 | 77.94% | 50.00% | 严重 |
| ResNet18 (40维, 增强) | 654 | 40 | 88.68% | 55.00% | 轻微改善 |
ResNet18 (25维, 增强) | 654 | 25 | 55.00% | 55.00% | 轻微改善 |
ResNet18 (30维, RF) | 654 | 30 | 45.00% | 45.00% | 严重 |
目前能做到的极限也就是40维度+数据增强的ResNet18了
总结
小样本训练对策
Q:218样本对4分类任务来说太少
A:
使用预训练模型(ResNet18)而非从零训练
冻结特征提取器参数,只训练分类器
# 冻结所有参数
for param in model.parameters():
param.requires_grad = False只解冻最后几层(如果需要微调)
for name, param in list(model.named_parameters())[-4:]:
param.requires_grad = True
强烈的数据增强(多倍)
# 随机裁剪:增加尺度不变性
transforms.RandomResizedCrop(224, scale=(0.8, 1.0))随机翻转:增加水平对称性
transforms.RandomHorizontalFlip(p=0.5)
随机旋转:增加旋转不变性
transforms.RandomRotation(degrees=15)
颜色抖动:增加颜色鲁棒性
transforms.ColorJitter(
brightness=0.2,
contrast=0.2,
saturation=0.2,
hue=0.1
)
高强度的正则化(L2、Dropout、早停)
特征降维(PCA)减少维度灾难
# 方法1:保留固定方差比例
pca = PCA(n_components=0.95, random_state=42)方法2:保留固定维度数
pca = PCA(n_components=40, random_state=42)
查看解释方差
print(f"Explained variance ratio: {pca.explained_variance_ratio_.sum()}")
过拟合对策
Q:训练集准确度高达94%,而测试集仅50%左右
A:
进行交叉验证
from sklearn.model_selection import StratifiedKFold分层K折交叉验证(保持类别比例)
skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
for fold, (train_idx, test_idx) in enumerate(skf.split(X, y), 1):
X_train, X_test = X[train_idx], X[test_idx]
y_train, y_test = y[train_idx], y[test_idx]
# 训练和评估
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
accuracy = accuracy_score(y_test, y_pred)
使用早停防止过拟合
降低模型复杂度
进行软投票集成平均概率
# 获取两个模型的概率
svm_proba = svm.predict_proba(X_pca)
mlp_proba = mlp.predict_proba(X_pca)平均概率
ensemble_proba = (svm_proba + mlp_proba) / 2
取最大概率作为预测
predictions = np.argmax(ensemble_proba, axis=1)
数据分布差异对策
Q:训练集和测试集分布的差异导致泛化能力差
A:
重新确定数据采集标准
收集更多数据
统一数据收集标准
成果
实现了什么
经过巨多轮的优化,我们实现了:
从传统特征升级到深度学习特征(ResNet18);
交叉验证准确率从69.71%提升到88.68%(+18.97%);
模型稳定性提升(标准差从5.69%降到3.66%);
测试了多种模型架构(SVM、MLP、RandomForest);
实现了数据增强策略;识别了数据分布差异问题。
核心发现
测试集数据准确度难以突破55%的根本原因主要来自:
训练集和测试集的分布差异较大;
样本量太少,对四分类来说654个扩展后的样本依然不够;
类别定义问题,不同类之间也存在相似建筑。
未来的优化方向
1. 使用更强的数据增强方式
2. 考虑测试其他的预训练好的模型
3. 重新检查数据集质量
4. 收集更多数据,扩充数据量
5. 层次化分类,先训练出高阶级-低阶级的二分类模型,在此基础上继续细分,提高效率
6. 特征融合,综合ResNet特征与19维手工特征(如颜色信息)
END
这次的实践,虽然有AI的大量辅助,但是也探索了很多小样本、多分类、高维度下的机器学习场景。
从传统的特征到深度学习,从单一模型到集成学习,从简单训练到数据增强,我们尝试了很多种不同的策略,选出了在当下条件下的最优解。
虽然当下的最优解不一定是全局条件的最优解,但我们至少建立了一个完整的、可拓展的机器学习流程,而且我们识别到了问题的根本原因在于数据分布的差异,为以后的优化提供了方向。机器学习不仅仅是调参,而是理解数据、理解问题、找到正确的优化方向。
与诸君共勉。
评论 暂无