☰
手语图像分类数据集实战:从数据加载到CNN训练全流程
2026/10/1 3:51:57 网站建设 项目流程

简介:这是一份面向图像分类学习与手语识别研究的聋哑人手语词汇图像分类数据集。数据已标注,共包含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 里有什么:类别名与划分关系的正确打开方式

摘要里说“分类个数 11:bad、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, transform=None): 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_idx:

from torchvision import datasets train_dataset = datasets.ImageFolder("train", transform=train_transform)

那为什么还要自定义Dataset?两者的差别在下表里:

对比维度ImageFolder自定义 Dataset
代码量极少中等
类别顺序按目录名排序,自动生成由你控制排序逻辑
文件名异常不做处理可以在加载层过滤或重命名
类别加权需要额外写采样器可以直接在样本列表上过滤
调试灵活性低高

我的建议是:第一次跑通流程用ImageFolder,省时间;进入调优阶段,尤其是发现某个类别总是混淆时,换成自定义Dataset,方便你只加载特定类别的样本做诊断。两个方案不冲突,数据集的目录结构对它们都很友好。

4. 训练一个 CNN 分类器:网络选型、超参设置与收敛判断

4.1 数据量只有 1,100 张:为什么先上小型 CNN

1,100 张图、11 个类别,平均每类 100 张,这个规模和 CIFAR-10 的单类样本量一个量级。在这种数据量下,直接上 ResNet50 这类深层网络很容易过拟合——参数太多,数据喂不饱,训练集 loss 一路降到接近 0,测试集精度却上不去。常见做法是先用一个三层卷积的小型 CNN 跑基线,把流程走通,拿到一个可靠的准确率数字,再去考虑是否换成预训练模型。

三层卷积的参数量大概在几十万量级,适合小数据集。结构选择上,我用的是“卷积 + 批归一化 + ReLU + 最大池化”堆叠三组,最后接全局平均池化和全连接层:

层输出尺寸说明
输入3 × 128 × 128RGB 图,统一 resize 到 128×128
Conv2d + BN + ReLU32 × 128 × 1283×3 卷积,padding=1,保持尺寸
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_classes=11): super().__init__() self.features = nn.Sequential( nn.Conv2d(3, 32, 3, padding=1), nn.BatchNorm2d(32), nn.ReLU(inplace=True), nn.MaxPool2d(2), nn.Conv2d(32, 64, 3, padding=1), nn.BatchNorm2d(64), nn.ReLU(inplace=True), nn.MaxPool2d(2), nn.Conv2d(64, 128, 3, padding=1), nn.BatchNorm2d(128), nn.ReLU(inplace=True), 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", transform=transform) test_dataset = SignLanguageDataset("test", transform=transform) train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True, num_workers=2) test_loader = DataLoader(test_dataset, batch_size=32, shuffle=False, num_workers=2) model = SignNet(num_classes=11) criterion = nn.CrossEntropyLoss() optimizer = torch.optim.Adam(model.parameters(), lr=1e-3, weight_decay=1e-4) scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=20, gamma=0.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(f"epoch {epoch+1}: loss={running_loss/len(train_dataset):.4f}, acc={acc:.4f}")

参数选择的逻辑:batch_size=32在 1,100 张图上够用,显存占用小;lr=1e-3是 Adam 的默认推荐值,配合StepLR每 20 个 epoch 把学习率减半,让后期收敛更稳;weight_decay=1e-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(p=0.3), transforms.RandomRotation(10), transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.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(weights=models.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 脚本可视化 → 单批过拟合 → 测试集验证”五步流程,再决定要不要上迁移学习,这套流程帮我挡掉了至少一半的翻车现场。希望帮到你。

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

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

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

立即咨询