简介:面向人工智能与计算机视觉初学者的神经网络手势识别完整项目包,使用Python与Jupyter Notebook实现,覆盖智能家居、虚拟现实、机器人交互等典型应用场景。资源共32个文件,包含6个Notebook、3个Python脚本、20张结果图像,以及YAML环境配置、说明文档等,整体仅2.78MB,便于下载与按目录检索。Notebook从基础图像处理入手,逐步完成边缘检测、尺寸调整、灰度化等预处理,并使用卷积神经网络完成手势分类;3个Python脚本分别负责数据生成、模型构建与模型训练,配合多个迭代版本Notebook,可清晰对照代码演进与调优思路。已有281人学习下载,适合希望通过实战项目掌握CNN、反向传播与超参数调优的深度学习入门者。
1. 这个手势识别 zip 的价值:从一张图片到一个类别的最小链路
一个 zip 压缩包,里面是 Python 写的、跑在 Jupyter Notebook 里的神经网络手势识别工程。下载它的人通常不是为了那个模型有多先进,而是想快速看到“摄像头画面进去、手势标签出来”的完整链路。这类项目最适合做毕设原型、课程作业和比赛预研:数据集不用从头搞,训练代码能直接改,先跑通再换自己的数据和网络结构。我拿到这类包的第一反应是别急着点运行,先回答三个问题:数据在哪、标签怎么来的、训练完的权重存到哪。这三个问题不弄清楚,后面每一步都可能翻车。判断一个包值不值得下载,也看这两点:是不是自带数据(或给出明确下载方式),训练和测试是否分开。如果只有模型代码没有数据,学习成本会翻倍。这套方案把数据、Notebook、Python 环境三者打包,省掉的是从零对接的成本,适合想在一两天内见到结果、再逐步换成自己数据的人。
2. 把 zip 变成能跑的实验:解压、环境与 Notebook 启动顺序
2.1 环境准备:用 miniconda 隔离 Python 版本,避开系统环境混乱
下载这类 zip 的人里,Windows 用户占了大多数,而 Windows 上最常见的失败原因不是代码问题,是系统 Python 环境太乱。系统里可能装过 3.7 的、3.11 的、各种 pip 包半新不旧,一个 Notebook 工程跑不起来时你根本分不清是代码问题还是依赖问题。所以我的习惯是:不碰系统 Python,直接用 miniconda 建一个独立环境。
miniconda 安装后如何使用 jupyter notebook,是这批 zip 下载用户问得最多的问题之一。其实流程很固定:先创建一个干净环境,再装依赖,最后在同目录启动 notebook。命令如下:
conda create -n gesture python=3.9 -y conda activate gesture选 python=3.9 而不是最新版,是因为 torch、opencv 这些图像处理依赖对最新 Python 版本的 wheel 支持往往滞后,3.9 和 3.10 是兼容面最稳的区间。环境建好后再装依赖:
pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu pip install jupyter opencv-python matplotlib scikit-learn先装 CPU 版 torch,而不是默认装 CUDA 版,是为了避免显卡驱动和 CUDA 动态链接库的问题。如果你的电脑有 NVIDIA 显卡且驱动正常,再换成 cu118 或 cu121 对应版本也不迟。装完验证一下:
import torch import cv2 print(torch.__version__) print(cv2.__version__)能打印出版本号,说明环境基本可用。然后在项目目录里启动:
jupyter notebook启动后浏览器没自动打开,就复制终端里输出的 token 地址手动访问。如果你习惯在 VS Code 里写代码,也可以在 VS Code 里选中 gesture 这个解释器再打开 ipynb,效果一样,还能直接断点调试。
2.2 解压后先看目录结构:用文件命名判断训练入口和推理入口
把 zip 解压前先说一个常见怪现象:解压时弹出输入密码的框,但包本身并没有加密内容,这叫 zip 伪加密,是打包工具把加密标记写错了。遇到这个不要急着去找密码,用 7-Zip 打开后重新“压缩为 zip”存一次就能绕开。命令行解压也可以:
unzip gesture_recognition.zip -d gesture_project cd gesture_project tree -L 2Windows 命令行没有 tree 就用dir /s,或者直接用资源管理器看。解压路径注意一点:整个路径不要带中文,后面 OpenCV 读图时中文目录会变成一整个黑匣子,这一步就能省掉后面大量排错时间。
解压完先别急着双击 ipynb,先看目录里有什么。一般会有 data/ 或 dataset/ 放图片,checkpoints/ 或 models/ 放训练权重,还会有一到两个 notebook 文件。命名里带 train 的是训练入口,带 inference、demo 或 predict 的是推理入口。有没有 requirements.txt 也看一眼,有就和 2.1 的依赖清单对比,没有就按 2.1 的装。数据目录里面如果是一个一个按手势类别建的文件夹,那标签就是文件夹名;如果是 CSV,看第二列是不是类别标签。这两个信息决定了后面模型代码要改哪里。
2.3 按序跑通 Notebook:从前 3 个单元格建立最小闭环
很多新手拿到 notebook 会直接点 Cell -> Run All,然后面对一屏红色报错不知道从哪下手。我一般只先跑前 3 个单元格:导入库、设路径、加载数据。这三个能跑通,后面训练和推理才有意义。
from pathlib import Path import os DATA_DIR = Path("data").resolve() # 改成你解压后的实际数据路径 CHECKPOINT_DIR = Path("checkpoints") CHECKPOINT_DIR.mkdir(exist_ok=True) print("数据目录:", DATA_DIR) print("图片数量:", len(list(DATA_DIR.rglob("*.jpg"))) + len(list(DATA_DIR.rglob("*.png"))))这里用Path.resolve()把相对路径转成绝对路径,是因为 notebook 的当前工作目录取决于你从哪个目录启动 jupyter,不转的话很容易出现“文件明明在,代码就是找不到”的情况。打印图片数量是为了确认解压完整,如果这里输出 0,后面训练会直接报空数据集错误。这个“先看数据能不能读进来”的习惯,能省掉后面一半的排错时间。
跑完这 3 个单元格后,建议再运行一下数据加载相关的单元格,确认每批图片的维度是(BATCH_SIZE, 通道, 高, 宽),标签是从 0 开始的连续整数。确认不了的话,停在这里先查数据,不要往下跑。小数据集场景下,数据没进对,模型再改也是白搭。
3. 数据与网络怎么配:从 CNN 选型到训练参数的手势识别落地设定
3.1 数据组织:文件夹结构就是标签
手势识别数据集常见两种来源:公共数据集和自建数据。公共数据集里常见的是 yolo 手势识别数据集那种图片加标注框的格式,图片旁边配一个 txt,里面是归一化的框坐标和类别编号;自建数据则简单得多,train 和 test 文件夹下按手势类别建子文件夹,一类一个文件夹。
我建议优先用文件夹结构,也就是 torchvision 的 ImageFolder 格式,因为标签直接由文件夹名字生成,不需要自己解析标注文件。yolo 格式要解析 txt、要处理框和图片的对应关系,代码量不小,除非你有现成脚本,否则没必要在起步阶段折腾它。
数据加载部分我一般这么写:
from torchvision import datasets, transforms train_transforms = transforms.Compose([ transforms.Resize((224, 224)), # 统一尺寸,方便进 CNN transforms.RandomHorizontalFlip(p=0.3), # 轻量数据增强 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]), ]) val_transforms = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ]) train_data = datasets.ImageFolder(root="data/train", transform=train_transforms) val_data = datasets.ImageFolder(root="data/test", transform=val_transforms)ImageFolder 会按子文件夹名字的字母序生成标签,不是按你建文件夹的顺序。所以第一次跑之前先把train_data.class_to_idx打出来看一眼,里面是{'类别名': 下标}的映射,训练完做推理时要靠它把下标换回手势名字,对应关系错了后面全乱。验证集不要用数据增强,否则评估出来的指标会带噪声。
DataLoader 也有一个 Windows 下很典型的坑:
from torch.utils.data import DataLoader train_loader = DataLoader(train_data, batch_size=32, shuffle=True, num_workers=0) val_loader = DataLoader(val_data, batch_size=32, shuffle=False, num_workers=0)num_workers在 Linux 上可以设成 4 或 8 加速数据读取,但在 Windows 上大于 0 经常报 BrokenPipe 错误,所以直接设 0 最省心。数据量小的时候这点速度差异无所谓。
3.2 模型选型:为什么是卷积神经网络,前馈全连接差在哪
标题里写的是“神经网络”,很多新手会以为随便一个全连接网络就能做。实际上,图像任务里直接用全连接网络(前馈神经网络)效果很差,原因是图像转成一维向量后,空间位置关系全丢了。左上角和右下角的同一个像素对全连接层来说没有任何区别,手势换个位置就认不出来。卷积神经网络(CNN)用卷积核在局部窗口上滑动,保留了空间局部性,同时靠权值共享把参数量压下来,所以图像分类任务里 CNN 是默认选择。
很多人会在 LSTM、Transformer 和 CNN 之间纠结,但手势识别里的静态图片分类,时间序列模型用不上;Transformer 在小数据集上也不如 CNN 好收敛。常见做法是这种三层卷积的小网络:
import torch.nn as nn class GestureCNN(nn.Module): def __init__(self, num_classes): super().__init__() self.features = nn.Sequential( nn.Conv2d(3, 32, kernel_size=3, padding=1), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(32, 64, kernel_size=3, padding=1), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(64, 128, kernel_size=3, padding=1), nn.ReLU(), nn.MaxPool2d(2), ) self.classifier = nn.Sequential( nn.AdaptiveAvgPool2d(1), # 不管输入多大,都池化成 1x1 nn.Flatten(), nn.Linear(128, num_classes), ) def forward(self, x): return self.classifier(self.features(x))三个卷积块加池化,最后用全局平均池化把特征压成 128 维再分类。这个结构对“比个数字、握拳、张开手掌”这种结构简单的手势足够用,堆太深在小数据集上反而容易过拟合。如果你的数据量很少,比如每类只有几十张,更稳的做法是直接用 torchvision 里的预训练 MobileNetV3 做迁移学习,冻结前面几层只训练分类头,能明显压低过拟合。注意迁移学习的学习率要用小一档,后面会讲。
顺带说一句:如果只是静态单手手势,也可以直接用 MediaPipe 提取手部关键点,再对关键点做分类,这样连图片分类模型都省了。但那是关键点方案,和标题里的神经网络手势识别侧重点不同。自己训 CNN 的好处是类别自己控制,复杂背景也能扛一扛,而不是依赖别人的手部检测质量。
3.3 训练参数:batch_size、学习率与 epoch 的常见设定与调参顺序
训练参数是第一眼最容易抄错的地方。不同教程给的数值差异很大,但它们大多是从 ImageNet 这类大任务上搬来的,手势识别这种小数据集直接抄会出问题。我一般用下面的参数作为起点:
| 参数 | 常见起点 | 调整方向 |
|---|---|---|
| batch_size | 32 | 显存不足降到 16/8;损失震荡可升到 64 |
| 学习率 | 1e-3 | 验证损失停滞或震荡时降到 1e-4 |
| epoch | 30 | 数据量小可以跑到 50,配合早停 |
| 图片尺寸 | 224 | 训练太慢可降到 128,精度略降但迭代快 |
| 类别数 | 按实际分类数 | 必须和模型最后的 Linear 输出对齐 |
训练循环的骨架长这样:
import torch from torch import nn model = GestureCNN(num_classes=len(train_data.classes)) # 类别数必须对齐 optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) criterion = nn.CrossEntropyLoss() best_loss = float("inf") for epoch in range(30): model.train() total_loss = 0.0 for images, labels in train_loader: optimizer.zero_grad() loss = criterion(model(images), labels) # 正向传播算损失 loss.backward() # 反向传播算梯度 optimizer.step() # 更新权重 total_loss += loss.item() val_loss = evaluate(model, val_loader) if val_loss < best_loss: best_loss = val_loss torch.save(model.state_dict(), "checkpoints/best_model.pth") print(f"epoch {epoch + 1}: 已保存最优模型,val_loss={val_loss:.4f}")配套的评估函数也贴一下:
@torch.no_grad() def evaluate(model, loader): model.eval() total = 0.0 for images, labels in loader: loss = nn.functional.cross_entropy(model(images), labels) total += loss.item() * len(images) return total / len(loader.dataset)代码里loss.backward()做的就是把梯度从输出端一层层传回去,模型训练就是反复做正向传播和反向传播,再配合optimizer.step()更新权重。这里有两个细节需要注意:evaluate里必须model.eval(),否则 BN 层在推理和训练两个状态下的行为不一致,验证指标会失真;另外保存的是state_dict而不是整个模型对象,这样换环境部署时只要结构相同就能 load。
如果训练中验证损失一直不降,优先调学习率而不是加 epoch。1e-3 跑 10 个 epoch 还不见收敛,就降到 1e-4。如果损失震荡厉害,先看 batch_size 是不是太小,再考虑调学习率。epoch 不是越多越好,配合上面“只保存验证损失最低的模型”的逻辑,跑 50 个 epoch 也不会把最好的权重冲掉,这是最省心的设置。
4. 手势识别 Notebook 落地避坑:5 个高频问题与排查思路
4.1 Jupyter Notebook 启动时提示找不到指定的程序
现象:点击启动 Jupyter Notebook 后弹窗提示“找不到指定的程序”,或者浏览器能打开但内核连不上,每个单元格一直转圈。原因:Windows 系统缺少 Visual C++ 运行库,或者 torch、opencv 的 DLL 依赖被安全软件清掉了。很多 zip 工程是从别的机器打包过来的,依赖指向的路径在新机器上根本不存在。解决:先装 VC++ Redistributable x64 运行库;然后删掉原环境,用 2.1 的 conda 流程重建,不要直接从旧机器 copy 整个环境目录过来。这一步是“经验玄学”重灾区,但我实测下来,重建环境比手动补 DLL 快得多。
4.2 zip 解压后中文路径把 cv2.imread 变成黑匣子
现象:图片路径从资源管理器复制到代码里,看起来完全正确,cv2.imread却返回 None。原因:OpenCV 的imread不支持中文路径,也不支持部分 Unicode 编码字符,返回 None 时不会报错,数据直接断在这里。解决:最彻底的办法是解压时就把整个工程放到全英文路径下;如果图已经存在中文目录里,可以用imdecode绕开:
import cv2 import numpy as np def imread_unicode(file_path): data = np.fromfile(str(file_path), dtype=np.uint8) return cv2.imdecode(data, cv2.IMREAD_COLOR)这段代码先把文件读成字节流再解码,能绕开imread的编码限制。如果np.fromfile也报错,那就是控制台编码问题,最终方案还是把目录改成英文。这个坑在 zip 下载场景里特别常见,因为很多打包者的目录名本身就是中文。
4.3 训练时 CUDA out of memory
现象:训练刚跑几步就报CUDA out of memory。原因:batch_size=32 在 224x224 的输入下,配合三层卷积,显存占用不小;如果你同时开着多个 notebook,显卡显存是被共享的。解决:batch_size 降到 16 或 8;评估时确认evaluate里加了@torch.no_grad();如果你用的是核显或者老显卡,干脆装 CPU 版 torch,不要硬扛 CUDA。小技巧:先只跑一个 epoch 观察显存占用,再决定要不要加大 batch 或加数据增强。
4.4 验证准确率虚高,换环境就翻车
现象:训练集和验证集准确率都到了 98%,把模型拿到真实场景拍几张图,准确率直接掉到 60%。原因:最常见的不是模型问题,是数据划分不干净。随机 shuffle 时,同一段视频的连续帧被分到了训练集和验证集两边,模型学的是背景和光线而不是手势本身。解决:按视频片段或拍摄批次划分数据,保证同一来源的图只出现在一边:
from sklearn.model_selection import GroupShuffleSplit # samples: 图片路径列表, labels: 类别下标, video_ids: 每张图所属的视频ID split = GroupShuffleSplit(n_splits=1, test_size=0.2, random_state=42) train_idx, val_idx = next(split.split(samples, labels, groups=video_ids))这里groups是每张图片来自哪个视频或哪次拍摄的编号,同一个编号的图不会被拆到两侧。这是手势识别最容易翻车的地方,没有之一。验证集指标只能说明模型在“同分布数据”上表现好,真实部署永远看的是新环境下的表现。
4.5 摄像头推理黑屏,画面卡住不动
现象:推理 notebook 打开后,摄像头画面全黑,或者VideoCapture一直报 -1。原因:摄像头被其他软件独占;macOS 没给终端或浏览器屏幕录制权限;老版本 OpenCV 和某些摄像头驱动不兼容。解决:先关掉微信、腾讯会议这类可能占用摄像头的软件;macOS 在“系统设置 -> 隐私与安全性 -> 屏幕录制”里给对应程序打勾;OpenCV 升到最新版。打开摄像头之前加一个判断:
cap = cv2.VideoCapture(0) if not cap.isOpened(): raise RuntimeError("无法打开摄像头,检查占用/权限/驱动") ret, frame = cap.read() if not ret: raise RuntimeError("读取帧失败,摄像头可能被占用")ret为 False 时不要继续处理后续帧,否则画面会一直卡住,半天查不出问题。这类摄像头问题大多不是模型代码的错,是系统权限和应用占用的问题,先排查环境再怀疑模型。
5. 从 Notebook 到可用:混淆矩阵、模型导出与实时推理的验证思路
5.1 用混淆矩阵找系统误判
准确率只能告诉你模型错得有多少,不能告诉错在哪。训练完成后第一件事是出混淆矩阵:
from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay import matplotlib.pyplot as plt all_preds, all_labels = [], [] for images, labels in val_loader: preds = model(images).argmax(dim=1) all_preds.extend(preds.tolist()) all_labels.extend(labels.tolist()) cm = confusion_matrix(all_labels, all_preds) ConfusionMatrixDisplay(cm, display_labels=train_data.classes).plot() plt.savefig("confusion_matrix.png", dpi=200)看哪两类互相混,常见的是比“1”和比“2”混、握拳和张开混。针对混淆对去补拍几十张对应手势,比盲目加 epoch 有效得多。
5.2 导出独立推理脚本,脱离 Notebook
验证完成后,把权重和模型结构从 notebook 里抽出来,放到一个独立 Python 文件里:
model = GestureCNN(num_classes=len(train_data.classes)) model.load_state_dict(torch.load("checkpoints/best_model.pth", map_location="cpu")) model.eval()load_state_dict只加载权重,模型结构必须在代码里重新定义一遍。推理前必须eval(),否则 BN 层和 Dropout 在训练状态下会输出不稳定结果。加载到 CPU 做推理不需要 GPU,省显存,部署也方便。实时推理的循环里有一个容易被忽略的细节:摄像头输入是 BGR 顺序,模型训练时用的是 RGB,推理前要做cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)再转 tensor,不然颜色通道一乱,模型输出会飘得莫名其妙。
我现在拿到任何 notebook 工程,第一件事永远是看数据怎么进模型,再谈训练。把这条习惯养成,翻车概率至少降一半。希望帮到你。
本文还有配套的精品资源,点击获取