如何构建神经网络将手语翻译成英语

作者选择Code Org作为Write for DOnations计划的一部分接受捐赠。

简介

计算机视觉是计算机科学的一个子领域,旨在从图像和视频中提取更高阶的理解。这为有趣的视频聊天过滤器、移动设备的人脸验证器和自动驾驶汽车等技术提供了动力。

在本教程中,您将使用计算机视觉为网络摄像头构建美式手语翻译器。在学习本教程的过程中,您将使用计算机视觉库 OpenCVPyTorch 构建深度神经网络,并使用 onnx导出神经网络。在构建计算机视觉应用程序时,您还将应用以下概念:

  • 您将使用与 如何应用计算机视觉构建基于情感的狗狗过滤器 教程中相同的三步法:预处理数据集、训练模型和评估模型。
  • 您还将扩展这些步骤中的每一步:使用数据增强来处理旋转或非居中的手,改变学习率计划以提高模型的准确性,以及导出模型以加快推理速度。
  • 在学习过程中,你还将探索机器学习中的相关概念。

本教程结束时,您将拥有一个美国手语翻译器和基础深度学习知识。您还可以访问本项目的 完整源代码

先决条件

要完成本教程,您需要以下材料:

第 1 步 - 创建项目并安装依赖项

让我们为这个项目创建一个工作区,并安装我们需要的依赖项。

在 Linux 发行版上,首先准备好系统软件包管理器,然后安装 Python3 virtualenv 软件包。使用:

1apt-get update
2apt-get upgrade
3apt-get install python3-venv

我们将工作区命名为 "SignLanguage":

1mkdir ~/SignLanguage

导航至 SignLanguage 目录:

1cd ~/SignLanguage

然后为项目创建一个新的虚拟环境:

1python3 -m venv signlanguage

激活你的环境:

1source signlanguage/bin/activate

然后安装 PyTorch,这是我们将在本教程中使用的 Python 深度学习框架。

在 macOS 上,使用以下命令安装 Pytorch:

1python -m pip install torch==1.2.0 torchvision==0.4.0

在 Linux 和 Windows 系统中,使用以下命令进行仅 CPU 的编译:

1pip install torch==1.2.0+cpu torchvision==0.4.0+cpu -f https://download.pytorch.org/whl/torch_stable.html
2pip install torchvision

现在安装 OpenCVnumpyonnx 的预打包二进制文件,它们分别是计算机视觉库、线性代数库、人工智能模型导出库和人工智能模型执行库。OpenCV "提供图像旋转等实用程序,"numpy "提供矩阵反转等线性代数实用程序:

1python -m pip install opencv-python==3.4.3.18 numpy==1.14.5 onnx==1.6.0 onnxruntime==1.0.0

在 Linux 发行版上,您需要安装 libSM.so

1apt-get install libsm6 libxext6 libxrender-dev

安装好依赖项后,让我们构建手语翻译器的第一个版本:手语分类器。

第 2 步 - 准备手语分类数据集

在接下来的三节中,您将使用神经网络建立一个手语分类器。您的目标是建立一个模型,接受手的图片作为输入,并输出一个字母。

建立机器学习分类模型需要以下三个步骤:

1.预处理数据:对标签应用 one-hot encoding,并用 PyTorch Tensors 封装数据。在增强数据上训练模型,使其能够应对 "异常 "输入,如偏离中心或旋转的手。 2.指定并训练模型使用 PyTorch 建立神经网络。定义训练参数,例如训练多长时间,然后运行随机梯度下降。你还将改变一个特定的训练超参数,即学习率计划。这些都将提高模型的准确性。 3.使用模型进行预测:在验证数据上评估神经网络,以了解其准确性。然后,将模型导出为 ONNX 格式,以加快推理速度。

在本节教程中,您将完成 3 个步骤中的第 1 步。您将下载数据,创建 "数据集 "对象以遍历数据,最后应用_数据增强_。在这一步结束时,您将以编程方式访问数据集中的图像和标签,并将其输入到模型中。

首先,将数据集下载到当前工作目录:

<$>[注] 注意 :在 macOS 上,"wget "默认不可用。为此,请安装 Homebrew 按照此 DigitalOcean 教程。然后,运行 brew install wget。 <$>

1wget https://assets.digitalocean.com/articles/signlanguage_data/sign-language-mnist.tar.gz

解压缩 zip 文件,其中包含一个 data/ 目录:

1tar -xzf sign-language-mnist.tar.gz

创建一个新文件,命名为 step_2_dataset.py

1nano step_2_dataset.py

与之前一样,导入必要的实用程序并创建用于保存数据的类。在这里进行数据处理时,您将创建训练数据集和测试数据集。您将实现 PyTorch 的 Dataset 接口,以便加载和使用 PyTorch 内置的数据管道来处理您的手语分类数据集:

 1[label step_2_dataset.py]
 2from torch.utils.data import Dataset
 3from torch.autograd import Variable
 4import torch.nn as nn
 5import numpy as np
 6import torch
 7
 8import csv
 9
10class SignLanguageMNIST(Dataset):
11    """Sign Language classification dataset.
12
13    Utility for loading Sign Language dataset into PyTorch. Dataset posted on
14    Kaggle in 2017, by an unnamed author with username `tecperson`:
15    https://www.kaggle.com/datamunge/sign-language-mnist
16
17    Each sample is 1 x 1 x 28 x 28, and each label is a scalar.
18    """
19    pass

