☰
手语词汇图像分类数据集:从数据加载到PyTorch训练全流程
2026/10/7 6:10:28 网站建设 项目流程

简介:聋哑人手语词汇图像分类数据集面向计算机视觉初学者、算法研究人员及特殊教育应用开发者,提供约1100张已标注图片,覆盖bad、good、friend等11个常见生活词汇类别,类别标签统一记录在JSON文件中,可直接用于图像分类、手势识别等模型的训练与效果验证。资源包共1108个文件,核心为1106张JPG图片,按训练集和测试集分目录存放,另附1个show.py可视化脚本和1个类别标注JSON文件;整包仅23.62MB,轻量易取用。读者既可以运行show脚本快速浏览各类别样本,也能基于已成体系的训练/测试划分直接开展CNN等网络训练与改进实验,作者还提供了相关分类网络改进系列文章可对照学习。目前已有108人学习浏览,适合作为图像分类入门练习、课程设计或小规模手势识别项目的基础数据。

1. 一个能直接跑分类流程的手语词汇数据集:先看两面再动手

手语词汇图像分类是个典型的入门级图像分类任务,但最劝退人的往往不是模型,而是数据。自建数据集要拍摄、标注、切片,动不动耗掉两三天,还没开始训练就先“翻车”。如果你正在做图像分类课设、手语识别项目,或者只是想快速验证一套 CNN 分类流程,这个约 1,100 张、已标注、划分好训练集和测试集的手语词汇数据集,能直接跳过数据准备阶段。它有 11 个分类,像 bad、good、friend 这些常用词,标注信息放在 json 文件里,还自带一个可视化脚本 show,跑一遍就能看到每张图和标签是否对齐。适合新手快速上手 PyTorch 分类全流程,也适合熟手拿来做数据质量分析和模型对比的基线数据。

2. 数据集的真实结构:json 命名、目录划分与图像质量先摸清

2.1 先看 json 和目录:类别映射与文件命名规则

拿到数据集后第一件事不是训练,而是打开 json 文件看类别映射。这个数据集的 11 个分类包含 bad、good、friend 等,具体类别名以 json 里的classes字段为准。我一般会先写个脚本把映射关系打印出来,确认类别名和索引的对应顺序,因为后面训练时ClassToIndex的顺序错一位,整个评估结果就全乱了。

查看类别映射的推荐做法:

import json with open('class_indices.json', 'r', encoding='utf-8') as f: class_dict = json.load(f) print(type(class_dict)) for k, v in class_dict.items(): print(k, '->', v)

这段代码的核心逻辑是把 json 读成字典后逐个打印键值对。参数说明:encoding='utf-8'是必须的,因为有些 json 里的类别名是中文,不加这个参数在 Windows 下会直接报UnicodeDecodeError。打印时留意键是字符串索引还是类别名,不同数据集的 json 组织方式不一样,像这个数据集也有可能是{'0': 'bad', '1': 'good'}这种结构,也可能是反过来{'bad': 0, 'good': 1}。

2.2 目录是按类别存放的,但文件命名会“坑”你

从项目原文能看到,图片文件命名类似my (2).jpg、me (76).jpg这种格式,前缀是采集批次或采集人代号,后面是编号。这意味着同一类图片分散在多个前缀下,不能靠文件名前缀判断类别,必须以所在目录为准。数据集的目录结构通常是:

dataset/ ├── train/ │ ├── bad/ │ │ ├── my (2).jpg │ │ └── me (76).jpg │ ├── good/ │ └── friend/ ├── test/ │ ├── bad/ │ └── good/ ├── class_indices.json └── show.py

训练集和测试集按类别分目录存放,这个结构对torchvision.datasets.ImageFolder是天然友好的。但有个细节值得注意:直接用ImageFolder加载时,目录名的字母排序就是类别索引顺序。比如目录名是bad、friend、good,那索引就是 0、1、2,而不是你在 json 里看到的顺序。如果想要类别索引一致,要么把目录重命名,要么自定义 Dataset,在__getitem__里用 json 做映射。

2.3 图像质量抽查:尺寸、亮度、清晰度决定了你的预处理策略

1,100 张图不算多,但图片质量参差不齐的情况很常见。有些可能是手机拍摄的,分辨率偏高;有些可能是网络图片,分辨率偏低。直接统一 resize 到 224×224 会丢失细节,统一用大尺寸又显浪费。所以拿到数据集先做一个全量统计,看宽度和高度的分布区间,再决定预处理管线的目标尺寸。

