
简介面向手语识别与图像分类任务内含覆盖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类别标签这里表示字母 gbotbottom 视角说明数据来自多视角采集不同角度拍同一手势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, encodingutf-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 是类别说明。注意读取时强制指定 encodingutf-8Windows 默认编码是 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(rotation90) plt.ylabel(样本数) plt.tight_layout() plt.savefig(class_distribution.png, dpi150)这里把所有 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(degrees15, fill0), T.ColorJitter(brightness0.3, contrast0.3, saturation0.2), T.RandomAffine(degrees0, 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% 的平移不做缩放原因是手势识别对尺度敏感——训练时缩放会教模型忽略手指的绝对大小而不同人手指长度差异本来就大真实使用场景里摄像头距离一变手势尺度就变过度缩放反而损伤泛化。参数说明degrees15 是允许图片正负 15 度旋转手腕自然倾斜通常不超过这个范围translate(0.05, 0.05) 是宽和高各最多平移 5%模拟手没完全居中的情况。ColorJitter 的三个值分别控制亮度、对比度、饱和度的随机扰动幅度0.2 到 0.3 是折中选择太大会让手部肤色变得不像肤色。测试阶段不做任何随机增强保证评估结果稳定。4.2 模型与训练参数ResNet18 起步别一上来就 ResNet502500 张的小数据集、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(weightsmodels.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(), lr0.005, momentum0.9, weight_decay1e-4 ) scheduler torch.optim.lr_scheduler.StepLR( optimizer, step_size8, gamma0.5 )关于优化器的选择小数据集上用 SGD 加 momentum 通常比 Adam 更稳。Adam 前期收敛快但后期在细粒度分类上容易欠拟合换成 SGD 微调后测试准确率通常能再涨两三个点。学习率 0.005 是针对全模型微调设的如果只训练最后一层 fclr 可以放到 0.01如果从头训练整个网络lr 要降到 0.001。weight_decay1e-4 是这个任务里不容易翻车的默认值。epoch 我一般设 30配合 StepLR 在 8、16、24 轮各降一半学习率。如果你用预训练权重做微调通常第 15 轮前后验证集就差不多稳定了如果到 25 轮还在涨说明学习率衰减太慢或数据增强不够。这里没有银弹但从 loss 曲线可以判断训练 loss 下降而验证 loss 回升就是过拟合信号需要回调增强强度。我整理了一份常用参数表直接抄作业也行参数推荐值说明输入尺寸224 x 224与 ImageNet 预训练权重匹配batch_size32显存不够就降到 16初始学习率0.005全模型微调只调 fc 用 0.01学习率衰减step8, gamma0.5每 8 轮减半weight_decay1e-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_namesclass_names) plt.figure(figsize(18, 14)) plt.imshow(cm, cmapBlues) plt.colorbar() plt.xticks(range(num_classes), class_names, rotation90) plt.yticks(range(num_classes), class_names) plt.xlabel(预测类别) plt.ylabel(真实类别) plt.savefig(confusion_matrix.png, dpi150) with open(classification_report.txt, w, encodingutf-8) as f: f.write(report)混淆矩阵建议按行归一化再画否则样本多的类别错误数量大色块深会掩盖少数类的真实错误率。归一化逻辑是 cm 的每一行除以该行总数画出来的是比例而非绝对数量。classification_report 里的 macro avg 比 accuracy 更值得关注它把 36 类一视同仁不会因为某几个多数类表现好就掩盖整体问题。提示保存报告强制用 utf-8 编码。Windows 下不指定默认写 gbk中文类别名打开全是乱码白跑一趟。5. 手语分类实战避坑五个我踩过的真实问题5.1 训练阶段的三个坑现象训练准确率 95%测试准确率只有 60%。原因典型过拟合小数据集上非常常见。模型把训练图里的背景纹路、光照分布都记住了手势本身的判别特征反而没学到。解决三招同时做。第一增强强度提到 RandomRotation(degrees20)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_workers4 或 8pin_memoryTrueGPU 训练时。同时把 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 坑掉一整天之后养成了一个死习惯每次拿到新的图像分类数据集都强制走一遍「命名拆解 → 类别核对 → 完整性扫描 → 冒烟测试」四步流程全部通过才开始调参。这份手语数据集本身质量不错命名规律完整训练测试划分也做了省了很多事但任何数据集都经不起「默认它完美」这四个字。按上面的流程过一遍你踩的坑会比我当年少得多希望帮到你。本文还有配套的精品资源点击获取