删除 "SignLanguageMNIST "类中的 "pass "占位符。取而代之的是添加一个生成标签映射的方法:

 1[label step_2_dataset.py]
 2    @staticmethod
 3    def get_label_mapping():
 4        """
 5        We map all labels to [0, 23]. This mapping from dataset labels [0, 23]
 6        to letter indices [0, 25] is returned below.
 7        """
 8        mapping = list(range(25))
 9        mapping.pop(9)
10        return mapping

标签范围从 0 到 25。但字母 J (9) 和 Z (25) 不包括在内。这意味着只有 24 个有效的标签值。为了使从 0 开始的所有标签值集合连续,我们将所有标签映射到 [0, 23]。从数据集标签[0, 23]到字母索引[0, 25]的映射由 "get_label_mapping "方法提供。

接下来,添加一个从 CSV 文件中提取标签和样本的方法。以下假设每行以 "标签 "开头,然后是 784 个像素值。这 784 个像素值代表一幅 28x28 图像:

 1[label step_2_dataset.py]
 2    @staticmethod
 3    def read_label_samples_from_csv(path: str):
 4        """
 5        Assumes first column in CSV is the label and subsequent 28^2 values
 6        are image pixel values 0-255.
 7        """
 8        mapping = SignLanguageMNIST.get_label_mapping()
 9        labels, samples = [], []
10        with open(path) as f:
11            _ = next(f)  # skip header
12            for line in csv.reader(f):
13                label = int(line[0])
14                labels.append(mapping.index(label))
15                samples.append(list(map(int, line[1:])))
16        return labels, samples

有关这些 784 值如何表示图像的解释,请参阅构建基于情感的狗狗过滤器,步骤 4

请注意,"csv.reader "迭代器中的每一行都是字符串列表;"int "和 "map(int, ...) "调用会将所有字符串转换为整数。在静态方法的正下方,添加一个函数来初始化我们的数据持有者:

 1[label step_2_dataset.py]
 2    def __init__(self,
 3            path: str="data/sign_mnist_train.csv",
 4            mean: List[float]=[0.485],
 5            std: List[float]=[0.229]):
 6        """
 7        Args:
 8            path: Path to `.csv` file containing `label`, `pixel0`, `pixel1`...
 9        """
10        labels, samples = SignLanguageMNIST.read_label_samples_from_csv(path)
11        self._samples = np.array(samples, dtype=np.uint8).reshape((-1, 28, 28, 1))
12        self._labels = np.array(labels, dtype=np.uint8).reshape((-1, 1))
13
14        self._mean = mean
15        self._std = std

该函数首先加载样本和标签。然后将数据封装到 NumPy 数组中。均值和标准偏差信息将在下面的 __getitem__部分进行解释。

__init__ 函数之后,直接添加一个 __len__ 函数。数据集 "需要这个方法来确定何时停止数据迭代:

1[label step_2_dataset.py]
2...
3    def __len__(self):
4        return len(self._labels)

最后,添加一个 __getitem__ 方法,返回一个包含样本和标签的字典:

 1[label step_2_dataset.py]
 2    def __getitem__(self, idx):
 3        transform = transforms.Compose([
 4            transforms.ToPILImage(),
 5            transforms.RandomResizedCrop(28, scale=(0.8, 1.2)),
 6            transforms.ToTensor(),
 7            transforms.Normalize(mean=self._mean, std=self._std)])
 8
 9        return {
10            'image': transform(self._samples[idx]).float(),
11            'label': torch.from_numpy(self._labels[idx]).float()
12        }

您使用了一种名为 "数据增强 "的技术,即在训练过程中对样本进行扰动,以增强模型对这些扰动的鲁棒性。特别是,通过 "RandomResizedCrop "随机放大图像的不同大小和不同位置。请注意,放大不会影响最终的手语类别;因此,标签不会被转换。此外,还要对输入进行归一化处理,以便将图像值重新缩放到期望值 [0, 1] 范围内,而不是 [0, 255];要做到这一点,在归一化处理时要使用数据集 _mean_std

完成后的 SignLanguageMNIST 类将如下所示:

 1[label step_2_dataset.py]
 2from torch.utils.data import Dataset
 3from torch.autograd import Variable
 4import torchvision.transforms as transforms
 5import torch.nn as nn
 6import numpy as np
 7import torch
 8
 9from typing import List
