ARTICLE DETAIL

资讯详情

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

ASL Alphabet图像分类实战:数据预处理、CNN与迁移学习避坑指南

ASL Alphabet图像分类实战:数据预处理、CNN与迁移学习避坑指南 简介面向深度学习图像分类与手语识别场景ASL Alphabet 美国手语字母表数据集内置29类图像样本涵盖A~Z、space、nothing、del共29个类别每类超2000张图片适合作为CNN、ResNet等模型的训练数据。压缩包约311MB共2000个文件其中1998张jpg图像按文件夹归类另附1个classes json字典文件和1个可视化脚本解压后无需额外处理即可直接用于训练与评估。数据划分清晰train目录含87000张训练图test目录含28张测试图目录名即类别标签便于快速搭建数据加载管道。目前已有513人学习使用。对图像分类入门、手势识别项目或数据预处理教学场景这套资源能省去大量收集整理时间配合json类别映射和可视化脚本可帮助快速验证模型效果并理解类别分布。1. 这不是又一个MNISTASL Alphabet数据集解决的是什么问题做图像分类入门时很多人会在 CIFAR-10 和 MNIST 上反复打转练到后面发现除了调参没什么新东西。换成 ASL Alphabet 这类 29 分类的美国手语字母表图像分类数据集难度立刻不一样A 和 S 都是握拳、M 和 N 只差拇指放的位置、U 和 V 的张开幅度肉眼都难分。这个数据集解决的恰恰是“类间差异极小、背景光照又乱”的真实图像分类问题适合用来练 CNN、迁移学习和图像分类模型的调参手感。想用手语识别做落地 demo 的从业者以及把深度学习图像分类当核心技能在学的新手都能从这套数据上拿到比 CIFAR-10 更有说服力的项目经验。2. ASL Alphabet的29类从哪里来目录结构、标注约定与数据检查2.1 29分类的真正含义26个字母之外还有哪几类ASL Alphabet 数据集最迷惑人的一点就是“29”这个数字。它并不是 29 个字母而是 26 个英文字母手势加上 3 个功能手势。不同公开版本对功能手势的命名略有差异常见的是 del、space、nothing 三类。del 表示删除上一个词space 表示单词之间的空格nothing 表示当前没有手势输入。这种设计是合理的手语交流里本来就有停顿和纠错分类器只认识字母的话落到真实场景会不停误触发。字母部分真正难的不是全部 29 类而是几个固定混淆组合。A、S、T 都是握拳区别只在拇指是贴着手指、压在侧面还是搭在食指上M 和 N 拇指放在不同指缝里小分辨率下几乎一样U 和 V 食指中指一个并拢一个张开差的就是那几度。还有一个隐藏问题是 J 和 Z。美式手语里这两个字母是动态手势需要手在空中划轨迹静态图片数据集只能截取某一帧导致这两个类在纯图像分类里天然吃亏。类别范围常见混淆对手势实际观察要点A / S / T握拳三兄弟拇指位置决定一切M / N拇指所在指缝分辨率不够就分不清U / V并指与开指角度差异极小J / Z动态字母被静态化单帧只能表示“某一刻”del / space / nothing功能手势与字母类形态差异大下载数据集后第一件事不是训练而是确认你手上的版本到底是不是 29 个文件夹。有些二次分享版本把功能手势删掉了只剩 26 类有些把字母图片做过去重。这些改动会直接影响训练脚本里的类别数量和标签映射先用脚本把目录结构盘清楚再谈其他。2.2 下载后先别训练用脚本核对目录、样本数和图片尺寸我一般拿到数据集先跑一遍目录体检看三样东西类别数是不是 29、每个类的样本数量是否均衡、图片尺寸是否统一。下面这个脚本可以直接放到数据集根目录跑import os from collections import Counter from PIL import Image root asl_alphabet_train class_dirs sorted([ d for d in os.listdir(root) if os.path.isdir(os.path.join(root, d)) ]) print(类别数:, len(class_dirs)) print(类名:, class_dirs) counts {} for c in class_dirs: files os.listdir(os.path.join(root, c)) counts[c] len(files) print(总样本数:, sum(counts.values())) print(最少样本类:, min(counts.items(), keylambda x: x[1])) print(最多样本类:, max(counts.items(), keylambda x: x[1])) sizes set() bad_images [] for c in class_dirs: cls_dir os.path.join(root, c) for f in os.listdir(cls_dir)[:500]: # 每个类抽查500张省时间 path os.path.join(cls_dir, f) try: with Image.open(path) as im: sizes.add(im.size) except Exception: bad_images.append(path) print(抽查到的图片尺寸:, sizes) print(损坏图片数量:, len(bad_images)) print(损坏示例:, bad_images[:3])这段脚本的核心价值是把“我以为的数据集”和“实际的数据集”对齐。类名用 sorted 排序保证后面训练脚本的标签顺序稳定样本数统计能直接看出类别不均衡程度如果某个类只有别人的一半后面要做加权采样尺寸抽查用 try 包住 Image.open防止个别损坏图片在训练中途把 dataloader 打崩。参数上有一个小建议抽查数量 500 是性价比比较高的值ASL 数据集单类通常几千张全量校验时间长500 张足够发现尺寸不统一或文件损坏的问题。如果跑出来的尺寸不止一种后面就要在 transform 里统一 resize如果损坏图片数量不为 0直接用脚本按路径删掉别留到训练时翻车。2.3 图片怎么读、按什么格式存先定 RGB 和 resize 策略图片读取方式看着是个小事实际会影响全流程。OpenCV 的 cv2.imread 读出来是 BGR 顺序PIL 读出来是 RGB。如果训练脚本里用 PIL测试脚本里用 OpenCV颜色通道顺序不一致会让模型表现断崖式下跌。我的习惯是统一走 OpenCV 处理在送入模型前用 cv2.cvtColor(img, cv2.COLOR_BGR2RGB) 转一次这样裁剪、缩放、保存中间结果都能用同一个库。存储格式方面如果做手部区域裁剪并落盘建议直接存 PNG。ASL 图片主要是手部纹理和边缘信息JPEG 压缩会在边界引入伪影对手势识别这种细节敏感任务不友好。当然只是训练时内存里做 transform不落盘那用什么格式无所谓。还有一个容易被忽略的点路径里别有中文和空格。Windows 下用 torchvision 的 ImageFolder 加载中文路径偶尔会出编码问题虽然概率不高但没必要赌。我习惯先把数据集路径改成 asl_alphabet_train 这种纯英文目录再动手。3. 把原始图片变成可训练样本手部裁剪、归一化与数据划分3.1 手部区域裁剪为什么裁掉背景比数据增强更有效ASL Alphabet 常见的公开版本里图片大多以手部为主体但背景并非完全干净。有人穿长袖、有人露手臂、有人背景里出现家具和墙。直接用整图训练模型很可能学到“袖口颜色”和“背景纹理”这种偷懒特征而不是手势本身。你在测试集上精度 98%换成真实摄像头画面立刻掉到 70%这类情况十个里有八个是背景泄漏。常见做法是用 MediaPipe Hands 先检测手部关键点把 21 个关键点的包围盒裁出来再外扩一定比例最后 resize 到模型输入尺寸。注意这一步并不需要精确分割手部轮廓包围盒就够用。import cv2 import mediapipe as mp mp_hands mp.solutions.hands hands mp_hands.Hands( static_image_modeTrue, max_num_hands1, min_detection_confidence0.5, ) def crop_hand(image, pad_ratio0.3): rgb cv2.cvtColor(image, cv2.COLOR_BGR2RGB) result hands.process(rgb) if not result.multi_hand_landmarks: return None h, w image.shape[:2] landmarks result.multi_hand_landmarks[0].landmark xs [lm.x for lm in landmarks] ys [lm.y for lm in landmarks] x1, x2 int(min(xs) * w), int(max(xs) * w) y1, y2 int(min(ys) * h), int(max(ys) * h) bw, bh x2 - x1, y2 - y1 pad_x, pad_y int(bw * pad_ratio), int(bh * pad_ratio) x1 max(0, x1 - pad_x) y1 max(0, y1 - pad_y) x2 min(w, x2 pad_x) y2 min(h, y2 pad_y) return image[y1:y2, x1:x2]pad_ratio 是这里最关键的参数。0.2 会贴着手边裁手指尖和手腕容易被切掉0.5 背景又混进来太多模型会重新开始偷看背景。我调下来 0.3 是通用性最好的值既能保住手指边缘又能把少量手腕带进来提供上下文。min_detection_confidence 设 0.5 是平衡点太低会把不是手的东西当手太高部分图片检测不到。原版 ASL Alphabet 数据集的图片质量普遍还行但如果下载到的是带复杂背景的版本或者你自己录视频想扩充数据这个裁剪步骤必须加。检测不到手的图片直接丢弃即可ASL 数据量本身够大不值得为个别坏样本手工标注。3.2 训练/验证/测试划分与类别均衡检查数据集划分不能偷懒。很多公开代码直接把一个文件夹下所有图片拿来做训练验证集和测试集都从同一批数据里随机抽这样最后报的精度没有参考意义。正确做法是分层划分每个类内部按比例抽 train、val、test保证三个集合里每个类都有样本而不是某些类在测试集里消失。import os import random import shutil random.seed(42) root asl_cropped # 裁剪后的数据集 out_root asl_split ratio {train: 0.8, val: 0.1, test: 0.1} for split in ratio: os.makedirs(os.path.join(out_root, split), exist_okTrue) for cls in sorted(os.listdir(root)): cls_dir os.path.join(root, cls) if not os.path.isdir(cls_dir): continue files sorted(os.listdir(cls_dir)) random.shuffle(files) n len(files) n_train int(n * ratio[train]) n_val int(n * ratio[val]) split_files { train: files[:n_train], val: files[n_train:n_train n_val], test: files[n_train n_val:], } for split_name, split_file_list in split_files.items(): out_cls os.path.join(out_root, split_name, cls) os.makedirs(out_cls, exist_okTrue) for f in split_file_list: shutil.copy( os.path.join(cls_dir, f), os.path.join(out_cls, f), ) print(划分完成目录结构) for split in ratio: total sum( len(os.listdir(os.path.join(out_root, split, cls))) for cls in os.listdir(os.path.join(out_root, split)) if os.path.isdir(os.path.join(out_root, split, cls)) ) print(f{split}: {total} 张)random.seed(42) 保证每次划分结果完全一致这个细节在多组对比实验里非常重要。没有固定 seed每次重新划分后训练集内容不同模型精度差异就说不清是模型改动带来的还是数据改动带来的。shutil.copy 用的是复制不是移动原始数据集保持不动方便后面重新划分或回滚。划分完以后我还会打印一个每类样本数直方图。ASL 数据集中 some 类可能只有其他类一半的样本如果差别超过 20%训练时要用 torch 的 WeightedRandomSampler 做类别权重采样或者用 WeightedCrossEntropyLoss 处理类别不均衡否则模型会对样本多的类过拟合、对样本少的类欠拟合。3.3 图像增强的度哪些增强在ASL上不能用图像增强不是越多越好尤其是对 ASL 这种方向敏感的数据集。最容易踩的坑是 RandomHorizontalFlip也就是水平翻转。美式手语标准姿势以右手为主左手图片通常被当作镜像样本。一旦开启水平翻转模型会把左手的 A 和右手的 A 当成两个不同类去学训练难度翻倍推理时真实世界的左手样本也会表现飘忽。我的处理是统一手势方向。训练前先把所有左手图片水平翻转为右手视角然后增强环节不再做任何水平翻转只保留下面这些操作import torchvision.transforms as T train_transform T.Compose([ T.Resize(256), T.RandomResizedCrop(224, scale(0.85, 1.0)), T.RandomRotation(degrees15), T.ColorJitter( brightness0.2, contrast0.2, saturation0.2, hue0.05, ), T.ToTensor(), T.Normalize( mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225], ), ]) val_transform T.Compose([ T.Resize(256), T.CenterCrop(224), T.ToTensor(), T.Normalize( mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225], ), ])RandomResizedCrop 的 scale 下限我刻意设成 0.85不往 0.5 或 0.08 那种通用值上靠。因为手势的判别信息全在手指尖和手指间隙裁剪区域缩得太小会直接把关键细节裁掉模型学到的全是残缺手指。RandomRotation 只给 15 度超过 30 度会把 V 和 U 这种靠角度区分的类搅成一团。ColorJitter 的 hue 给 0.05 意思一下就行手部肤色本身差异不大hue 太大会让肤色失真到不像人手。验证集和测试集不要加任何随机增强只做 Resize 和 CenterCrop。增强只服务于训练集的泛化验证集加随机增强会让 val loss 每轮都在抖早停和模型选择全都会被带偏。4. 用ResNet18还是自建小CNN迁移学习训练与参数设置4.1 从零训练小CNN先验证代码链路别急着追精度第一次跑 ASL 分类我不建议直接上预训练大模型。先搭一个几十万参数的小 CNN 把数据读取、加载器、训练循环、验证流程全部跑通确认链路没有 bug再换大模型才有意义。小模型训练快一个 epoch 十几秒适合排查代码问题。import torch import torch.nn as nn class ASLNet(nn.Module): def __init__(self, num_classes29): super().__init__() self.features nn.Sequential( nn.Conv2d(3, 32, kernel_size3, padding1), nn.BatchNorm2d(32), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), nn.Conv2d(32, 64, kernel_size3, padding1), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), nn.Conv2d(64, 128, kernel_size3, 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))输入 224x224 的 RGB 图片经过三次卷积和三次池化最终特征图是 28x28再通过 AdaptiveAvgPool2d 压成 1x1。BatchNorm2d 在这里不是可有可无的它能让小 batch 下的梯度更新更稳。如果训练中 loss 震荡剧烈先检查 batch size 是不是太小BN 在 batch size 小于 16 时统计量会很不稳定。训练这个网络用 AdamWlr 从 1e-3 起步batch size 64跑 30 个 epoch 就够了。小模型的目标不是拿第一而是确认 loss 能稳定下降、val accuracy 能达到 60% 以上。如果连 60% 都达不到大概率是数据预处理有问题不是模型问题先往回查裁剪和归一化。4.2 迁移学习ResNet18分层学习率与微调策略代码链路确认没问题后再换 ResNet18 做迁移学习。ASL 图像和 ImageNet 场景差异挺大但底层特征通用边缘、纹理、颜色渐变这些低级视觉特征可以直接迁移手指尖和指缝本质上就是边缘组合。不用从头训练。import torchvision.models as models model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) model.fc nn.Linear(model.fc.in_features, 29) for name, param in model.named_parameters(): if name.startswith((layer3, layer4, fc)): param.requires_grad True else: param.requires_grad False params [ {params: [p for n, p in model.named_parameters() if p.requires_grad], lr: 1e-4}, ] optimizer torch.optim.AdamW(params, lr1e-4)注意这里的冻结策略layer3、layer4 和 fc 层参与训练前面的 layer1、layer2 只做特征提取。BN 层是个例外即使 requires_gradFalse它的 running_mean 和 running_var 在 forward 时仍然会更新所以冻结前面的层不影响 BN 统计量的合理性这个细节不用担心。很多人在迁移学习上翻车是因为只换了 fc 层lr 还按从头训练的 1e-3 来。预训练模型的特征已经很好新加的分类头需要大一点的学习率但主干特征层只需要微调统一用大 lr 会把预训练特征冲掉。分级设置是更稳妥的做法分类头 lr 用 1e-3主干后两层用 1e-4。上面的代码为了简洁统一用了 1e-4实际调参时建议拆成两组参数分别设 lr。4.3 训练循环、早停与最优权重一套可复用的训练模板训练循环不必每次重写我习惯维护一份稳定模板换数据集时只改路径和模型。def train_one_model(model, train_loader, val_loader, epochs30, patience5): device torch.device(cuda if torch.cuda.is_available() else cpu) model model.to(device) criterion nn.CrossEntropyLoss() optimizer torch.optim.AdamW(model.parameters(), lr2e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_maxepochs) best_val_acc 0.0 wait 0 for epoch in range(epochs): model.train() total_loss 0.0 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() total_loss loss.item() * x.size(0) model.eval() correct 0 total 0 with torch.no_grad(): for x, y in val_loader: x, y x.to(device), y.to(device) pred model(x).argmax(dim1) correct (pred y).sum().item() total y.size(0) val_acc correct / total train_loss_avg total_loss / len(train_loader.dataset) print(fepoch {epoch1}/{epochs} ftrain_loss{train_loss_avg:.4f} val_acc{val_acc:.4f}) if val_acc best_val_acc: best_val_acc val_acc torch.save(model.state_dict(), best_asl.pth) wait 0 else: wait 1 if wait patience: print(早停触发加载最佳权重) break scheduler.step()CrossEntropyLoss 自带 softmax模型最后一层不需要额外接 softmax。CosineAnnealingLR 配 AdamW 在这个数据规模下表现稳定T_max 等于 epochs让学习率在训练后期平滑降下来。早停 patience 设 5连续 5 个 epoch 验证集精度不涨就停避免无意义地烧算力。这里最容易被忽略的是 model.train() 和 model.eval() 的切换。train 模式会启用 dropout 和 BN 的 batch 统计eval 模式用 BN 的全局统计量。忘记切回 eval 模式推理结果会抖动尤其是 BN 层比较多的时候。另外 torch.save 只保存 state_dict不保存模型结构后面推理时要用相同模型结构先实例化再 load。4.4 要不要上ViT和Swin Transformer数据量决定的边界最近几年做图像分类绕不开 Vision Transformer 这类模型社区里也常拿 ViT 和 CNN 做比较最新的一些下游任务甚至默认用 Swin Transformer 当骨干网络。但在 ASL 这种中等规模数据集上纯从零训练 ViT 不是好选择。ViT 没有 CNN 的归纳偏置需要大量数据才能把全局注意力训好几万张 29 类想从零训到实用的水平投入产出比很低。正确姿势还是 ImageNet 预训练权重做迁移。如果机器显存够、追求更高精度上限可以试 Swin-T 或者 ViT-B/16 的预训练版本输入分辨率提到 256 或 384效果可能比 ResNet18 高半个点到两个点。但这同时意味着训练时间翻倍、部署体积变大。如果你只是做入门验证或小 demoResNet18 和 ResNet34 是性价比最稳的选择。模型输入分辨率相对推理速度小数据下表现自建小CNN224最快上限较低ResNet18224快稳定首选Swin-T256中显存充足可尝试ViT-B/16384慢数据不足容易欠拟合5. 避坑与常见问题ASL分类里翻车的5个真实场景5.1 训练精度高真机摄像头一测就崩现象数据集测试集上 val accuracy 达到 97% 以上把模型接到摄像头实时画面上准确率突然掉到 60% 多而且错误集中在 A、S、T 这几个类。原因模型学会了数据集里的背景特征和肤色分布数据集的图片大多是同类环境拍摄手部周围背景固定模型拿背景当辅助信号。真实摄像头画面背景完全不重叠辅助信号失效。解决回退到裁剪环节确认训练图片全部是裁剪后的手部区域给训练数据加入不同背景的扩充样本在测试时跟踪模型预测的置信度如果大多数预测概率都在 0.6 以下说明输入分布和训练分布已经偏离先检查前处理链路是不是和训练时完全一致。5.2 M和N、U和V永远在混现象其他类精度都不错混淆矩阵里 M/N、U/V 两两互混严重M 经常被预测成 NV 经常被预测成 U。原因这些字母手势本身差异极小M 和 N 的拇指位置偏差可能只有几毫米经过 224x224 的 resize 后关键像素点被压缩掉。另一个可能是 RandomRotation 的角度太大把 U 的微张开角度转成 V 的样子。解决输入分辨率从 224 提到 256保留更多手指细节Rotation 角度从 15 度降到 10 度在裁剪手部区域时 pad_ratio 不要过大背景混入越少这类细节型分类越容易区分。做数据可视化时把 M 和 N 的样本并排打印出来肉眼确认裁剪是否把拇指切掉了。5.3 J和Z精度怎么调都上不去现象所有类里 J 和 Z 的精度明显低于其他类换 ResNet18、加大数据增强都无效稳定卡在一个水平。原因这是 ASL 本身的特性不是你的代码问题。J 和 Z 是动态手势静态图片数据集里只有一帧这一帧缺少运动轨迹信息模型只能靠那一帧手型和剩余的运动痕迹猜测人和模型都容易认错。解决降低心理预期静态图分类里 J、Z 精度做到 80% 以上已经不错。要做真实场景的 J 和 Z 识别需要拿到连续视频帧用两帧或多帧输入的模型或者对视频流做随时间滑动的分类投票。单张图片硬扛 J/Z 是在和物理信息量作对。5.4 训练loss下降很慢验证集还在震荡现象前几个 epoch train loss 下降不明显batch 之间的 loss 抖动剧烈val accuracy 上蹿下跳模型看起来像没在学。原因大概率学习率设置偏大且没做 warmup加上 batch size 太小时 BN 统计量不稳定。ASL 图片经过裁剪后尺寸分布杂乱batch 内样本差异大小 batch 下的梯度方向不够稳定。解决把 lr 降到 1e-4 或 2e-4batch size 提到至少 32BN 层在小 batch 下才能稳定工作。训练器里加 5 个 epoch 的线性 warmup让 lr 从 1e-5 慢慢爬到设定值。这一套组合对中小数据集非常有效。5.5 左右手图片混在一起导致同类分裂现象测试集单独看没问题一旦把训练集的图片手工检查发现同一类字母里既有左手又有右手模型学出来的特征空间是双峰的。原因ASL 标准姿势以右手为基准左手手势是镜像。模型把右手 A 和左手 A 当成了两个不同的视觉模式等于在 29 类的任务里偷偷做了一个 58 类的隐式分类数据利用率下降。解决统一手势方向。用 MediaPipe 检测到手部关键点后如果识别为左手做水平翻转再进入训练流程把左手样本全部翻成右手视角。推理时同样处理保证模型永远只看到同一个手向的输入。6. 让模型真正可用视频帧置信度、动态字母与J/Z的扩展单张图片的分类准确率只是第一步把模型接到真实视频流上还需要处理连续帧的抖动和置信度问题。最简单的做法是滑窗投票把最近 7 帧的预测结果放进一个队列取出现次数最多的类别作为当前输出这样单帧的误判会被周围帧纠正连续识别的手势会比单帧稳定很多。import cv2 import torch from collections import deque def predict_frame(frame, topk3): roi crop_hand(frame, pad_ratio0.3) if roi is None: return None img cv2.resize(roi, (224, 224)) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) x val_transform(img).unsqueeze(0) with torch.no_grad(): logits model(x) prob torch.softmax(logits, dim1)[0] idx prob.argsort(descendingTrue)[:topk] return [(classes[i], float(prob[i])) for i in idx] vote_buffer deque(maxlen7) def vote_label(frame): pred predict_frame(frame) if pred is not None: vote_buffer.append(pred[0][0]) if len(vote_buffer) 3: return None return max(set(vote_buffer), keyvote_buffer.count)maxlen 取 7在 30 帧每秒的摄像头下大约 0.23 秒的投票窗口识别延迟不会明显抖动也基本被滤掉。窗口太短比如 3单帧误判的影响还在窗口太长比如 15手指切换字母时响应明显变慢。投票时只取第一候选类别不把置信度低的结果混进来。如果 predict_frame 里的最高置信度低于 0.5建议直接丢弃这一帧而不是强行投票可以再加一个置信度阈值判断。J 和 Z 这种动态字母在视频流里有天然优势。连续帧里手型会有明显变化轨迹可以在投票的同时记录前几帧的裁剪区域中心点坐标当坐标位移超过阈值时额外判断为“动作字母”再交给两帧分类器或简单的轨迹方向分类。这里不需要复杂模型用每帧手部包围盒的中心点序列算个位移方向就能把动态字母和静态字母区分开。我自己的项目里吃过一个大亏模型静态分类精度 98%上一套实时识别就用单帧预测结果演示现场频繁误触后来加了 7 帧投票和置信度阈值误触才基本消失。现在做任何视觉分类的实时落地我都会先问一句输出要不要过平滑而不是直接拿单帧结果上屏。另一个教训是摄像头自动白平衡和自动曝光会让画面色调持续漂移训练时的 ColorJitter 增强严格来说救不了这种动态漂移最好在部署端固定曝光和手动白平衡省得模型在颜色分布上反复踩坑。ASL 数据集这套流程跑完你会对“类间细粒度差异”这件事有非常具体的体感再去碰其他细分类任务会顺畅很多。希望这些参数和坑位对你有帮助。本文还有配套的精品资源点击获取
返回列表