图像尺寸统计的推荐做法:

from PIL import Image import os import numpy as np def scan_image_sizes(root_dir): widths, heights = [], [] for sub_dir in os.listdir(root_dir): sub_path = os.path.join(root_dir, sub_dir) if not os.path.isdir(sub_path): continue for fname in os.listdir(sub_path): if fname.lower().endswith(('.jpg', '.jpeg', '.png')): with Image.open(os.path.join(sub_path, fname)) as im: w, h = im.size widths.append(w) heights.append(h) print('width range: %d - %d' % (min(widths), max(widths))) print('height range: %d - %d' % (min(heights), max(heights))) print('median size: (%d, %d)' % ( int(np.median(widths)), int(np.median(heights)))) scan_image_sizes('path/to/dataset')

这段脚本遍历每个类别目录,读取每张图的宽高,最后输出整体分布区间和中位数。参数说明:.jpg、.jpeg、.png后缀都要覆盖,因为移动端采集的图片经常混有.jpeg后缀。with Image.open(...)的好处是处理完自动关闭文件句柄,数据集图片多时避免文件句柄泄漏。中位数比平均值更抗异常值干扰,个别超高分辨率的图会把平均值拉高。

提示:扫描完如果发现图中位数在 500×500 左右,训练时 resize 到 224 即可;如果中位数只有 100×100 左右,建议保留到 128 分辨率,强行放大到 224 只会增加计算量,不会带来精度提升。

3. 数据加载与预处理:从 show 脚本到自建 PyTorch Dataset

3.1 show 脚本的作用:先可视化再训练,避免标签错位

这个数据集自带show.py,它的作用是把某类别的图片和对应标签一起展示出来。很多时候图片和标签错位的问题,比如目录放错、文件名乱序,只有可视化才能暴露出来。常见做法是把 show 脚本跑在测试集上,因为测试集的标签错位后果更严重。

show 脚本的核心逻辑一般是加载图片目录、用 matplotlib 拼接展示:

import matplotlib.pyplot as plt from PIL import Image import os def show_category(category_dir, num_samples=8): fig, axes = plt.subplots(2, 4, figsize=(12, 6)) axes = axes.flatten() fnames = os.listdir(category_dir) for i in range(min(num_samples, len(fnames))): img = Image.open(os.path.join(category_dir, fnames[i])) axes[i].imshow(img) axes[i].set_title(fnames[i]) axes[i].axis('off') plt.tight_layout() plt.show() show_category('dataset/train/good')

这段代码加载指定目录下前 8 张图片,用 2×4 的子图排列显示。参数说明:num_samples控制显示数量,建议一次看 8-16 张,太多的话屏幕放不下。运行后重点检查两件事:一是图片内容是否和目录名一致,比如good目录里不应该出现竖中指的图;二是图片有没有损坏,比如全黑、全白、严重过曝。如果发现个别损坏图,记下文件名,在训练前处理掉。

3.2 自定义 Dataset 的三个关键点:路径拼接、标签映射、transform 策略

虽然ImageFolder可以直接用,但我更推荐自定义 Dataset,原因有三:一是类别顺序可控,二是路径拼接灵活,三是可以为训练集和测试集配置不同的 transform。

一个可复用的常规实现:

import torch from torch.utils.data import Dataset from PIL import Image import os class HandSignDataset(Dataset): def __init__(self, root_dir, transform=None): self.root_dir = root_dir self.transform = transform self.classes = sorted(os.listdir(root_dir)) self.class_to_idx = {cls: idx for idx, cls in enumerate(self.classes)} self.images = [] self.labels = [] for cls in self.classes: cls_dir = os.path.join(root_dir, cls) for fname in os.listdir(cls_dir): if fname.lower().endswith(('.jpg', '.jpeg', '.png')): self.images.append(os.path.join(cls_dir, fname)) self.labels.append(self.class_to_idx[cls]) def __len__(self): return len(self.images) def __getitem__(self, idx): img_path = self.images[idx] image = Image.open(img_path).convert('RGB') label = self.labels[idx] if self.transform: image = self.transform(image) return image, label

这个 Dataset 的逻辑绕过了 json 映射,直接用目录名排序生成类别索引。参数说明:convert('RGB')很重要,因为有些图片是灰度图或 RGBA 四通道图,统一转成 RGB 可以避免后期 batch 组装时通道数不一致导致的报错。sorted(os.listdir(root_dir))保证类别顺序稳定,不会因为文件系统顺序不同而改变。