10
11import csv
12
13class SignLanguageMNIST(Dataset):
14    """Sign Language classification dataset.
15
16    Utility for loading Sign Language dataset into PyTorch. Dataset posted on
17    Kaggle in 2017, by an unnamed author with username `tecperson`:
18    https://www.kaggle.com/datamunge/sign-language-mnist
19
20    Each sample is 1 x 1 x 28 x 28, and each label is a scalar.
21    """
22
23    @staticmethod
24    def get_label_mapping():
25        """
26        We map all labels to [0, 23]. This mapping from dataset labels [0, 23]
27        to letter indices [0, 25] is returned below.
28        """
29        mapping = list(range(25))
30        mapping.pop(9)
31        return mapping
32
33    @staticmethod
34    def read_label_samples_from_csv(path: str):
35        """
36        Assumes first column in CSV is the label and subsequent 28^2 values
37        are image pixel values 0-255.
38        """
39        mapping = SignLanguageMNIST.get_label_mapping()
40        labels, samples = [], []
41        with open(path) as f:
42            _ = next(f)  # skip header
43            for line in csv.reader(f):
44                label = int(line[0])
45                labels.append(mapping.index(label))
46                samples.append(list(map(int, line[1:])))
47        return labels, samples
48
49    def __init__(self,
50            path: str="data/sign_mnist_train.csv",
51            mean: List[float]=[0.485],
52            std: List[float]=[0.229]):
53        """
54        Args:
55            path: Path to `.csv` file containing `label`, `pixel0`, `pixel1`...
56        """
57        labels, samples = SignLanguageMNIST.read_label_samples_from_csv(path)
58        self._samples = np.array(samples, dtype=np.uint8).reshape((-1, 28, 28, 1))
59        self._labels = np.array(labels, dtype=np.uint8).reshape((-1, 1))
60
61        self._mean = mean
62        self._std = std
63
64    def __len__(self):
65        return len(self._labels)
66
67    def __getitem__(self, idx):
68        transform = transforms.Compose([
69            transforms.ToPILImage(),
70            transforms.RandomResizedCrop(28, scale=(0.8, 1.2)),
71            transforms.ToTensor(),
72            transforms.Normalize(mean=self._mean, std=self._std)])
73
74        return {
75            'image': transform(self._samples[idx]).float(),
76            'label': torch.from_numpy(self._labels[idx]).float()
77        }

和以前一样,现在您将通过加载 SignLanguageMNIST 数据集来验证我们的数据集实用程序函数。在文件末尾的 SignLanguageMNIST 类之后添加以下代码:

1[label step_2_dataset.py]
2def get_train_test_loaders(batch_size=32):
3    trainset = SignLanguageMNIST('data/sign_mnist_train.csv')
4    trainloader = torch.utils.data.DataLoader(trainset, batch_size=batch_size, shuffle=True)
5
6    testset = SignLanguageMNIST('data/sign_mnist_test.csv')
7    testloader = torch.utils.data.DataLoader(testset, batch_size=batch_size, shuffle=False)
8    return trainloader, testloader

这段代码使用 "SignLanguageMNIST "类初始化数据集。然后,对于训练集和验证集,它将数据集封装在一个 DataLoader 中。这将把数据集转化为可迭代数据,以便稍后使用。

现在,您将验证数据集实用程序是否正常运行。使用 DataLoader 创建一个示例数据集加载器,并打印该加载器的第一个元素。在文件末尾添加以下内容:

1[label step_2_dataset.py]
2if __name__ == '__main__':
3    loader, _ = get_train_test_loaders(2)
4    print(next(iter(loader)))

你可以在此 (repository) 中检查你的文件是否与 step_2_dataset 文件匹配。退出编辑器,运行脚本:

1python step_2_dataset.py

这将输出以下一对张量。我们的数据管道输出了两个样本和两个标签。这表明我们的数据管道已经启动并准备就绪:

 1[secondary_label Output]
 2{'image': tensor([[[[ 0.4337, 0.5022, 0.5707,  ..., 0.9988, 0.9646, 0.9646],
 3          [ 0.4851, 0.5536, 0.6049,  ..., 1.0502, 1.0159, 0.9988],
 4          [ 0.5364, 0.6049, 0.6392,  ..., 1.0844, 1.0844, 1.0673],
 5          ...,
 6          [-0.5253, -0.4739, -0.4054,  ..., 0.9474, 1.2557, 1.2385],
 7          [-0.3369, -0.3369, -0.3369,  ..., 0.0569, 1.3584, 1.3242],
 8          [-0.3712, -0.3369, -0.3198,  ..., 0.5364, 0.5364, 1.4783]]],
 9
10        [[[ 0.2111, 0.2796, 0.3481,  ..., 0.2453, -0.1314, -0.2342],
11          [ 0.2624, 0.3309, 0.3652,  ..., -0.3883, -0.0629, -0.4568],
12          [ 0.3309, 0.3823, 0.4337,  ..., -0.4054, -0.0458, -1.0048],
13          ...,
14          [ 1.3242, 1.3584, 1.3927,  ..., -0.4054, -0.4568, 0.0227],
15          [ 1.3242, 1.3927, 1.4612,  ..., -0.1657, -0.6281, -0.0287],
16          [ 1.3242, 1.3927, 1.4440,  ..., -0.4397, -0.6452, -0.2856]]]]), 'label': tensor([[24.],
17        [11.]])}

现在,您已经验证了数据管道的有效性。第一步--预处理数据--到此为止,这一步包括增强数据以提高模型的稳健性。接下来,您将定义神经网络和优化器。

第 3 步 - 使用深度学习构建和训练手语分类器

有了正常运行的数据管道,您现在将定义一个模型,并在数据上对其进行训练。具体来说,您将建立一个有六层的神经网络,定义损失和优化器,最后优化神经网络预测的损失函数。在这一步结束时,您将拥有一个可以正常工作的手语分类器。

