Classificação de sinal de tráfego

Aaron Wong Blocked Desbloquear Seguir Seguindo 30 de dezembro

Tendo acabado o semestre na NYU, pensei em compartilhar os resultados de uma das tarefas mais interessantes que tive. Como o título sugere, este será outro post sobre o concurso de classificação de sinais de trânsito encontrado em http://benchmark.ini.rub.de/ .

Para alguns antecedentes, estou atualmente estudando para o mestrado na NYU. Como muitos dos outros estudantes de lá, aproveitei a oportunidade de fazer a aula de visão computacional ensinada pelo renomado professor Rob Fergus.

Legal, mas o que torna essa tarefa mais interessante que qualquer outra tarefa de qualquer outro professor? Esta lição de casa foi dada na forma de uma competição de Kaggle. Quanto maior sua classificação na tabela de classificação particular, maior será sua nota.

Para aqueles que não sabem o que é o placar privado em Kaggle, há duas tabelas de classificação diferentes em uma competição de Kaggle. A tabela de classificação pública e a tabela de classificação privada. Enquanto a competição está ativa, todas as submissões são mostradas na tabela de classificação pública. Esta classificação é baseada na sua pontuação de 50% dos dados do teste. Quando a competição termina, sua pontuação é calculada a partir dos outros 50% dos dados do teste para classificá-lo na tabela de classificação privada. Obviamente, você não sabe quais dados são usados em qual placar. Isso evita que você ajuste os dados de teste.

O requisito para obter uma nota de aprovação era obter pelo menos 90% de precisão nos dados do teste. Para alcançar este marco, comecei com uma rede neural convolucional simples e um conjunto de validação escolhido de forma ingenua. Cada sinal de trânsito tinha diferentes imagens tiradas em posições variadas com uma quantidade variável de luz. Eu peguei os três primeiros tipos de imagens para cada sinal de tráfego e os movi para um diretório diferente para usar como o conjunto de validação.

Representação da arquitetura da CNN utilizada. 2 camadas conv, seguidas por 2 camadas lineares. Não tenho certeza de como as pessoas desenham seus belos diagramas de arquitetura de rede… Eu usei http://alexlenail.me/NN-SVG/LeNet.html .

 nclasses = 43 
 classe BaseNet (nn.Module): 
def __init __ (self):
super (BaseNet, self) .__ init __ ()
self.conv1 = nn.Conv2d (3, 10, kernel_size = 5)
self.conv2 = nn.Conv2d (10, 20, kernel_size = 5)
self.conv2_drop = nn.Dropout2d ()
 self.fc1 = nn.Linear (500, 50) 
self.fc2 = nn.Linear (50, nclasses)
 def forward (self, x): 
x = F.relu (F.max_pool2d (self.conv1 (x), 2))
x = F.relu (F.max_pool2d (self.conv2_drop (self.conv2 (x)), 2))
 x = x.view (-1, 500) 
x = F.relu (self.fc1 (x))
x = F.dropout (x, treinamento = self.training)
 x = self.fc2 (x) 
return F.log_softmax (x)

A primeira rede consistia em 2 camadas de convolução seguidas de 2 camadas lineares. Eu usei um escalonador de degraus para diminuir a taxa de aprendizagem em 0,1 após cada 5 épocas. Este modelo simples foi surpreendentemente capaz de atingir 89% de precisão após 25 épocas.

Valores de precisão de validação após a execução por 25 épocas.

Infelizmente, treinar esse modelo para mais épocas não pareceu bom, já que podemos ver no gráfico dos valores de perda de validação que os valores de perda se estabilizaram.

Perdas de validação para uma corrida de 25 épocas no modelo simples da CNN.

Como isso era um pouco abaixo da meta de precisão de 90%, pensei em por que não adicionar mais algumas camadas de convolução e outra camada linear. Puxa, vamos jogar algumas camadas de normalização e abandono para evitar problemas de ajuste no começo do jogo.

Novo modelo CNN com normalização em lote e dropouts incluídos entre as camadas conv.

Eu adicionei mais 2 camadas conv e mais 1 camada linear. Eu também adicionei a normalização em lotes e o dropout após a segunda e quarta camadas conv. A normalização do lote também foi inserida após as primeira e segunda camadas lineares.

 nclasses = 43 
 classe DeepNet (nn.Module): 
def __init __ (self):
super (DeepNet, self) .__ init __ ()
self.conv1 = nn.Conv2d (3, 32, kernel_size = 3)
self.conv2 = nn.Conv2d (32, 64, kernel_size = 3)
self.conv_bn1 = nn.BatchNorm2d (64)
self.conv2_drop = nn.Dropout2d ()

self.conv3 = nn.Conv2d (64, 128, kernel_size = 3)
self.conv4 = nn.Conv2d (128, 256, kernel_size = 3)
self.conv_bn2 = nn.BatchNorm2d (256)
self.conv4_drop = nn.Dropout2d ()

self.fc1 = nn.Linear (6400, 512)
self.fc1_bn = nn.BatchNorm1d (512)

self.fc2 = nn.Linear (512, 512)
self.fc2_bn = nn.BatchNorm1d (512)

