Coletânea Python do ZERO às Redes Neurais Artificiais

Fernando Feltrin · Capítulo 272 de 287

Páginas do PDF

Coletânea Python do ZERO às Redes Neurais Artificiais

Classificação de Dígitos Manuscritos – MNIST Dataset

Classificação de Dígitos Manuscritos – MNIST

Dataset

Dados os nossos passos iniciais nesta parte de identificação e reconhecimento de dígitos manuscritos, hora de fazer o mesmo tipo de processamento agora de forma mais robusta, via rede neural artificial. Desta vez estaremos usando o dataset MNIST, base de dados integrante da biblioteca Keras, com maiores amostras de imagem e imagens também armazenadas em uma resolução maior.

Lembre-se que no exemplo anterior nosso grande problema foi trabalhar sobre uma baixa margem de precisão devido os dados de nossa base que não colaboravam para um melhor processamento.

 

Sem mais delongas vamos direto ao ponto, direto ao código. Como sempre, inicialmente criamos um novo arquivo de nome MNIST.py que armazenará nossos códigos, dentro de si começamos realizando as devidas importações.

Não muito diferente do que estamos habituados, realizamos a importação da matplotlib.pyplot (para plotagem/exibição das imagens via console), da base de dados mnist, das ferramentas Sequential, Dense, Flatten e Dropout de seus referidos módulos.

Também importamos np_utils (para nossas transformações de formato de matriz), em seguida importamos Conv2D (ferramenta pré-configurada para criação de camadas de rede neural adaptada a processamento de dados de imagem), MaxPooling2d (ferramenta que criará micromatrizes indexadas para identificação de padrões pixel a pixel) e por fim BatchNormalization (ferramenta que realiza uma série de processos internos para diminuir a margem de erro quando feita a leitura pixel a pixel da imagem).

 

Na sequência criamos as variáveis que armazenarão os dados de entrada e de saída importados da base mnist.

 

Fazendo uma rápida leitura via explorador de variáveis podemos ver que de fato nossas variáveis foram criadas. Note que imediatamente podemos deduzir o tamanho dessa base de dados, uma vez que temos 60000 amostras separadas para treino e 10000 separadas para teste. Um número consideravelmente maior do que a base usada no exemplo anterior (1797 amostras apenas).

Repare nos dados de entrada que eles possuem uma configuração adicional (28,28,1), que significa que cada amostra está numa configuração de matrix 28x28 de uma camada de profundidade (pode existir processamento sobre voxels, onde haverão mais camadas de profundidade/volume).

 

Em seguida fazendo uma simples plotagem, nos mesmos moldes do modelo anterior, pegamos a amostra de índice número 0 de nossa base de entradas para treino, apenas por conformidade instanciamos a nomenclatura da amostra de índice número 0 de nossa base de saída de treino.

Selecionado e executado o bloco de código anterior, podemos ver via console que das amostras de saída categorizadas como “número 5”, a imagem apresentada é um número 5 que podemos deduzir pelo seu formato ter sido escrito à mão. Repare a qualidade da imagem, muito superior em relação a do exemplo anterior.

 

Iniciando a fase de polimento de nossos dados, o que faremos em cima de uma base de dados de imagens de números é converter essas informações de mapeamento de pixels para um formato array onde se possa ser aplicado funções. Sendo assim, criamos nossa variável de nome etreino que recebe sobre si a função .reshape( ), redimensionando todas amostras para o formato 28x28 de 1 camada. O mesmo é feito para eteste.

 

Repare que as variáveis etreino e eteste foram atualizadas para o novo formato.

 

Em seguida realizamos uma nova alteração e atualização sobre etreino e eteste, dessa vez por meio da função .astype( ) convertemos o seu conteúdo para o tipo float32 (números com casas decimais).

 

Prosseguindo alteramos e atualizamos etreino e eteste mais uma vez, dessa vez dividindo o valor de cada amostra por 255.

 

Finalizada a formatação de nossos dados de entrada, hora de realizar o polimento sobre os dados de saída. Aqui simplesmente aplicamos a função .to_categorical( ) que altera e atualiza os dados em 10 categorias diferentes (números de 0 a 9).

 

Feito o reajuste de nossos dados de saída temos agora dados matriciais compatíveis tanto para o cruzamento entre eles quanto para aplicação de funções. Note que terminamos com 60000 amostras que servirão para treinar nossa rede neural, realizaremos testes sobre 10000 amostras dessas, as categorizando em números de 0 a 9.

