O autor selecionou a Code Org para receber uma doação como parte do programa Write for DOnations.
Visão computacional é um subcampo da ciência da computação que visa extrair um entendimento de ordem superior de imagens e vídeos. Isso possibilita tecnologias como filtros divertidos de bate-papo por vídeo, autenticador facial do seu dispositivo móvel, e carros autônomos.
Neste tutorial, você utilizará a visão computacional para construir um tradutor da Linguagem Americana de Sinais para sua webcam. Ao trabalhar ao longo do tutorial, você usará o OpenCV
, uma biblioteca de visão computacional, o PyTorch
para construir uma rede neural profunda e o onnx
para exportar sua rede neural. Você também aplicará os seguintes conceitos ao construir uma aplicação de visão computacional:
Ao final deste tutorial, você terá tanto um tradutor da Linguagem Americana de Sinais quanto um conhecimento básico de deep learning (aprendizado profundo). Você também pode acessar o código-fonte completo para este projeto.
Para concluir este tutorial, você precisará do seguinte:
Vamos criar um espaço de trabalho para este projeto e instalar as dependências que precisaremos.
Em distribuições Linux, comece preparando seu gerenciador de pacotes do sistema e instale o pacote virtualenv do Python3. Use:
- apt-get update
- apt-get upgrade
- apt-get install python3-venv
Vamos chamar nosso espaço de trabalho de SignLanguage
:
- mkdir ~/SignLanguage
Vá até o diretório SignLanguage
:
- cd ~/SignLanguage
A seguir, crie um novo ambiente virtual para o projeto:
- python3 -m venv signlanguage
Ative seu ambiente:
- source signlanguage/bin/activate
Em seguida, instale o PyTorch, um framework de deep-learning para Python que usaremos neste tutorial.
No macOS, instale o Pytorch com o seguinte comando:
- python -m pip install torch==1.2.0 torchvision==0.4.0
No Linux e Windows, utilize os seguintes comandos para uma compilação CPU-only:
- pip install torch==1.2.0+cpu torchvision==0.4.0+cpu -f https://download.pytorch.org/whl/torch_stable.html
- pip install torchvision
Agora, instale os binários pré-compilados para o OpenCV
, numpy
, e onnx
, que são bibliotecas para visão computacional, álgebra linear, exportação do modelo de IA, e execução do modelo de IA, respectivamente. O OpenCV
oferece utilitários como rotações de imagem, e o numpy
oferece utilitários de álgebra linear, como inversão de matriz:
- python -m pip install opencv-python==3.4.3.18 numpy==1.14.5 onnx==1.6.0 onnxruntime==1.0.0
Em distribuições Linux, você precisará instalar a libSM.so
:
- apt-get install libsm6 libxext6 libxrender-dev
Com as dependências instaladas, vamos construir a primeira versão do nosso tradutor de linguagem de sinais: um classificador de linguagem de sinais.
Nestas três seções seguintes, você construirá um classificador de linguagem de sinais usando uma rede neural. Seu objetivo é gerar um modelo que aceita uma imagem de uma mão como entrada e retorna uma letra.como saída:
Os três passos seguintes são necessários para construir um modelo de classificação de aprendizagem de máquina:
Nesta seção do tutorial, você cumprirá o passo 1 de 3. Você baixará os dados, criará um objeto Dataset
para iterar através dos seus dados, e, finalmente, aplicará o aumento de dados. Ao final deste passo, você terá uma maneira programática de acessar imagens e rótulos em seu dataset para alimentar seu modelo.
Primeiro, baixe o dataset para seu diretório de trabalho atual:
Nota: no macOS, o wget
não está disponível por padrão. Para fazer isso, instale o Homebrew seguindo este tutorial da DigitalOcean. A seguir, execute brew install wget
.
- wget https://assets.digitalocean.com/articles/signlanguage_data/sign-language-mnist.tar.gz
Descompacte o arquivo zip, que contém um diretório data/
:
- tar -xzf sign-language-mnist.tar.gz
Crie um novo arquivo, chamado step_2_dataset.py
:
- nano step_2_dataset.py
Como antes, importe os utilitários necessários e crie a classe que conterá seus dados. Para o processamento de dados aqui, você criará os datasets de treinamento e de teste. Você implementará a interface Dataset
do PyTorch, permitindo que você carregue e utilize o pipeline de dados embutido do PyTorch para seu dataset de classificação de linguagem de sinais:
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
Exclua o placeholder pass
na classe SignLanguageMNIST
. No seu lugar, adicione um método para gerar um mapeamento de rótulos:
@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
Os rótulos variam de 0 a 25. No entanto, as letras J (9) e Z (25) são excluídas. Isso significa que existem apenas 24 valores de rótulo válidos. Para que o conjunto de todos os valores de rótulo a partir de 0 seja contíguo, mapeamos todos os rótulos para [0, 23]. Este mapeamento entre rótulos de dataset [0, 23] para índices de letra [0, 25] é fornecido pelo método get_label_mapping
.
A seguir, adicione um método para extrair rótulos e amostras de um arquivo CSV. O seguinte pressupõe que cada linha começa com o label
e que então é seguido por 784 valores de pixel. Esses 784 valores de pixel representam uma imagem 28x28
:
@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
Para uma explicação de como esses 784 valores representam uma imagem, consulte o Passo 4 do tutorial Build an Emotion-Based Dog Filter
Observe que cada linha no csv.reader
iterável é uma lista de strings: as invocações int
e map(int ...)
convertem todas as strings para inteiros. Diretamente abaixo do nosso método estático, adicione uma função que inicializará nosso armazenador de dados:
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
Essa função começa carregando as amostras e rótulos. A seguir, agrupa os dados em matrizes do NumPy. As informações de média e desvio padrão serão explicadas em breve, na seção __getitem__
a seguir.
Diretamente após a função __init__
, adicione uma função __len__
. O Dataset
requer este método para determinar quando parar de iterar os dados:
...
def __len__(self):
return len(self._labels)
Por fim, adicione um método __getitem__
, que retorna um dicionário com a amostra e o rótulo:
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()
}
Use uma técnica chamada de aumento de dados, onde amostras são perturbadas durante o treinamento, para aumentar a robustez do modelo a essas perturbações. Em particular, faça um zoom aleatório na imagem variando quantidades e em diferentes localizações, através do RandomResizedCrop
. Observe que o zoom não deve afetar a classe final da linguagem de sinais; assim, o rótulo não é transformado. Além disso, você normaliza as entradas para que os valores da imagem sejam redimensionados para o intervalo de [0, 1] na expectativa, em vez de [0, 255]; para conseguir isso, utilize o dataset _mean
e _std
ao normalizar.
Sua classe SignLanguageMNIST
completa se parecerá com a seguinte:
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()
}
Como antes, você agora verificará nossas funções utilitárias de dataset carregando o dataset SignLanguageMNIST
. Adicione o código a seguir ao final do seu arquivo após a classe SignLanguageMNIST
:
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
Este código inicializa o dataset usando a classe SignLanguageMNIST
. A seguir, para os datasets de treinamento e de validação, ele agrupa o dataset em um DataLoader
. Isso traduzirá o dataset para um iterável para ser usado mais tarde.
Agora, você verificará se os utilitários de dataset estão funcionando. Crie um dataset de amostra usando o DataLoader
e imprima o primeiro elemento desse carregador. Adicione o seguinte ao final do seu arquivo:
if __name__ == '__main__':
loader, _ = get_train_test_loaders(2)
print(next(iter(loader)))
Você pode verificar se seu arquivo corresponde ao arquivo step_2_dataset
neste (repositório). Saia do seu editor e execute o script com o seguinte:
- python step_2_dataset.py
Isso resulta no seguinte par de tensores. Nosso pipeline de dados fornece duas amostras e dois rótulos. Isso indica que nosso pipeline de dados está funcionando e pronto para uso:
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.]])}
Você verificou agora que seu pipeline de dados funciona. Isso conclui o primeiro passo — pré-processando seus dados — que agora inclui aumento de dados para maior robustez do modelo. A seguir, você definirá a rede neural e o otimizador.
Com um pipeline de dados funcionando, você agora definirá um modelo e o treinará nos dados. Em particular, você construirá uma rede neural com seis camadas, definirá uma perda, um otimizador e, por fim, otimizará a função de perdas para as previsões da sua rede neural. Ao final deste passo, você terá um classificador de linguagem de sinais funcionando.
Crie um novo arquivo chamado step_3_train.py
:
- nano step_3_train.py
Importe os utilitários necessários:
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
Defina uma rede neural PyTorch que inclua três camadas convolucionais, seguidas de três camadas totalmente conectadas. Adicione isto ao final do seu script existente:
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
Agora, inicialize a rede neural, defina uma função de perdas e defina hiperparâmetros de otimização, adicionando o código a seguir ao final do script:
def main():
net = Net().float()
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(net.parameters(), lr=0.01, momentum=0.9)
Por fim, você treinará para dois epochs:
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")
Você define um epoch para ser uma iteração de treinamento onde cada amostra de treinamento foi usada exatamente uma vez. No final da função main, os parâmetros do modelo serão salvos em um arquivo chamado "checkpoint.pth"
.
Adicione o código a seguir ao final do seu script para extrair image
e label
do dataset loader e, em seguida, agrupe cada um em uma Variable
PyTorch:
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)))
Esse código também executará a passagem para frente e depois retropropagará pela perda e pela rede neural
No final do seu arquivo, adicione o seguinte para invocar a função main
:
if __name__ == '__main__':
main()
Verifique novamente se o arquivo corresponde ao seguinte:
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()
Salve e saia. A seguir, inicie nosso treinamento de prova de conceito executando:
- python step_3_train.py
Você verá uma saída semelhante à seguinte à medida que a rede neural treina:
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
Para uma menor perda, aumente o número de epochs para 5, 10, ou até 20. No entanto, após um determinado período de treinamento, a perda de rede deixará de diminuir com o aumento do tempo de treinamento. Para contornar esse problema, à medida que o tempo de treinamento aumenta, você introduzirá uma programação de taxa de aprendizagem, que diminui a taxa de aprendizagem ao longo do tempo. Para entender porque isso funciona, consulte a visualização de Distill em “Why Momentum Really Works”.
Atualize sua função main
com as seguintes duas linhas, definindo um agendador
e invocando scheduler.step
. Além disso, altere o número de epochs para 12
:
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")
Verifique se seu arquivo corresponde ao arquivo do passo 3 neste repositório. O treinamento será executado por cerca de 5 minutos. Sua saída se parecerá com o seguinte:
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
A perda final obtida é 0,007608
, que é 3 ordens de grandeza menor que a perda inicial 3.20
. Isso conclui o segundo passo do nosso fluxo de trabalho, onde configuramos e treinamos a rede neural. Dito isto, por menor que seja esse valor de perda, ele tem pouco significado. Para colocar o desempenho do modelo em perspectiva, vamos calcular sua precisão — a percentagem de imagens que o modelo classificou corretamente.
Você agora avaliará seu classificador de linguagem de sinais calculando sua precisão no dataset de validação, um conjunto de imagens que o modelo não viu durante o treinamento. Isso fornecerá um melhor senso de desempenho do modelo do que forneceu o valor da perda final. Além disso, você adicionará utilitários para salvar nosso modelo treinado no final do treinamento e carregará nosso modelo pré-treinado ao realizar inferência.
Crie um novo arquivo, chamado step_4_evaluate.py
.
- nano step_4_evaluate.py
Importe os utilitários necessários:
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
A seguir, defina um utilitário para avaliar o desempenho da rede neural. A seguinte função compara a letra prevista pela rede neural com a letra verdadeira, para uma única imagem:
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
é uma lista de probabilidades de classe para cada amostra. Por exemplo, outputs
, para uma única amostra podem ser [0.1, 0.3, 0.4, 0.2]
. labels
é uma lista de classes de rótulos. Por exemplo, a classe do rótulo pode ser 3
.
Y = ...
converte os rótulos em uma matriz do NumPy. A seguir, Yhat = np.argmax(...)
converte as probabilidades da classe outputs
para classes previstas. Por exemplo, a lista de probabilidades de classe [0.1, 0.3, 0.4, 0.2]
geraria a classe prevista 2
, pois o valor de 0.4 do índice 2 é o maior valor.
Como tanto Y
como Yhat
são classes agora, você pode compará-las. Yhat = = Y
verifica se a classe prevista corresponde à classe de rótulo, e np.sum(...)
é um truque que calcula o número de valores verdadeiros. Em outras palavras, np.sum
gerará o número de amostras que foram classificadas corretamente.
Adicione a segunda função batch_evaluate
, que aplica a primeira função evaluate
a todas as imagens:
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
O batch
é um grupo de imagens armazenado como um único tensor. Primeiro, você incrementa o número total de imagens que você está avaliando (n
) pelo número de imagens neste lote. A seguir, você executa a inferência na rede neural com este lote de imagens, output = net(...)
. A verificação de tipo if isinstance(...)
converte as saídas em uma matriz do NumPy, se necessário. Por fim, você usará evaluate
para calcular o número de amostras corretamente classificadas. Na conclusão da função, você calcula o percentual de amostras que você classificou corretamente, score / n
.
Por fim, adicione o script a seguir para aproveitar os utilitários anteriores:
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()
Isso carrega uma rede neural pré-treinada e avalia seu desempenho no dataset da linguagem de sinais fornecido. Especificamente, o script aqui retorna precisão nas imagens que você usou para treinar e um conjunto separado de imagens que você colocou para fins de teste, chamado de conjunto de validação.
Em seguida, você exportará o modelo PyTorch para um binário ONNX. Este arquivo binário, pode então ser usado em produção para executar a inferência com seu modelo. Mais importante, o código em execução neste binário não precisa de uma cópia da definição original da rede. No final da função validate
adicione o seguinte:
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)
Isso exporta o modelo ONNX, verifica o modelo exportado e, em seguida, executa a inferência com o modelo exportado. Verifique com atenção se o seu arquivo corresponde ao arquivo do Passo 4 neste repositório:
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()
Para usar e avaliar o ponto de verificação da última etapa, execute o seguinte:
- python step_4_evaluate.py
Isso gerará uma saída semelhante à seguinte, afirmando que o modelo exportado, não apenas funciona, mas também concorda com o modelo PyTorch original:
Output========== PyTorch ==========
Training accuracy: 99.9
Validation accuracy: 97.4
========== ONNX ==========
Training accuracy: 99.9
Validation accuracy: 97.4
Sua rede neural atinge uma precisão de treinamento de 99,9% e uma precisão de validação de 97.4%. Esta diferença entre a precisão de treinamento e a de validação indica que seu modelo está sobreajustado. Isso significa que, em vez de aprender padrões generalizáveis, seu modelo memorizou os dados de treinamento. Para entender as implicações e causas do sobreajuste consulte Understanding Bias-Variance Tradeoffs.
Neste ponto, completamos um classificador de linguagem de sinais. Em essência, nosso modelo pode distinguir corretamente a ambiguidade entre sinais, quase todo o tempo. Este é um modelo razoavelmente bom, por isso vamos passar para a fase final da nossa aplicação. Vamos usar este classificador de linguagem de sinais em uma aplicação de webcam em tempo real.
O próximo objetivo será vincular a câmera do computador ao classificador de linguagem de sinais. Você coletará a entrada da câmera, classificará a linguagem de sinais exibida, e, em seguida, informará o sinal classificado para o usuário.
Agora, crie um script Python para o detector facial. Crie o arquivo step_6_camera.py
usando o nano
ou seu editor de texto favorito:
- nano step_5_camera.py
Adicione o seguinte código ao arquivo:
"""Test for sign language classification"""
import cv2
import numpy as np
import onnxruntime as ort
def main():
pass
if __name__ == '__main__':
main()
Este código importa o OpenCV, que contém seus utilitários de imagem, e o runtime do ONNX, que é tudo o que você precisa para executar a inferência com seu modelo. O restante do código é típico de um programa Python.
Agora, substitua pass
na função main
pelo seguinte código, que inicializa um classificador de linguagem de sinais usando os parâmetros que você treinou anteriormente. Além disso, adicione um mapeamento de índices para letras e estatísticas de imagem:
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")
Você usará elementos deste script de teste da documentação oficial do OpenCV. Especificamente, você atualizará o corpo da função main
. Comece inicializando um objeto VideoCapture
que é definido para capturar uma entrada ativa da câmera do seu computador. Coloque isto no final da função main
:
def main():
...
# create runnable session with exported model
ort_session = ort.InferenceSession("signlanguage.onnx")
cap = cv2.VideoCapture(0)
A seguir, adicione um loop while
, que lê da câmera em cada timestep:
def main():
...
cap = cv2.VideoCapture(0)
while True:
# Capture frame-by-frame
ret, frame = cap.read()
Escreva uma função utilitária que faça o corte central do quadro da câmera. Coloque esta função antes do main
:
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
A seguir, pegue o corte central do quadro da câmera, converta para a escala de cinza, normalize e redimensione para 28x28
. Coloque isto dentro do loop while
dentro da função main
:
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
Ainda dentro do loop while
, execute a inferência com o runtime ONNX. Converta as saídas para um índice de classe, depois, para uma letra:
...
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)]
Exiba a letra prevista dentro do quadro e apresente o quadro de volta ao usuário:
...
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)
No final do loop while
, adicione este código para verificar se o usuário obtém o caractere q
e, caso positivo, saia da aplicação. Esta linha interrompe o programa por 1 milissegundo. Adicione o seguinte:
...
cv2.imshow("Sign Language Translator", frame)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
Por fim, termine a captura e feche todas as janelas. Coloque isto fora do loop while
para encerrar a função main
.
...
while True:
...
if cv2.waitKey(1) & 0xFF == ord('q'):
break
cap.release()
cv2.destroyAllWindows()
Verifique novamente se seu arquivo corresponde ao seguinte ou a este repositório:
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()
Saia do seu arquivo e execute o script.
- python step_5_camera.py
Assim que o script for executado, uma janela aparecerá com sua entrada da webcam ativa. A letra prevista da linguagem de sinais será mostrada no topo à esquerda. Levante a mão e faça seu sinal favorito para ver o classificador em ação. Aqui estão alguns resultados de amostra mostrando a letra L e D.
Ao testar, observe que o pano de fundo precisa ser bastante claro para este tradutor funcionar. Essa é uma consequência infeliz da limpeza do dataset. Se o dataset incluísse imagens de sinais de mão com planos de fundo variados, a rede seria menos sensível a planos de fundo ruidosos. No entanto, o dataset apresenta planos de fundo brancos e mãos bem centralizadas. Como resultado, este tradutor da webcam funciona melhor quando sua mão também está centralizada e em um plano de fundo em branco.
Isso conclui a aplicação do tradutor de linguagem de sinais.
Neste tutorial, você construiu um tradutor da linguagem americana de sinais usando visão computacional e um modelo de aprendizagem de máquina. Em particular, você viu novos aspectos do treinamento de modelo de aprendizagem de máquina — especificamente aumento de dados para robustez do modelo, programação de taxas de aprendizagem para menor perda, e exportação de modelos de IA usando o ONNX para uso em produção. Isso terminou em um aplicativo de visão computacional em tempo real, que traduz a linguagem de sinais em letras usando um pipeline que você criou. Vale ressaltar que o combate à fragilidade do classificador final pode ser realizado com qualquer um ou todos os métodos a seguir. Para uma exploração mais aprofundada, tente os seguintes tópicos para aprimorar sua aplicação:
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!
Sign up for Infrastructure as a Newsletter.
Working on improving health and education, reducing inequality, and spurring economic growth? We'd like to help.
Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.