新建一个名为 step_3_train.py 的文件:

1nano step_3_train.py

导入必要的实用程序:

1[label step_3_train.py]
2from torch.utils.data import Dataset
3from torch.autograd import Variable
4import torch.nn as nn
5import torch.nn.functional as F
6import torch.optim as optim
7import torch
8
9from step_2_dataset import get_train_test_loaders

定义一个 PyTorch 神经网络,其中包括三个卷积层,然后是三个全连接层。将其添加到现有脚本的末尾:

 1[label step_3_train.py]
 2class Net(nn.Module):
 3    def __init__(self):
 4        super(Net, self).__init__()
 5        self.conv1 = nn.Conv2d(1, 6, 3)
 6        self.pool = nn.MaxPool2d(2, 2)
 7        self.conv2 = nn.Conv2d(6, 6, 3)
 8        self.conv3 = nn.Conv2d(6, 16, 3)
 9        self.fc1 = nn.Linear(16 * 5 * 5, 120)
10        self.fc2 = nn.Linear(120, 48)
11        self.fc3 = nn.Linear(48, 24)
12
13    def forward(self, x):
14        x = F.relu(self.conv1(x))
15        x = self.pool(F.relu(self.conv2(x)))
16        x = self.pool(F.relu(self.conv3(x)))
17        x = x.view(-1, 16 * 5 * 5)
18        x = F.relu(self.fc1(x))
19        x = F.relu(self.fc2(x))
20        x = self.fc3(x)
21        return x

现在,通过在脚本末尾添加以下代码来初始化神经网络、定义损失函数和优化超参数:

1[label step_3_train.py]
2def main():
3    net = Net().float()
4    criterion = nn.CrossEntropyLoss()
5    optimizer = optim.SGD(net.parameters(), lr=0.01, momentum=0.9)

最后,您将接受两个阶段的训练:

 1[label step_3_train.py]
 2def main():
 3    net = Net().float()
 4    criterion = nn.CrossEntropyLoss()
 5    optimizer = optim.SGD(net.parameters(), lr=0.01, momentum=0.9)
 6
 7    trainloader, _ = get_train_test_loaders()
 8    for epoch in range(2):  # loop over the dataset multiple times
 9        train(net, criterion, optimizer, trainloader, epoch)
10    torch.save(net.state_dict(), "checkpoint.pth")

您可以定义一个_epoch______________________________迭代训练,其中每个训练样本都被精确使用过一次。在主函数结束时,模型参数将被保存到一个名为 "checkpoint.pth "的文件中。

在脚本末尾添加以下代码,从数据集加载器中提取 imagelabel,然后用 PyTorch Variable 封装:

 1[label step_3_train.py]
 2def train(net, criterion, optimizer, trainloader, epoch):
 3    running_loss = 0.0
 4    for i, data in enumerate(trainloader, 0):
 5        inputs = Variable(data['image'].float())
 6        labels = Variable(data['label'].long())
 7        optimizer.zero_grad()
 8
 9        # forward + backward + optimize
10        outputs = net(inputs)
11        loss = criterion(outputs, labels[:, 0])
12        loss.backward()
13        optimizer.step()
14
15        # print statistics
16        running_loss += loss.item()
17        if i % 100 == 0:
18            print('[%d, %5d] loss: %.6f' % (epoch, i, running_loss / (i + 1)))

该代码还将运行前向传递,然后通过损失和神经网络进行反向传播。

在文件末尾添加以下内容,以调用 main 函数:

1[label step_3_train.py]
2if __name__ == '__main__':
3    main()

仔细检查您的文件是否与以下内容一致:

 1[label step_3_train.py]
 2from torch.utils.data import Dataset
 3from torch.autograd import Variable
 4import torch.nn as nn
 5import torch.nn.functional as F
 6import torch.optim as optim
 7import torch
 8
 9from step_2_dataset import get_train_test_loaders
10
11class Net(nn.Module):
12    def __init__(self):
13        super(Net, self).__init__()
14        self.conv1 = nn.Conv2d(1, 6, 3)
15        self.pool = nn.MaxPool2d(2, 2)
16        self.conv2 = nn.Conv2d(6, 6, 3)
17        self.conv3 = nn.Conv2d(6, 16, 3)
18        self.fc1 = nn.Linear(16 * 5 * 5, 120)
19        self.fc2 = nn.Linear(120, 48)
20        self.fc3 = nn.Linear(48, 25)
21
22    def forward(self, x):
23        x = F.relu(self.conv1(x))
24        x = self.pool(F.relu(self.conv2(x)))
25        x = self.pool(F.relu(self.conv3(x)))
26        x = x.view(-1, 16 * 5 * 5)
27        x = F.relu(self.fc1(x))
28        x = F.relu(self.fc2(x))
29        x = self.fc3(x)
30        return x
31
32def main():
33    net = Net().float()
34    criterion = nn.CrossEntropyLoss()
35    optimizer = optim.SGD(net.parameters(), lr=0.01, momentum=0.9)
36
37    trainloader, _ = get_train_test_loaders()
38    for epoch in range(2):  # loop over the dataset multiple times
39        train(net, criterion, optimizer, trainloader, epoch)
40    torch.save(net.state_dict(), "checkpoint.pth")
41
42def train(net, criterion, optimizer, trainloader, epoch):
43    running_loss = 0.0
44    for i, data in enumerate(trainloader, 0):
45        inputs = Variable(data['image'].float())
46        labels = Variable(data['label'].long())
47        optimizer.zero_grad()
48
49        # forward + backward + optimize
50        outputs = net(inputs)
51        loss = criterion(outputs, labels[:, 0])
52        loss.backward()
53        optimizer.step()
54
55        # print statistics
56        running_loss += loss.item()
57        if i % 100 == 0:
58            print('[%d, %5d] loss: %.6f' % (epoch, i, running_loss / (i + 1)))
59
60if __name__ == '__main__':
61    main()

