☰
小样本轨道故障图像识别实战:CNN与YOLOv5分类对比
2026/10/6 2:54:33 网站建设 项目流程

简介:面向铁路轨道故障识别与图像分类场景的算法工程师、科研人员及学习者,一份已标注数据集涵盖损坏与未损坏两类轨道状态图像,约380张,已按训练集、验证集、测试集分目录存放,JSON文件中给出具体分类标签,可直接用于CNN或YOLOv5等分类模型的训练与评估。压缩包共387个文件,主体为368张jpg图片,兼有少量jpeg/webp图片,另含1个json标注文件、1个Python可视化脚本及配套说明图片,整体大小约159.73MB,便于快速下载解压后离线使用。目前已有192人浏览学习,数据按类别归入不同文件夹,运行自带show.py脚本即可一键预览样本,降低数据核查与可视化门槛。配合作者发布的CNN分类网络专题和基于YOLOv5的分类教程,读者能从中获得从数据准备、脚本可视化到模型训练评估的完整实践路径,尤其适合刚接触轨道视觉分类的初学者作为入门数据集。

1. 铁路轨道故障图像识别数据集:先读图,再谈模型

铁路轨道故障图像识别数据集,入手之后要记住的第一件事:380 张已标注图片,别指望靠模型规模取胜。这套资源解决的是轨道巡检场景里最实际的二分类问题——判断一张轨道照片是「损坏」还是「未损坏」,数据已经按这两个类别标注完成,并且预先切分好了训练集、验证集、测试集,目录结构对主流训练框架基本是开箱即用。配套还给了一个 show 脚本用于可视化数据集,另外附了 CNN 分类项目和 YOLOv5 分类两条技术路线的教程。适合两类人:一是刚接触图像分类、想用最短路径跑通完整流程的初学者;二是需要小样本数据快速验证迁移学习方案是否可行的从业者。一个反直觉的结论是:这个资源的价值不在准确率,而在 380 张图能暴露出的数据划分、标注噪声、过拟合问题,比几千张图更典型、更集中。

2. 数据集结构与标注规则:目录、json 和 show 脚本的配合

2.1 目录结构:为什么「同类图片放一起」对训练最省事

拿到数据集之后先别急着配环境,第一件事是观察目录。作者在说明里写得很明确:已经划分好训练集、验证集、测试集,并且「存放各自的同一类数据图片」。翻译成实际布局就是数据集根目录下有三个子目录,每个子目录里再按类别分子文件夹:

dataset/ ├── train/ │ ├── damaged/ │ └── normal/ ├── val/ │ ├── damaged/ │ └── normal/ └── test/ ├── damaged/ └── normal/

这种排布的含金量在于:PyTorch 的 torchvision.datasets.ImageFolder 和 Keras 的 image_dataset_from_directory 都能直接吃进这种结构,开发者不需要写任何自定义 Dataset 类。这是「目录即标签」的工程范本,相比 CVAT 或 LabelImg 导出的 VOC 格式(需要解析 XML 定位标注框),省掉了一整层中间转换环节。

实操上我建议一拿到数据集就统计各子目录的图片数量,别靠眼睛数。常见做法是用一条命令看全貌:

find dataset/train -type f | sed 's/.*\.//' | sort | uniq -c

这条命令统计 train 目录里图片格式的分布,如果混入了 .png 和 .jpg,说明目录里可能存在格式混用。再细分到类别统计数量,用下面这段:

for d in train val test; do for c in damaged normal; do echo "$d/$c: $(ls dataset/$d/$c | wc -l) 张" done done

参数说明:$d 遍历三个子集,$c 遍历两个类别,ls 管道接 wc -l 统计每个类别文件夹里的图片数。输出会直接告诉你 train 里是不是只有两百多张、val 和 test 各剩多少。这步要看的是类别有没有漏放图片、目录名有没有写错。

逻辑上需要注意:ImageFolder 是按目录名自动生成类别 id 的,目录名「damaged」「normal」会直接变成 0 和 1 标签,所以目录名一定不能乱。如果你发现目录名是「good」「bad」「defect」这类别名,优先统一成和 json 里一致的名称,否则后面画混淆矩阵和统计报告时还得做一层名称映射,徒增噪音。