self.fc3 = nn.Linear (512, nclasses)
 def forward (self, x): 
x = self.conv1 (x)
x = F.relu (F.max_pool2d (
self.conv2_drop (self.conv_bn1 (self.conv2 (x))), 2))

x = self.conv3 (x)
x = F.relu (F.max_pool2d (self.conv4_drop) (
self.conv_bn2 (self.conv4 (x))), 2))

x = x.view (-1, self.num_flat_features (x))

x = F.relu (self.fc1_bn (self.fc1 (x)))
x = F.dropout (x, treinamento = self.training)

x = F.relu (self.fc2_bn (self.fc2 (x)))
x = F.dropout (x, treinamento = self.training)

x = self.fc3 (x)
return F.log_softmax (x)
 def num_flat_features (self, x): 
size = x.size () [1:]
num_features = 1
para s em tamanho:
num_features * = s
return num_features

Executar este modelo por 25 épocas me colocou com 96% de precisão de validação! Este era apenas um modelo que consistia em um monte de camadas convolucionais e lineares esmagadas. Isso deve fazer você se perguntar sobre o desempenho que pode ser alcançado em redes que mostraram ter um bom desempenho em conjuntos de dados muito mais complicados, como o ImageNet.

Precisão de validação após 25 épocas com CNN mais profunda.

Agora era hora de tirar as grandes armas. Como consegui atingir 96% de precisão com uma rede tão simples, não acreditei que fosse necessário usar algo como o ResNet152. Na verdade, tenho certeza de que uma rede desse tamanho teria sérios problemas com o ajuste excessivo. Optei pelo menor ResNet18.

ResNet18, imagem encontrada nas imagens do Google.

 nclasses = 43 
 classe ResNet (nn.Module): 
def __init __ (self):
super (ResNet, self) .__ init __ ()
self.resnet = resnet18 ()
self.resnet.fc = nn.Linear (512, nclasses)
 def forward (self, x): 
x = self.resnet (x)
return F.log_softmax (x)

Precisão de treinamento usando a ResNet18. Precisão de validação usando o ResNet18.

Isso me colocou com precisão de validação de 98% com 3798 sinais de trânsito classificados corretamente de 3870 após a execução por 25 épocas. Como podemos ver nos gráficos, a precisão do treinamento é de 100%, então provavelmente não teremos mais precisão neste modelo, mesmo se o corremos por mais épocas.

Como a precisão do treinamento estava em 100%, era seguro assumir que o modelo estava super adaptando o conjunto de treinamento e, portanto, não alcançando uma precisão de validação mais alta. Nesse momento, fazia sentido examinar algumas das imagens que o modelo classifica incorretamente.

Imagens classificadas incorretamente no conjunto de validação.

As imagens com as quais o modelo mais teve dificuldades foram as imagens que tinham iluminação ruim ou inconsistente. Eu decidi usar o ColorJittering para randomizar o brilho nas imagens de treinamento. Algumas das imagens classificadas incorretamente também pareciam estar fora do centro, então eu também usei RandomAffine para adicionar rotações aleatórias, dimensionamento e traduções para as imagens de treinamento. Isso me levou a usar as seguintes transformações para aumento de dados:

 data_transforms = transforms.Compose ([ 
transformações.Resize ((224, 224)),
transforma.ColorJitter (0,8, contraste = 0,4),
transforms.RandomAffine (15,
escala = (0,8, 1,2),
translate = (0,2, 0,2)),
transform.ToTensor (),
transforma.Normalize ((0,3337, 0,3064, 0,3171),
(0,2672, 0,2564, 0,2629)
])
 validation_data_transforms = transforms.Compose ([ 
transformações.Resize ((224, 224)),
transform.ToTensor (),
transforma.Normalize ((0,3337, 0,3064, 0,3171),
(0,2672, 0,2564, 0,2629)
])

Observe que não realizo as transformações aleatórias nas imagens de validação. Realizamos as transformações durante o treinamento para criar diferentes variações aleatórias das imagens de treinamento. Isso tem o efeito de aumentar o tamanho do nosso conjunto de treinamento e, portanto, nos ajudar a lutar contra o excesso de encaixe. Nós não queremos tornar mais difícil para o nosso modelo durante a validação classificar as imagens de validação.

Precisão de treinamento usando ResNet18 com aumento de dados. Exatidão de validação usando ResNet18 com aumento de dados.

A precisão de validação chega a 98% novamente, embora desta vez seja possível obter mais algumas imagens classificadas corretamente. Este modelo foi capaz de classificar 3818 sinais de trânsito corretamente de 3870. Para obter o modelo final de submissão, combinei os conjuntos de treinamento e validação para obter ainda mais dados de treinamento. Eu carreguei os parâmetros do modelo do modelo que foi capaz de classificar corretamente 3818 imagens e treinei para mais 3 épocas. O envio deste modelo final deu-me uma pontuação de 0,99283. Não é tão ruim.

O repositório que contém os modelos e o script para executar o treinamento pode ser encontrado em https://github.com/AaronCCWong/german-traffic-sign-recognition .