☰
30种球类运动图像识别数据集:从数据划分到PyTorch训练实践
2026/10/1 5:15:03 网站建设 项目流程

简介:这份30种球类运动图像识别数据集,面向图像分类、目标检测方向的学生、研究者和AI爱好者,可直接用于训练分类网络,也可作为YOLOv5等检测框架的分类预处理数据。数据已在data目录下按训练集、验证集、测试集三个文件夹存放,其中训练集3595张、验证集150张、测试集150张,覆盖篮球、足球、棒球、台球、高尔夫等30个常见球类类别;同时提供JSON类别字典文件,标签映射一目了然,配套的Python脚本还能一键可视化样本,方便快速检查图片质量与类别分布。资源共2000个文件,以1998张JPG图片为主,辅以1个Py脚本和1个JSON文件,压缩包整体约76.74MB,以7z格式发布,包体适中、便于传输;数据集目录结构清晰,按类别分文件夹存储,下载后无需重新整理即可直接导入训练流程。目前已有190人学习下载,尤其适合需要已整理球类图片数据、希望省去爬取清洗与手动划分环节的课程设计、算法练手或毕业设计场景。

1. 30种球类运动图像识别数据集到底解决什么问题

拿到一份“30种球类运动图像识别数据集”时,最让人头疼的往往不是模型,而是数据。做过深度学习图像识别的人都懂:网上爬来的图要清洗、要重命名、要按类别分文件夹,还要自己写脚本按比例拆训练集和验证集,最后再手工维护一份类别名和数字ID的对照表。这套流程走下来少说两三天,中间还容易翻车——文件夹名对不上、图像损坏、验证集里混进了训练集的数据。而这份数据集把最磨人的环节跳过了:30类球类图像已经按类别分好文件夹,训练、验证、测试划分也做好了,还附带类别字典文件,拿到就能直接喂给训练脚本。对做深度学习图像识别入门、课程设计、算法对比实验的开发者来说,这是最理想的起步素材。

2. 读懂这份数据集:目录结构、划分逻辑与类别字典文件的设计

2.1 目录结构的约定:为什么“文件夹保存”比清单式更省事

以常见做法为例,拿到压缩包解压后,典型目录长这样:

ball_dataset/ ├── train/ │ ├── basketball/ # 图片1.jpg 2.jpg ... │ ├── football/ │ ├── volleyball/ │ ├── baseball/ │ └── ... # 共30个子文件夹 ├── val/ │ └── (同train的30个类别子文件夹) ├── test/ │ └── (同train的30个类别子文件夹) ├── classes.json # 类别字典文件 └── classes.txt # 类别字典文件(纯文本版)

文件夹名即类别标签,这是计算机视觉领域最通用的约定。PyTorch的torchvision.datasets.ImageFolder、Keras的flow_from_directory、FastAI的ImageDataLoaders.from_folder原生支持这种结构,连写扫描逻辑都省了。与之相对的是“清单式”数据:一个CSV里写图片路径和标签,图片散落在若干目录。清单式的好处是灵活,但坏处是后期每次换模型、换框架都要重新解析一遍清单,路径写错一个就是批量报错。文件夹保存的方式自带可读性,哪怕不写代码,人眼扫一眼目录就能核对数据规模。

我看到有些数据集还会在子文件夹里附带info.json记录拍摄来源、分辨率统计,这个属于加分项,不影响训练流程。还有一点值得注意:test目录在多数竞赛场景里是不带标签的,但这份数据集的test目录依然保留了类别子文件夹,说明它设计上允许你用监督方式评估最终模型,而不是让你自己另找标注。

2.2 训练/验证/测试划分:比例、随机种子与文件夹落盘

数据集在划分时通常按8:1:1或7:2:1的比例切分训练、验证、测试三份。这里的比例不是随便拍的,它取决于两个因素:类别总数(30类已属中粒度分类)和每类图片数量。如果每类有数百张图,8:1:1足够;如果每类只有几十张,7:2:1甚至6:2:2更稳妥,因为验证集太小会导致调参时的评估指标抖动剧烈。

划分时有一个关键操作容易被忽略:分层划分。也就是先对每个类别单独打乱、再按比例抽取,保证每类数据在三个集合里都有且比例一致。用代码表达就是:

from sklearn.model_selection import train_test_split # image_paths 是某类别下全部图片路径列表 train_paths, temp_paths = train_test_split( image_paths, test_size=0.2, random_state=42 ) val_paths, test_paths = train_test_split( temp_paths, test_size=0.5, random_state=42 )