保存并退出。然后,运行我们的概念验证培训:

1python step_3_train.py

在神经网络训练过程中,你会看到类似下面的输出结果:

 1[secondary_label Output]
 2[0, 0] loss: 3.208171
 3[0, 100] loss: 3.211070
 4[0, 200] loss: 3.192235
 5[0, 300] loss: 2.943867
 6[0, 400] loss: 2.569440
 7[0, 500] loss: 2.243283
 8[0, 600] loss: 1.986425
 9[0, 700] loss: 1.768090
10[0, 800] loss: 1.587308
11[1, 0] loss: 0.254097
12[1, 100] loss: 0.208116
13[1, 200] loss: 0.196270
14[1, 300] loss: 0.183676
15[1, 400] loss: 0.169824
16[1, 500] loss: 0.157704
17[1, 600] loss: 0.151408
18[1, 700] loss: 0.136470
19[1, 800] loss: 0.123326

为了降低损失,可以将历元数增加到 5、10 甚至 20。但是,训练一段时间后,网络损耗将不再随着训练时间的增加而降低。为了避免这个问题,随着训练时间的增加,可以引入学习率计划,随着时间的推移降低学习率。要了解这种方法的原理,请参阅 Distill 的可视化演示:"为什么动量真的有效"

用以下两行修改你的 main 函数,定义一个 scheduler 并调用 scheduler.step。此外,将历时数改为 12

 1[label step_3_train.py]
 2def main():
 3    net = Net().float()
 4    criterion = nn.CrossEntropyLoss()
 5    optimizer = optim.SGD(net.parameters(), lr=0.01, momentum=0.9)
 6    scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.1)
 7
 8    trainloader, _ = get_train_test_loaders()
 9    for epoch in range(12):  # loop over the dataset multiple times
10        train(net, criterion, optimizer, trainloader, epoch)
11        scheduler.step()
12    torch.save(net.state_dict(), "checkpoint.pth")

检查您的文件是否与此 资源库中的第 3 步文件一致。训练将持续约 5 分钟。输出结果如下:

 1[secondary_label Output]
 2[0, 0] loss: 3.208171
 3[0, 100] loss: 3.211070
 4[0, 200] loss: 3.192235
 5[0, 300] loss: 2.943867
 6[0, 400] loss: 2.569440
 7[0, 500] loss: 2.243283
 8[0, 600] loss: 1.986425
 9[0, 700] loss: 1.768090
10[0, 800] loss: 1.587308
11...
12[11, 0] loss: 0.000302
13[11, 100] loss: 0.007548
14[11, 200] loss: 0.009005
15[11, 300] loss: 0.008193
16[11, 400] loss: 0.007694
17[11, 500] loss: 0.008509
18[11, 600] loss: 0.008039
19[11, 700] loss: 0.007524
20[11, 800] loss: 0.007608

最终得到的损失为 "0.007608",比起始损失 "3.20 "小了 3 个数量级。至此,工作流程的第二步--建立和训练神经网络--结束。尽管这个损失值很小,但它的意义并不大。为了正确看待模型的性能,我们将计算其准确率--模型正确分类图像的百分比。

步骤 4 - 评估手语分类器

现在,您将通过计算验证集(模型在训练过程中没有看到的一组图像)上的准确率来评估您的手语分类器。这将比最终损失值更能反映模型的性能。此外,您还将添加实用程序,以便在训练结束时保存训练好的模型,并在执行推理时加载预先训练好的模型。

创建一个新文件,名为 step_4_evaluate.py

1nano step_4_evaluate.py

导入必要的实用程序:

 1[label step_4_evaluate.py]
 2from torch.utils.data import Dataset
 3from torch.autograd import Variable
 4import torch.nn as nn
 5import torch.nn.functional as F
 6import torch.optim as optim
 7import torch
 8import numpy as np
 9
10import onnx
11import onnxruntime as ort
12
13from step_2_dataset import get_train_test_loaders
14from step_3_train import Net

接下来,定义一个实用程序来评估神经网络的性能。下面的函数将神经网络预测的字母与真实的字母进行了比较:

1[label step_4_evaluate.py]
2def evaluate(outputs: Variable, labels: Variable) -> float:
3    """Evaluate neural network outputs against non-one-hotted labels."""
4    Y = labels.numpy()
5    Yhat = np.argmax(outputs, axis=1)
6    return float(np.sum(Yhat == Y))

输出 "是每个样本的类别概率列表。例如,单个样本的 outputs 可以是 [0.1,0.3,0.4,0.2]。标签 "是标签类别的列表。例如,标签类别可以是 3

