
简介这是一份面向图像分类学习与手语识别研究的聋哑人手语词汇图像分类数据集。数据已标注共包含bad、good、friend等11个类别约1100张真实手语词汇图像样本并预先拆分为训练集与测试集便于直接用于卷积神经网络模型的训练与验证适合计算机视觉初学者及从事手语识别相关任务的研究者参考使用。压缩包内共1108个文件其中包含1106张jpg格式图片、1个json标注文件和1个Python可视化脚本json文件用于查看完整类别映射脚本可快速浏览样本与其标签对应情况。整体资源包大小23.62MB体积适中轻量易用目前已有107人学习下载。通过该数据集使用者能够完整走通图像分类的读取、训练、测试与可视化流程也可结合作者给出的分类网络改进方案进一步对模型结构或训练策略进行优化有效提升分类准确率。1. 手语图像分类数据集1,100 张图解决 11 类词汇识别做手语识别项目卡点通常不在模型而在数据。公开的手势数据集要么类别太大要么标注语言不一致想快速验证一个图像分类基线并不容易。这套聋哑人手语词汇图像分类数据集正好把这件事简化到可以直接开工11 个类别bad、good、friend 等日常手语词汇大约 1,100 张已标注图片训练集和测试集划分完毕还附带一个可视化 show 脚本和数据说明 json 文件。对刚接触 CNN 图像分类的开发者来说它是走通“数据标注整理 → 数据加载 → 模型训练 → 测试验证”全流程的最小闭环对做手势识别或听障辅助应用的人来说它可以作为早期基线和功能验证的数据来源省掉自己爬图、清洗、标类的时间。整体规模不大但足够把一个分类任务从零跑到有结论。2. 数据集结构与 json 标注先看懂 bad、good、friend 的分类组织2.1 解压后先看结构目录与文件各就各位拿到压缩包我一般不会直接跑脚本而是先看一眼目录长什么样。这个数据集的图片命名比较特殊比如my (2).jpg、me (76).jpg这种带空格和括号的文件名说明它不是某个工具批量导出的标准命名而是人工收集后直接丢进来的。用 tree 命令扫一遍最直观# 在数据集根目录执行避免递归太深 tree -L 2 .常见的组织方式是这样. ├── train │ ├── bad │ ├── good │ ├── friend │ └── ... ├── test │ ├── bad │ ├── good │ ├── friend │ └── ... ├── show.py ├── data.json └── show.jpg注意两个细节。第一train和test下面按类别建了同名子目录同一类图片放进同一个文件夹这种结构对 PyTorch 的ImageFolder和自定义Dataset都是直接可用的你不需要额外写映射关系。第二show.jpg大概率是数据提供方贴出来的一张预览图给作者博客用的你训练时不参与任何流程不用管它的具体内容。真正决定数据边界的是data.json和两个类别目录。文件名里的空格和括号是第一个要留意的坑。如果你在 shell 里写for f in train/bad/*.jpg带空格的路径会被拆成两段在 zsh 里括号还有可能触发通配符展开。最稳妥的方式是全程用 Python 的pathlib或os.listdir去遍历不要在 shell 层面做批量操作。后面所有脚本我都会避开直接拼接 shell 命令。2.2 json 里有什么类别名与划分关系的正确打开方式摘要里说“分类个数 11bad、good、friend 等【具体查看 json 文件】”所以data.json是这份资源的“地图”训练前必须先把它的结构读明白。虽然每份资源的 json 字段不完全一样但常见做法是保存类别清单、类别到数字 ID 的映射以及训练集和测试集各自的文件列表{ class_names: [bad, good, friend], num_classes: 11, class_to_idx: { bad: 0, good: 1, friend: 2 }, train: { bad: [my (1).jpg, my (2).jpg, my (105).jpg], good: [my (3).jpg, my (106).jpg] }, test: { bad: [me (71).jpg, me (72).jpg], good: [me (75).jpg, me (76).jpg] } }上面这段是我按这类小数据集最常见的写法补的示例具体字段名要以你解压后看到的实际 json 为准。解读的重点有三个class_names决定类别顺序class_to_idx决定训练时 label 的整数编号train和test两个部分决定哪些图片进入哪个集合。这三个信息必须对齐否则后面加载数据时很容易出现“目录里有图片但 json 没登记”或者“类别编号错位”的问题。关于 11 个类别目前能确认的是 bad、good、friend 这三个单词的拼接含义。我建议你先在 json 里把class_names完整打印出来再对着train目录里的子文件夹核对一遍。这类手语词汇数据集的类别通常是一组高频日常词比如问候类、评价类、关系类具体语义可以从my和me两组前缀推测但别脑补以 json 实际内容为准。数据标注质量决定模型上限如果发现某些类别语义相近比如 bad 和某个负面词训练时就要特别关注混淆矩阵。2.3 先做一次体检每类样本量统计在训练之前我习惯先统计每个类别的图片数量。这个小步骤能提前暴露类别不平衡问题避免模型训练完成后准确率虚高。用 Python 遍历类别目录即可import os from collections import Counter train_root train counter Counter() for class_name in sorted(os.listdir(train_root)): class_dir os.path.join(train_root, class_name) if not os.path.isdir(class_dir): continue n len([f for f in os.listdir(class_dir) if f.lower().endswith((.jpg, .jpeg, .png))]) counter[class_name] n print(f{class_name}: {n} 张) print(合计:, sum(counter.values()))这段代码的逻辑很直接遍历train下的每个子目录统计扩展名为 jpg、jpeg、png 的文件数量。注意我加了isdir判断因为目录里如果有杂散文件会被过滤掉。跑完你会看到每类图片数目的分布如果某个类别只有 40 张另一个有 150 张就意味着模型会对样本多的类别偏置。后续做数据增强或者类别加权时这份统计就是你调整的依据。这套数据集总共约 1,100 张、11 个类别平均每类 100 张上下算是小样本分类的典型场景如果某几类明显偏少训练时就要盯紧它们的召回率。3. 可视化与数据加载show 脚本和自定义 DataLoader 的两种姿势3.1 运行 show 脚本用眼睛确认标注质量数据集的说明里提到“如果想可视化数据集可以运行资源中的 show 脚本”。这个脚本的核心用途就是帮你用眼睛检查图片内容和标注是不是一致。手语词汇图像很容易出现歧义不同标注者对着同一个手势可能给出不同的语义或者某张图背景太复杂导致模型学不到手势本身。训练前把样本可视化一遍比训完再翻车要划算得多。常见的 show 脚本实现思路是随机从每个类别抽 1 到 2 张图用 matplotlib 拼成网格展示图片上方标注类别名。它的依赖一般是numpy、matplotlib有些实现还会用到opencv-python读取图片。运行方式很简单python show.py如果报ModuleNotFoundError: No module named cv2说明缺少 OpenCV 绑定安装后重跑就行pip install opencv-python脚本本身不涉及训练逻辑它的价值在于给你一个“肉眼基线”确认 bad 这一类里放的是不是都是同一个手势确认 friend 和其他类之间有没有视觉上很接近的样本。这一步做完你对数据的信心会提高很多。需要注意如果你看到某张图旋转了 90 度或者被裁掉了关键区域不要急着在训练时靠翻转强行修正先检查这张图是不是本身就拍歪了。3.2 自定义 Dataset把图片路径与标签稳定对齐可视化确认之后下一步是把数据送进模型。虽然这个数据集可以直接用ImageFolder但我更推荐先写一个自定义Dataset因为文件名里有空格和括号你可以在加载层就把这些问题处理干净而且自定义代码对后续做类别过滤、样本加权、调试都更可控import os import torch from torch.utils.data import Dataset from PIL import Image class SignLanguageDataset(Dataset): def __init__(self, root_dir, transformNone): self.samples [] # 每个元素是 (图片绝对路径, label整数) self.transform transform self.classes sorted(os.listdir(root_dir)) self.class_to_idx {c: i for i, c in enumerate(self.classes)} for class_name in self.classes: class_dir os.path.join(root_dir, class_name) if not os.path.isdir(class_dir): continue for fname in os.listdir(class_dir): if fname.lower().endswith((.jpg, .jpeg, .png)): path os.path.join(class_dir, fname) self.samples.append((path, self.class_to_idx[class_name])) def __len__(self): return len(self.samples) def __getitem__(self, idx): path, label self.samples[idx] img Image.open(path).convert(RGB) if self.transform is not None: img self.transform(img) return img, label这段代码有几个关键点。self.classes sorted(...)是为了保证类别顺序稳定避免不同机器上os.listdir返回顺序不一致导致 label 错位每次__getitem__都重新打开图片而不是在初始化时全部读进内存是因为 1,100 张图全读进来大概几百 MB内存小的机器会扛不住按需读取更稳convert(RGB)会把灰度图或带透明通道的 PNG 统一转成三通道避免后面网络输入维度对不上。初始化时传入的transform建议至少包含Resize和ToTensor。先定一个统一尺寸比如 128×128 或 224×224具体看你的显存和训练速度需求第 4 章会展开讲参数怎么配。3.3 ImageFolder 与自定义 Dataset 怎么选torchvision.datasets.ImageFolder是这个数据集结构的天然匹配者它要求根目录下每个子文件夹代表一个类别而这套数据集的train和test恰好就是这么组织的。用ImageFolder的代码量更少还能直接拿到dataset.classes和dataset.class_to_idxfrom torchvision import datasets train_dataset datasets.ImageFolder(train, transformtrain_transform)那为什么还要自定义Dataset两者的差别在下表里对比维度ImageFolder自定义 Dataset代码量极少中等类别顺序按目录名排序自动生成由你控制排序逻辑文件名异常不做处理可以在加载层过滤或重命名类别加权需要额外写采样器可以直接在样本列表上过滤调试灵活性低高我的建议是第一次跑通流程用ImageFolder省时间进入调优阶段尤其是发现某个类别总是混淆时换成自定义Dataset方便你只加载特定类别的样本做诊断。两个方案不冲突数据集的目录结构对它们都很友好。4. 训练一个 CNN 分类器网络选型、超参设置与收敛判断4.1 数据量只有 1,100 张为什么先上小型 CNN1,100 张图、11 个类别平均每类 100 张这个规模和 CIFAR-10 的单类样本量一个量级。在这种数据量下直接上 ResNet50 这类深层网络很容易过拟合——参数太多数据喂不饱训练集 loss 一路降到接近 0测试集精度却上不去。常见做法是先用一个三层卷积的小型 CNN 跑基线把流程走通拿到一个可靠的准确率数字再去考虑是否换成预训练模型。三层卷积的参数量大概在几十万量级适合小数据集。结构选择上我用的是“卷积 批归一化 ReLU 最大池化”堆叠三组最后接全局平均池化和全连接层层输出尺寸说明输入3 × 128 × 128RGB 图统一 resize 到 128×128Conv2d BN ReLU32 × 128 × 1283×3 卷积padding1保持尺寸MaxPool2d32 × 64 × 642×2 池化尺寸减半Conv2d BN ReLU64 × 64 × 64通道翻倍提取更高阶特征MaxPool2d64 × 32 × 32尺寸减半Conv2d BN ReLU128 × 32 × 32通道再翻倍MaxPool2d128 × 16 × 16尺寸减半AdaptiveAvgPool2d128 × 1 × 1全局池化不依赖输入尺寸Linear11输出类别数批归一化放在卷积和 ReLU 之间作用是稳定训练。全局平均池化代替传统的 Flatten 全连接能减少参数量并让网络对输入尺寸更宽容。这样设计即使某张图 reszie 到 128×128 时比例有点变形模型也能正常工作。4.2 编写训练脚本从数据加载到保存最优模型网络定义用 PyTorch 写出来并不复杂import torch import torch.nn as nn class SignNet(nn.Module): def __init__(self, num_classes11): super().__init__() self.features nn.Sequential( nn.Conv2d(3, 32, 3, padding1), nn.BatchNorm2d(32), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), nn.Conv2d(32, 64, 3, padding1), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), nn.Conv2d(64, 128, 3, padding1), nn.BatchNorm2d(128), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), ) self.classifier nn.Sequential( nn.AdaptiveAvgPool2d(1), nn.Flatten(), nn.Linear(128, num_classes), ) def forward(self, x): return self.classifier(self.features(x))训练循环配合数据加载器一起写from torch.utils.data import DataLoader from torchvision import transforms transform transforms.Compose([ transforms.Resize((128, 128)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) train_dataset SignLanguageDataset(train, transformtransform) test_dataset SignLanguageDataset(test, transformtransform) train_loader DataLoader(train_dataset, batch_size32, shuffleTrue, num_workers2) test_loader DataLoader(test_dataset, batch_size32, shuffleFalse, num_workers2) model SignNet(num_classes11) criterion nn.CrossEntropyLoss() optimizer torch.optim.Adam(model.parameters(), lr1e-3, weight_decay1e-4) scheduler torch.optim.lr_scheduler.StepLR(optimizer, step_size20, gamma0.5) best_acc 0.0 for epoch in range(50): model.train() running_loss 0.0 for images, labels in train_loader: optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() * images.size(0) # 每个 epoch 结束在测试集上验证一次 model.eval() correct 0 total 0 with torch.no_grad(): for images, labels in test_loader: outputs model(images) _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() acc correct / total if acc best_acc: best_acc acc torch.save(model.state_dict(), best_model.pth) print(fepoch {epoch1}: loss{running_loss/len(train_dataset):.4f}, acc{acc:.4f})参数选择的逻辑batch_size32在 1,100 张图上够用显存占用小lr1e-3是 Adam 的默认推荐值配合StepLR每 20 个 epoch 把学习率减半让后期收敛更稳weight_decay1e-4做 L2 正则缓解小数据的过拟合。保存模型时只存state_dict而不是整个模型对象这是好习惯——换机器或换 Python 版本时不容易因为类定义路径不同而反序列化失败。4.3 怎么判断收敛loss 曲线的三个信号训练跑起来后最关键的是看 loss 和准确率的变化趋势而不是死等 50 个 epoch 跑完。我一般每 5 个 epoch 输出一次记录重点盯三个信号第一训练 loss 稳步下降测试 acc 同步上升这是正常收敛。第二训练 loss 降得很低但测试 acc 停滞或下降说明过拟合开始这时候要停训练不要等它继续恶化。第三训练 loss 一直在 2.4 左右约等于 log11不下降说明模型没学到有效特征优先检查数据加载顺序和图片内容有没有问题而不是调学习率。这套数据集因为类别多、样本少过拟合是常态而不是例外。如果你发现测试 acc 在第 20 个 epoch 左右达到峰值后开始下降说明已经找到了最优模型torch.save里记录的best_acc就是你该信任的数字。5. 常见问题与避坑五个让训练翻车的细节5.1 cv2.imread 读图片返回 None现象数据能加载但训练到一半出现TypeError: NoneType object is not subscriptable或者图片张量里全是 0。原因图片路径包含空格或括号cv2.imread对这类路径的处理不严格更隐蔽的原因是某些图片本身编码损坏OpenCV 静默返回 None 而 Pillow 可能抛异常。解决统一用 PIL 读取Image.open在Dataset.__getitem__里加一层校验。PIL 读不出来就跳过或打印路径不要让它混进训练集。这个数据集里my (105).jpg这类带空格的路径是最常见的触发点在加载层做防御式检查比在训练层排查省力得多。5.2 图像尺寸不一致导致 batch 拼接报错现象RuntimeError: stack expects each tensor to be equal size发生在 DataLoader 取 batch 的时候。原因数据集中图片原始分辨率不一有些是 720×1280有些是 480×640。如果 transform 里漏了Resize加载出来的 tensor 尺寸不同torch.stack直接崩。解决transforms.Compose里Resize((128, 128))必须放在ToTensor()之前。另外注意Resize的参数是 (宽, 高)别写反。写成Resize((128, 128))正方形没问题但如果是Resize(128)它只会把短边缩放到 128长边按比例缩放结果尺寸还是不一致。5.3 类别不平衡导致准确率虚高现象测试 acc 到了 85%但看每个类别的召回率发现样本多的三四个类接近 100%样本少的类只有 50%。原因模型偏向样本量大的类别整体准确率被多数类拉高掩盖了少数类失效。解决训练前跑一遍 2.3 节的统计脚本如果发现最大类和最小类数量差距超过 2 倍就用WeightedRandomSampler或者在后处理时看混淆矩阵。最简单的方法是先不处理训练完打印每类的 precision 和 recall确认问题真实存在后再上采样少数类。别只看整体 acc 就下结论这是新手最容易翻车的地方。5.4 show 脚本报缺依赖或中文路径乱码现象运行python show.py报ModuleNotFoundError: No module named matplotlib或者图片标题里的类别名显示成一堆方块。原因环境里缺可视化库系统字体不支持中文或某些特殊字符。解决缺依赖就pip install matplotlib。类名是英文单词bad、good、friend一般不会乱码如果 json 里的类名包含非英文字符给 matplotlib 指定中文字体路径。更省事的方案是不依赖系统字体直接用plt.text打印英文标签。5.5 反复在训练集上验证把训练 acc 当成绩现象训练 50 个 epoch 后 acc 高达 98%提交结果时测试集 acc 只有 60%差距巨大。原因验证集合用错。这个数据集已经划分了train和test但有些同学为了省事拿训练集的前 20% 当验证集甚至直接在训练集上算准确率。解决严格遵守自带划分test目录里的图片只能用于最终评估。调参阶段的验证集可以从train里再切出 10%但最终汇报必须用test。这套数据集的价值就在于划分已经替你做好不需要自己重复造轮子。6. 进阶验证用数据增强和迁移学习榨出更多精度小型 CNN 跑通之后如果想进一步提升测试集准确率我建议从两个方向入手。第一个是数据增强第二个是迁移学习两者可以叠加。数据增强的配置很直接train_transform transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(p0.3), transforms.RandomRotation(10), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.2), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ])翻转概率设在 0.3 而不是 0.5是因为手语手势里左右手含义可能不同过度翻转反而制造错误样本旋转角度限制在 10 度以内避免手势语义被破坏。颜色抖动对手势识别有正收益因为不同拍摄环境的光照差异很大。迁移学习也是性价比很高的路径用 PyTorch 自带预训练权重把最后一层全连接换成 11 类输出即可from torchvision import models model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) model.fc torch.nn.Linear(model.fc.in_features, 11)训练时把学习率调低到1e-4只训练fc层先跑 10 个 epoch再解冻全部参数微调。这类 Transformer 图像分类模型在小数据集上容易过拟合1,100 张图喂不饱 ViT 级别的参数量ResNet18 是更稳妥的上限。从那以后我每次拿到新数据集都会强制走一遍“结构检查 → 类别统计 → show 脚本可视化 → 单批过拟合 → 测试集验证”五步流程再决定要不要上迁移学习这套流程帮我挡掉了至少一半的翻车现场。希望帮到你。本文还有配套的精品资源点击获取