作者选择Code Org作为Write for DOnations计划的一部分接受捐赠。
简介
计算机视觉是计算机科学的一个子领域,旨在从图像和视频中提取更高阶的理解。这为有趣的视频聊天过滤器、移动设备的人脸验证器和自动驾驶汽车等技术提供了动力。
在本教程中,您将使用计算机视觉为网络摄像头构建美式手语翻译器。在学习本教程的过程中,您将使用计算机视觉库 OpenCV
、PyTorch
构建深度神经网络,并使用 onnx
导出神经网络。在构建计算机视觉应用程序时,您还将应用以下概念:
- 您将使用与 如何应用计算机视觉构建基于情感的狗狗过滤器 教程中相同的三步法:预处理数据集、训练模型和评估模型。
- 您还将扩展这些步骤中的每一步:使用数据增强来处理旋转或非居中的手,改变学习率计划以提高模型的准确性,以及导出模型以加快推理速度。
- 在学习过程中,你还将探索机器学习中的相关概念。
本教程结束时,您将拥有一个美国手语翻译器和基础深度学习知识。您还可以访问本项目的 完整源代码。
先决条件
要完成本教程,您需要以下材料:
- 至少 1GB 内存的 Python 3 本地开发环境。您可以按照 如何安装和设置 Python 3 的本地编程环境 来配置所需的一切。
- 用于实时图像检测的网络摄像头。
- 推荐)Build an Emotion-Based Dog Filter;本教程未明确使用,但强化了相同的思想,并以其为基础。
第 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
现在安装 OpenCV
、numpy
和 onnx
的预打包二进制文件,它们分别是计算机视觉库、线性代数库、人工智能模型导出库和人工智能模型执行库。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 "的文件中。
在脚本末尾添加以下代码,从数据集加载器中提取 image
和 label
,然后用 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 是最大值。
由于 Y
和 Yhat
现在都是类,因此可以对它们进行比较。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 领域(例如多云环境)吗?
- 对抗性实例:假设对手故意设计图像来欺骗你的模型。如何设计此类图像?如何对付此类图像?