Y = ... "将标签转换为 NumPy 数组。接下来,Yhat = np.argmax(...)输出类别概率转换为预测类别。例如,类别概率列表[0.1, 0.3, 0.4, 0.2]将产生预测类别2`,因为索引 2 的值 0.4 是最大值。

由于 YYhat 现在都是类,因此可以对它们进行比较。Yhat == Y 检查预测的类别是否与标签类别相匹配,而 np.sum(...) 则是计算真-Y 值数量的技巧。换句话说,np.sum 会输出正确分类的样本数。

添加第二个函数 batch_evaluate,它将第一个函数 evaluate 应用于所有图像:

 1[label step_4_evaluate.py]
 2def batch_evaluate(
 3        net: Net,
 4        dataloader: torch.utils.data.DataLoader) -> float:
 5    """Evaluate neural network in batches, if dataset is too large."""
 6    score = n = 0.0
 7    for batch in dataloader:
 8        n += len(batch['image'])
 9        outputs = net(batch['image'])
10        if isinstance(outputs, torch.Tensor):
11            outputs = outputs.detach().numpy()
12        score += evaluate(outputs, batch['label'][:, 0])
13    return score / n

批次 "是以单个张量存储的一组图像。首先,将评估图像的总数(n)按这批图像的数量递增。然后,利用这批图像在神经网络上运行推理,即 outputs = net(...)。如果需要,类型检查 if isinstance(...) 会将输出转换为 NumPy 数组。最后,使用 evaluate 计算正确分类的样本数。函数结束时,计算正确分类样本的百分比,即 score / n

最后,添加以下脚本以利用前面的实用程序:

 1[label step_4_evaluate.py]
 2def validate():
 3    trainloader, testloader = get_train_test_loaders()
 4    net = Net().float()
 5
 6    pretrained_model = torch.load("checkpoint.pth")
 7    net.load_state_dict(pretrained_model)
 8
 9    print('=' * 10, 'PyTorch', '=' * 10)
10    train_acc = batch_evaluate(net, trainloader) * 100.
11    print('Training accuracy: %.1f' % train_acc)
12    test_acc = batch_evaluate(net, testloader) * 100.
13    print('Validation accuracy: %.1f' % test_acc)
14
15if __name__ == '__main__':
16    validate()

该脚本将加载预训练的神经网络,并在提供的手语数据集上评估其性能。具体来说,脚本会输出用于训练的图像和用于测试的单独图像集(称为_验证集_)的准确性。

接下来,您将把 PyTorch 模型导出为 ONNX 二进制文件。然后,这个二进制文件就可以在生产中使用,用你的模型运行推理。最重要的是,运行这个二进制文件的代码不需要原始网络定义的副本。在 validate 函数的末尾添加以下内容:

 1[label step_4_evaluate.py]
 2    trainloader, testloader = get_train_test_loaders(1)
 3
 4    # export to onnx
 5    fname = "signlanguage.onnx"
 6    dummy = torch.randn(1, 1, 28, 28)
 7    torch.onnx.export(net, dummy, fname, input_names=['input'])
 8
 9    # check exported model
10    model = onnx.load(fname)
11    onnx.checker.check_model(model)  # check model is well-formed
12
13    # create runnable session with exported model
14    ort_session = ort.InferenceSession(fname)
15    net = lambda inp: ort_session.run(None, {'input': inp.data.numpy()})[0]
16
17    print('=' * 10, 'ONNX', '=' * 10)
18    train_acc = batch_evaluate(net, trainloader) * 100.
19    print('Training accuracy: %.1f' % train_acc)
20    test_acc = batch_evaluate(net, testloader) * 100.
21    print('Validation accuracy: %.1f' % test_acc)

这将导出 ONNX 模型,检查导出的模型,然后使用导出的模型运行推理。请仔细检查您的文件是否与该 资源库中的第 4 步文件一致:

 1[label step_4_evaluate.py]
 2from torch.utils.data import Dataset
 3from torch.autograd import Variable
 4import torch.nn as nn
 5import torch.nn.functional as F
 6import torch.optim as optim
 7import torch
 8import numpy as np
 9
