
简介聋哑人手语词汇图像分类数据集面向计算机视觉初学者、算法研究人员及特殊教育应用开发者提供约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, encodingutf-8) as f: class_dict json.load(f) print(type(class_dict)) for k, v in class_dict.items(): print(k, -, v)这段代码的核心逻辑是把 json 读成字典后逐个打印键值对。参数说明encodingutf-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 Dataset3.1 show 脚本的作用先可视化再训练避免标签错位这个数据集自带show.py它的作用是把某类别的图片和对应标签一起展示出来。很多时候图片和标签错位的问题比如目录放错、文件名乱序只有可视化才能暴露出来。常见做法是把 show 脚本跑在测试集上因为测试集的标签错位后果更严重。show 脚本的核心逻辑一般是加载图片目录、用 matplotlib 拼接展示import matplotlib.pyplot as plt from PIL import Image import os def show_category(category_dir, num_samples8): 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, transformNone): 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(p0.5), transforms.RandomRotation(10), transforms.ColorJitter(brightness0.2, contrast0.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, transformtrain_transform) test_dataset HandSignDataset(dataset/test, transformtest_transform) train_loader DataLoader(train_dataset, batch_size16, shuffleTrue, num_workers0) test_loader DataLoader(test_dataset, batch_size32, shuffleFalse, num_workers0)参数说明num_workers0是 Windows 下的保险设置Windows 上多进程数据加载经常报BrokenPipeError新手阶段先用 0 跑通流程后面再调高。Linux 上可以设 4。batch_size32在测试集上速度更快而且测试时不需要更新梯度内存占用也小。常见误区是把shuffleTrue用在测试集上。如果你发现每次评估准确率都不同先检查测试集是不是开了 shuffle。shuffle 本身不会改变评估结果因为不管顺序怎样所有样本都会被遍历一遍但如果你习惯只看部分 batch开 shuffle 会影响稳定性。4. 训练与评估从 LeNet 到 ResNet 的完整落盘验证4.1 选模型1,100 张图撑不起大模型从轻量级开始数据规模决定了模型选型上限。1,100 张图、11 个分类每类才 100 张左右这种规模用 ResNet50 这类深度模型很容易过拟合用 ViT 更是自讨苦吃。我一般这样选模型参数量适合场景预期效果LeNet-56 万快速验证流程训练快精度有限简单 CNN3~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(pretrainedTrue) 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(), lr1e-3) scheduler optim.lr_scheduler.StepLR(optimizer, step_size10, gamma0.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函数前向传播算损失反向传播更新梯度同时累计损失和准确率。参数说明pretrainedTrue表示使用 ImageNet 预训练权重model.fc替换成输出为 11 的全连接层。criterion用交叉熵内部已经包含了 softmax所以模型输出层不需要再加 softmax。step_size10, gamma0.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(seed42): 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的长度没对齐。shuffleTrue时每个 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之前加strictFalse跳过形状不匹配的层或者先加载权重再换分类头。前者简单粗暴后者更安全。推荐做法model models.resnet18(pretrainedTrue) state_dict torch.load(pretrained_weights.pth) model.load_state_dict(state_dict, strictFalse) model.fc nn.Linear(num_ftrs, 11)顺序上的区别在于先加载权重再换 fc 层加载时 fc 形状匹配不会报警告先换 fc 再加载就必须用strictFalse。6. 把数据集用在刀刃上迁移学习与可视化验证的技巧迁移学习是这个数据集最容易出效果的用法。ResNet18 在 ImageNet 上预训练过的底层特征对通用形状、边缘、纹理有很强的提取能力手语手势和日常物体的底层视觉特征有重叠所以直接微调比从头训练收敛更快、精度更高。微调时有个技巧先冻结 backbone只训练新加的全连接层跑 5 个 epoch 让分类头先稳定下来再解冻全部参数用更小的学习率联合微调。model models.resnet18(pretrainedTrue) 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(), lr1e-3) # 训练 5 个 epoch 后解冻 for param in model.parameters(): param.requires_grad True optimizer optim.Adam(model.parameters(), lr1e-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 脚本人工抽查视觉质量最后才进训练环节。这两个步骤总共也就十分钟但过滤掉的坑比训练中排查的更多。“先看数据再跑模型”这条习惯帮我节省了很多返工时间希望帮到你。本文还有配套的精品资源点击获取