简介:这份资源面向图像识别与机器学习方向的初学者及进阶开发者,提供一套基于VGG卷积神经网络的自然灾害图像分类完整项目,可用于洪水、地震、火山、风暴、森林火灾等场景的自动识别与分类实践。压缩包共29个文件,约1.54MB,包含5个Python脚本、2个Jupyter Notebook、7张示例图片,以及cfg配置、csv数据索引、txt日志和md说明文档,覆盖数据预处理、模型训练、可视化与结果评估等环节。项目围绕CNN特征提取与类别匹配展开,涉及图像归一化、尺寸调整、数据增强及Kappa、召回率等指标记录,并配有数据可视化笔记帮助理解样本分布。已有70人学习,适合希望掌握VGG迁移应用、跑通灾害图像分类流程的读者参考与复现。
1. 从一张灾后航拍图说起:VGG 做自然灾害图像分类到底靠不靠谱
你手里有一批灾后航拍图,可能是洪水淹没的街道、山体滑坡切断的公路、森林火灾过后的焦土,也可能是地震后成片倒塌的房屋。现在要做的不是看图写报告,而是让模型自动把「洪水 / 滑坡 / 火灾 / 地震 / 正常」分出来。这就是「基于 VGG 的自然灾害图像分类」要解决的事:用 VGG 这种经典卷积网络做特征提取,把灾害场景图像分到预定义类别里。
它适合谁?一类是做遥感、应急管理、灾害评估的工程师,手里有标注数据但不知道怎么选模型;另一类是刚入门图像分类的开发者,想找一个结构清晰、迁移学习资料多、显存吃得消的骨干网络跑通全流程。VGG 不是最新的图像分类模型,但它结构规整、特征层次分明,在灾害场景这种类间差异大、类内差异也大的任务上,作为 baseline 非常稳。森林图像分类、城市内涝识别、滑坡检测这些细分方向,都能用同一套流程迁移过去。
2. VGG 为什么在灾害图像上还能打:结构、迁移与选型理由
2.1 VGG 的卷积堆叠到底给了灾害图像什么
VGG 的核心设计思想是用连续的小卷积核(3×3)堆叠替代大卷积核,在感受野不变的前提下增加非线性层数。灾害图像的特点是:洪水、火灾、滑坡这些类别在颜色、纹理、边缘分布上有明显差异,但同一类内部因为光照、拍摄角度、季节变化又差异巨大。VGG 的 5 个卷积块逐级提取从边缘到纹理再到语义的特征,浅层抓水面的波纹和火焰的亮斑,深层抓整体场景布局,这种层次性对灾害分类很关键。
常见做法是用 VGG16 或 VGG19 在 ImageNet 上的预训练权重做初始化,然后替换最后的全连接层输出维度为你的类别数。为什么不用从头训练?灾害图像数据集通常几千到几万张,从头训 VGG 这种参数量 1.3 亿级别的网络,过拟合几乎是必然的。迁移学习让卷积层保留通用特征提取能力,只微调高层和分类头,收敛快且泛化好。
2.2 数据准备:灾害图像分类的数据集怎么组织
假设你的数据目录结构是这样的,这是最常见也最不容易翻车的组织方式:
dataset/ ├── train/ │ ├── flood/ │ ├── landslide/ │ ├── fire/ │ ├── earthquake/ │ └── normal/ ├── val/ │ ├── flood/ │ └── ... └── test/ ├── flood/ └── ...每个类别一个文件夹,文件夹名就是类别名。这种结构可以直接被torchvision.datasets.ImageFolder或tf.keras.utils.image_dataset_from_directory读取,省去自己写标签映射的麻烦。
数据量建议:每类至少 300~500 张,少于这个数就得靠强数据增强或合成数据补。灾害图像有个坑——类别不平衡。正常场景的图往往远多于灾害图,直接训练模型会偏向多数类。解决办法有两个:一是用加权采样,二是损失函数加类别权重。
2.3 用 PyTorch 搭一个 VGG16 灾害分类器的最小可跑代码
下面这段代码是我一般会用的最小可跑版本,基于 torchvision 的 VGG16 预训练权重:
import torch import torch.nn as nn import torch.optim as optim from torch.utils.data import DataLoader from torchvision import datasets, transforms, models # 数据增强:训练集用强增强,验证集只做 resize 和归一化 train_tf = transforms.Compose([ transforms.Resize((224, 224)), # VGG 标准输入尺寸 transforms.RandomHorizontalFlip(), # 灾害图像水平翻转通常合理 transforms.RandomRotation(15), # 小角度旋转,模拟不同拍摄角度 transforms.ColorJitter(0.2, 0.2, 0.2), # 模拟光照变化 transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) # ImageNet 统计量 ]) val_tf = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) train_ds = datasets.ImageFolder('dataset/train', transform=train_tf) val_ds = datasets.ImageFolder('dataset/val', transform=val_tf) train_loader = DataLoader(train_ds, batch_size=32, shuffle=True, num_workers=4) val_loader = DataLoader(val_ds, batch_size=32, shuffle=False, num_workers=4) # 加载预训练 VGG16 model = models.vgg16(weights=models.VGG16_Weights.IMAGENET1K_V1) # 冻结卷积层,只训练分类头(数据少时推荐) for param in model.features.parameters(): param.requires_grad = False # 替换分类头:VGG16 原版是 1000 类,改成你的类别数 num_classes = len(train_ds.classes) model.classifier[6] = nn.Linear(4096, num_classes) device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = model.to(device) # 类别不平衡时给少数类更高权重 class_counts = [len(os.listdir(f'dataset/train/{c}')) for c in train_ds.classes] weights = torch.tensor([sum(class_counts) / (num_classes * c) for c in class_counts]).to(device) criterion = nn.CrossEntropyLoss(weight=weights) optimizer = optim.Adam(model.classifier.parameters(), lr=1e-3) for epoch in range(15): model.train() for imgs, labels in train_loader: imgs, labels = imgs.to(device), labels.to(device) optimizer.zero_grad() loss = criterion(model(imgs), labels) loss.backward() optimizer.step() # 每个 epoch 后在验证集上评估 model.eval() correct = total = 0 with torch.no_grad(): for imgs, labels in val_loader: imgs, labels = imgs.to(device), labels.to(device) preds = model(imgs).argmax(1) correct += (preds == labels).sum().item() total += labels.size(0) print(f'Epoch {epoch+1}, Val Acc: {correct/total:.4f}')逻辑说明:先冻结features部分,只训练classifier,这样即使数据只有几千张也能稳定收敛。classifier[6]是 VGG16 分类头的最后一层全连接,原输出 1000 维,替换成你的类别数。损失函数用带权重的交叉熵,权重按类别频率倒数计算,缓解不平衡。
参数说明:batch_size=32在 8GB 显存上跑 VGG16 的 224×224 输入基本是上限,显存不够就降到 16。lr=1e-3是分类头单独训练时的常用值,如果后面解冻卷积层做微调,学习率要降到 1e-4 甚至 1e-5。RandomRotation(15)不要设太大,灾害图像里滑坡方向、洪水流向有语义,旋转超过 30 度可能把语义转没了。
2.4 微调策略:什么时候该解冻卷积层
冻结卷积层训练 10~15 个 epoch 后,如果验证集准确率还在涨但涨幅变小,可以解冻最后两个卷积块(features[24:]之后的部分)做微调。微调时学习率要调小,一般设 1e-4,并且用 SGD 比 Adam 更稳。我一般会分两阶段:第一阶段冻结训 15 epoch,第二阶段解冻后 5 个 block 训 10 epoch,学习率用余弦退火从 1e-4 降到 1e-6。
注意:解冻后 batch size 要减半,因为卷积层参与训练后显存占用会明显上升。如果验证集准确率反而下降,说明解冻太多或学习率太大,退回只解冻最后一个 block。
3. 训练完不算完:评估、可视化与模型导出
3.1 混淆矩阵比准确率更能暴露问题
灾害分类里准确率有欺骗性。如果测试集里 70% 是正常场景,模型全猜正常也有 70% 准确率。所以必须看混淆矩阵:
from sklearn.metrics import confusion_matrix, classification_report import seaborn as sns import matplotlib.pyplot as plt model.eval() all_preds, all_labels = [], [] with torch.no_grad(): for imgs, labels in val_loader: imgs = imgs.to(device) preds = model(imgs).argmax(1).cpu().numpy() all_preds.extend(preds) all_labels.extend(labels.numpy()) cm = confusion_matrix(all_labels, all_preds) sns.heatmap(cm, annot=True, fmt='d', xticklabels=train_ds.classes, yticklabels=train_ds.classes) plt.ylabel('True') plt.xlabel('Predicted') plt.show() print(classification_report(all_labels, all_preds, target_names=train_ds.classes))重点看两件事:哪两个类别互相混淆最多,以及少数类的召回率。洪水和正常水面、滑坡和正常山地,这两组是最容易混的。如果洪水召回率低于 0.7,说明模型没学到洪水的关键特征,要么加数据,要么在增强里加更多水面纹理变化。
3.2 用 Grad-CAM 看模型到底在看哪里
Grad-CAM 能生成热力图,告诉你模型分类时关注图像的哪个区域。如果模型分类「火灾」时热力图集中在天空而不是火焰区域,说明它学的是错误特征。实现上可以用pytorch-grad-cam库,指定目标层为model.features[28](VGG16 最后一个卷积块的输出):
from pytorch_grad_cam import GradCAM from pytorch_grad_cam.utils.image import show_cam_on_image target_layers = [model.features[28]] cam = GradCAM(model=model, target_layers=target_layers) grayscale_cam = cam(input_tensor=img_tensor.unsqueeze(0)) visualization = show_cam_on_image(img_np, grayscale_cam[0], use_rgb=True)这一步在项目验收或论文里很加分,也能帮你判断模型是不是在「作弊」——比如靠图片角落的水印或拍摄设备特征分类。
3.3 导出 ONNX 做部署
训练完的 PyTorch 模型要落到实际系统里,导出 ONNX 是最通用的做法:
dummy_input = torch.randn(1, 3, 224, 224).to(device) torch.onnx.export( model, dummy_input, 'vgg16_disaster.onnx', input_names=['input'], output_names=['output'], dynamic_axes={'input': {0: 'batch_size'}, 'output': {0: 'batch_size'}}, opset_version=11 )dynamic_axes让导出的模型支持变长 batch,部署时不用固定 batch size。opset_version=11兼容性最好,TensorRT、OpenVINO、ONNX Runtime 都支持。导出后一定要用 ONNX Runtime 跑一遍验证输出和 PyTorch 一致,差异超过 1e-3 就要查原因,常见的是归一化参数没对齐。
4. 避坑与排查:VGG 灾害分类里最容易翻车的 5 个地方
4.1 验证集准确率很高但实际用起来一塌糊涂
现象:训练时验证集准确率 95%,但拿新拍的灾害图去测,错得离谱。
原因:验证集和训练集来自同一批数据、同一分布,甚至同一拍摄设备。模型记住了设备特征而不是灾害特征。
解决:划分数据时按拍摄批次或地理区域划分,不要随机打乱。如果数据来自多个来源,留一个来源整体做测试。另外检查图像 EXIF 信息是否被模型间接利用,必要时在预处理里统一去除。
4.2 损失不下降,准确率卡在随机水平
现象:训练几个 epoch 后 loss 几乎不变,准确率在 1/类别数 附近晃。
原因:最常见的是归一化参数用错。VGG 预训练权重期望的输入是 ImageNet 统计量归一化后的,如果你用了[0,1]或[-1,1]归一化,预训练特征完全对不上。
解决:确认transforms.Normalize的均值和标准差是[0.485, 0.456, 0.406]和[0.229, 0.224, 0.225]。另外检查标签映射有没有错位,ImageFolder按文件夹名排序生成类别索引,如果文件夹名有大小写或数字前缀,顺序可能和你预期不一致。
4.3 显存溢出,batch size 降到 1 还是 OOM
现象:8GB 显存跑 VGG16 224×224,batch size 设 8 还是报 CUDA out of memory。
原因:VGG16 的全连接层参数量巨大,第一层全连接是 25088×4096,光这一层就占几百 MB。如果解冻了卷积层,中间激活值也占显存。
解决:先确认只训练分类头时是否还 OOM。如果还 OOM,把输入尺寸从 224 降到 192 或 160,VGG 支持任意尺寸输入但太小会丢特征。另一个办法是用梯度累积:batch size 设 4,累积 8 次梯度再更新,等效 batch size 32。
4.4 数据增强把灾害特征增强没了
现象:加了随机裁剪、旋转、颜色抖动后,验证准确率反而比不加还低。
原因:灾害图像里有些特征对方向敏感。比如滑坡的滑动方向、洪水淹没的方向性纹理,随机旋转或翻转后语义就变了。颜色抖动太大也会把火焰的橙红色抖成其他颜色。
解决:水平翻转一般安全,垂直翻转和大幅旋转慎用。颜色抖动幅度控制在 0.1~0.2。如果类别里有「火灾」这种颜色敏感的,颜色抖动可以关掉,改用亮度对比度微调。
4.5 推理速度太慢,单张图要几百毫秒
现象:部署到边缘设备或服务端,VGG16 推理一张图要 200ms 以上,QPS 上不去。
原因:VGG16 参数量 1.38 亿,浮点运算量约 155 亿次,在 CPU 上跑就是慢。全连接层是主要瓶颈。
解决:部署时把全连接层换成全局平均池化加一个线性层,参数量从 1.38 亿降到 1500 万左右,精度损失通常不到 1 个百分点。或者用 ONNX Runtime 的量化功能做 INT8 量化,速度能提升 2~3 倍。如果精度要求不高,直接换 MobileNetV3 或 EfficientNet-B0 做蒸馏,VGG 当教师模型。
5. 把 VGG 灾害分类器推到能用的程度:三个进阶技巧
第一个技巧是用测试时增强(TTA)提点。推理时对同一张图做原图、水平翻转、小角度旋转三种变换,分别预测后取平均概率。这个操作不增加训练成本,在灾害分类这种类间边界模糊的任务上通常能提 1~3 个百分点。代码上就是把val_tf复制三份不同变换,推理时循环跑再平均。
第二个技巧是分层学习率。解冻卷积层微调时,浅层用更小的学习率(1e-5),深层用稍大的(1e-4),分类头用 1e-3。PyTorch 里可以通过给optimizer传不同参数组实现:
optimizer = optim.SGD([ {'params': model.features[:24].parameters(), 'lr': 1e-5}, {'params': model.features[24:].parameters(), 'lr': 1e-4}, {'params': model.classifier.parameters(), 'lr': 1e-3} ], momentum=0.9, weight_decay=5e-4)这样浅层通用特征不会被大学习率破坏,深层和分类头能快速适应灾害数据。
第三个技巧是模型集成。VGG16 和 VGG19 各训一个,推理时概率平均。两个模型结构相似但深度不同,错误模式有差异,集成后准确率通常比单模型高 2~4 个百分点。如果嫌两个 VGG 太慢,用 VGG16 加 ResNet50 集成,速度差不多但多样性更好。
验证方法上,我习惯留一个「对抗测试集」:专门收集那些容易混淆的边界样本,比如洪水退去后的泥地、火灾后的焦土和正常暗色地面。这个测试集不参与训练和调参,只在最后验收时跑一次。如果这个集上准确率能到 80% 以上,模型才算真正可用。
最后说个血泪经验:灾害图像分类项目里,数据质量比模型选择重要得多。我见过太多人花一周调 VGG 结构,不如花一天把标注错的样本清理一遍。标注一致性、类别定义清晰、边界样本处理规则统一,这三件事做到位,VGG16 冻结训练就能给你惊喜。模型是黑匣子,但数据不是,后悔药没得吃,不如一开始就把数据管好。希望帮到你。
本文还有配套的精品资源,点击获取