实例化训练集和测试集时,transform 通常不同:

from torchvision import transforms train_transform = transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(p=0.5), transforms.RandomRotation(10), transforms.ColorJitter(brightness=0.2, contrast=0.2), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) test_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]) ])

3.3 DataLoader 参数怎么设:batch size、shuffle 和 num_workers

数据集只有 1,100 张图,batch size 设 16 或 32 都合理。我一般用 16,因为类别多但每类图少,batch 太大会让每个 batch 里同类样本占比过高,梯度更新方向偏颇。shuffle 在训练时设为 True,测试时设为 False,这是铁律。

from torch.utils.data import DataLoader train_dataset = HandSignDataset('dataset/train', transform=train_transform) test_dataset = HandSignDataset('dataset/test', transform=test_transform) train_loader = DataLoader(train_dataset, batch_size=16, shuffle=True, num_workers=0) test_loader = DataLoader(test_dataset, batch_size=32, shuffle=False, num_workers=0)

参数说明:num_workers=0是 Windows 下的保险设置,Windows 上多进程数据加载经常报BrokenPipeError,新手阶段先用 0 跑通流程,后面再调高。Linux 上可以设 4。batch_size=32在测试集上速度更快,而且测试时不需要更新梯度,内存占用也小。

常见误区是把shuffle=True用在测试集上。如果你发现每次评估准确率都不同,先检查测试集是不是开了 shuffle。shuffle 本身不会改变评估结果,因为不管顺序怎样,所有样本都会被遍历一遍,但如果你习惯只看部分 batch,开 shuffle 会影响稳定性。

4. 训练与评估:从 LeNet 到 ResNet 的完整落盘验证

4.1 选模型:1,100 张图撑不起大模型,从轻量级开始

数据规模决定了模型选型上限。1,100 张图、11 个分类,每类才 100 张左右,这种规模用 ResNet50 这类深度模型很容易过拟合,用 ViT 更是自讨苦吃。我一般这样选:

模型参数量适合场景预期效果
LeNet-56 万快速验证流程训练快,精度有限
简单 CNN(3~4 层卷积)10~50 万数据集规模匹配精度可接受,速度极快
ResNet18(迁移学习)1,100 万想冲精度,配合预训练权重收敛快,精度较高
ResNet50 / ViT2,500 万+数据量不足,容易过拟合不推荐

我的建议是先跑一个 3 层 CNN 或 ResNet18(迁移学习),先把整个训练、评估、保存流程跑通,再考虑换更大的模型。如果直接用 ResNet50,训练时间翻好几倍,精度还不一定更好。

4.2 训练脚本:损失函数、优化器与学习率的搭配

图像分类的标准搭配是交叉熵损失 + Adam 优化器 + StepLR 学习率衰减。学习率初始值 1e-3,每 10 个 epoch 衰减为原来的 0.1 倍。数据集小,训练轮次不用太多,20 个 epoch 足够看到收敛趋势。

import torch import torch.nn as nn import torch.optim as optim from torchvision import models model = models.resnet18(pretrained=True) num_ftrs = model.fc.in_features model.fc = nn.Linear(num_ftrs, 11) device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = model.to(device) criterion = nn.CrossEntropyLoss() optimizer = optim.Adam(model.parameters(), lr=1e-3) scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.1) def train_one_epoch(model, loader, criterion, optimizer, device): model.train() total_loss = 0.0 correct = 0 total = 0 for images, labels in loader: images, labels = images.to(device), labels.to(device) outputs = model(images) loss = criterion(outputs, labels) optimizer.zero_grad() loss.backward() optimizer.step() total_loss += loss.item() * images.size(0) _, predicted = torch.max(outputs, 1) correct += (predicted == labels).sum().item() total += labels.size(0) avg_loss = total_loss / total acc = correct / total return avg_loss, acc for epoch in range(1, 21): train_loss, train_acc = train_one_epoch( model, train_loader, criterion, optimizer, device) scheduler.step() print('Epoch %02d | Loss: %.4f | Acc: %.2f%%' % ( epoch, train_loss, train_acc * 100))