Agora damos início a criação da estrutura de nossa rede neural artificial convolucional, você verá que o modelo em si segue um certo padrão que você já está familiarizado, apenas acrescentando ou alterando algumas funções que serão responsáveis pelo processamento e detecção de padrões sobre esses dados providos de imagens.

Dessa forma iniciamos o modelo criando nosso classificador, que inicialmente só executa a ferramenta Sequential, em seguida criamos a camada de entrada e nossa primeira camada oculta, note que dessa vez não estamos usando Dense para isso, mas Conv2D que recebe como parâmetro input_shape = (28,28,1), enquanto anteriormente definíamos um número de neurônios para camada de entrada, agora estamos definindo que a camada de entrada é uma matrix 28x28 de uma camada de profundidade.

O método de ativação da mesma é ‘relu’ e os parâmetros referentes a primeira camada oculta, que nesse caso é o chamado operador de convolução que irá pegar os dados brutos da matriz de entrada, realizar a detecção de suas características, parear com uma nova matriz aplicando funções pixel a pixel e por fim gerando uma série de mapas de características.

Esta é uma forma de redimensionar a imagem, diminuindo seu tamanho e convertendo as informações relevantes de padrões de pixels que compõe a imagem para que seu processamento seja facilitado.

Em seguida é realizado o processo de normalização desses dados, você já deve ter notado que em imagens de baixa qualidade, em torno do traço existem pixels borrados, imprecisos, aqui o operador simplesmente verificará qual a relevância desses pixels e, sempre que possível, irá eliminar do mapa os desnecessários para reduzir ainda mais a quantidade de dados a serem processados.

Repare que aqui via MaxPooling é feito um novo redimensionamento para uma matrix 2x2, contendo um mapa com apenas as características relevantes a imagem. O que deve ficar bem entendido aqui é que não estamos simplesmente redimensionando uma imagem reduzindo seu tamanho de largura e altura, estamos criando um sistema de indexação interno que pegará a informação de cada pixel e sua relação com seus vizinhos, gerando um mapa de características e reduzindo o número de linhas e colunas de sua matriz, no final do processo temos um mapeamento que consegue reconstruir a imagem original (ou algo muito próximo a ela) mas que para processamento via rede neural possui um formato matricial pequeno e simples.

Como sempre, a segunda camada oculta agora não conta mais com o parâmetro dos dados de entrada.

 

Dando sequência após algumas camadas de processamento, redimensionando os dados de entrada, é executada a ferramenta Flatten que por sua vez irá pegar essas matrizes e seus referentes mapas de características e irá transformar isto em uma lista, onde cada valor (de cada linha) atuará como um neurônio nesta camada intermediária, finalizada essa etapa agora criamos camadas densas, aplicamos dropout em 20% (ou seja, de cada camada o operador pegará 20% dos neurônios e simplesmente irá subtraí-los da camada, a fim de também diminuir seu tamanho) e assim chegamos a camada de saída, com 10 neurônios (já que estamos classificando dados em números de 0 a 9) que por sua vez tem ativação ‘softmax’, função de ativação essa muito parecida com a nossa velha conhecida ‘sigmoid’, porém capaz de categorizar amostras em múltiplas saídas.

Por fim, criamos a estrutura que compila a rede, definindo sua função de perda como ‘categorical_crossentropy’ que como já visto anteriormente, verifica quais dados não foram possíveis de classificar quanto sua proximidade aos vizinhos, otimizador ‘adam’ e como métrica foco em precisão por meio do parâmetro ‘accuracy’. Última parte, por meio da função .fit( ) alimentamos a rede com os dados de etreino e streino, definimos que os valores dos pesos serão atualizados a cada 128 amostras, que executaremos a rede 10 vezes e que haverá um teste de validação comparando os dados encontrados com os de eteste e steste.

Selecionado todo bloco de código, se não houver nenhum erro de sintaxe a rede começará a ser executada. Você irá reparar que, mesmo com todas reduções que fizemos, e mesmo executando nosso modelo apenas 10 vezes, o processamento dessa rede se dará por um bom tempo, dependendo de seu hardware, até mesmo mais de uma hora.

 

Terminada a execução da rede note que, feito todo processamento de todas camadas e parâmetros que definimos chegamos a excelentes resultados, onde a função de perda nos mostra que apenas 1% dos dados não puderam ser classificados corretamente, enquanto val_acc nos retorna uma margem de acertos/precisão de 99% neste tipo de classificação.