random_state=42这类固定随机种子是划分的生命线,否则每次运行得到不同划分,实验就没法复现。test_size=0.2先把20%留出来,再对剩余20%二等分得到各占总样本10%的验证集和测试集。为什么先拆测试再拆验证而不是一口气拆三份?因为这样可以只跑一次拆分代码,且测试集是严格保密的——在多次调参后,模型如果对验证集产生了隐式过拟合(比如针对验证集指标调了很久的阈值),测试集仍然是干净的最终裁判。

2.3 类别字典文件的两种格式:JSON与TXT怎么选

类别字典文件是整个数据集的“翻译官”。模型输出的0到29这30个数字,必须通过它才能还原成“篮球”“足球”“排球”这样的可读类别名。两种主流格式的区别在于设计意图:

JSON格式适合机器解析,直接进代码。常见结构有两种:

{ "0": "basketball", "1": "football", "2": "volleyball" }

或者反过来的键值对:

{ "basketball": 0, "football": 1, "volleyball": 2 }

TXT版本则是一行一个类名,按索引顺序排列:

basketball football volleyball

我一般建议以JSON为主要消费对象。原因有三:第一,Python的json库解析天然容错,不会像split('\n')那样在尾随换行符上出Bug;第二,JSON可以扩展字段,比如以后想加别名、加中文名、加入样本数统计,都不破坏旧结构;第三,部署端如果用C++或Java,JSON的解析库遍地都是,而TXT按行解析还要自己约定编码。纯文本版留着给人看的,训练代码统一读JSON。

3. 从文件夹数据集到可用的识别模型:用PyTorch把30类球类跑通

3.1 数据加载:用ImageFolder还是自定义Dataset

既然数据集已经按ImageFolder约定摆好了目录结构,训练代码根本不需要手写Dataset。下面是加载核心逻辑:

from torchvision import datasets, transforms from torch.utils.data import DataLoader train_transform = transforms.Compose([ transforms.Resize((256, 256)), # 先放大一点,给随机裁剪留空间 transforms.RandomResizedCrop(224), # 随机裁剪到224 transforms.RandomHorizontalFlip(), # 水平翻转增强 transforms.ColorJitter(0.3, 0.3, 0.3), # 颜色抖动 transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) val_transform = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) train_dataset = datasets.ImageFolder('./ball_dataset/train', transform=train_transform) val_dataset = datasets.ImageFolder('./ball_dataset/val', transform=val_transform) # 类别字典在ImageFolder里也能取 class_to_idx = train_dataset.class_to_idx print(class_to_idx) # {'basketball': 0, 'football': 1, ...}

这里复用ImageNet的均值标准差做归一化,因为等下要加载的是在ImageNet上预训练过的ResNet或者EfficientNet权重,输入分布必须与预训练一致。RandomResizedCrop(224)比固定裁剪更抗过拟合——它能模拟球在不同距离、不同视角下的尺度变化。球类识别中一个典型问题是图片里球的大小差异极大,特写镜头中的足球占据画面主体,远景画面中的足球可能只有几十个像素,而模型输入必须是固定尺寸,这种随机裁剪相当于人为制造尺度扰动。ColorJitter对球类也很有针对性,不同比赛场地的灯光条件差异大,颜色抖动模拟的就是这种环境变化。

3.2 训练参数:图像尺寸、batch、学习率与预训练权重怎么定

图像尺寸方面,224x224是性价比最高的起点,ResNet和EfficientNet系列的标准输入都是这个值。不要一上来就用448尺寸,训练速度慢一倍,精度收益通常不到1个点。batch size在不爆显存的前提下尽量大——比如8G显存配ResNet50,batch为32算安全值;显存不够就降到16。学习率跟迁移学习策略绑定,用ImageNet预训练做微调的常见做法是:

import torch import torch.nn as nn from torchvision import models model = models.resnet50(weights=models.ResNet50_Weights.IMAGENET1K_V2) num_classes = 30 # 替换最后一层全连接 in_features = model.fc.in_features model.fc = nn.Linear(in_features, num_classes) # 微调策略:前几层冻结,后几层放开 for name, param in model.named_parameters(): if 'layer4' not in name and 'fc' not in name: param.requires_grad = False optimizer = torch.optim.AdamW( filter(lambda p: p.requires_grad, model.parameters()), lr=1e-4, weight_decay=1e-4 ) scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=30)

冻结到layer4之前的层,让低层特征保持通用,只训练高层语义特征和最后的分类头。这么做有几个实际收益:显存占用更少、训练更快、在中小数据集上更不容易过拟合。如果你发现验证集loss在几个epoch后不再下降,可以把layer3也解冻,把学习率降到3e-5继续训练。AdamW的weight_decay在迁移学习场景里比Adam的L2实现更稳,不挤占学习率的有效空间。

3.3 训练脚本核心代码与日志观察点

训练主循环的关键部分如下:

criterion = nn.CrossEntropyLoss() num_epochs = 30 best_acc = 0.0 for epoch in range(num_epochs): model.train() running_loss = 0.0 correct = 0 total = 0 for inputs, labels in train_loader: inputs, labels = inputs.to(device), labels.to(device) optimizer.zero_grad() outputs = model(inputs) loss = criterion(outputs, labels) loss.backward() optimizer.step() running_loss += loss.item() * inputs.size(0) _, predicted = torch.max(outputs, 1) total += labels.size(0) correct += (predicted == labels).sum().item() train_acc = 100.0 * correct / total train_loss = running_loss / total print(f'Epoch {epoch+1}/{num_epochs}, Train Acc: {train_acc:.2f}%, Loss: {train_loss:.4f}') # 验证 model.eval() val_correct = 0 val_total = 0 with torch.no_grad(): for inputs, labels in val_loader: inputs, labels = inputs.to(device), labels.to(device) outputs = model(inputs) _, predicted = torch.max(outputs, 1) val_total += labels.size(0) val_correct += (predicted == labels).sum().item() val_acc = 100.0 * val_correct / val_total print(f'Val Acc: {val_acc:.2f}%') if val_acc > best_acc: best_acc = val_acc torch.save(model.state_dict(), 'best_model.pth') scheduler.step()

训练时盯三个关键指标:训练loss是否稳步下降、训练acc和验证acc的差距是否拉大、验证acc是否还在爬升。如果训练acc很快冲到95%以上而验证acc停在80%上下,说明过拟合,优先削弱数据增强外的正则化手段——减小模型容量或加Dropout。如果两三轮内训练acc就超过90%,多半是学习率设大了,应立即回退到1e-4以下。每轮打印一次就够了,不要每步打印,刷屏反而看不清趋势。

4. 避坑指南:球类图像识别的5个常见翻车点

4.1 现象:loss下降正常,但某些类别准确率几乎是0;原因:类别样本数差了一个数量级

30种球类的图片数据天然不平衡。足球、篮球这类大众运动的图片可能有几千张,而板球、冰壶这些冷门球类可能只有两三百张。用CrossEntropyLoss训练时,模型倾向于把不确定样本全判成高频类,导致少数类精确率和召回率双双惨不忍睹。解决思路是给少数类加权:

from torch.utils.data import WeightedRandomSampler import numpy as np labels = np.array([s for _, s in train_dataset.samples]) class_counts = np.bincount(labels) class_weights = 1.0 / class_counts sample_weights = class_weights[labels] sampler = WeightedRandomSampler(sample_weights, num_samples=len(sample_weights), replacement=True) train_loader = DataLoader(train_dataset, batch_size=32, sampler=sampler)

WeightedRandomSampler让少数类在一个epoch里被重复采样,相当于过采样,比直接改loss函数更直观。注意采样时replacement=True意味着同一个样本可以被多次选中,epoch数就不用刻意增加。

4.2 现象:验证集里篮球和排球互相认错、橄榄球和足球互相认错;原因:外观特征过于接近

30种球类里好几类长得像:排球、篮球、水球都是圆形胶皮质地,只差纹理花纹和颜色;橄榄球和足球在低分辨率下都是黑白块。模型如果只能捕捉颜色和圆度,混淆是必然的。解决方向是增强细粒度特征:把输入尺寸从224提到256或288,让小纹理有更多像素承载;另外加上RandomErasing或Cutout做随机遮挡增强,强迫模型不依赖某个局部区域:

from torchvision.transforms import RandomErasing train_transform.transforms.append(RandomErasing(p=0.25, scale=(0.02, 0.2)))

还有一个实用技巧是看混淆矩阵定位具体哪些类别互相纠缠,然后针对性地收集这两种球在不同光线和角度下的图片,比盲目增加所有类别的数据效率高得多。

4.3 现象:报错“Found no valid image files”;原因:子文件夹名或图片扩展名不规范

大多数人拿到的数据集是第三方整理的,偶尔出现文件夹多了空格、图片是.jpeg或.JPG大写扩展名、还有几张某类图片混进了另一个文件夹的情况。ImageFolder默认只要找到至少一个合法图片文件就不会报错,但非法文件会被静默跳过,干扰计数。稳妥做法是拿到的第一步先写个脚本清点:

find ball_dataset -type f \( -name "*.jpg" -o -name "*.jpeg" -o -name "*.png" \) | wc -l

如果数量和你预期对不上,再用Python遍历每个子文件夹统计数量,定位到具体是哪个类少了图。这个检查只需要几分钟,却能避免训练跑到一半才发现某个类只有几十张图的血泪教训。

4.4 现象:验证集指标虚高,部署到新图片上准确率暴跌;原因:数据泄漏

数据泄漏最常见的方式是划分前没打乱,图片按文件名排序后前80%进训练集、后20%进验证集——如果这些图片来自同一场比赛的连续帧,训练集和验证集会有极高的相似度。许多球类数据集由视频抽帧生成,连续帧之间差异甚微,模型在验证集上就是开卷考试。更隐蔽的泄漏是预处理阶段用全量数据(含验证集)计算均值和标准差做归一化,这虽然影响小但确实不规范。解决方法是训练前先检查是否有连续帧泄漏——随机抽几张验证集图片看看是不是和训练集某张几乎一样。如果是,就得按来源分组重新划分。

4.5 现象:代码跑通但预测结果张冠李戴,类别名对不上号;原因:索引映射错位

训练时PyTorch的ImageFolder按文件夹名字母排序分配索引,索引0对应字母序第一位的类名。而部署时如果直接用另一个顺序的类名列表来映射输出,结果全错。这个坑最坑的地方在于它不会报错——准确率看起来正常,但每个预测结果都串位。正确做法是训练时把class_to_idx存下来,部署时直接用同一个字典索引,永不重新生成:

import json with open('class_to_idx.json', 'w') as f: json.dump(train_dataset.class_to_idx, f, indent=2) with open('classes.json', 'r') as f: class_to_idx = json.load(f)

类别字典文件在这里的价值就体现出来了——它是训练和部署之间唯一的权威映射来源。

5. 把类别字典用到部署端:映射文件驱动的推理代码与验证方法

模型训练结束只是第一步,真正落地时会发现推理代码和训练代码的差异不小。训练时标签在数据加载器里自动转换,部署时输入是一张裸图片,输出是一个概率分布。这时候类别字典文件承担了最后一步的翻译工作。一个可复用的推理脚本片断如下:

import json from PIL import Image import torch import torchvision.transforms as transforms from torchvision import models device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') # 1. 加载类别字典 with open('classes.json', 'r', encoding='utf-8') as f: idx_to_class = {int(k): v for k, v in json.load(f).items()} # 2. 加载模型结构并注入权重 model = models.resnet50(weights=None) model.fc = torch.nn.Linear(model.fc.in_features, len(idx_to_class)) model.load_state_dict(torch.load('best_model.pth', map_location=device)) model.to(device) model.eval() # 3. 单张图片推理 def predict(image_path): img = Image.open(image_path).convert('RGB') tensor = val_transform(img).unsqueeze(0).to(device) with torch.no_grad(): logits = model(tensor) probs = torch.softmax(logits, dim=1) conf, pred_idx = torch.max(probs, 1) return idx_to_class[pred_idx.item()], conf.item()

这里有两个验证步骤不能省。第一,从classes.json生成的idx_to_class映射要和训练时的class_to_idx严格互逆,否则输出概率和类别名对不上。第二,用测试集跑一遍整体准确率之后,务必做混淆矩阵和单类准确率分析,只看总准确率会掩盖个别冷门类完全不可用的问题。

我这两年做图像识别项目养成的习惯是:无论数据多干净,都先跑一个5轮小训练验证数据流,确认loss能下降再上完整训练。数据集结构这类低级问题,往往在前几轮就暴露了,绝不等到30个epoch跑完才检查。一个好的数据集就像一把好用的工具,但对于拿到手的数据,第一件事永远是核查结构、核对字典、做一次小规模冒烟测试。希望这份球类数据集的拆解思路,能让你少走几步弯路。

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

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

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

立即咨询