这段代码的核心是train_one_epoch函数:前向传播算损失,反向传播更新梯度,同时累计损失和准确率。参数说明:pretrained=True表示使用 ImageNet 预训练权重,model.fc替换成输出为 11 的全连接层。criterion用交叉熵,内部已经包含了 softmax,所以模型输出层不需要再加 softmax。step_size=10, gamma=0.1意味着第 10 个 epoch 后学习率变成 1e-4,第 20 个 epoch 变成 1e-5。

注意:替换model.fc后,model.parameters()包含预训练层和新 fc 层。如果想让新 fc 层用更大的学习率,可以拆成两组参数分别设置 lr,但这个数据集规模下没太大必要。

4.3 评估与保存:只看测试集准确率,不迷信训练集指标

训练集准确率很容易趋近 100%,关键指标在测试集。评估脚本每轮结束后手动加载测试集跑一遍:

def evaluate(model, loader, device): model.eval() correct = 0 total = 0 with torch.no_grad(): for images, labels in loader: images, labels = images.to(device), labels.to(device) outputs = model(images) _, predicted = torch.max(outputs, 1) correct += (predicted == labels).sum().item() total += labels.size(0) return correct / total test_acc = evaluate(model, test_loader, device) print('Test accuracy: %.2f%%' % (test_acc * 100)) torch.save(model.state_dict(), 'hand_sign_model.pth')

评估时强制torch.no_grad(),否则 PyTorch 会构建计算图,内存占用飙升,测试速度也变慢。model.eval()切换 BatchNorm 和 Dropout 的行为,Dropout 关闭、BatchNorm 使用全局统计量。保存用state_dict()而不是整个 model,文件更小,加载更灵活,加载时只需要新建模型再load_state_dict。

4.4 训练稳定性:固定随机种子,防止结果漂移

PyTorch 的随机性来自多个层面:模型权重初始化、DataLoader 的 shuffle 顺序、CUDA 上的算子执行顺序。不固定 seed 的话,同样的数据、同样的代码,两次训练结果可能差好几个百分点。固定随机种子的推荐做法:

import random import numpy as np import torch def set_seed(seed=42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) torch.backends.cudnn.deterministic = True torch.backends.cudnn.benchmark = False set_seed(42)

参数说明:cudnn.deterministic = True让 cuDNN 使用确定性算法,代价是牺牲部分速度。cudnn.benchmark = False关闭自动调优,保证两次运行的算子选择一致。数据规模小、单次训练几秒钟到一个分钟级别,这点速度损失可以接受。要从根上排查复现问题,先把torch.backends.cudnn.benchmark设为 False 试试,很多时候差异就是它导致的。

5. 避坑专项:五天跑通流程的四条血泪记录

5.1 同名文件覆盖分类测试集,重跑一次才知道“白训了”

现象:训练完保存模型,第二次加载模型评估时准确率下降 10 多个百分点,但代码没有改动。

原因:数据集中的图片命名存在多处同名,比如my (2).jpg可能在train/good和test/good同时出现。如果你用脚本做过数据清洗、移动或复制操作,某些脚本在 Windows 上对同名文件会直接覆盖,导致训练集或测试集的图片被替换,标签和内容错位。

解决:任何移动、复制操作之前,先跑一遍全库文件名重复检查。文件名相同的图片,不管路径如何,都单独列出人工判断。如果确认是不同内容,批量重命名,加上类别前缀,例如good_my_2.jpg,避免后续操作再踩坑。

5.2 shuffle 后训练集和测试集数目对不上,DataLoader 报错

现象:DataLoader创建成功,但训练到第 2 个 epoch 时抛IndexError: index out of range。

原因:自定义 Dataset 的__len__和__getitem__不一致,通常是self.images和self.labels的长度没对齐。shuffle=True时每个 epoch 都会重新打乱索引,索引越界才会暴露;如果不 shuffle,可能一直侥幸跑完。

解决:在__init__末尾加断言assert len(self.images) == len(self.labels)。另外注意os.listdir可能包含隐藏文件,比如 macOS 的.DS_Store,如果你的代码只判断后缀.jpg没有问题,但如果用了else分支就可能把非图片文件也加入列表。

5.3 训练集和测试集的预处理不一致,精度高但泛化差

现象:训练集用了一些数据增强(随机旋转、色彩抖动),测试集只用 resize 和归一化,最终训练准确率 98%,测试准确率 85%,且随机的数据增强强度越大差距越大。