从工程角度看,「子目录即标签」还提供一个隐藏好处:后续想切到 YOLOv5 或 YOLOv8 的分类模式,或者接进 TensorFlow,目录结构可以原样复用。第一次做数据准备的人容易忽略这点,等到想换第二个框架时才意识到要重排一遍目录,白白浪费半小时。

2.2 标注文件:json 里有什么,怎么读

摘要里写的是「分类个数【2】:损坏、未损坏【具体查看json文件】」。作者把类别定义放在 json 里,意味着类别名可能有标准版也可能有简写,实操中一定要先读一遍再写训练代码。我在拿到任何标注数据集时,第一个动作就是检查 json 的顶层结构:

import json with open("dataset.json", "r", encoding="utf-8") as f: data = json.load(f) # 先打印顶层字段,确认数据是列表还是字典 for k, v in data.items(): if isinstance(v, list): print(f"{k}: list, 长度 {len(v)}") elif isinstance(v, dict): print(f"{k}: dict, 键 {list(v.keys())}") else: print(f"{k}: {v}")

这段代码先做结构勘察。常见的 json 结构要么是根层放 categories 列表,要么是 images 列表里每条记录带 label 字段,先打印顶层类型能避免下一步用错下标。

接着处理类别信息和划分统计:

# 如果结构是 {"categories": [{"id": 0, "name": "normal"}, ...]} for cat in data.get("categories", []): print(cat["id"], cat["name"]) # 如果结构是 {"train": [...], "val": [...], "test": [...]},统计各划分里的样本数 for split in ["train", "val", "test"]: if split in data: print(split, len(data[split]))

参数说明:encoding="utf-8" 是强制指定解码方式,Windows 下 json 文件如果带中文说明字段,缺这个参数会有概率触发 cp936 解码错误;data.get("categories", []) 是安全取值,json 结构不完整时返回空列表而不是抛 KeyError,适合对标注文件先做探查再决定后续逻辑。

这里给一个直接的提醒:读 json 的目的是确认两件事——类别 id 与图片数量的对应关系,以及训练时标签的来源。380 张图手动对一轮用不了十分钟,但能省掉后面排查标签错位的大把时间。如果 json 里除了类别还记录了图片路径,建议顺手校验几条路径在磁盘上是否真实存在,路径失效的情况在搬运数据集中很常见。

2.3 show 脚本:可视化不是看热闹,是看门道

作者提到资源里带 show 脚本,可以「可视化数据集」。这个脚本的存在非常实用:380 张小数据集,最稀缺的是对数据本身的理解,而不是模型。可视化要回答三个问题:类别是否平衡、图片分辨率是否一致、损坏形态有没有明显规律。

常见的脚本逻辑是遍历每个类别文件夹,按网格布局把图片铺开:

import os import random import matplotlib.pyplot as plt from PIL import Image data_root = "dataset/train" classes = ["damaged", "normal"] fig, axes = plt.subplots(2, 3, figsize=(12, 8)) for i, cls in enumerate(classes): cls_dir = os.path.join(data_root, cls) imgs = os.listdir(cls_dir) random.seed(0) sample = random.sample(imgs, min(3, len(imgs))) for j, img_name in enumerate(sample): img_path = os.path.join(cls_dir, img_name) img = Image.open(img_path) axes[i, j].imshow(img) axes[i, j].set_title(f"{cls}: {img.size}") axes[i, j].axis("off") plt.tight_layout() plt.savefig("dataset_overview.png", dpi=150)

逻辑说明:对每个类别随机抽 3 张图,标题打印图片真实宽高,这样一页图能同时看到类别分布和分辨率差异。如果损坏类的图普遍比未损坏类暗,说明拍摄条件不一致,训练预处理环节需要加入亮度归一化或对比度增强。random.seed(0) 保证每次抽样结果一致,复现时不会因为随机抽样对不上。

参数说明:figsize=(12, 8) 是画布尺寸,对应 2 行 3 列的子图排布;dpi=150 保证保存的图能看清纹理细节。如果在远程服务器上训练,plt.savefig 比 plt.show 更实用,不需要 X11 转发。

可视化这步看出的门道往往超出预期。比如我经常发现轨道图里有大量空白道床区域——碎石、枕木占了画面主体,真正的损坏区域只占很小比例。这种「目标小、背景杂」的结构会把模型注意力带偏,后面就需要考虑裁剪或加权损失来修正。

3. 用 CNN 二分类跑通基线:迁移学习与训练参数设计

