本文内容较新 · 今天更新
最后更新: 2026年02月13日
预计阅读时间: 70.7 分钟
17182 字 4 图 250 字/分

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

train_feature_distribution.png

把所有样本映射到这样一个二维图上,问题很明显了,官员阶级的建筑风格混合了其他三个阶级的风格,导致神经网络很难学习到相关的信息,所以我开始考虑支持向量机来进行分类任务了。

支持向量机 SVM

借用知乎上的图,支持向量机的原理是通过找出一个最大间隔,将东西分类

51ffa338-e745-4d0f-a1c9-ec5e326aef5a.jpeg

而我们的输入数据有19维的数据,即所有样本在一个19维的超平面里,我们的目的是找到另外的超平面,将样本大致分为四类

当然,我们现有的MLP也没有落下,我重新设计了一个新的Hybrid MLP,他是把SVM中某个样本距离边界的距离再作为一个输入交给MLP,扩展了MLP的输入,初步看起来效果还挺好

图片真实标签SVM预测Baseline MLPHybrid 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.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]
    )
])


这一番操作下来是好了一些,但依然让我不满意

13849fa3-be1b-4540-9cd5-b7ab8a3f7e9d.png

增加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%

轻微

还记得一开始的这张图吗?

train_feature_distribution.png

我们不看绿色的部分,会发现皇帝、亲王、平民的建筑有一个比较明显的分界线,但是官员阶级建筑的加入,直接造成平民、亲王阶级的建筑被混淆到一起。

总结一下我目前使用过的所有的版本,在目前这个受限的条件下选出了一个比较优秀的解,剩下的问题就靠数据集了

版本

样本数

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

4088.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:

  • 重新确定数据采集标准

  • 收集更多数据

  • 统一数据收集标准

成果 

实现了什么

  1. 经过巨多轮的优化,我们实现了:

  2. 从传统特征升级到深度学习特征(ResNet18);

  3. 交叉验证准确率从69.71%提升到88.68%(+18.97%);

  4. 模型稳定性提升(标准差从5.69%降到3.66%);

  5. 测试了多种模型架构(SVM、MLP、RandomForest);

  6. 实现了数据增强策略;识别了数据分布差异问题。

核心发现

  1. 测试集数据准确度难以突破55%的根本原因主要来自:

  2. 训练集和测试集的分布差异较大;

  3. 样本量太少,对四分类来说654个扩展后的样本依然不够;

  4. 类别定义问题,不同类之间也存在相似建筑。

未来的优化方向

1. 使用更强的数据增强方式

2. 考虑测试其他的预训练好的模型

3. 重新检查数据集质量

4. 收集更多数据,扩充数据量

5. 层次化分类,先训练出高阶级-低阶级的二分类模型,在此基础上继续细分,提高效率

6. 特征融合,综合ResNet特征与19维手工特征(如颜色信息)

END

这次的实践,虽然有AI的大量辅助,但是也探索了很多小样本、多分类、高维度下的机器学习场景。

从传统的特征到深度学习,从单一模型到集成学习,从简单训练到数据增强,我们尝试了很多种不同的策略,选出了在当下条件下的最优解。

虽然当下的最优解不一定是全局条件的最优解,但我们至少建立了一个完整的、可拓展的机器学习流程,而且我们识别到了问题的根本原因在于数据分布的差异,为以后的优化提供了方向。机器学习不仅仅是调参,而是理解数据、理解问题、找到正确的优化方向。

与诸君共勉。