10import onnx
11import onnxruntime as ort
12
13from step_2_dataset import get_train_test_loaders
14from step_3_train import Net
15
16def evaluate(outputs: Variable, labels: Variable) -> float:
17    """Evaluate neural network outputs against non-one-hotted labels."""
18    Y = labels.numpy()
19    Yhat = np.argmax(outputs, axis=1)
20    return float(np.sum(Yhat == Y))
21
22def batch_evaluate(
23        net: Net,
24        dataloader: torch.utils.data.DataLoader) -> float:
25    """Evaluate neural network in batches, if dataset is too large."""
26    score = n = 0.0
27    for batch in dataloader:
28        n += len(batch['image'])
29        outputs = net(batch['image'])
30        if isinstance(outputs, torch.Tensor):
31            outputs = outputs.detach().numpy()
32        score += evaluate(outputs, batch['label'][:, 0])
33    return score / n
34
35def validate():
36    trainloader, testloader = get_train_test_loaders()
37    net = Net().float().eval()
38
39    pretrained_model = torch.load("checkpoint.pth")
40    net.load_state_dict(pretrained_model)
41
42    print('=' * 10, 'PyTorch', '=' * 10)
43    train_acc = batch_evaluate(net, trainloader) * 100.
44    print('Training accuracy: %.1f' % train_acc)
45    test_acc = batch_evaluate(net, testloader) * 100.
46    print('Validation accuracy: %.1f' % test_acc)
47
48    trainloader, testloader = get_train_test_loaders(1)
49
50    # export to onnx
51    fname = "signlanguage.onnx"
52    dummy = torch.randn(1, 1, 28, 28)
53    torch.onnx.export(net, dummy, fname, input_names=['input'])
54
55    # check exported model
56    model = onnx.load(fname)
57    onnx.checker.check_model(model)  # check model is well-formed
58
59    # create runnable session with exported model
60    ort_session = ort.InferenceSession(fname)
61    net = lambda inp: ort_session.run(None, {'input': inp.data.numpy()})[0]
62
63    print('=' * 10, 'ONNX', '=' * 10)
64    train_acc = batch_evaluate(net, trainloader) * 100.
65    print('Training accuracy: %.1f' % train_acc)
66    test_acc = batch_evaluate(net, testloader) * 100.
67    print('Validation accuracy: %.1f' % test_acc)
68
69if __name__ == '__main__':
70    validate()

要使用和评估上一步的检查点,请运行以下程序:

1python step_4_evaluate.py

这将产生与下面类似的输出结果,证明您导出的模型不仅可以运行,而且与原始 PyTorch 模型一致:

1[secondary_label Output]
2========== PyTorch ==========
3Training accuracy: 99.9
4Validation accuracy: 97.4
5========== ONNX ==========
6Training accuracy: 99.9
7Validation accuracy: 97.4

您的神经网络的训练准确率为 99.9%,验证准确率为 97.4%。训练准确率和验证准确率之间的差距表明你的模型正在_过度拟合。这意味着你的模型不是在学习可通用的模式,而是在记忆训练数据。要了解过拟合的影响和原因,请参阅 Understanding Bias-Variance Tradeoffs

至此,我们完成了一个手语分类器。从本质上讲,我们的模型几乎在所有时候都能正确区分不同的手势。这是一个相当不错的模型,因此我们将进入应用的最后阶段。我们将在实时网络摄像头应用中使用这个手语分类器。

第 5 步 - 连接摄像机信号

下一个目标是将电脑摄像头与手语分类器连接起来。您将收集摄像头输入,对显示的手语进行分类,然后将分类后的手语反馈给用户。

现在为人脸检测器创建一个 Python 脚本。使用 nano 或你喜欢的文本编辑器创建文件 step_6_camera.py

1nano step_5_camera.py

在文件中添加以下代码

 1[label step_5_camera.py]
 2"""Test for sign language classification"""
 3import cv2
 4import numpy as np
 5import onnxruntime as ort
 6
 7def main():
 8    pass
 9
10if __name__ == '__main__':
11    main()

这段代码导入了 OpenCV(包含图像实用程序)和 ONNX 运行时(使用模型进行推理所需的全部程序)。代码的其余部分是典型的 Python 程序模板。

现在用下面的代码替换main函数中的pass,使用之前训练的参数初始化手语分类器。此外,还要添加从索引到字母和图像统计的映射:

1[label step_5_camera.py]
2def main():
3    # constants
4    index_to_letter = list('ABCDEFGHIKLMNOPQRSTUVWXY')
5    mean = 0.485 * 255.
6    std = 0.229 * 255.
7
8    # create runnable session with exported model
9    ort_session = ort.InferenceSession("signlanguage.onnx")

您将使用 OpenCV 官方文档中 测试脚本 的元素。具体来说,您将更新 main 函数的主体。首先,初始化一个 VideoCapture 对象,将其设置为捕获来自计算机摄像头的实时画面。将其放在 main 函数的末尾:

1[label step_5_camera.py]
2def main():
3    ...
4    # create runnable session with exported model
5    ort_session = ort.InferenceSession("signlanguage.onnx")
6
7    cap = cv2.VideoCapture(0)

然后添加一个 while 循环,在每个时间点读取摄像头的数据:

1[label step_5_camera.py]
2def main():
3    ...
4    cap = cv2.VideoCapture(0)
5    while True:
6        # Capture frame-by-frame
7        ret, frame = cap.read()

编写一个实用程序,用于获取摄像机画面的中心裁切值。将此函数放在 main 之前:

1[label step_5_camera.py]
2def center_crop(frame):
3    h, w, _ = frame.shape
4    start = abs(h - w) // 2
5    if h > w:
6        frame = frame[start: start + w]
7    else:
8        frame = frame[:, start: start + h]
9    return frame

接下来,对摄像机画面进行中心裁剪,转换为灰度,归一化并调整为 28x28。将此操作放在 main 函数中的 while 循环内:

 1[label step_5_camera.py]
 2def main():
 3    ...
 4    while True:
 5        # Capture frame-by-frame
 6        ret, frame = cap.read()
 7
 8        # preprocess data
 9        frame = center_crop(frame)
10        frame = cv2.cvtColor(frame, cv2.COLOR_RGB2GRAY)
11        x = cv2.resize(frame, (28, 28))
12        x = (frame - mean) / std