3.1 选型理由:为什么 CNN 在这个规模下比手工特征稳

380 张图、二分类、轨道故障检测,为什么不建议用传统 CV 方案?轨道表面裂纹的特征是高对比度、细长、方向随机,手工特征如 Canny 边缘或方向梯度直方图需要人工调阈值,而 380 张样本根本不够调出对光照、灰尘、油渍鲁棒的阈值。CNN 的核心优势是由数据驱动学特征,浅层卷积核学到边缘纹理,深层学到语义形状,预训练模型把 ImageNet 上亿张图的通用视觉先验带过来,迁移到轨道场景只需少量样本微调。

另一个现实理由是配套资源里作者明确给了 CNN 分类项目的教程,数据集按 ImageFolder 组织,两边严丝合缝。与其另起炉灶搞特征工程,不如先把迁移学习这条路跑通,拿到指标后再决定要不要做细化。如果你习惯用 YOLO 系列框架,下面第 5 章会给一条完全复用目录结构的对比路线。

3.2 训练脚本核心:ImageFolder 加载与 ResNet18 微调

配好环境之后,第一步不是去调参,而是拿最基础的 ResNet18 跑一个能收敛的版本,确认数据、代码、硬件全链路没问题。我一般这样写:

import torch import torch.nn as nn import torch.optim as optim from torchvision import datasets, transforms, models from torch.utils.data import DataLoader # 预处理:轨道图过拟合风险高,先用轻量增强 transform = transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(p=0.5), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) train_ds = datasets.ImageFolder("dataset/train", transform=transform) val_ds = datasets.ImageFolder("dataset/val", transform=transform) train_loader = DataLoader(train_ds, batch_size=16, shuffle=True, num_workers=4) val_loader = DataLoader(val_ds, batch_size=16, shuffle=False, num_workers=4) # 加载预训练 ResNet18,替换最后一层全连接 model = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1) model.fc = nn.Linear(model.fc.in_features, 2) # 冻结除 fc 外的所有层,小数据集先只训练分类头 for param in model.parameters(): param.requires_grad = False for param in model.fc.parameters(): param.requires_grad = True model = model.cuda() criterion = nn.CrossEntropyLoss() optimizer = optim.Adam(model.fc.parameters(), lr=1e-3) for epoch in range(15): model.train() train_loss = 0.0 for images, labels in train_loader: images, labels = images.cuda(), labels.cuda() optimizer.zero_grad() outputs = model(images) loss = criterion(outputs, labels) loss.backward() optimizer.step() train_loss += loss.item() * images.size(0) # 每轮做一次验证 model.eval() correct = 0 total = 0 with torch.no_grad(): for images, labels in val_loader: images, labels = images.cuda(), labels.cuda() outputs = model(images) _, preds = torch.max(outputs, 1) correct += (preds == labels).sum().item() total += labels.size(0) val_acc = correct / total print(f"epoch {epoch+1:02d} | loss {train_loss/len(train_ds):.4f} | val_acc {val_acc:.4f}")

逻辑说明:用 ResNet18 而不是自己搭网络,是因为 ImageNet 预训练权重已经包含丰富的纹理和边缘特征,轨道图虽然领域特殊,但底层视觉模式仍然通用。冻结所有层只训练最后的全连接层,是小数据集上降低过拟合最有效的做法——380 张样本不足以让深层卷积权重稳定更新,强行解冻会让模型在第二个 epoch 就开始震荡。DataLoader 里 num_workers=4 是让 CPU 提前加载下一批图片,磁盘 IO 慢的时候能明显提速。

参数说明:batch_size=16 是基于 380 张的画像设置的,太小如 4 会导致梯度噪声大、loss 抖动;太大如 64 会让每次验证集的统计都基于少量图片,acc 波动大。lr=1e-3 对 Adam 是安全起点,只训练一层全连接时这个学习率稍偏快,但 15 个 epoch 内一般不会翻车。epoch=15 是刻意保守——这个数据量下 10 轮左右 val acc 就到天花板,再跑多是纯浪费。

如果验证集效果正常,这一版就可以作为基线。注意第一次跑不要上花活:不用学习率调度器、不加早停、不调 Dropout,最朴素的模型先跑出来,后面每一步改动才有对比参照。

3.3 训练输出怎么读:loss 曲线和过拟合信号

