Coletânea Python do ZERO às Redes Neurais Artificiais
Classificação Binária via Rede Neural Artificial – Breast Cancer Dataset
Classificação Binária via Rede Neural Artificial –
Breast Cancer Dataset
Uma das aplicações mais comuns em data science, como você deve ter percebido, é a classificação de dados para os mais diversos fins. Independentemente do tipo de dados e da quantidade de dados a serem processados, normalmente haverá uma fase inicial onde será feito o tratamento desses dados para que se possa extrair informações relevantes dos mesmos.
Tenha em mente que por vezes o problema que estaremos abstraindo é a simples classificação de dados, porém haverão situações onde a classificação será apenas parte do tratamento dos mesmos. Se tratando de redes neurais é comum fazer o seu uso para identificação de padrões a partir de dados alfanuméricos ou imagens e com base nesses padrões realizar aprendizado de máquina para que seja feita a análise de dados.
Seguindo esse raciocínio lógico, uma das aplicações que ganhou
destaque e vem evoluindo exponencialmente é a aplicação de redes neurais dentro da imaginologia médica, exames de imagem computadorizados como raios-x, tomografia computadorizada, ressonância magnética hoje possuem tecnologia para se obter imagens de alta definição fidedignas da anatomia humana para que se possam investigar possíveis patologias.
Ainda dentro da radiologia médica, uma das mais importantes aplicações
de redes neurais foi para contribuir com a identificação de diferentes tipos de câncer a partir de exames de imagem. Tenha em mente que a ideia não é jamais substituir o profissional médico radiologista que é o especialista nisso, mas oferecer ferramentas que auxiliem ou de alguma forma aumente a precisão de seus diagnósticos para que os pacientes recebam tratamentos mais adequados.
Como exemplo deste tipo de aplicação de rede neural, usaremos um
dataset bastante conhecido que com base num banco de dados de mais de 560 imagens de exames de mamografia para treinar uma rede neural artificial que possa a partir dessa estrutura, classificar imagens quanto a presença ou não de tumores. Por fim esse modelo pode ser adaptado para a partir de novas imagens realizar tal classificação, ou salvo para novos problemas computacionais que de alguma forma possam envolver classificação binária, classificação do tipo “ou uma coisa ou outra”.
Lembrando que, como mencionado algumas vezes nos capítulos anteriores, estaremos trabalhando de forma procedural, cada vez será usado um referencial teórico mais enxuto enquanto avançaremos para exemplos práticos de maior complexidade.
Como sempre o processo se inicia com a importação das bibliotecas, módulos e ferramentas a serem utilizados. De imediato importamos a biblioteca Pandas e a referenciamos como pd, para que na sequência possamos por meio de sua função .read_csv( ) importar dados a partir de planilhas Excel.
Se o processo de importação correu como esperado no explorador de variáveis é possível visualizar tais variáveis. Como estamos trabalhando inicialmente com a biblioteca pandas note que os dados são importados e reconhecidos como DataFrame, em outras bibliotecas essa nomenclatura é a usual matriz. Vale lembrar que para importação acontecer corretamente o arquivo deve estar na mesma pasta onde você salvou seu arquivo de código, ou especificar o caminho completo no parâmetro de importação.
Abrindo entradas podemos ver que de fato, existem 569 amostras, com 30 parâmetros para cada uma delas (valores individuais, de perda e de média para raio, textura, perímetro, área, borramento, compactação, concavidade, pontos de cavidade, simetria, dimensão fractal). Com base em todos esses dados as amostras serão classificadas como positivo ou negativo para o diagnóstico de câncer.
Em seguida realizamos a importação da ferramenta train_test_split e dividimos nossas amostras de nossa base de dados para que possam ser treinadas e testadas individualmente por nossa rede neural. Lembrando que esta etapa é uma fase de certificação da integridade de nossos processos e resultados, valores próximos em comparação mostram que nossa rede está configurada corretamente.
Em seguida fazemos mais umas importações, primeiro, da biblioteca keras, depois dos módulos Sequential e Dense que como já havíamos feito outrora, servem como estruturas pré-configuradas de nossa rede multicamada interconectada. O único diferencial aqui é que do módulo layers da biblioteca keras também importamos Dropout. Esse módulo usaremos na sequência, por hora, raciocine que quando estamos trabalhando com redes neurais com muitas camadas, ou muitos neurônios por camada, onde esses tem valores de dados muito próximos, podemos determinar que aleatoriamente se pegue um percentual desses neurônios e simplesmente os subtraia da configuração para ganharmos performance.
Em outras palavras, imagine uma camada com 30 neurônios, onde cada neurônio desse faz suas 30 conexões com a camada subsequente, sendo que o impacto individual ou de pequenas amostras deles é praticamente nulo na rede, sendo assim, podemos reduzir a quantia de neurônios e seus respectivos nós ganhando performance em processamento e não perdendo consistência dos dados.
Logo após criamos aquela estrutura de rede neural que estamos começando a ficar familiarizados. Repare nas diferenças em relação ao exemplo anterior, desta vez, aqueles 30 parâmetros de classificação de cada amostra é transformado em neurônios de nossa camada de entrada, a camada oculta por sua vez tem 16 neurônios e a camada de saída apenas um, pois trata-se de uma classificação binária (0 ou 1, 0 para negativo e 1 para positivo de câncer para uma devida amostra).
Outra grande diferença que agora nessa estrutura, na camada onde há a fase de compilação as funções de perda e métricas também são diferentes do modelo anterior ,agora não estamos classificando objetos de forma categórica, mas de forma binária.
Por fim, lembre-se que uma vez definida a fase onde existe a função .fit( ), ao selecionar e executar esse bloco de código a rede começará seu processamento, aqui, alimentada com os dados contidos em streino e streino, atualizando os valores a cada 10 rodadas por meio do parâmetro batch_size = 10 e executando o reprocessamento da rede 100 vezes de acordo com o parâmetro epochs = 100.
Executada uma primeira vez a rede com esses parâmetros note que ela nos retorna uma taxa de precisão sobre sua classificação binária de apenas 62%, o que pode ser interpretada como uma margem de erro muito alta nesses moldes de processamento, o que precisamos fazer nesses casos são reajustes para aumentar essa taxa de precisão/acertos, em problemas reais, para fins profissionais, dependendo do contexto uma taxa de acerto por volta de 90% já é considerada baixa... 62% está muito fora desse padrão.
Criamos nosso previsor, que realiza as devidas previsões sobre eteste por meio da função .predict( ).
Realizando as importações das ferramentas confusion_matrix e accuracy_score do módulo metrics da biblioteca sklearn podemos realizar estes testes complementar e verificar se existem erros ou inconsistências no processamento desses dados ou se realmente por hora a eficiência de nossa rede está muito baixa.
Criamos então uma variável de nome margem_acertos que aplica a função da ferramenta accuracy_score sobre os dados de steste e previsor. Da mesma forma criamos uma variável de nome matriz_confusão que aplica o teste homônimo também sobre os dados de steste e previsor. Por fim criamos uma variável de nome resultadoMatrizConfusão que faz o levantamento desse parâmetro de comparação sobre os dados de eteste e steste.
Selecionando todo bloco de código e executando é possível verificar nas variáveis criadas que de fato se comprovou que, executando testes individuais, o padrão de 62% de taxa de acerto se manteve. Identificado que o problema é a eficiência de nossa rede neural, hora de trabalhar para corrigir isso. Mas antes, como de praxe, vamos executar a validação cruzada sobre nossa base de dados.
Para isso, vamos aproveitar e já fazer as importações do KerasClassifier assim como do cross_val_score, das suas respectivas bibliotecas como mostrado acima.
Dando sequência, assim como feito no exemplo anterior, vamos criar nossa validação cruzada em forma de função. Para isso definimos valCruzada( ), sem parâmetros mesmo, e dentro dela criamos aquela estrutura de rede neural densa e sequencial como estamos habituados.
Note que o único diferencial é que entre a primeira camada oculta e a cama oculta subsequente existe uma cama de Dropout, parametrizada em 0.2, na prática o que esta camada faz é, do seu número total de neurônios da camada, pegar 20% deles aleatoriamente e simplesmente os descartar de forma a não gerar impacto negativo no processamento. Inicialmente essa camada que, havia 16 neurônios, passa a ter 13, e dessa forma, somando os outros métodos de reajustes dos pesos e processamento entre camadas, se busca uma integridade de dados igual, porém com performance maior.
Raciocine que eu uma camada de 16 neurônios este impacto é mínimo, mas em uma rede que, apenas como exemplo, possua mais de 100 neurônios por camada, assim como mais de 5 camadas ocultas, o parâmetro definido como dropout para elas irá gerar grande impacto de performance. Como sempre, é interessante realizar testes com diferentes valores de parâmetro e comparar os resultados.
Em seguida executamos nosso classificador via KerasClassifier, passando nossa função valCruzada( ) como parâmetro, atualizando os registros a cada 10 rodadas, executando a rede 100 vezes. Logo após é feita de fato a validação cruzada, apenas recordando, esta etapa pega nossa base de dados e divide em um número x de partes iguais e testa individualmente elas para que se verifique as devidas taxas de precisão e margem de erro.
Por fim, executando as linhas de código acima temos as variáveis dedicadas aos resultados das execuções de nossa validação cruzada.
No explorador de variáveis podemos conferir que como resultado de nossa validação cruzada agora temos uma taxa de precisão de 91%. Essa discrepância muito provavelmente está acontecendo por algum erro de leitura dos dados, já que agora, aplicando testes sobre partes individuais o percentual não só saiu do padrão anterior, mas aumentou consideravelmente.
Realizando Tuning do Modelo
Aqui para não continuar na mesmice do exemplo anterior, vamos fazer um processo bastante comum que é o reajuste manual dos parâmetros da rede, assim como os testes para se verificar qual configuração de parâmetros retorna melhores resultados. Até então o que eu lhe sugeri foi usar os parâmetros usuais assim como suas pré-configurações, apenas alterando as taxas de reajustes dos pesos e número de execuções da rede. Porém haverão situações onde este tipo de ajuste, na camada mais superficial da rede não surtirá impacto real, sendo necessário definir e testar parâmetros internos manualmente para se buscar melhores resultados.
Consultando a documentação de nossas ferramentas podemos notar que existe uma série de parâmetros internos a nossa disposição. Como saber qual se adapta melhor a cada situação? Infelizmente em grande parte dos casos teremos de, de acordo com sua descrição na documentação, realizar testes com o mesmo inclusive de comparação com outros parâmetros da mesma categoria para encontrar o que no final das contas resultará em resultados de processamento mais íntegros.
Em algumas literaturas esse processo é chamado de tuning. Para também sair fora da rotina vamos aplicar tal processo a partir de um arquivo novo apenas com o essencial para o processo de tunagem. Outro ponto para por hora finalizar nossa introdução é que essa fase em especial costuma ser a mais demorada, uma vez que de acordo com o número de parâmetros a serem testados, cada teste é uma nova execução da rede neural.
Seguindo a mesma lógica de sempre, toda vez que criamos um novo arquivo de código, primeira etapa é realizar as devidas importações das bibliotecas e módulos necessários. Repare que na última linha existe uma ferramenta nova sendo importada, trata-se da GridSearchCV, do módulo model_selection da biblioteca sklearn.
Em suma essa ferramenta permite de fato criarmos parâmetros manualmente para alimentar nossa rede neural, assim como passar mais de um parâmetro a ser testado para por fim serem escolhidos os que em conjunto mostraram melhores resultados de processamento.
Em seguida por meio da função .read_csv( ) da biblioteca pandas importamos novamente nossas bases de dados de entrada e saída.
S e o processo ocorreu sem erros, as devidas variáveis foram criadas.
Logo após já podemos criar nossa rede, dentro de uma função criada especificamente para ela de nome tuningClassificador( ), repare que até então não havíamos passado nenhum tipo de parâmetro em nossas funções, apenas seu bloco de código interno, dessa vez, como iremos definir os parâmetros manualmente, instanciados por uma variável, é interessante declara-los já como parâmetro da função.
Em seguida criamos a mesma estrutura da rede neural anterior, sequencial, 30 neurônios na camada de entrada, 1 na camada de saída e dropouts entre elas, porém note que os parâmetros internos a cada camada agora possuem nomes genéricos para substituição.
Por fim na camada de compilação também é mantido a métrica binary_accuracy uma vez que esta é uma classificação binária. Encerrando o bloco, apenas é dado o comando para retornar a própria função.
A seguir criamos uma variável de nome classificadorTunado, que recebe como atributo o KerasClassifier, que por sua vez tem em sua ativação nossa função tuningClassificador( ).
Da mesma forma criamos uma nova variável, agora de nome parâmetros, que pela sintaxe recebe em forma de dicionário os parâmetros a serem instanciados em nossa rede assim como os valores a serem testados.
Repare por exemplo que ‘activation’ quando instanciada, usará ‘relu’ em uma das suas fases de testes assim como ‘tanh’ em outra fase de teste, posteriormente a GridSearchCV irá verificar qual desses dois parâmetros retornou melhor resultado e irá nos sinalizar este.
Logo após criamos uma variável de nome tunagem, que recebe como atributo a ferramenta GridSearchCV, que por sua vez tem como parâmetros estimator = classificadorTunado, o que em outras palavras significa que ele usará o KerasClassifier rodando nossa função tuningClassificador( ) sob os parâmetros, também tem como parâmetro param_grid = parametros, o que significa que o classificador irá receber os parâmetros que definimos manualmente na variável parametros, por fim scoring = ‘accuracy’ que simplesmente estará aplicando métodos focados em precisão.
Por fim, criamos uma variável de nome tunagem, que aplica sobre si a função .fit( ) passando para a mesma como parâmetros entradas e saidas. Como das outras vezes, ao selecionar essa linha de código e executar, a função .fit( ) dará início a execução da rede neural.
Como dito anteriormente, este processo de testar todos os parâmetros buscando os melhores é bastante demorado, dependendo muito do hardware de sua máquina, dependendo de sua configuração, esse processo pode literalmente demorar algumas horas.
Acompanhando o processo via console você de fato irá notar que essa rede será processada e reprocessada muitas vezes, cada vez voltando a estaca zero e repetindo todo o processo com os parâmetros definidos anteriormente.
Para finalizar o processo de tuning, podemos como de costume usar funções que nos mostrem de forma mais clara os resultados. Inicialmente criamos uma variável de nome melhores_parametros que por fim aplica sobre tunagem o método best_params_, por fim criamos uma variável de nome melhor_margem_precisao que sobre tunagem aplica o método best_score_. Selecionando e executando esse bloco de código temos acesso a tais dados.
Via explorador de variáveis você pode verificar quais os parâmetros que foram selecionados quanto a sua margem de precisão. Uma vez que você descobriu quais são os melhores parâmetros você pode voltar ao seu código original, fazer as devidas substituições e rodar novamente sua rede neural artificial.
Por fim também está disponível para visualização a variável dedicada a retornar o percentual de acerto de nosso processo de tuning. Assim como o valor aproximado exibido no console no processo de testes, agora podemos ver de forma clara que usando dos melhores parâmetros essa rede chega a uma margem de precisão de 97% em sua classificação binária.
Código Completo:
Realizando Testes Sobre Uma Amostra
Uma das propriedades importantes de uma rede neural após construída,
treinada e testada é que a partir deste ponto podemos salvar esse modelo para reutilização. Uma das práticas mais comuns em uma rede como esta, de classificação binária, é a partir do modelo pronto e treinado reutilizar a mesma para que se teste alguma amostra individualmente.
Seguindo a linha de raciocínio deste exemplo, lembre-se que aqui estamos passando uma série de atributos para que seja verificado se naquela imagem de mamografia o resultado é positivo para câncer ou negativo para câncer. Dessa forma, podemos pegar uma amostra individual de nossa base de dados e aplicar o teste da rede neural sobre a mesma.
A partir de um novo arquivo iniciamos como sempre com as importações necessárias.
Realizamos a importação de nossa base de dados, dividida em dados de entrada e de saída a partir dos seus respectivos arquivos do tipo .csv.
Como de praxe realizamos a verificação se não houve nenhum erro no processo tanto no processo de importação quanto de criação das respectivas variáveis.
Criamos como usualmente fizemos anteriormente a estrutura de nossa rede neural, agora já parametrizada com os melhores parâmetros encontrados no processo de tuning.
Selecionando e executando todo bloco de código de nossa rede neural acompanhamos via console o processo de execução da mesma assim como se houve algum erro no processo. Lembrando que a esta altura o mais comum é se houver erros, estes ser erros de sintaxe, pois da estrutura lógica da rede até aqui você já deve compreender.
Dando sequência, vamos ao que realmente interessa. Uma vez feito todo processo de importação das bibliotecas, módulos, base de dados e executada a rede neural uma vez para seu respectivo treino, hora de aplicar teste sobre uma amostra. Para isso, simplesmente criamos uma variável de nome objeto que recebe como atributo uma array do tipo numpy onde selecionamos, aqui nesse exemplo, uma linha aleatória em nossa base de dados com seus respectivos 30 atributos previsores.
Por fim criamos duas variáveis, uma de nome previsorA que recebe como atributo a função .predict( ) parametrizada com nosso objeto criado anteriormente, executando tudo sobre nossa rede neural. Da mesma forma, criamos uma variável de nome previsorB que simplesmente irá conferir se previsorA for maior do que 0.5, automaticamente essa expressão nos retornará um dado True ou False.
De fato, executando todo último bloco de código são criadas as respectivas variáveis. A variável previsorA nos deu como retorno um número extremamente pequeno, porém, como mencionado anteriormente, o ideal é expormos nossas informações de forma clara.
O previsorB por sua vez, com base em previsorA nos retorna False, que em outras palavras significa que para aquela amostra o resultado é negativo para câncer.
Salvando o Modelo Para Reutilização
Como dito anteriormente, uma prática comum é a reutilização de um
modelo de rede uma vez que esse está pronto, sem erros de código, treinado já com os melhores parâmetros, para que ele possa ser ferramenta para solucionar certos problemas computacionais de mesmo tipo. Isto pode ser feito de forma bastante simples, salvando o código da rede neural (obviamente) assim como sua estrutura funcional e pesos por meio de arquivos do tipo .py, .json e .h5. Raciocine que em bases de dados pequenas, como esta que estamos usando para testes, o processamento dela pela rede se dá em poucos segundos, porém, em determinadas situações reais é interessante você ter uma rede pronta sem a necessidade de ser treinada novamente (ainda mais com casos onde são usadas bases com milhões de dados como em big data).
Repare que esta é simplesmente a mesma estrutura criada anteriormente, já com as devidas importações, já com toda estrutura da rede neural, etc... Simplesmente você pode reaproveitar todo esse bloco de código em um novo arquivo para poupar seu tempo.
O processo em si, de salvar nosso modelo para reutilização basicamente de dá da seguinte forma. Inicialmente criamos uma variável de nome classificador_json que recebe toda estrutura de nossa rede neural classificadorB com a função .to_json( ), essa função permite salvar esse modelo neste formado de arquivo nativo para as IDEs Python.
Em seguida entramos com o método with open( ) que por sua vez recebe como parâmetro inicialmente ‘classificador_binario.json’, ‘w’, isto nada mais é do que o comando que estamos dando para que se crie um arquivo com o nome classificador_binario.json na mesma pasta onde estão seus arquivos de código, na sequência existe a condição as json_file: que permite definirmos algum atributo interno.
Dentro de json_file simplesmente aplicamos sobre si a função .write( ) passando como parâmetro todo o conteúdo de classificador_json.
Por fim, fazemos um processo parecido para salvar os pesos, mas agora, usando uma função interna da biblioteca pandas mesmo, então, alocando sobre classificadorB a função .save_weights( ) e definindo como parâmetro também o nome do arquivo que guardará esses pesos, no formato h5. Selecionando e rodando esse bloco de código serão criados os respectivos arquivos na pasta onde está o seu código.
Carregando Uma Rede Neural Artificial Pronta Para Uso
Como sempre, começando pelas importações, note que há a importação de uma ferramenta ainda não mencionada nos exemplos anteriores. A ferramenta model_from_json, como o próprio nome indica, serve para importar modelos prontos, desde que estejam no formado json, esta ferramenta faz parte do módulo models da biblioteca keras.
Em seguida criamos uma variável de nome arquivo, que recebe como atributo ao método open, parametrizado com o nome de nosso arquivo salvo anteriormente em forma de string, assim como o segundo parâmetro ‘r’, uma vez que agora estamos fazendo a leitura do arquivo.
Na sequência criamos uma variável de nome estrutura_rede que recebe os dados de arquivo pela função .read( ). Por fim, podemos fechar o arquivo carregado por meio da função .close( ). Pode ficar tranquilo que uma vez feita a leitura ela ficará pré-alocada em memória para nosso interpretador, o arquivo em si pode ser fechado normalmente.
Se o processo de leitura a partir do arquivo json foi feita corretamente, a respectiva variável será criada.
Abrindo a variável estrutura_rede é possível ver que de fato todos os parâmetros de nossa rede neural estão salvos de forma que nosso interpretador conseguirá fazer a leitura e uso dos mesmos. A partir daqui, é possível criar qualquer modelo previsor, instanciando nosso classificador.