仍然在 while 循环中,使用 ONNX 运行时运行推理。将输出转换为类索引,然后再转换为字母:

1[label step_5_camera.py]
2        ...
3        x = (frame - mean) / std
4
5        x = x.reshape(1, 1, 28, 28).astype(np.float32)
6        y = ort_session.run(None, {'input': x})[0]
7
8        index = np.argmax(y, axis=1)
9        letter = index_to_letter[int(index)]

在框架内显示预测的字母,并将框架显示给用户:

1[label step_5_camera.py]
2        ...
3        letter = index_to_letter[int(index)]
4
5        cv2.putText(frame, letter, (100, 100), cv2.FONT_HERSHEY_SIMPLEX, 2.0, (0, 255, 0), thickness=2)
6        cv2.imshow("Sign Language Translator", frame)

while 循环的末尾,添加以下代码,检查用户是否点击了 q 字符,如果是,则退出应用程序。这一行将使程序停止 1 毫秒。添加以下内容:

1[label step_5_camera.py]
2        ...
3        cv2.imshow("Sign Language Translator", frame)
4
5        if cv2.waitKey(1) & 0xFF == ord('q'):
6            break

最后,释放捕获并关闭所有窗口。将此操作放在while循环之外,以结束main函数。

 1[label step_5_camera.py]
 2...
 3
 4    while True:
 5        ...
 6        if cv2.waitKey(1) & 0xFF == ord('q'):
 7            break
 8
 9    cap.release()
10    cv2.destroyAllWindows()

请仔细检查您的文件是否与以下或这个 版本库 匹配:

 1[label step_5_camera.py]
 2import cv2
 3import numpy as np
 4import onnxruntime as ort
 5
 6def center_crop(frame):
 7    h, w, _ = frame.shape
 8    start = abs(h - w) // 2
 9    if h > w:
10        return frame[start: start + w]
11    return frame[:, start: start + h]
12
13def main():
14    # constants
15    index_to_letter = list('ABCDEFGHIKLMNOPQRSTUVWXY')
16    mean = 0.485 * 255.
17    std = 0.229 * 255.
18
19    # create runnable session with exported model
20    ort_session = ort.InferenceSession("signlanguage.onnx")
21
22    cap = cv2.VideoCapture(0)
23    while True:
24        # Capture frame-by-frame
25        ret, frame = cap.read()
26
27        # preprocess data
28        frame = center_crop(frame)
29        frame = cv2.cvtColor(frame, cv2.COLOR_RGB2GRAY)
30        x = cv2.resize(frame, (28, 28))
31        x = (x - mean) / std
32
33        x = x.reshape(1, 1, 28, 28).astype(np.float32)
34        y = ort_session.run(None, {'input': x})[0]
35
36        index = np.argmax(y, axis=1)
37        letter = index_to_letter[int(index)]
38
39        cv2.putText(frame, letter, (100, 100), cv2.FONT_HERSHEY_SIMPLEX, 2.0, (0, 255, 0), thickness=2)
40        cv2.imshow("Sign Language Translator", frame)
41
42        if cv2.waitKey(1) & 0xFF == ord('q'):
43            break
44
45    cap.release()
46    cv2.destroyAllWindows()
47
48if __name__ == '__main__':
49    main()

退出文件并运行脚本。

1python step_5_camera.py

脚本运行后,会弹出一个窗口,显示网络摄像头的实时画面。左上角将显示预测的手语字母。举起你的手,做出你最喜欢的手势,看看你的分类器在做什么。下面是一些显示字母 L 和** D** 的示例结果。

手语'L'的 OpenCV 示例程序截图](assets/signlanguage_neural/image_1.png) 手语'D'的 OpenCV 示例程序截图](assets/signlanguage_neural/image_2.png)

测试时请注意,该翻译器需要相当清晰的背景才能工作。这是数据集干净程度造成的不幸后果。如果数据集包含了背景杂乱的手势图像,那么该网络就能很好地应对嘈杂的背景。然而,该数据集的特点是背景空白,手部居中。因此,当您的手同样居中并置于空白背景中时,网络摄像头翻译器的效果最佳。

手语翻译应用程序到此结束。

结论

在本教程中,您使用计算机视觉和机器学习模型构建了一个美国手语翻译器。特别是,您看到了训练机器学习模型的新方面--尤其是增强模型稳健性的数据、降低损失的学习率计划,以及使用 ONNX 导出人工智能模型供生产使用。最后,我们完成了一个实时计算机视觉应用,该应用利用我们构建的管道将手语翻译成字母。值得注意的是,解决最终分类器的脆性问题可以采用以下任何一种或所有方法。如需进一步了解,请尝试以下主题,以改进您的应用:

  • 泛化:这不是计算机视觉中的一个子课题,而是所有机器学习中的一个永恒问题。参见 Understanding Bias-Variance Tradeoffs
  • 领域适应:假设你的模型是在领域 A(例如阳光充足的环境)中训练出来的。您能将模型快速适应 B 领域(例如多云环境)吗?
  • 对抗性实例:假设对手故意设计图像来欺骗你的模型。如何设计此类图像?如何对付此类图像?
Published At
Categories with 技术
comments powered by Disqus