如何建立神经网络将手语翻译成英语
作为 Write for DOnations 计划的一部分,作者选择了 Code Org 来接受捐赠。
介绍
计算机视觉是计算机科学的一个子领域,旨在从图像和视频中提取更高层次的理解。 这为有趣的视频聊天过滤器、移动设备的面部验证器和自动驾驶汽车等技术提供了动力。
在本教程中,您将使用计算机视觉为您的网络摄像头构建美国手语翻译器。 在学习本教程时,您将使用计算机视觉库 OpenCV、PyTorch 构建深度神经网络,并使用 onnx 导出您的神经网络。 在构建计算机视觉应用程序时,您还将应用以下概念:
- 您将使用与 如何应用计算机视觉构建基于情感的狗过滤器 教程中使用的相同的三步方法:预处理数据集、训练模型和评估模型。
- 您还将扩展这些步骤中的每一个:使用数据增强来解决旋转或非居中的手,更改学习率计划以提高模型准确性,并导出模型以加快推理速度。
- 在此过程中,您还将探索 机器学习 中的相关概念。
在本教程结束时,您将拥有美国手语翻译和基础深度学习知识。 您还可以访问该项目的完整源代码。
先决条件
要完成本教程,您将需要以下内容:
- 具有至少 1GB RAM 的 Python 3 本地开发环境。 您可以按照如何为Python 3安装和设置本地编程环境来配置您需要的一切。
- 用于实时图像检测的工作网络摄像头。
- (推荐)构建基于情感的狗过滤器; 本教程没有明确使用,但同样的想法得到了加强和建立。
第 1 步 - 创建项目并安装依赖项
让我们为这个项目创建一个工作区并安装我们需要的依赖项。
在 Linux 发行版上,首先准备系统包管理器并安装 Python3 virtualenv 包。 采用:
apt-get update apt-get upgrade apt-get install python3-venv
我们将调用我们的工作区 SignLanguage
:
mkdir ~/SignLanguage
导航到 SignLanguage
目录:
cd ~/SignLanguage
然后为项目创建一个新的虚拟环境:
python3 -m venv signlanguage
激活您的环境:
source signlanguage/bin/activate
然后安装 PyTorch,这是我们将在本教程中使用的 Python 深度学习框架。
在 macOS 上,使用以下命令安装 Pytorch:
python -m pip install torch==1.2.0 torchvision==0.4.0
在 Linux 和 Windows 上,对仅 CPU 构建使用以下命令:
pip install torch==1.2.0+cpu torchvision==0.4.0+cpu -f https://download.pytorch.org/whl/torch_stable.html pip install torchvision
现在为 OpenCV
、numpy
和 onnx
安装预打包的二进制文件,它们分别是用于计算机视觉、线性代数、AI 模型导出和 AI 模型执行的库。 OpenCV
提供图像旋转等实用程序,numpy
提供矩阵求逆等线性代数实用程序:
python -m pip install opencv-python==3.4.3.18 numpy==1.14.5 onnx==1.6.0 onnxruntime==1.0.0
在 Linux 发行版上,您需要安装 libSM.so
:
apt-get install libsm6 libxext6 libxrender-dev
安装依赖项后,让我们构建我们的手语翻译器的第一个版本:手语分类器。
第 2 步——准备手语分类数据集
在接下来的三个部分中,您将使用神经网络构建手语分类器。 您的目标是生成一个模型,该模型接受一张手的图片作为输入并输出一个字母。
构建机器学习分类模型需要以下三个步骤:
- 预处理数据:将 单热编码 应用于您的标签,并将您的数据包装在 PyTorch 张量中。 在增强数据上训练你的模型,为“不寻常”的输入做好准备,比如偏心或旋转的手。
- 指定和训练模型:使用 PyTorch 设置神经网络。 定义训练超参数——比如训练多长时间——并运行随机梯度下降。 您还将改变特定的训练超参数,即学习率计划。 这些将提高模型的准确性。
- 使用模型运行预测:根据验证数据评估神经网络以了解其准确性。 然后,将模型导出为一种名为 ONNX 的格式,以获得更快的推理速度。
在本教程的这一部分,您将完成第 1 步,共 3 步。 您将下载数据,创建一个 Dataset
对象来迭代您的数据,最后应用 数据扩充 。 在此步骤结束时,您将有一种编程方式来访问数据集中的图像和标签,以提供给您的模型。
首先,将数据集下载到您当前的工作目录:
注意:在macOS上,wget
默认不可用。 为此,请按照此 DigitalOcean 教程 安装 Homebrew 。 然后,运行 brew install wget
。
wget https://assets.digitalocean.com/articles/signlanguage_data/sign-language-mnist.tar.gz
解压 zip 文件,其中包含一个 data/
目录:
tar -xzf sign-language-mnist.tar.gz
创建一个新文件,命名为 step_2_dataset.py
:
nano step_2_dataset.py
和以前一样,导入必要的实用程序并创建将保存您的数据的类。 对于此处的数据处理,您将创建训练和测试数据集。 您将实现 PyTorch 的 Dataset
接口,允许您为手语分类数据集加载和使用 PyTorch 的内置数据管道:
step_2_dataset.py
from torch.utils.data import Dataset from torch.autograd import Variable import torch.nn as nn import numpy as np import torch import csv class SignLanguageMNIST(Dataset): """Sign Language classification dataset. Utility for loading Sign Language dataset into PyTorch. Dataset posted on Kaggle in 2017, by an unnamed author with username `tecperson`: https://www.kaggle.com/datamunge/sign-language-mnist Each sample is 1 x 1 x 28 x 28, and each label is a scalar. """ pass
删除 SignLanguageMNIST
类中的 pass
占位符。 取而代之的是,添加一个方法来生成标签映射:
step_2_dataset.py
@staticmethod def get_label_mapping(): """ We map all labels to [0, 23]. This mapping from dataset labels [0, 23] to letter indices [0, 25] is returned below. """ mapping = list(range(25)) mapping.pop(9) return mapping
标签范围从 0 到 25。 但是,不包括字母 J (9) 和 Z (25)。 这意味着只有 24 个有效的标签值。 为了使从 0 开始的所有标签值的集合是连续的,我们将所有标签映射到 [0, 23]。 这种从数据集标签 [0, 23] 到字母索引 [0, 25] 的映射由 get_label_mapping
方法提供。
接下来,添加一种从 CSV 文件中提取标签和样本的方法。 下面假设每行以 label
开头,然后是 784 个像素值。 这 784 个像素值代表 28x28
图像:
step_2_dataset.py
@staticmethod def read_label_samples_from_csv(path: str): """ Assumes first column in CSV is the label and subsequent 28^2 values are image pixel values 0-255. """ mapping = SignLanguageMNIST.get_label_mapping() labels, samples = [], [] with open(path) as f: _ = next(f) # skip header for line in csv.reader(f): label = int(line[0]) labels.append(mapping.index(label)) samples.append(list(map(int, line[1:]))) return labels, samples
有关这 784 个值如何表示图像的说明,请参阅 构建基于情感的狗过滤器,步骤 4 。
注意 csv.reader
迭代中的每一行都是一个字符串列表; int
和 map(int, ...)
调用将所有字符串转换为整数。 在我们的静态方法正下方,添加一个将初始化我们的数据持有者的函数:
step_2_dataset.py
def __init__(self, path: str="data/sign_mnist_train.csv", mean: List[float]=[0.485], std: List[float]=[0.229]): """ Args: path: Path to `.csv` file containing `label`, `pixel0`, `pixel1`... """ labels, samples = SignLanguageMNIST.read_label_samples_from_csv(path) self._samples = np.array(samples, dtype=np.uint8).reshape((-1, 28, 28, 1)) self._labels = np.array(labels, dtype=np.uint8).reshape((-1, 1)) self._mean = mean self._std = std
此功能首先加载样本和标签。 然后它将数据包装在 NumPy 数组中。 平均值和标准偏差信息将在下面的 __getitem__
部分中简要说明。
在 __init__
函数之后直接添加 __len__
函数。 Dataset
需要此方法来确定何时停止迭代数据:
step_2_dataset.py
... def __len__(self): return len(self._labels)
最后,添加一个 __getitem__
方法,该方法返回一个包含样本和标签的字典:
step_2_dataset.py
def __getitem__(self, idx): transform = transforms.Compose([ transforms.ToPILImage(), transforms.RandomResizedCrop(28, scale=(0.8, 1.2)), transforms.ToTensor(), transforms.Normalize(mean=self._mean, std=self._std)]) return { 'image': transform(self._samples[idx]).float(), 'label': torch.from_numpy(self._labels[idx]).float() }
您使用一种称为 数据增强 的技术,其中样本在训练期间受到扰动,以提高模型对这些扰动的鲁棒性。 特别是,通过 RandomResizedCrop
以不同的数量和不同的位置随机放大图像。 请注意,放大不应影响最终的手语课; 因此,标签不会被转换。 您还可以对输入进行归一化,以便将图像值重新缩放到预期的 [0, 1] 范围,而不是 [0, 255]; 为此,在标准化时使用数据集 _mean
和 _std
。
您完成的 SignLanguageMNIST
类将如下所示:
step_2_dataset.py
from torch.utils.data import Dataset from torch.autograd import Variable import torchvision.transforms as transforms import torch.nn as nn import numpy as np import torch from typing import List import csv class SignLanguageMNIST(Dataset): """Sign Language classification dataset. Utility for loading Sign Language dataset into PyTorch. Dataset posted on Kaggle in 2017, by an unnamed author with username `tecperson`: https://www.kaggle.com/datamunge/sign-language-mnist Each sample is 1 x 1 x 28 x 28, and each label is a scalar. """ @staticmethod def get_label_mapping(): """ We map all labels to [0, 23]. This mapping from dataset labels [0, 23] to letter indices [0, 25] is returned below. """ mapping = list(range(25)) mapping.pop(9) return mapping @staticmethod def read_label_samples_from_csv(path: str): """ Assumes first column in CSV is the label and subsequent 28^2 values are image pixel values 0-255. """ mapping = SignLanguageMNIST.get_label_mapping() labels, samples = [], [] with open(path) as f: _ = next(f) # skip header for line in csv.reader(f): label = int(line[0]) labels.append(mapping.index(label)) samples.append(list(map(int, line[1:]))) return labels, samples def __init__(self, path: str="data/sign_mnist_train.csv", mean: List[float]=[0.485], std: List[float]=[0.229]): """ Args: path: Path to `.csv` file containing `label`, `pixel0`, `pixel1`... """ labels, samples = SignLanguageMNIST.read_label_samples_from_csv(path) self._samples = np.array(samples, dtype=np.uint8).reshape((-1, 28, 28, 1)) self._labels = np.array(labels, dtype=np.uint8).reshape((-1, 1)) self._mean = mean self._std = std def __len__(self): return len(self._labels) def __getitem__(self, idx): transform = transforms.Compose([ transforms.ToPILImage(), transforms.RandomResizedCrop(28, scale=(0.8, 1.2)), transforms.ToTensor(), transforms.Normalize(mean=self._mean, std=self._std)]) return { 'image': transform(self._samples[idx]).float(), 'label': torch.from_numpy(self._labels[idx]).float() }
和以前一样,您现在将通过加载 SignLanguageMNIST
数据集来验证我们的数据集实用程序功能。 在 SignLanguageMNIST
类之后将以下代码添加到文件末尾:
step_2_dataset.py
def get_train_test_loaders(batch_size=32): trainset = SignLanguageMNIST('data/sign_mnist_train.csv') trainloader = torch.utils.data.DataLoader(trainset, batch_size=batch_size, shuffle=True) testset = SignLanguageMNIST('data/sign_mnist_test.csv') testloader = torch.utils.data.DataLoader(testset, batch_size=batch_size, shuffle=False) return trainloader, testloader
此代码使用 SignLanguageMNIST
类初始化数据集。 然后对于训练集和验证集,它将数据集包装在 DataLoader
中。 这会将数据集转换为可迭代以供以后使用。
现在您将验证数据集实用程序是否正常运行。 使用 DataLoader
创建示例数据集加载器并打印该加载器的第一个元素。 将以下内容添加到文件末尾:
step_2_dataset.py
if __name__ == '__main__': loader, _ = get_train_test_loaders(2) print(next(iter(loader)))
您可以检查您的文件是否与此 (repository) 中的 step_2_dataset
文件匹配。 退出编辑器并使用以下命令运行脚本:
python step_2_dataset.py
这将输出以下一对张量。 我们的数据管道输出两个样本和两个标签。 这表明我们的数据管道已启动并准备就绪:
Output{'image': tensor([[[[ 0.4337, 0.5022, 0.5707, ..., 0.9988, 0.9646, 0.9646], [ 0.4851, 0.5536, 0.6049, ..., 1.0502, 1.0159, 0.9988], [ 0.5364, 0.6049, 0.6392, ..., 1.0844, 1.0844, 1.0673], ..., [-0.5253, -0.4739, -0.4054, ..., 0.9474, 1.2557, 1.2385], [-0.3369, -0.3369, -0.3369, ..., 0.0569, 1.3584, 1.3242], [-0.3712, -0.3369, -0.3198, ..., 0.5364, 0.5364, 1.4783]]], [[[ 0.2111, 0.2796, 0.3481, ..., 0.2453, -0.1314, -0.2342], [ 0.2624, 0.3309, 0.3652, ..., -0.3883, -0.0629, -0.4568], [ 0.3309, 0.3823, 0.4337, ..., -0.4054, -0.0458, -1.0048], ..., [ 1.3242, 1.3584, 1.3927, ..., -0.4054, -0.4568, 0.0227], [ 1.3242, 1.3927, 1.4612, ..., -0.1657, -0.6281, -0.0287], [ 1.3242, 1.3927, 1.4440, ..., -0.4397, -0.6452, -0.2856]]]]), 'label': tensor([[24.], [11.]])}
您现在已验证您的数据管道是否正常工作。 第一步——预处理数据——到此结束,现在包括数据增强以提高模型的稳健性。 接下来,您将定义神经网络和优化器。
第 3 步——使用深度学习构建和训练手语分类器
使用功能正常的数据管道,您现在将定义一个模型并在数据上对其进行训练。 特别是,您将构建一个有六层的神经网络,定义损失、优化器,最后为您的神经网络预测优化损失函数。 在这一步结束时,您将拥有一个有效的手语分类器。
创建一个名为 step_3_train.py
的新文件:
nano step_3_train.py
导入必要的实用程序:
step_3_train.py
from torch.utils.data import Dataset from torch.autograd import Variable import torch.nn as nn import torch.nn.functional as F import torch.optim as optim import torch from step_2_dataset import get_train_test_loaders
定义一个 PyTorch 神经网络,其中包括三个卷积层,然后是三个全连接层。 将此添加到现有脚本的末尾:
step_3_train.py
class Net(nn.Module): def __init__(self): super(Net, self).__init__() self.conv1 = nn.Conv2d(1, 6, 3) self.pool = nn.MaxPool2d(2, 2) self.conv2 = nn.Conv2d(6, 6, 3) self.conv3 = nn.Conv2d(6, 16, 3) self.fc1 = nn.Linear(16 * 5 * 5, 120) self.fc2 = nn.Linear(120, 48) self.fc3 = nn.Linear(48, 24) def forward(self, x): x = F.relu(self.conv1(x)) x = self.pool(F.relu(self.conv2(x))) x = self.pool(F.relu(self.conv3(x))) x = x.view(-1, 16 * 5 * 5) x = F.relu(self.fc1(x)) x = F.relu(self.fc2(x)) x = self.fc3(x) return x
现在初始化神经网络,定义损失函数,并通过在脚本末尾添加以下代码来定义优化超参数:
step_3_train.py
def main(): net = Net().float() criterion = nn.CrossEntropyLoss() optimizer = optim.SGD(net.parameters(), lr=0.01, momentum=0.9)
最后,您将训练两个 epochs:
step_3_train.py
def main(): net = Net().float() criterion = nn.CrossEntropyLoss() optimizer = optim.SGD(net.parameters(), lr=0.01, momentum=0.9) trainloader, _ = get_train_test_loaders() for epoch in range(2): # loop over the dataset multiple times train(net, criterion, optimizer, trainloader, epoch) torch.save(net.state_dict(), "checkpoint.pth")
您将 epoch 定义为训练的迭代,其中每个训练样本都只使用了一次。 在 main 函数结束时,模型参数将被保存到一个名为 "checkpoint.pth"
的文件中。
将以下代码添加到脚本的末尾,以从数据集加载器中提取 image
和 label
,然后将它们分别包装在 PyTorch Variable
中:
step_3_train.py
def train(net, criterion, optimizer, trainloader, epoch): running_loss = 0.0 for i, data in enumerate(trainloader, 0): inputs = Variable(data['image'].float()) labels = Variable(data['label'].long()) optimizer.zero_grad() # forward + backward + optimize outputs = net(inputs) loss = criterion(outputs, labels[:, 0]) loss.backward() optimizer.step() # print statistics running_loss += loss.item() if i % 100 == 0: print('[%d, %5d] loss: %.6f' % (epoch, i, running_loss / (i + 1)))
此代码还将运行前向传播,然后通过损失和神经网络进行反向传播。
在文件末尾,添加以下内容以调用 main
函数:
step_3_train.py
if __name__ == '__main__': main()
仔细检查您的文件是否与以下内容匹配:
step_3_train.py
from torch.utils.data import Dataset from torch.autograd import Variable import torch.nn as nn import torch.nn.functional as F import torch.optim as optim import torch from step_2_dataset import get_train_test_loaders class Net(nn.Module): def __init__(self): super(Net, self).__init__() self.conv1 = nn.Conv2d(1, 6, 3) self.pool = nn.MaxPool2d(2, 2) self.conv2 = nn.Conv2d(6, 6, 3) self.conv3 = nn.Conv2d(6, 16, 3) self.fc1 = nn.Linear(16 * 5 * 5, 120) self.fc2 = nn.Linear(120, 48) self.fc3 = nn.Linear(48, 25) def forward(self, x): x = F.relu(self.conv1(x)) x = self.pool(F.relu(self.conv2(x))) x = self.pool(F.relu(self.conv3(x))) x = x.view(-1, 16 * 5 * 5) x = F.relu(self.fc1(x)) x = F.relu(self.fc2(x)) x = self.fc3(x) return x def main(): net = Net().float() criterion = nn.CrossEntropyLoss() optimizer = optim.SGD(net.parameters(), lr=0.01, momentum=0.9) trainloader, _ = get_train_test_loaders() for epoch in range(2): # loop over the dataset multiple times train(net, criterion, optimizer, trainloader, epoch) torch.save(net.state_dict(), "checkpoint.pth") def train(net, criterion, optimizer, trainloader, epoch): running_loss = 0.0 for i, data in enumerate(trainloader, 0): inputs = Variable(data['image'].float()) labels = Variable(data['label'].long()) optimizer.zero_grad() # forward + backward + optimize outputs = net(inputs) loss = criterion(outputs, labels[:, 0]) loss.backward() optimizer.step() # print statistics running_loss += loss.item() if i % 100 == 0: print('[%d, %5d] loss: %.6f' % (epoch, i, running_loss / (i + 1))) if __name__ == '__main__': main()
保存并退出。 然后,通过运行以下命令启动我们的概念验证培训:
python step_3_train.py
当神经网络训练时,您将看到类似于以下内容的输出:
Output[0, 0] loss: 3.208171 [0, 100] loss: 3.211070 [0, 200] loss: 3.192235 [0, 300] loss: 2.943867 [0, 400] loss: 2.569440 [0, 500] loss: 2.243283 [0, 600] loss: 1.986425 [0, 700] loss: 1.768090 [0, 800] loss: 1.587308 [1, 0] loss: 0.254097 [1, 100] loss: 0.208116 [1, 200] loss: 0.196270 [1, 300] loss: 0.183676 [1, 400] loss: 0.169824 [1, 500] loss: 0.157704 [1, 600] loss: 0.151408 [1, 700] loss: 0.136470 [1, 800] loss: 0.123326
为了获得更低的损失,您可以将 epoch 的数量增加到 5、10 甚至 20。 但是,经过一定的训练时间后,网络损失将不再随着训练时间的增加而减少。 为了回避这个问题,随着训练时间的增加,您将引入学习率计划,该计划会随着时间的推移降低学习率。 要了解这为何有效,请参阅 Distill 在 “为什么 Momentum 真正有效” 中的可视化。
使用以下两行修改 main
函数,定义 scheduler
并调用 scheduler.step
。 此外,将 epoch 数更改为 12
:
step_3_train.py
def main(): net = Net().float() criterion = nn.CrossEntropyLoss() optimizer = optim.SGD(net.parameters(), lr=0.01, momentum=0.9) scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.1) trainloader, _ = get_train_test_loaders() for epoch in range(12): # loop over the dataset multiple times train(net, criterion, optimizer, trainloader, epoch) scheduler.step() torch.save(net.state_dict(), "checkpoint.pth")
检查您的文件是否与此 存储库 中的步骤 3 文件匹配。 培训将持续约 5 分钟。 您的输出将类似于以下内容:
Output[0, 0] loss: 3.208171 [0, 100] loss: 3.211070 [0, 200] loss: 3.192235 [0, 300] loss: 2.943867 [0, 400] loss: 2.569440 [0, 500] loss: 2.243283 [0, 600] loss: 1.986425 [0, 700] loss: 1.768090 [0, 800] loss: 1.587308 ... [11, 0] loss: 0.000302 [11, 100] loss: 0.007548 [11, 200] loss: 0.009005 [11, 300] loss: 0.008193 [11, 400] loss: 0.007694 [11, 500] loss: 0.008509 [11, 600] loss: 0.008039 [11, 700] loss: 0.007524 [11, 800] loss: 0.007608
得到的最终损失为0.007608
,比起始损失3.20
小3个数量级。 我们工作流程的第二步到此结束,我们在此设置和训练神经网络。 话虽如此,只要这个损失值很小,它就没有什么意义。 为了透视模型的性能,我们将计算其准确性——模型正确分类的图像的百分比。
第 4 步——评估手语分类器
现在,您将通过计算 验证集 上的准确度来评估您的手语分类器,这是模型在训练期间没有看到的一组图像。 这将提供比最终损失值更好的模型性能。 此外,您将添加实用程序以在训练结束时保存我们的训练模型,并在执行推理时加载我们的预训练模型。
创建一个名为 step_4_evaluate.py
的新文件。
nano step_4_evaluate.py
导入必要的实用程序:
step_4_evaluate.py
from torch.utils.data import Dataset from torch.autograd import Variable import torch.nn as nn import torch.nn.functional as F import torch.optim as optim import torch import numpy as np import onnx import onnxruntime as ort from step_2_dataset import get_train_test_loaders from step_3_train import Net
接下来,定义一个实用程序来评估神经网络的性能。 对于单个图像,以下函数将神经网络的预测字母与真实字母进行比较:
step_4_evaluate.py
def evaluate(outputs: Variable, labels: Variable) -> float: """Evaluate neural network outputs against non-one-hotted labels.""" Y = labels.numpy() Yhat = np.argmax(outputs, axis=1) return float(np.sum(Yhat == Y))
outputs
是每个样本的类别概率列表。 例如,单个样本的 outputs
可能是 [0.1, 0.3, 0.4, 0.2]
。 labels
是标签类别的列表。 例如,标注分类可能是 3
。
Y = ...
将标签转换为 NumPy 数组。 接下来,Yhat = np.argmax(...)
将 outputs
类概率转换为预测类。 例如,类别概率列表 [0.1, 0.3, 0.4, 0.2]
将产生预测类别 2
,因为索引 2 的值 0.4 是最大值。
由于 Y
和 Yhat
现在都是类,因此您可以比较它们。 Yhat == Y
检查预测类别是否与标签类别匹配,np.sum(...)
是一种计算真值数量的技巧。 换句话说,np.sum
将输出正确分类的样本数。
添加第二个函数 batch_evaluate
,它将第一个函数 evaluate
应用于所有图像:
step_4_evaluate.py
def batch_evaluate( net: Net, dataloader: torch.utils.data.DataLoader) -> float: """Evaluate neural network in batches, if dataset is too large.""" score = n = 0.0 for batch in dataloader: n += len(batch['image']) outputs = net(batch['image']) if isinstance(outputs, torch.Tensor): outputs = outputs.detach().numpy() score += evaluate(outputs, batch['label'][:, 0]) return score / n
batch
是存储为单个张量的一组图像。 首先,将要评估的图像总数 (n
) 增加此批次中的图像数。 接下来,您使用这批图像 outputs = net(...)
在神经网络上运行推理。 如果需要,类型检查 if isinstance(...)
会转换 NumPy 数组中的输出。 最后,您使用 evaluate
来计算正确分类的样本数。 在函数结束时,您计算正确分类的样本百分比 score / n
。
最后,添加以下脚本以利用上述实用程序:
step_4_evaluate.py
def validate(): trainloader, testloader = get_train_test_loaders() net = Net().float() pretrained_model = torch.load("checkpoint.pth") net.load_state_dict(pretrained_model) print('=' * 10, 'PyTorch', '=' * 10) train_acc = batch_evaluate(net, trainloader) * 100. print('Training accuracy: %.1f' % train_acc) test_acc = batch_evaluate(net, testloader) * 100. print('Validation accuracy: %.1f' % test_acc) if __name__ == '__main__': validate()
这会加载一个预训练的神经网络,并在提供的手语数据集上评估其性能。 具体来说,此处的脚本会输出您用于训练的图像的准确性,以及您为测试目的而放置的一组单独的图像,称为 验证集 。
接下来,您将 PyTorch 模型导出到 ONNX 二进制文件。 然后可以在生产中使用此二进制文件对您的模型进行推理。 最重要的是,运行此二进制文件的代码不需要原始网络定义的副本。 在 validate
函数的末尾,添加以下内容:
step_4_evaluate.py
trainloader, testloader = get_train_test_loaders(1) # export to onnx fname = "signlanguage.onnx" dummy = torch.randn(1, 1, 28, 28) torch.onnx.export(net, dummy, fname, input_names=['input']) # check exported model model = onnx.load(fname) onnx.checker.check_model(model) # check model is well-formed # create runnable session with exported model ort_session = ort.InferenceSession(fname) net = lambda inp: ort_session.run(None, {'input': inp.data.numpy()})[0] print('=' * 10, 'ONNX', '=' * 10) train_acc = batch_evaluate(net, trainloader) * 100. print('Training accuracy: %.1f' % train_acc) test_acc = batch_evaluate(net, testloader) * 100. print('Validation accuracy: %.1f' % test_acc)
这会导出 ONNX 模型,检查导出的模型,然后使用导出的模型运行推理。 仔细检查您的文件是否与此 存储库 中的第 4 步文件匹配:
step_4_evaluate.py
from torch.utils.data import Dataset from torch.autograd import Variable import torch.nn as nn import torch.nn.functional as F import torch.optim as optim import torch import numpy as np import onnx import onnxruntime as ort from step_2_dataset import get_train_test_loaders from step_3_train import Net def evaluate(outputs: Variable, labels: Variable) -> float: """Evaluate neural network outputs against non-one-hotted labels.""" Y = labels.numpy() Yhat = np.argmax(outputs, axis=1) return float(np.sum(Yhat == Y)) def batch_evaluate( net: Net, dataloader: torch.utils.data.DataLoader) -> float: """Evaluate neural network in batches, if dataset is too large.""" score = n = 0.0 for batch in dataloader: n += len(batch['image']) outputs = net(batch['image']) if isinstance(outputs, torch.Tensor): outputs = outputs.detach().numpy() score += evaluate(outputs, batch['label'][:, 0]) return score / n def validate(): trainloader, testloader = get_train_test_loaders() net = Net().float().eval() pretrained_model = torch.load("checkpoint.pth") net.load_state_dict(pretrained_model) print('=' * 10, 'PyTorch', '=' * 10) train_acc = batch_evaluate(net, trainloader) * 100. print('Training accuracy: %.1f' % train_acc) test_acc = batch_evaluate(net, testloader) * 100. print('Validation accuracy: %.1f' % test_acc) trainloader, testloader = get_train_test_loaders(1) # export to onnx fname = "signlanguage.onnx" dummy = torch.randn(1, 1, 28, 28) torch.onnx.export(net, dummy, fname, input_names=['input']) # check exported model model = onnx.load(fname) onnx.checker.check_model(model) # check model is well-formed # create runnable session with exported model ort_session = ort.InferenceSession(fname) net = lambda inp: ort_session.run(None, {'input': inp.data.numpy()})[0] print('=' * 10, 'ONNX', '=' * 10) train_acc = batch_evaluate(net, trainloader) * 100. print('Training accuracy: %.1f' % train_acc) test_acc = batch_evaluate(net, testloader) * 100. print('Validation accuracy: %.1f' % test_acc) if __name__ == '__main__': validate()
要使用和评估上一步的检查点,请运行以下命令:
python step_4_evaluate.py
这将产生类似于以下的输出,确认您导出的模型不仅有效,而且与您的原始 PyTorch 模型一致:
Output========== PyTorch ========== Training accuracy: 99.9 Validation accuracy: 97.4 ========== ONNX ========== Training accuracy: 99.9 Validation accuracy: 97.4
你的神经网络达到了 99.9% 的训练准确率和 97.4% 的验证准确率。 训练和验证准确度之间的差距表明您的模型是 过度拟合 。 这意味着您的模型不是学习可概括的模式,而是记住了训练数据。 要了解过度拟合的含义和原因,请参阅 Understanding Bias-Variance Tradeoffs。
至此,我们完成了一个手语分类器。 从本质上讲,我们的模型几乎可以一直正确地消除符号之间的歧义。 这是一个相当不错的模型,因此我们进入应用程序的最后阶段。 我们将在实时网络摄像头应用程序中使用这个手语分类器。
第 5 步 - 链接相机源
您的下一个目标是将计算机的摄像头连接到您的手语分类器。 您将收集摄像头输入,对显示的手语进行分类,然后将分类后的手语报告给用户。
现在为面部检测器创建一个 Python 脚本。 使用 nano
或您喜欢的文本编辑器创建文件 step_6_camera.py
:
nano step_5_camera.py
将以下代码添加到文件中:
step_5_camera.py
"""Test for sign language classification""" import cv2 import numpy as np import onnxruntime as ort def main(): pass if __name__ == '__main__': main()
此代码导入包含您的图像实用程序的 OpenCV 和 ONNX 运行时,这是您使用模型运行推理所需的全部内容。 其余代码是典型的 Python 程序样板。
现在将 main
函数中的 pass
替换为以下代码,该代码使用您之前训练的参数初始化手语分类器。 另外添加从索引到字母和图像统计信息的映射:
step_5_camera.py
def main(): # constants index_to_letter = list('ABCDEFGHIKLMNOPQRSTUVWXY') mean = 0.485 * 255. std = 0.229 * 255. # create runnable session with exported model ort_session = ort.InferenceSession("signlanguage.onnx")
您将使用官方 OpenCV 文档中的 测试脚本 的元素。 具体来说,您将更新 main
函数的主体。 首先初始化一个 VideoCapture
对象,该对象设置为从您的计算机摄像头捕获实时信息。 将其放在 main
函数的末尾:
step_5_camera.py
def main(): ... # create runnable session with exported model ort_session = ort.InferenceSession("signlanguage.onnx") cap = cv2.VideoCapture(0)
然后添加一个 while
循环,在每个时间步从相机读取:
step_5_camera.py
def main(): ... cap = cv2.VideoCapture(0) while True: # Capture frame-by-frame ret, frame = cap.read()
编写一个实用函数,获取相机帧的中心裁剪。 将此函数放在 main
之前:
step_5_camera.py
def center_crop(frame): h, w, _ = frame.shape start = abs(h - w) // 2 if h > w: frame = frame[start: start + w] else: frame = frame[:, start: start + h] return frame
接下来,对相机框架进行中心裁剪,转换为灰度,归一化,然后调整为 28x28
。 将其放在 main
函数内的 while
循环中:
step_5_camera.py
def main(): ... while True: # Capture frame-by-frame ret, frame = cap.read() # preprocess data frame = center_crop(frame) frame = cv2.cvtColor(frame, cv2.COLOR_RGB2GRAY) x = cv2.resize(frame, (28, 28)) x = (frame - mean) / std
仍在 while
循环中,使用 ONNX 运行时运行推理。 将输出转换为类索引,然后转换为字母:
step_5_camera.py
... x = (frame - mean) / std x = x.reshape(1, 1, 28, 28).astype(np.float32) y = ort_session.run(None, {'input': x})[0] index = np.argmax(y, axis=1) letter = index_to_letter[int(index)]
在框架内显示预测的字母,并将框架显示给用户:
step_5_camera.py
... letter = index_to_letter[int(index)] cv2.putText(frame, letter, (100, 100), cv2.FONT_HERSHEY_SIMPLEX, 2.0, (0, 255, 0), thickness=2) cv2.imshow("Sign Language Translator", frame)
在 while
循环结束时,添加此代码以检查用户是否点击了 q
字符,如果是,则退出应用程序。 此行将程序暂停 1 毫秒。 添加以下内容:
step_5_camera.py
... cv2.imshow("Sign Language Translator", frame) if cv2.waitKey(1) & 0xFF == ord('q'): break
最后,释放捕获并关闭所有窗口。 将它放在 while
循环之外以结束 main
函数。
step_5_camera.py
... while True: ... if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows()
仔细检查您的文件是否匹配以下或此 存储库 :
step_5_camera.py
import cv2 import numpy as np import onnxruntime as ort def center_crop(frame): h, w, _ = frame.shape start = abs(h - w) // 2 if h > w: return frame[start: start + w] return frame[:, start: start + h] def main(): # constants index_to_letter = list('ABCDEFGHIKLMNOPQRSTUVWXY') mean = 0.485 * 255. std = 0.229 * 255. # create runnable session with exported model ort_session = ort.InferenceSession("signlanguage.onnx") cap = cv2.VideoCapture(0) while True: # Capture frame-by-frame ret, frame = cap.read() # preprocess data frame = center_crop(frame) frame = cv2.cvtColor(frame, cv2.COLOR_RGB2GRAY) x = cv2.resize(frame, (28, 28)) x = (x - mean) / std x = x.reshape(1, 1, 28, 28).astype(np.float32) y = ort_session.run(None, {'input': x})[0] index = np.argmax(y, axis=1) letter = index_to_letter[int(index)] cv2.putText(frame, letter, (100, 100), cv2.FONT_HERSHEY_SIMPLEX, 2.0, (0, 255, 0), thickness=2) cv2.imshow("Sign Language Translator", frame) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows() if __name__ == '__main__': main()
退出您的文件并运行脚本。
python step_5_camera.py
脚本运行后,将弹出一个窗口,其中包含您的实时网络摄像头提要。 预测的手语字母将显示在左上角。 举起你的手,做你最喜欢的标志,看看你的分类器在起作用。 下面是一些显示字母 L 和 D 的示例结果。
测试时,请注意背景需要相当清晰才能使此翻译器正常工作。 这是数据集清洁度的不幸结果。 如果数据集包含具有各种背景的手势图像,则该网络将对嘈杂的背景具有鲁棒性。 但是,该数据集具有空白背景和良好居中的手。 因此,当您的手同样居中并放在空白背景上时,此网络摄像头翻译器的效果最佳。
手语翻译应用程序到此结束。
结论
在本教程中,您使用计算机视觉和机器学习模型构建了美国手语翻译器。 特别是,您看到了训练机器学习模型的新方面——特别是用于模型稳健性的数据增强、用于降低损失的学习率计划以及使用 ONNX 导出 AI 模型以供生产使用。 然后最终形成了一个实时计算机视觉应用程序,该应用程序使用您构建的管道将手语翻译成字母。 值得注意的是,可以通过以下任何或所有方法来解决最终分类器的脆性问题。 如需进一步探索,请尝试以下主题以改进您的应用程序:
- 泛化:这不是计算机视觉中的一个子主题,而是贯穿所有机器学习的一个持续问题。 请参阅 了解偏差-方差权衡 。
- 领域适应:假设您的模型是在领域 A 中训练的(例如,阳光充足的环境)。 您能否将模型快速适应域 B(例如,多云环境)?
- 对抗性示例:假设对手故意设计图像以欺骗您的模型。 你怎么能设计这样的图像? 你怎么能对抗这样的图像?