Código Completo:

 

Aqui quero apenas fazer um breve resumo porque acho necessário para melhor entendimento. Sem entrar em detalhes da estrutura de nosso modelo, raciocine que o processo de classificação de imagens é bastante complexo, demanda muito processamento, uma vez que a grosso modo, uma imagem que temos em tela é simplesmente um conjunto de pixels em suas devidas posições com suas respectivas cores.

Internamente, para que possamos realizar processamento via rede neural artificial, devemos fazer uma série de transformações/conversões para que essa imagem se transforme em uma série de mapas matriciais onde possam se aplicar funções.

Para isso, temos que fazer o redimensionamento de tal imagem, sem perda de informação, de uma matriz grande para pequena, que será convertida em dados de camada para a rede neural, por fim, a rede irá aprender a reconhecer as características e padrões de tais matrizes e reconstruir toda essa informação novamente em imagem.

Infográfico mostrando desde a leitura da imagem em sua forma matricial, geração de mapas e polimento, conversão em neurônios e interação com camada subsequente.

Lembrando que este processo é apenas a estrutura da rede, assim como nos modelos anteriores, uma vez que nossa rede esteja funcionando podemos aplicar testes e fazer previsões em cima do mesmo.

 

Muito bem, realizado o processamento convencional de nossa rede neural convolucional, hora de aplicar algum teste para verificar a integridade de nossos resultados. Neste tipo de rede neural um dos testes mais eficazes é o nosso velho conhecido teste de validação cruzada, onde podemos separar nossa base de dados em partes e testá-las individualmente.

Aqui, lembrando que temos um modelo que está processando dados a partir de imagens, usaremos uma ferramenta um pouco diferente para o mesmo fim, a StratifiedKFold da biblioteca sklearn. O processo das importações é o mesmo de sempre.

 

Na sequência criamos uma variável de nome seed com valor inicial 5, que em sua função subsequente fará a alimentação de nossa ferramenta StratifiedKFold com valores randômicos dentro desse intervalo.

Logo após realizamos aquele procedimento de importação dos dados de nossa base de dados assim como a distribuição desses dados em variáveis de treino, teste, entrada e de saída. Assim como as devidas conversões de formato da mesma forma que realizamos anteriormente na criação de nosso modelo convolucional.

 

Em seguida criamos uma variável de nome kfold que inicializa a ferramenta StratifiedKFold, note que definimos os parâmetros n_splits = 5, que significa que a base será dividida em 5 partes iguais, shuffle = True define que as amostras sejam pegas de forma aleatória, para evitar testes “viciados” e random_state que recebe o valor 5 que havíamos definido anteriormente em seed.

 

Na etapa seguinte criamos duas variáveis a e b que por meio da função .zeros( ) cria arrays preenchidas com números 0 a serem substituídos durante a aplicação de alguma função.

 

Dando sequência criamos a estrutura de nosso teste de validação cruzada dentro de um laço de repetição, onde são criadas as variáveis temporárias evalcruzada e svalcruzada referentes aos dados para entrada e saída. Por meio da função kfold.split( ) então é feita a divisão e separação de acordo com o formato que havíamos parametrizado anteriormente.

Note que dentro existe uma estrutura de rede neural assim como as anteriores, apenas agora condensada para poupar linhas aqui do livro. Esta rede por sua vez é alimentada com os dados que forem passados vindos de entradas e saídas sobre evalcruzada assim como seu teste de performance é realizado com os dados atribuídos para entradas e saidas sobre svalcruzada.

Por fim o retorno de todo esse processamento (lembrando que aqui nessa fase a rede será executada novamente) irá alimentar resultados por meio da função .append( ) (uma vez que resultados está em formato de lista).

 

Via console podemos ver que após as novas execuções da rede, agora cruzando dados de amostras aleatórias, houve uma margem de acerto de 99%, compatível com o resultado obtido na rede neural do modelo.

 

Por fim é criada a variável media que realiza o cruzamento e divisão dos dados obtidos nas linhas e colunas de resultados.

Via explorador de variáveis é possível visualizar um resultado quase idêntico ao da execução da validação cruzada, isso é perfeitamente normal desde que os valores sejam muito aproximados (isto se dá porque em certas métricas de visualização de dados pode ocorrer “arredondamentos” que criam uma variação para mais ou para menos, mas sem impacto nos resultados), 98% de margem de acerto no processo de classificação.

 

Código Completo (Validação Cruzada):