简介:面向手语识别与图像分类任务,内含覆盖36类手语动作的标注图像,适合学生、科研人员及开发者用作CNN分类网络训练与改进的基准数据。压缩包共2000个文件,包括1998张JPEG格式的手语图像、1个Python可视化脚本和1个JSON标签文件,整体大小28.58MB;图像按训练集、测试集分目录存放,每类数据集中管理,标签文件可查看具体类别映射。已有443人学习/下载。数据中的手部区域已完成分割与裁剪,可直接输入模型;运行附带脚本可快速浏览样本图像,结合JSON标注能便捷完成类别标签的读取与评估。对于正在尝试卷积神经网络结构改进、数据增强或多类别手势识别的读者,这套数据是一个开箱即用的实践入口。
1. 手语图像分类数据集:拿到手先看这三点再动手
做手语识别的人,十有八九把时间耗在数据上而不是模型上。我拆这份「已标注、约 2,500 张」的手语图像分类数据集时,第一反应是先把家底摸清楚:36 个类别,0、1、a、b 这类手指数字和字母都在里面,训练集和测试集已经划好,还带了一个 show 脚本。对想快速验证图像分类算法的人来说,这等于省掉了两三天整理数据的功夫。但我劝你别解压完就无脑开训,拿到的第一时间要确认三件事:JSON 里的类别映射和文件夹是否一致、训练测试划分里有没有漏网之鱼、图片尺寸和通道是否统一。这三件事直接决定后面训练脚本会不会中途翻车。
2. 读懂文件命名与目录结构:从 hand5_g_bot_seg_3_cropped 说起
2.1 命名拆解:横杠之外的隐藏信息
看这份数据集的文件名:hand5_g_bot_seg_3_cropped.jpeg。它不是随手起的。我拆过不少手势数据集,这种命名通常遵循「主体_标签_视角_片段_序号_处理方式」的规则:
- hand5:第 5 个手部样本或录制场景编号,用来区分不同采集来源;
- g:类别标签,这里表示字母 g;
- bot:bottom 视角,说明数据来自多视角采集,不同角度拍同一手势;
- seg_3:第 3 个分割片段,可能是从连续视频流里截出来的帧段;
- cropped:已经做过手部区域裁剪,不是原始画面帧。
这个命名规律的价值在于:如果你要做跨视角泛化实验,可以直接用命名里的视角字段做分组,不用重新标注。比如把 bot 视角单独抽出来当测试集,看模型在其他视角上的表现,就知道这个模型到底是学会了手势本身,还是只记住了某个摄像头角度下的背景。手语数据最容易出这种幻觉式高准确率,背景一旦变化,分数立刻垮掉。
另外,16 张图都属于 cropped 版本,说明数据源大概率是视频帧经过手部分割网络处理后裁出来的。这类预处理有个通病:分割不干净时会把袖子、桌面边缘一起裁进来,等于给每个类别偷偷加了背景特征。后面训练时如果发现模型对某两个类别区分得异常轻松,先别高兴,去翻几张大图看看背景是不是已经把类别写脸上了。
2.2 JSON 标注文件:先确认 36 类映射对不对
资源里说分类个数 36,具体查看 json 文件。这种规模的分类数据集,JSON 一般有两种组织方式:一种是像 COCO 那种几十 MB 的大结构,包含 image_id、category_id、annotations 全套字段;另一种是轻量级类别映射表,每个类别一个 id,对应文件夹名或标签名。这份数据集大概率是轻量映射,毕竟 2500 张图配一个巨型 COCO JSON 属于杀鸡用牛刀,而且 COCO 结构对新手不友好,写脚本时引用路径容易错。读取时最关键的是确认 id 和类别名没有错位,错一位,整个训练就是在给错误标签打工。
我一般会写一小段代码先把映射打出来核对:
import json with open("label_map.json", "r", encoding="utf-8") as f: label_map = json.load(f) # 打印前 40 个映射,确认 36 个类别是否完整 for k, v in list(label_map.items())[:40]: print(k, "->", v)这段代码把 label_map.json 里的键值对逐行打出来,key 是文件夹名或短标签,value 是类别说明。注意读取时强制指定 encoding="utf-8",Windows 默认编码是 gbk,不指定的话中文说明大概率乱码。如果打印结果发现 key 和实际文件夹名对不上,后面训练脚本会以 JSON 为准,导致 Dataset 加载时一个个报「文件不存在」。
我习惯再补一行断言,确认 JSON 的 key 集合和目录名集合完全一致,避免漏掉某个类没看。类名里数字和字母混在一起时,人工一个个比对很容易看花眼,断言让脚本替我盯。参数说明:list(label_map.items()) 取前 40 个是因为 36 类都在这,多取几个只是兜底;如果 JSON 里还存了图片路径等其他字段,items() 会把它们也打出来,看见多余的字段不用慌,筛选出以标签命名的键即可。
2.3 目录结构核对:训练集与测试集是否完整
划分了训练集、测试集,意味着目录应该是 train/ 和 test/ 两层,每个类别一个子文件夹;另一种做法是只有一层图片目录,再用一个额外的 split JSON 记录每张图属于 train 还是 test。这两种方式处理逻辑完全不一样,前者直接按目录读,后者要先读 JSON 再过滤。我建议先写个遍历脚本确认到底属于哪一种:
from pathlib import Path import collections root = Path("sign_language_dataset") for split in ["train", "test"]: split_dir = root / split if not split_dir.exists(): print(f"[缺失] {split} 目录不存在") continue cls_counter = collections.Counter() for sub_dir in split_dir.iterdir(): imgs = list(sub_dir.glob("*.jpeg")) + list(sub_dir.glob("*.jpg")) cls_counter[sub_dir.name] = len(imgs) print(split, "类别数:", len(cls_counter), "总图片数:", sum(cls_counter.values())) empty = [c for c, n in cls_counter.items() if n == 0] if empty: print("空类别:", empty)遍历逻辑很简单:对 train 和 test 分别统计子目录数量和图片总量。图片数之和应该和「约 2500 张」对得上;如果差得远,要么有子目录没遍历到,要么文件名后缀不只有 jpeg。实际调试里,空目录是隐藏最深的坑——分类网络按文件夹数填 num_classes,结果某个类一张图都没有,训练进度条照常跑,但验证时该类 accuracy 永远是 0,不仔细看分类报告根本发现不了。如果目录结构是第二种(图片全在一个文件夹 + split JSON),就把上面代码改成先读 split 文件,再按图片路径分组统计,逻辑是等价的。
提示:统计图片数用 len(list(...)) 会把所有路径先加载进内存,2500 张图完全没问题;如果以后换到几万张的数据集,建议改成 sum(1 for _ in sub_dir.glob("*.jpeg")),内存占用更低。
3. 可视化与数据体检:show 脚本之外的自查清单
3.1 先跑 show 脚本:把数据真正看一遍
数据集的 show 脚本是为可视化设计的,运行前先确认依赖装齐了:matplotlib 和 PIL 缺一不可。脚本默认读当前目录下的图片路径,所以建议在数据根目录执行,别在脚本所在目录执行,否则它会一脸茫然地找图片。直接运行:
python show.py --data_dir ./sign_language_dataset --num_samples 5show 脚本会从每个类别里抽 num_samples 张图拼成网格,方便人眼确认整份数据的长相。为什么要抽 5 张而不是全抽?2500 张图拼成一张大图,单张缩得太小,手指细节和背景噪声全都看不出问题。抽 5 张刚好能看出该类别的姿态多样性:如果 5 张图几乎一模一样,说明数据冗余严重,训练时信息量不够。如果脚本不支持 --num_samples 参数,直接打开 show.py 改里面的变量,通常叫 n_samples 或 num_show,改成 5 再跑。
看网格图时重点看三处:一是手部主体是否占画面主体,裁剪太紧会把手指切掉半截;二是背景是否统一,统一的纯色背景会让模型走捷径,测试时换个环境就失灵;三是同一类别的手势姿态差异大不大,手语字母里很多手势只有指尖方向的区别,如果某类样本姿态单一,模型很容易学成死模板。
3.2 类别分布统计:不平衡问题提前暴露
手语字母里,形状相近的类别(m 和 n、a 和 e、r 和 v)样本数如果不平衡,模型会把少数类硬归到多数类里,因为这样整体 loss 更低。上一章的 Counter 逻辑继续延伸,可以做一个整体分布统计,直接画柱状图:
import collections from pathlib import Path import matplotlib.pyplot as plt img_paths = list(Path("sign_language_dataset").rglob("*.jpeg")) labels = [p.parent.name for p in img_paths] counter = collections.Counter(labels) plt.figure(figsize=(12, 5)) plt.bar(counter.keys(), counter.values()) plt.xticks(rotation=90) plt.ylabel("样本数") plt.tight_layout() plt.savefig("class_distribution.png", dpi=150)这里把所有 jpeg 文件按父目录名分组统计。用 rglob 递归查找,不管目录嵌套几层都能找到。保存成图片而不是终端打印,是因为 36 个类别的数字只印成一列看不出形状;柱状图一眼就能看出哪些类是短板。如果发现某个类只有 20 张,测试集里它可能只有 4 张,模型对它基本靠猜,这时候就要考虑用 WeightedRandomSampler 给少数类加权,或者干脆把这类单独拿出来做二次训练。
3.3 图片完整性检查:损坏文件与异常通道
数据集在传输和解压过程中偶尔会损坏图片,训练时 PIL 读取失败会让整个 DataLoader 随机崩溃。我的习惯是训练前全量读一遍图片文件头,不解码完整图像,速度很快:
from PIL import Image from pathlib import Path bad_files = [] for p in Path("sign_language_dataset").rglob("*.jpeg"): try: with Image.open(p) as im: im.verify() # 只校验文件完整性,不解码像素 except Exception as e: bad_files.append((str(p), str(e))) print("损坏图片数量:", len(bad_files)) for path, err in bad_files[:10]: print(path, err)im.verify() 是 PIL 的轻量校验,只读文件头和数据校验块,比 im.load() 快得多,2500 张图全量 verify 大概几秒。如果有损坏文件,直接删掉或单独移到一个 backup 目录,不要留在训练集里。否则 DataLoader 的 collate 阶段随机报错,而且每次报错的图片都不一样,看起来像内存泄漏,其实是某张坏图在作怪。
提示:verify() 之后想再正常读取同一张图,需要重新 Image.open(),因为 verify 会把图片对象置为不可再读状态。只看完整性时无所谓,但别在 verify 后直接调 transform。
4. 训练手语分类基线:ResNet 迁移学习的参数落地
4.1 数据预处理与增强:不用花哨,但要匹配手语场景
手语图像分类的本质是细粒度手势识别,类别间差异集中在手指形态和朝向上。预处理管线里最重要的不是追最新的图像分类模型,而是把增强策略和手势特点对齐:手部平移、小角度旋转、亮度扰动都有帮助;水平翻转要慎用,因为某些手指字母翻转后语义就变了,或者变成根本不存在的手势。我的默认管线是这样:
# transforms.py 片段 import torchvision.transforms as T train_transform = T.Compose([ T.Resize((224, 224)), T.RandomRotation(degrees=15, fill=0), T.ColorJitter(brightness=0.3, contrast=0.3, saturation=0.2), T.RandomAffine(degrees=0, translate=(0.05, 0.05)), T.ToTensor(), T.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ]) test_transform = T.Compose([ T.Resize((224, 224)), T.ToTensor(), T.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ])这里的 mean/std 是 ImageNet 预训练统计值。如果加载预训练权重,就必须保持一致,否则第一层输出的分布就歪了,迁移学习效果大打折扣。RandomAffine 只做 5% 的平移,不做缩放,原因是手势识别对尺度敏感——训练时缩放会教模型忽略手指的绝对大小,而不同人手指长度差异本来就大,真实使用场景里摄像头距离一变,手势尺度就变,过度缩放反而损伤泛化。
参数说明:degrees=15 是允许图片正负 15 度旋转,手腕自然倾斜通常不超过这个范围;translate=(0.05, 0.05) 是宽和高各最多平移 5%,模拟手没完全居中的情况。ColorJitter 的三个值分别控制亮度、对比度、饱和度的随机扰动幅度,0.2 到 0.3 是折中选择,太大会让手部肤色变得不像肤色。测试阶段不做任何随机增强,保证评估结果稳定。
4.2 模型与训练参数:ResNet18 起步,别一上来就 ResNet50
2500 张的小数据集、36 分类,我建议用 ResNet18 预训练权重起步。理由很实际:数据量小,ResNet50 的参数量是 ResNet18 的几倍,在 2500 张图上更容易过拟合;手语分类的难点在局部细节,ResNet18 的层数已经够用,训练速度快一倍,迭代验证想法更方便。给一段可直接跑的 PyTorch 核心代码:
import torch import torch.nn as nn import torchvision.models as models num_classes = 36 device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1) model.fc = nn.Linear(model.fc.in_features, num_classes) model = model.to(device) criterion = nn.CrossEntropyLoss() optimizer = torch.optim.SGD( model.parameters(), lr=0.005, momentum=0.9, weight_decay=1e-4 ) scheduler = torch.optim.lr_scheduler.StepLR( optimizer, step_size=8, gamma=0.5 )关于优化器的选择:小数据集上用 SGD 加 momentum 通常比 Adam 更稳。Adam 前期收敛快,但后期在细粒度分类上容易欠拟合,换成 SGD 微调后测试准确率通常能再涨两三个点。学习率 0.005 是针对全模型微调设的;如果只训练最后一层 fc,lr 可以放到 0.01;如果从头训练整个网络,lr 要降到 0.001。weight_decay=1e-4 是这个任务里不容易翻车的默认值。
epoch 我一般设 30,配合 StepLR 在 8、16、24 轮各降一半学习率。如果你用预训练权重做微调,通常第 15 轮前后验证集就差不多稳定了;如果到 25 轮还在涨,说明学习率衰减太慢或数据增强不够。这里没有银弹,但从 loss 曲线可以判断:训练 loss 下降而验证 loss 回升,就是过拟合信号,需要回调增强强度。
我整理了一份常用参数表,直接抄作业也行:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| 输入尺寸 | 224 x 224 | 与 ImageNet 预训练权重匹配 |
| batch_size | 32 | 显存不够就降到 16 |
| 初始学习率 | 0.005 | 全模型微调;只调 fc 用 0.01 |
| 学习率衰减 | step=8, gamma=0.5 | 每 8 轮减半 |
| weight_decay | 1e-4 | 防止权重过大,配合 BN 层 |
| 训练轮数 | 30 | 小数据集足够收敛 |
4.3 评估:混淆矩阵告诉你错在哪一类
只看 accuracy 不够。手语 36 类里,形状相近的字母经常被互相误判,混淆矩阵能直接告诉你模型把哪两类搞混了。用 sklearn 生成并保存:
from sklearn.metrics import confusion_matrix, classification_report import matplotlib.pyplot as plt import numpy as np # y_true 为测试集真实标签,y_pred 为模型预测结果,都是 0~35 的类别索引 cm = confusion_matrix(y_true, y_pred) report = classification_report(y_true, y_pred, target_names=class_names) plt.figure(figsize=(18, 14)) plt.imshow(cm, cmap="Blues") plt.colorbar() plt.xticks(range(num_classes), class_names, rotation=90) plt.yticks(range(num_classes), class_names) plt.xlabel("预测类别") plt.ylabel("真实类别") plt.savefig("confusion_matrix.png", dpi=150) with open("classification_report.txt", "w", encoding="utf-8") as f: f.write(report)混淆矩阵建议按行归一化再画,否则样本多的类别错误数量大,色块深,会掩盖少数类的真实错误率。归一化逻辑是 cm 的每一行除以该行总数,画出来的是比例而非绝对数量。classification_report 里的 macro avg 比 accuracy 更值得关注,它把 36 类一视同仁,不会因为某几个多数类表现好就掩盖整体问题。
提示:保存报告强制用 utf-8 编码。Windows 下不指定默认写 gbk,中文类别名打开全是乱码,白跑一趟。
5. 手语分类实战避坑:五个我踩过的真实问题
5.1 训练阶段的三个坑
现象:训练准确率 95%,测试准确率只有 60%。
原因:典型过拟合,小数据集上非常常见。模型把训练图里的背景纹路、光照分布都记住了,手势本身的判别特征反而没学到。
解决:三招同时做。第一,增强强度提到 RandomRotation(degrees=20),ColorJitter 再加一点;第二,fc 层后面加 Dropout(0.3),ResNet 本身有 BN 层,但最后的全连接层是轻量决策层,容易被少数样本带偏;第三,学习率从 0.005 降到 0.002,让模型收敛得更细。我在类似规模的手语数据集上试过,这套组合能把测试准确率拉回 80% 以上。
现象:DataLoader 报错,提示 "Error loading image",每次报错的图片都不一样。
原因:数据集里有一两张损坏图片,PIL 读取失败。报错随机是因为 DataLoader 开了多进程,每个 worker 轮流取数据,谁先撞上坏图谁先抛异常。
解决:用第 3.3 节的 im.verify() 全量扫一遍,把坏图隔离出去。另外注意文件名编码:图片名里有中文或特殊字符时,Windows 下 Path.glob 可能匹配不到,统一重命名为纯英文编号最省事。
现象:训练时 GPU 占用率不高,但 loss 迟迟不降。
原因:小数据集上常见误区是数据加载卡了瓶颈而不是模型问题。手语图片分辨率差异大,Resize 到 224 之前就加载原图,内存带宽被白白消耗;也可能是 DataLoader 的 num_workers 设成 0,所有预处理都在主线程跑,GPU 一直等数据。
解决:DataLoader 里设置 num_workers=4 或 8,pin_memory=True(GPU 训练时)。同时把 transform 改成先 Resize 到 256x256 再 RandomCrop 到 224x224,做一次粗缩放,减少后续运算量。这个改动通常能把训练速度拉高一倍。
5.2 数据与划分的两个坑
现象:JSON 里显示 36 个类别,训练脚本读出来只有 34 个。
原因:两个类别的目录是空的,或者目录里只有隐藏文件,rglob 匹配图片时把空目录漏掉了。于是 num_classes 明明写 36,实际参与训练的只有 34 类。
解决:训练脚本开头加一个启动断言,遍历所有类别目录并检查图片数,图片数为 0 直接抛异常并打印目录名,而不是静默跳过。这样数据问题会在训练第一步暴露,而不是在 eval 阶段发现某类准确率为 0 再回头查数据。
现象:模型在验证集上对 bot 视角的准确率特别差,其他视角正常。
原因:划分训练测试时用了随机划分,同一段视频的连续帧同时进了两个集合。模型等于已经见过这个场景的背景和光线,测试准确率被高估;反过来如果某个视角只在测试集出现,准确率就会暴跌。
解决:按文件名里的视角字段分组划分,同一视角的图只进训练集或只进测试集。这就是第 2.1 节命名规律的实际应用——数据集已经把视角和片段编码在文件名里,划分时把它们当作一个整体,而不是逐张随机抽。这会让准确率数字变难看一点,但那是真实泛化水平,不是自欺欺人。
6. 验证与进阶:从「能跑」到「可信」的两步走
6.1 冒烟测试:先证明管线能跑
拿到数据集配一套训练代码后,别直接全量跑 30 个 epoch。我的习惯是先做冒烟测试:随机挑每个类别 10% 的样本,batch_size 拉大,只跑 2 个 epoch。目标不是看准确率,而是确认三件事:loss 在下降且没有出现 NaN、验证集能正常算指标、checkpoint 保存后能恢复。这一步十分钟内跑完,能筛掉 80% 的配置错误,比如类别数对不上、图片读不了、设备显存不够。冒烟测试过了,再放心跑全量训练。
6.2 进阶:从分类走向检索与检测
如果这个分类数据集跑明白了,下一步可以把手势识别做成更实用的系统。一种做法是把 ResNet 倒数第二层的高维特征抽出来做手势检索,测试集里传一张新图,返回最相似的几张,这在教学场景里比硬分类更友好。另一种做法是升级成检测任务:用 YOLO 系列训练自己的数据集,框出手部区域再做分类。但要注意,这份数据集的 JSON 是分类标签不是检测框标注,直接拿来喂 YOLO 不够,需要先想办法把裁剪框的位置还原成原始画面的标注,或者重新标注一批数据。
我从那次被坏图和错位 JSON 坑掉一整天之后,养成了一个死习惯:每次拿到新的图像分类数据集,都强制走一遍「命名拆解 → 类别核对 → 完整性扫描 → 冒烟测试」四步流程,全部通过才开始调参。这份手语数据集本身质量不错,命名规律完整,训练测试划分也做了,省了很多事,但任何数据集都经不起「默认它完美」这四个字。按上面的流程过一遍,你踩的坑会比我当年少得多,希望帮到你。
本文还有配套的精品资源,点击获取