原因:严格来说这不是 bug,但常见误用是测试集也加了数据增强。有些新手为了“数据不够增强来凑”,给训练集设置了随机旋转 30 度、随机裁剪、色彩抖动,测试集也跟着用了 RandomHorizontalFlip,结果评估时同一张图每次预测结果不同,准确率波动很大。

解决:测试集 transform 只保留 Resize、ToTensor、Normalize,不做任何随机增强。训练集的增强力度也要控制,RandomRotation 不超过 10 度,ColorJitter 的 brightness 和 contrast 都设在 0.2 以内,太强会改变手语手势的基本形状,模型反而学不到区分性特征。

5.4 直接跑 torchvision 预训练模型,加载权重时尺寸不匹配

现象:RuntimeError: size mismatch for fc.weight: copying a param with shape torch.Size([1000, 512]) ...。

原因:把model.fc换成输出 11 的分类头后,预训练权重的fc.weight形状是[1000, 512],新分类头形状是[11, 512],加载时自然不匹配。

解决:model.load_state_dict之前加strict=False,跳过形状不匹配的层,或者先加载权重再换分类头。前者简单粗暴,后者更安全。推荐做法:

model = models.resnet18(pretrained=True) state_dict = torch.load('pretrained_weights.pth') model.load_state_dict(state_dict, strict=False) model.fc = nn.Linear(num_ftrs, 11)

顺序上的区别在于:先加载权重再换 fc 层,加载时 fc 形状匹配,不会报警告;先换 fc 再加载,就必须用strict=False。

6. 把数据集用在刀刃上:迁移学习与可视化验证的技巧

迁移学习是这个数据集最容易出效果的用法。ResNet18 在 ImageNet 上预训练过的底层特征对通用形状、边缘、纹理有很强的提取能力,手语手势和日常物体的底层视觉特征有重叠,所以直接微调比从头训练收敛更快、精度更高。微调时有个技巧:先冻结 backbone,只训练新加的全连接层,跑 5 个 epoch 让分类头先稳定下来,再解冻全部参数用更小的学习率联合微调。

model = models.resnet18(pretrained=True) for param in model.parameters(): param.requires_grad = False num_ftrs = model.fc.in_features model.fc = nn.Linear(num_ftrs, 11) optimizer = optim.Adam(model.fc.parameters(), lr=1e-3) # 训练 5 个 epoch 后解冻 for param in model.parameters(): param.requires_grad = True optimizer = optim.Adam(model.parameters(), lr=1e-4)

这个分段微调的好处是避免一开始就用大步长更新骨干网络参数,把预训练学到的特征破坏掉。参数说明:冻结阶段只更新 fc 层,学习率 1e-3;解冻阶段全部参数参与更新,学习率降到 1e-4,精细调整特征提取器。两个阶段各 10 个 epoch,总共 20 个 epoch,以这个数据集的规模,单次训练在 GPU 上通常几分钟内就能完成。

验证迁移学习效果时,看训练曲线和混淆矩阵的配合。训练曲线关注测试 loss 有没有先降后升的过拟合拐点,混淆矩阵则重点看哪些类别互相混淆。手语词汇里bad和good这类手势形态差异较大的,一般不会混;容易混的是friend和类似五指张开的词,因为手指角度稍微偏一点就可能被分到相邻类别。把混淆矩阵打印出来:

import torch.nn.functional as F from sklearn.metrics import confusion_matrix import numpy as np all_preds = [] all_labels = [] model.eval() with torch.no_grad(): for images, labels in test_loader: images = images.to(device) outputs = model(images) _, preds = torch.max(outputs, 1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.numpy()) cm = confusion_matrix(all_labels, all_preds) print(cm)

运行后如果对角线数值明显大于非对角线,说明模型学到了区分性特征;如果某两类的混淆集中在相邻位置,比如第 3 类和第 7 类大量互混,考虑两个方向:一是增加这两类图片的数据增强强度,二是检查原始数据里这两类的图片是否存在标注错误。还有一种可视化思路是将错误分类的图片单独保存成文件,按错误类型归档,直接看图片来定位是光线问题还是手势角度问题。

从那以后,我每次拿新数据集都会先跑一遍统计脚本看尺寸分布,再跑 show 脚本人工抽查视觉质量,最后才进训练环节。这两个步骤总共也就十分钟,但过滤掉的坑比训练中排查的更多。“先看数据,再跑模型”这条习惯帮我节省了很多返工时间,希望帮到你。

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

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

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

立即咨询