ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

手语图像分类数据集实战:从2500张标注图到迁移学习基线

手语图像分类数据集实战:从2500张标注图到迁移学习基线 简介这份手语图像分类数据集面向计算机视觉入门者、课程设计或毕业设计选题的学生以及需要验证CNN分类网络改进效果的开发者解决手语识别任务中标注数据难获取、类别覆盖不足的问题。资源共约2000个文件以1998张jpeg手语图像为主另含1个json标注文件和1个Python脚本压缩包约28.58MB图像按训练集与测试集分别存放于同一类别目录下json文件记录了36个分类标签涵盖0、1、a、b等字符show脚本可用于快速可视化数据分布与样本质量。目前已有442人学习下载。读者可直接获得一套开箱即用的标注数据省去采集与清洗成本配合json标签快速构建数据加载流程并借助可视化脚本检查类别均衡与图像质量为后续CNN分类网络改进实验提供稳定基线适合作为手语识别、图像分类方向的基础训练与对比验证素材。1. 手语图像分类数据集怎么用从 2500 张标注图到可复现的分类基线手语图像分类数据集【已标注约2,500张数据】这类资源真正的价值不在“有多少张”而在于标注是否干净、类别是否均衡、能不能直接喂进训练管线。我拿到手的场景通常是想做一个手语字母或常用词的识别 demo但自己拍数据成本太高于是找一份已标注的小规模数据集先跑通链路。2,500 张这个量级很微妙——它不足以从零训练一个大模型但足够做迁移学习、验证数据增强策略、跑通从划分到部署的完整流程。适合两类人一是想快速验证手语识别可行性的算法工程师二是需要一个小型多分类数据集练手 YOLOv8 分类或 ResNet 微调的学生。下面按“先看清数据、再跑通基线、最后避坑”的顺序讲。2. 先摸清数据底细2500 张标注图到底该怎么读2.1 目录结构与标注格式的三种常见形态手语图像分类数据集通常以“类别名即文件夹名”的形式组织这是最省事的 ImageFolder 结构。但“已标注”三个字背后可能是三种东西文件夹分类标签、CSV 清单、或者 COCO 风格的 JSON。我一般先跑一段脚本把结构打印出来不靠猜。import os from collections import Counter root sign_language_dataset # 统计每个类别文件夹下的图片数量 class_counts {} for cls in sorted(os.listdir(root)): cls_path os.path.join(root, cls) if os.path.isdir(cls_path): imgs [f for f in os.listdir(cls_path) if f.lower().endswith((.jpg, .jpeg, .png, .bmp))] class_counts[cls] len(imgs) total sum(class_counts.values()) print(f类别数: {len(class_counts)}, 总图片: {total}) for cls, n in class_counts.items(): print(f{cls}: {n} ({n/total*100:.1f}%))这段脚本做两件事确认类别数、暴露类别不均衡。2,500 张如果分 26 个字母类平均每类不到 100 张一旦某些类只有三四十张训练时就会明显偏向多数类。参数上注意扩展名过滤要覆盖 jpg/jpeg/png/bmp很多数据集混用格式漏掉一种就会少算。如果输出里出现total远小于预期先怀疑有嵌套子目录或非图片文件混入。2.2 用图像尺寸和通道分布判断是否需要预处理手语图像常见两种来源一种是摄像头实拍尺寸杂乱一种是裁剪好的手部区域尺寸统一。这直接决定你要不要做 resize 和 padding。跑一段统计尺寸分布的代码比肉眼翻图快得多。from PIL import Image import os, random sizes [] sample_root sign_language_dataset all_imgs [] for cls in os.listdir(sample_root): p os.path.join(sample_root, cls) if os.path.isdir(p): all_imgs [os.path.join(p, f) for f in os.listdir(p)] # 随机抽 200 张看尺寸分布避免全量读取太慢 for path in random.sample(all_imgs, min(200, len(all_imgs))): with Image.open(path) as im: sizes.append(im.size) from collections import Counter print(Counter(sizes).most_common(10))如果最常见的尺寸高度集中说明数据已经过统一裁剪可以直接 resize 到 224×224 或 640×640。如果尺寸五花八门就要考虑保持长宽比的 letterbox 处理否则手部会被拉伸变形手语的关键是手指相对位置形变会直接毁掉特征。这里有个血泪经验手语分类对水平翻转很敏感左右手镜像后语义可能完全不同所以数据增强里horizontal_flip要慎用甚至禁用。2.3 划分训练验证集时别踩类别泄漏的坑同一只手、同一背景连续拍的图如果被随机分到训练和验证集验证准确率会虚高。正确做法是按“拍摄批次”或“人”来划分但小数据集往往没有这个元信息。退而求其次我一般用分层抽样保证每类比例一致并固定随机种子。import os, shutil, random from sklearn.model_selection import train_test_split random.seed(42) root sign_language_dataset out split_dataset all_items [] for cls in os.listdir(root): p os.path.join(root, cls) if os.path.isdir(p): for f in os.listdir(p): all_items.append((os.path.join(p, f), cls)) paths [x[0] for x in all_items] labels [x[1] for x in all_items] train_p, val_p, train_y, val_y train_test_split( paths, labels, test_size0.2, stratifylabels, random_state42) for split, ps, ys in [(train, train_p, train_y), (val, val_p, val_y)]: for p, y in zip(ps, ys): dst os.path.join(out, split, y) os.makedirs(dst, exist_okTrue) shutil.copy(p, dst)stratifylabels是关键参数它保证每个类别在训练和验证集里比例一致避免某个类全跑进验证集。random_state42固定后结果可复现。复制而非移动是为了保留原始数据后面做增强实验时不用重新解压。3. 用迁移学习跑通第一个分类基线3.1 为什么 2500 张不适合从零训练2,500 张图、假设 20 个类每类 125 张。从零训练一个 ResNet 或 ViT参数动辄上千万这个数据量必然过拟合训练集准确率能到 99%验证集卡在 60% 上不去。常见做法是加载 ImageNet 预训练权重冻结主干只训练分类头或者用较小学习率微调最后几个 stage。手语图像和 ImageNet 的自然图像分布差异大但底层边缘、纹理特征仍然可迁移这是迁移学习在小数据集上屡试不爽的原因。3.2 基于 torchvision 的最小可跑训练脚本下面这段是我常用的基线模板结构清晰、参数集中方便你改。import torch import torch.nn as nn from torch.utils.data import DataLoader from torchvision import datasets, transforms, models data_dir split_dataset # 训练集做增强验证集只做 resize 和归一化 train_tf transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomRotation(10), # 手语对旋转有一定容忍度 transforms.ColorJitter(0.2, 0.2), # 应对不同光照 transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]), ]) val_tf transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]), ]) train_ds datasets.ImageFolder(f{data_dir}/train, train_tf) val_ds datasets.ImageFolder(f{data_dir}/val, val_tf) train_loader DataLoader(train_ds, batch_size32, shuffleTrue, num_workers4) val_loader DataLoader(val_ds, batch_size32, shuffleFalse, num_workers4) device cuda if torch.cuda.is_available() else cpu model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) # 冻结主干只训练最后的全连接层 for p in model.parameters(): p.requires_grad False model.fc nn.Linear(model.fc.in_features, len(train_ds.classes)) model model.to(device) criterion nn.CrossEntropyLoss() optimizer torch.optim.Adam(model.fc.parameters(), lr1e-3) for epoch in range(15): model.train() for x, y in train_loader: x, y x.to(device), y.to(device) optimizer.zero_grad() loss criterion(model(x), y) loss.backward() optimizer.step() # 验证 model.eval() correct total 0 with torch.no_grad(): for x, y in val_loader: x, y x.to(device), y.to(device) pred model(x).argmax(1) correct (pred y).sum().item() total y.size(0) print(fepoch {epoch}: val_acc{correct/total:.4f})逻辑说明冻结主干后只有model.fc参与训练参数量从千万级降到几万小数据集上收敛快且不易过拟合。batch_size32在 2,500 张规模下每轮约 60 多个 step15 轮几分钟就能跑完。RandomRotation(10)是手语场景下相对安全的增强但别开太大旋转 90 度手语语义就变了。归一化用的 ImageNet 均值方差因为主干是 ImageNet 预训练的保持一致才能对齐特征分布。3.3 关键参数怎么调学习率、冻结层数、批大小学习率是第一个要动的。只训练分类头时1e-3通常没问题如果解冻最后两个 stage 做微调要降到1e-4甚至1e-5否则预训练权重会被大梯度冲垮。冻结层数上数据量越小冻结越多2,500 张我一般先只训 fc 层看验证准确率如果欠拟合训练准确率也低再解冻layer4。批大小受显存限制但小数据集上 batch 太大反而降低泛化32 或 64 比较稳。判断是否过拟合看训练和验证准确率的差距差距超过 15 个点就该加 dropout 或更强增强。4. 数据增强与类别不均衡的处理边界4.1 手语场景下哪些增强能用、哪些是雷不是所有增强都适合手语。水平翻转是最大的雷左手比划和右手比划在很多手语体系里是不同含义翻转后标签就错了。垂直翻转同理。可以放心用的有小角度旋转±15 度内、亮度对比度扰动、随机裁剪注意别裁掉手部、轻微缩放。高斯噪声和模糊可以模拟低质量摄像头但要控制强度。我一般把增强策略写成一个配置字典方便对比实验。# 安全增强组合按需增删 safe_aug { rotation: 15, # 角度上限 brightness: 0.2, contrast: 0.2, scale: (0.9, 1.1), # 缩放范围 hflip: False, # 手语禁用 vflip: False, }4.2 类别不均衡时用加权采样还是加权损失如果第 2 章统计出来某些类只有三四十张直接训练会让模型偏向多数类。两种主流做法一是WeightedRandomSampler让少样本类被采样的概率提高二是给CrossEntropyLoss传weight参数。前者改变每个 epoch 看到的数据分布后者改变损失贡献。小数据集上我更倾向加权损失因为它不改变数据本身实现简单。from collections import Counter import torch labels [y for _, y in train_ds.samples] counts Counter(labels) n_classes len(train_ds.classes) weights torch.tensor( [len(labels) / (n_classes * counts[i]) for i in range(n_classes)], dtypetorch.float32).to(device) criterion nn.CrossEntropyLoss(weightweights)weights的计算逻辑是“总样本数除以类别数乘该类样本数”样本越少的类权重越大。注意权重别设得太极端否则少数类会被过度强调导致多数类崩掉一般控制在 5 倍以内。如果加权后验证集上少数类召回率上来了但整体准确率下降说明权重过大回调即可。5. 避坑与排查手语数据集训练中最容易翻车的五件事5.1 验证准确率异常高接近 100%现象第一个 epoch 验证准确率就 95% 以上。原因训练集和验证集存在重复图片或同一批次泄漏。解决用图片哈希去重确认划分时没有同一只手的连续帧跨集。import hashlib def file_hash(path): with open(path, rb) as f: return hashlib.md5(f.read()).hexdigest() # 对 train 和 val 分别算哈希求交集5.2 训练损失不下降一直卡在高位现象loss 在几个 epoch 内几乎不变。原因学习率过大导致震荡或归一化参数和预训练权重不匹配。解决先把学习率降到1e-4试确认归一化用的是 ImageNet 的均值和方差。5.3 显存爆掉batch 只能设到 8现象CUDA out of memory。原因图片分辨率设太高或num_workers过多导致内存泄漏。解决把输入从 640 降到 224num_workers设为 CPU 核数的一半并在训练循环里用torch.cuda.empty_cache()。5.4 某些类别始终识别不出来现象混淆矩阵里某几类互相错分严重。原因这些类视觉上太相似或样本量太少。解决先看这几类的样本图确认标注没错再考虑针对性增强或合并易混类。5.5 推理时单张图预测结果和验证集不一致现象验证集准确率 85%但拿单张图预测总是错。原因推理时的预处理和验证集不一致比如忘了归一化或 resize 方式不同。解决把验证集的 transform 抽成一个函数推理时复用同一个。6. 把基线推到可用混淆矩阵定位与推理脚本固化跑通基线只是开始真正决定这个数据集值不值得继续投入的是你能不能快速定位错分。我习惯在每个实验后画混淆矩阵看哪些类在互相“打架”。手语里形近字母比如握拳类的几个最容易混看到混淆矩阵后可以针对性补数据或调整增强。import numpy as np from sklearn.metrics import confusion_matrix, classification_report model.eval() all_pred, all_true [], [] with torch.no_grad(): for x, y in val_loader: x x.to(device) pred model(x).argmax(1).cpu().numpy() all_pred.extend(pred) all_true.extend(y.numpy()) cm confusion_matrix(all_true, all_pred) print(classification_report(all_true, all_pred, target_namesval_ds.classes, digits3))classification_report直接给出每类的 precision、recall、f1比只看整体准确率有用得多。如果某类 recall 低于 0.5基本可以判定这类需要补样本或单独处理。混淆矩阵里非对角线的热点就是下一步优化的方向。推理脚本要固化避免每次手写预处理。我一般把模型加载、transform、预测封装成一个类训练完直接导出权重推理时只依赖这个类和权重文件。class SignClassifier: def __init__(self, ckpt, classes, devicecpu): self.classes classes self.device device self.model models.resnet18(weightsNone) self.model.fc nn.Linear(self.model.fc.in_features, len(classes)) self.model.load_state_dict(torch.load(ckpt, map_locationdevice)) self.model.eval().to(device) self.tf val_tf # 复用验证集 transform def predict(self, img_path): from PIL import Image img Image.open(img_path).convert(RGB) x self.tf(img).unsqueeze(0).to(self.device) with torch.no_grad(): prob torch.softmax(self.model(x), 1) conf, idx prob.max(1) return self.classes[idx.item()], conf.item()这个类把“预处理必须和验证集一致”这条规则固化进代码避免第 5.5 条那个坑。weightsNone是因为要加载自己训练的权重不能再用 ImageNet 的。map_location保证在 CPU 上也能加载 GPU 训的权重。最后说个我自己的习惯每次拿到一个新的手语数据集先花二十分钟做数据体检——统计类别分布、看尺寸、抽查标注、去重再动手写训练代码。这二十分钟能省掉后面几小时的排查。2,500 张这个量级跑通基线大概半天但把数据摸透、把坑填平才是这个数据集真正能产出价值的地方。希望帮到你。本文还有配套的精品资源点击获取
返回列表