ACASB-JavaSpringBoot+Python异构双引擎项目实战 - 2 - 一些问题
继上回，方案看似很好，所以我设计了一个19维度的参数，从归一化后（统一调整为400*400大小的图像），大概是不同颜色的占比、图像中的线条结构等信息，然后拿去训练了一个浅层的神经网络（128 64 32），先测试了一下简单的二分类，即把宫廷建筑和平民建筑分类，效果很好。我以为这就完了，结果按照预期方案进行四分类的时候出乱子了，发现训练效果非常容易就过拟合了把所有样本映射到这样一个二维图上，问题很明显了，官员阶级的建筑风格混合了其他三个阶级的风格，导致神经网络很难学习到相关的信息，所以我开始考虑支持向量机来进行分类任务了。支持向量机 SVM借用知乎上的图，支持向量机的原理是通过找出一个最大间隔，将东西分类而我们的输入数据有19维的数据，即所有样本在一个19维的超平面里，我们的目的是找到另外的超平面，将样本大致分为四类当然，我们现有的MLP也没有落下，我重新设计了一个新的Hybrid MLP，他是把SVM中某个样本距离边界的距离再作为一个输入交给MLP，扩展了MLP的输入，初步看起来效果还挺好图片真实标签SVM预测Baseline MLPHybrid MLProyal/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.49610.7968+60.6%prince/1.jpg亲王0.45520.6163+35.4%official/1.jpg官员0.57100.7844+37.4%civi/1.jpg平民0.87430.7936-9.2%准确度表模型优化前优化后变化SVM65.91%59.09%-6.82%Hybrid MLP63.64%59.09%-4.55%Baseline MLP56.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 Image class 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_score class 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 RandomForestClassifier self.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维度交叉验证测试集过拟合程度传统特征218NaN69.71%55.00%严重ResNet18 (125维)21812569.71%55.00%严重ResNet18 (25维)2182580.70%47.50%严重ResNet18 (50维)2185077.07%50.00%严重ResNet18 (35维, RBF)2183577.94%50.00%严重ResNet18 (40维, 增强)6544088.68%55.00%轻微改善ResNet18 (25维, 增强)6542555.00%55.00%轻微改善ResNet18 (30维, RF)6543045.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个扩展后的样本依然不够；类别定义问题，不同类之间也存在相似建筑。未来的优化方向使用更强的数据增强方式考虑测试其他的预训练好的模型重新检查数据集质量收集更多数据，扩充数据量层次化分类，先训练出高阶级-低阶级的二分类模型，在此基础上继续细分，提高效率特征融合，综合ResNet特征与19维手工特征（如颜色信息）END这次的实践，虽然有AI的大量辅助，但是也探索了很多小样本、多分类、高维度下的机器学习场景。从传统的特征到深度学习，从单一模型到集成学习，从简单训练到数据增强，我们尝试了很多种不同的策略，选出了在当下条件下的最优解。虽然当下的最优解不一定是全局条件的最优解，但我们至少建立了一个完整的、可拓展的机器学习流程，而且我们识别到了问题的根本原因在于数据分布的差异，为以后的优化提供了方向。机器学习不仅仅是调参，而是理解数据、理解问题、找到正确的优化方向。与诸君共勉。