训练输出看起来简单,但要学会看曲线。最典型的状况是 train loss 一路降、val acc 在 70% 附近徘徊。这证明模型在分类头上已经记住了训练集里的噪声,但没有真正学到轨道损伤的特征。出现这种现象时优先做三件事:加强数据增强的强度(加旋转、加裁剪)、把 lr 降到 3e-4、把 15 个 epoch 改成 20 并每轮同时打印 val loss。

还有一个常见误区是盲目相信「val acc 高」就等于模型能用。小数据集上 val 集只有几十张图,val acc 波动 ±5% 都是正常噪声。正确做法是记录每一轮的 val_acc 和 val_loss,观察曲线是否平滑上升,而不是盯某一轮的峰值。如果 train acc 到了 99% 而 val acc 还在 80% 上下抖动,这就是过拟合前兆,下面第四章会展开讲。

提示:迁移学习小数据集的第一原则是「能冻结就冻结」。先跑通,再逐层解冻,这个顺序能避免大量无效调参。

4. 小数据集的避坑清单:380 张图训练中的五个典型问题

图像识别社区流传一句话:「小数据集跑通容易,跑稳难。」380 张图放在今天的大模型语境下算是微型数据,但恰恰是这个规模最容易踩坑,而且每个坑的表现都极具迷惑性。这一章按实际排错的经验,列出五个高发问题,每个都按现象、原因、解决三步来写。

4.1 现象:准确率 92%,测试集却全是未损坏类

原因:数据集中损坏类样本占比低,模型学到的是「全猜未损坏」的捷径。二分类里如果损坏图只占 20%,直接全部预测为未损坏就能拿到 80% 准确率,而交叉熵损失也发现这条路损失更低。

解决:打印混淆矩阵而不是只看总体 acc,对损坏类单独看召回率;给损坏类加权,或者直接把 CrossEntropyLoss 的 weight 参数设成样本比例的倒数。

class_counts = [len(os.listdir("dataset/train/damaged")), len(os.listdir("dataset/train/normal"))] weights = torch.tensor([1.0 / c for c in class_counts], dtype=torch.float32) weights = weights / weights.sum() * 2 # 归一化到均值 1 criterion = nn.CrossEntropyLoss(weight=weights.cuda())

参数说明:1.0 / c 是对类别做倒数加权,样本少的损坏类获得更高权重;归一化到均值 1 是为了让整体 loss 量级和未加权时接近,lr 不用重调。这个方法比随机过采样更干净,不改变图片本身的分布。使用时要确认 class_counts 的顺序和 ImageFolder 的类别顺序一致,否则权重会加反。

4.2 现象:验证集准确率高,加入同批次拍摄的新图后性能骤降

原因:数据划分泄漏。轨道巡检拍摄时往往连续快门连拍,相邻几帧的光照、角度、位置几乎一样,若随机划分把同一连拍序列的图片同时切进 train 和 val,模型的「高准确率」实际是记住了场景,不是泛化。

解决:划分前按文件名时间戳或拍摄批次做分组,同一组图片整体划到一个子集中。380 张图如果来源是几十个拍摄片段,至少要保证 val 和 test 里的图片不与 train 来自同一段连拍。实操上我一般先把文件名按字典序排序,再按间隔取样做划分,这样相邻帧不会散落到两个集合里。排序后还可以直接观察文件名前缀,同前缀的通常是同一批拍摄。

4.3 现象:train acc 冲上 99%,val loss 反而开始回升

原因:过拟合的教科书症状。模型在分类头上可以轻松记住 380 张图的全部标签,但记忆不等于学习。这个数据量下全连接层参数量相对图片数明显偏多,训练到后半段开始死记噪声。

解决:加强数据增强(随机旋转 ±15°、随机裁剪到 200×200 再缩放到 224)、增加 Dropout(例如 0.3)、或者将模型继续冻结只训练 fc。我的经验是:小数据集优先控制自由度,参数量大的层解冻越多,过拟合来得越快。判断是否要停训,看 val loss 连续三轮不降反升,就回退到之前一轮的权重。

4.4 现象:json 里标注是损坏,但图片里看不出损坏痕迹

原因:标注噪声。损坏的定义边界在轨道场景里比较模糊,轨面锈迹、道床碎石阴影、油污反光很容易被标成「损坏」;另一个方向是轻微裂纹在低分辨率下肉眼难辨。

解决:跑 show 脚本把疑似损坏的图片全部人工过一遍,把标注有疑问的图片单独放一个文件夹,先不参与训练,模型跑通后再单独预测这批图,看预测结果与人工判断的吻合度。如果标注错误率超过 10%,先修标注再训练,因为网络会学标注的「平均观点」,错标越多,模型输出越飘。

4.5 现象:解冻全部层训练后,val acc 从 80% 掉到 65%

原因:迁移学习的基础设置踩坑。预训练模型在 ImageNet 上收敛得很好,380 张轨道图对深层卷积权重的更新信号太弱、太偏,直接解冻全部层会让模型跳出 ImageNet 的特征空间,而新数据又不足以填回这个位置。

解决:按阶段解冻——先只训练 fc,val acc 稳定后再解冻最后一个残差块 layer4,lr 降到 1e-4。什么时候解冻?看 val acc 连续多个 epoch 不再上涨。代码实现就是在 epoch 计数达到阈值后,把 model.layer4 里所有参数的 requires_grad 置为 True,并给优化器重新设一个更小的 lr。这一步是我踩过最深、也是最容易被忽略的开关,翻车率极高。

5. 进阶验证:数据增强、YOLOv5 分类模式与双框架交叉验证

5.1 数据增强的工程化:用 Albumentations 替代 torchvision 的固定组合

torchvision 的 transforms 对小数据集来说力度不够灵活。我一般会用 Albumentations 做增强,它和 PyTorch 的 DataLoader 配合起来只需要改动预处理部分:

import albumentations as A from albumentations.pytorch import ToTensorV2 aug = A.Compose([ A.Resize(224, 224), A.RandomRotate90(p=0.3), A.RandomBrightnessContrast(brightness_limit=0.2, contrast_limit=0.2, p=0.5), A.GaussNoise(var_limit=(10, 50), p=0.2), A.Normalize(mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)), ToTensorV2() ])

参数说明:RandomBrightnessContrast 模拟不同时间拍摄的光照差异,对轨道巡检数据尤其重要;GaussNoise 模拟传感器噪点,p=0.2 是保守比例,太高会把纹理细节淹没。注意 Albumentations 的 Normalize 和 torchvision 的行为不同,以及 resize 放在随机操作之前,避免裁剪后输入尺寸不一致。使用时要保持训练和验证两套预处理分离——验证集只做 Resize、Normalize、ToTensor,不做随机扰动。

5.2 YOLOv5 分类模式:目录结构直接复用

配套资源里专门提到基于 YOLOv5 的分类教程。YOLOv5 分类模式最省事的地方是它沿用「子目录即标签」的组织方式,这个数据集解压后可以直接用,不需要额外转格式:

python classify/train.py --model yolov5s-cls.pt \ --data dataset/ \ --epochs 30 \ --img 224 \ --batch 16 \ --lr 0.001

参数说明:--data 指向数据集根目录,YOLO 会自动扫描根目录下的 train 和 val 文件夹,以子目录名作为类别名;yolov5s-cls 是分类专用预训练权重,size 最小,适合 380 张的小数据;--img 224 与上面 CNN 的输入分辨率保持一致,方便两个框架的结果互相对标。同理,YOLOv8 的分类模式也支持相同的数据组织方式,可以按需切换。

这样做的价值在于双框架交叉验证:如果 ResNet18 和 YOLOv5s-cls 两个完全不同结构的模型,在同一测试集上给出相近的 acc 和混淆矩阵,说明模型学到的是稳定的轨道损伤语义特征,而不是某个框架的数据处理偏好。

5.3 结果对比:以混淆矩阵和测试集报告收尾

两个框架都训练完后,把结果整理成对照表:记录 val acc、val 混淆矩阵、单类召回率三个指标。分类任务本身就是二分类,真正的合格标准是损坏类召回率是否足够高——在轨道巡检场景里,宁可把未损坏误判成损坏(多一次人工复核),也不能把损坏漏过去。

我现在拿到任何新的小数据集,都会先跑一遍可视化脚本做人工核对,然后按「CNN 基线 → 数据增强 → 第二框架交叉验证」的顺序走。这套顺序帮我挡掉了至少三次数据泄漏和两次标注噪声的翻车事故。模型训练这种事,坑总是藏在数据准备里,参数反而是最不常出问题的地方。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询