# -*- coding: utf-8 -*-
"""Desafio_CyberLabs.ipynb

Automatically generated by Colaboratory.

Original file is located at https://colab.research.google.com/drive/1o5txnZiNYXfwjhikOJQQsIe5aRbsbwcy

# Classificador para os números 0 e 5 do dataset MNIST

Ao desenvolver um classificador, há duas estratégias a serem seguidas:
Criar um modelo capaz de identificar especificamente as classes desejadas ou criar um modelo capaz de identificar todos os outros cenários, emitindo um alerta para aqueles que fogem dessa regra. A melhor estratégia varia com o cenário.

Nesse caso, é mais inteligente treinar o modelo para aprender a classificar os números 0 e 5, ao invés de ensina-lo a identificar os números 1,2,3,4,6,7,8,9.

Para isso, usaremos algumas bibliotecas de aprendizado profundo e visão computacional, como numpy, matplotlib, sklearn, keras e tensorflow.
"""

import numpy as np
import keras
import matplotlib
from matplotlib import pyplot as plt
import tensorflow as tf
from sklearn.model_selection import train_test_split, KFold
from tensorflow import keras
from tensorflow.keras import layers
import random


# Carregando o conjunto de dados completo do MNIST
mnist = tf.keras.datasets.mnist
(X_all_train, Y_all_train), (X_all_test, Y_all_test) = mnist.load_data() 

""" Na forma atual, o conjunto de dados está preparado para um modelo de classificação 
multiclasse, ou seja, um modelo que seja capaz de identificar cada dígito separadamente.
Para moldar o conjunto de dados para esse desafio, criaremos um modelo de classificação binária:
Ou o digito é igual a 5 ou 0 --> tendo rótulo igual a 1
Ou o digito é diferente disso --> tendo rótulo igual a 0"""

# Modulando os rótulos do conjunto de treino
for x in range (Y_all_train.shape[0]):
  if (Y_all_train[x]!= 0 and Y_all_train[x]!= 5):
    Y_all_train[x]=0
  else:
    Y_all_train[x]=1

# Modulando os rótulos do conjunto de teste
for x in range (Y_all_test.shape[0]):
  if (Y_all_test[x]!= 0 and Y_all_test[x]!= 5):
    Y_all_test[x]=0
  else:
    Y_all_test[x]=1

# Visualizando nossos novos rótulos -> nao deve conter outro número além de 0 e 1
print(Y_all_train[:10])
print(Y_all_test[:10])

"""Como estamos utilizando um conjunto de dados previamente tratado, não precisamos nos preocupar com outliers ou imagens deformadas. Entretanto, é uma boa tática visualizar algumas amostras do conjunto para facilitar a criação do modelo, particionando corretamente o conjunto de dados de acordo com a quantidade de amostras nele presente.
O conjunto de dados está divido em aproximadamente 85% para treino e 15% para teste.
"""

# Analisando o conjunto de dados para treino
print (X_all_train.shape)
print (X_all_test.shape)

# Mostrando as duas primeiras imagens do conjunto de treino
imagem = plt.figure(figsize=(5, 5))
imagem.add_subplot(2, 2, 1)
plt.imshow(X_all_train[0],cmap='gray')
plt.axis('on')
imagem.add_subplot(2, 2, 2)
plt.imshow(X_all_train[1],cmap='gray')
plt.axis('on')

"""Logo, podemos notar que temos 60000 imagens para treino e outras 10000 para teste, cada uma com 28x28 pixels

Antes de iniciarmos a criação do modelo, podemos dar mais alguns ajustes do conjunto de dados.
Uma boa tática é realizar a normalização dos dados de treino, o que pode agilizar o tempo de aprendizado do modelo, convergindo para um modelo ideal mais rapidamente. Além disso, a normalização garante que, previamente, nenhuma característica tenha um peso maior que a outra, já que é o trabalho do modelo descobrir isso.
"""

# Normalizando o conjunto de treino com a normalização Min-Max
X_all_train = X_all_train.astype('float32')
X_all_train /= 255

"""Buscando um modelo mais robusto e confiável, vamos usar a técnica de validação cruzada k-fold, que consiste em dividir o conjunto de dados em k partes, treinando um modelo em cada um deles, com exceção de um que será usado para avaliar o modelo. Esse procedimento é realizado k vezes, modificando o conjunto da avaliação. Assim, diminuimos as chances de haver sobreajuste do modelo quanto ao conjunto de dados utilizado para treino.

Para isso, iremos reorganizar o conjunto de dados, unindo os conjuntos de treino e teste para separalá-los posteriormente numa proporção de 70% - 30%.
"""

# Unindo os dados
X_all_train = X_all_train.reshape(X_all_train.shape[0],28,28,1)
X_all_test = X_all_test.reshape(X_all_test.shape[0],28,28,1)
data=np.concatenate((X_all_train, X_all_test), axis=0)
labels=np.concatenate((Y_all_train, Y_all_test), axis=0)

# Iniciando a validação cruzada com k = 5

x_train, x_test, y_train, y_test = train_test_split(data, labels, test_size=0.3, random_state=182)
kfold = KFold(n_splits=5, shuffle=True, random_state=182)

# Listas para armazenar as métricas para cada fold
acc = []
prec = []
rec = []
auc = []

contador = 0;

for train, test in kfold.split(data, labels):
  contador+=1
  x_train, x_test = data[train], data[test]
  y_train, y_test = labels[train], labels[test]

  """Aqui começamos a definir o modelo a ser criado. Dentre os algoritmos de visão computacional, a Rede Neural 
  Convolucional (CNN - Convolutional Neural Network) apresenta ótimos resultados na análise de imagens. Por ser 
  um modelo mais robusto, essa rede pode apresentar um alto custo computacional se analisar imagens grandes e 
  ter uma estrutura profunda, isso é, muitas camadas escondidas.
  Como a tarefa atual é simples, não é necessário usar uma rede profunda, como os modelos Inception da Google,
  vamos desenvolver nossa própria rede para melhor se adequar à tarefa."""

  model = keras.Sequential([
  layers.Input(shape=(28, 28,1)),
 
  layers.Conv2D(16, 5, activation='relu',padding='valid'), # 128

  layers.Conv2D(16,5, activation='tanh',padding='valid'), #1024

  layers.MaxPooling2D(pool_size=(2, 2)), # 12

  layers.Conv2D(32,3, activation='relu',padding='valid'), # 

  layers.Conv2D(64,3, activation='relu',padding='valid'), # 4

  layers.Conv2D(64,3, activation='tanh',padding='same'), # 256

  layers.Dense(units=16, activation='relu'),
  
  layers.Dropout(rate=0.4),

  layers.Flatten(),

  layers.Dense(1, activation='sigmoid')])

  #Definindo as métricas de interesse
  METRICS = [keras.metrics.TruePositives(name='tp'), keras.metrics.FalsePositives(name='fp'), keras.metrics.TrueNegatives(name='tn'), keras.metrics.FalseNegatives(name='fn'), keras.metrics.BinaryAccuracy(name='accuracy'), keras.metrics.Precision(name='precision'), keras.metrics.Recall(name='recall'), keras.metrics.AUC(name='auc')]

  #Aqui usamos o otimizador Adam, um método de descida gradiente estocástico adaptativo. A entropia cruzada binária foi usada como função de perda, já que trasformei a classificação multiclasse em uma classificação binária
  model.compile(optimizer=keras.optimizers.Adam(learning_rate=0.001), loss = 'binary_crossentropy', metrics=METRICS)
  model.build()
  model.summary() # aprensentando uma visão geral do modelo criado
  
  #EarlyStopping - caso a perda não diminua em 3 épocas seguidas, o treinamento é finalizado
  callbacks = [tf.keras.callbacks.EarlyStopping(monitor='loss', mode='min', patience=3, restore_best_weights=True)] 
    
  #Treinando o modelo em 5 épocas, com tamanho do batch igual a 128
  history = model.fit(x_train, y_train, epochs=5, batch_size=128, callbacks=callbacks)

  print("Avaliando o "+ str(contador)+"° modelo: \n")
  results = model.evaluate(x_test, y_test)
  print("[loss, tp, fp, tn, fn, accuracy, precision, recall, auc]: " + str(results))
  acc.append(results[5])
  prec.append(results[6])
  rec.append(results[7])
  auc.append(results[8])

#Apresentando os resultados encontrados
print("Resultados para acurácia: (%f +/- %f)\n" % (np.mean(acc), np.std(acc)))
print("Resultados para precisão: (%f +/- %f)\n" % (np.mean(prec), np.std(prec)))
print("Resultados para sensibilidade: (%f +/- %f)\n" % (np.mean(rec), np.std(rec)))
print("Resultados para auc: (%f +/- %f)\n" % (np.mean(auc), np.std(auc)))

"""Podemos testar o modelo com o método predict para confirmar que nenhuma confusão foi feita durante o processo."""

# Visualizando algumas imagens

a = random.randint(500,510)
b = random.randint(510,520)

imagem = plt.figure(figsize=(7, 7))
x_test = x_test.reshape(x_test.shape[0],28,28)
figure,image = plt.subplots(1,b-a, figsize=(20,20))

contador = 0
for i in range(a,b):
  image[contador].imshow(x_test[i].reshape((28,28)),cmap='gray')
  image[contador].axis('off')
  image[contador].set_title(str(i) + "° - " + str(y_test[i]) + " ")
  contador+=1

# Usando o método predict para confirmar os resultados obtidos
x_test = x_test.reshape(x_test.shape[0],28,28,1)
y_pred = model.predict(x_test)
y_pred = np.round(y_pred).astype(int)

# Se esses números corresponderem ao número das imagens com 0 ou 5, nosso modelo está funcionando corretamente
for i in range(a,b):
  if y_test[i] == 1:
    print(i)

# Checando se todas as imagens nesse intervalo tiveram uma predição correta
for i in range(a,b):
  print(y_pred[i], end="")

"""Assim, podemos concluir que o modelo está funcionando corretamente, obtendo ótimos resultados.
Em cenário diferente, onde os resultados poderiam estar ruins ou inconclusivos, ainda teriamos outras atividades a realizar: Utilizar uma normalização que melhor atendesse o conjunto de dados, modificar o número de épocas, o tamanho do batch e o passo de aprendizagem, além de realizar a otimização de hiperparâmetros na rede, escolhendo a quantidade de filtros, as funções de ativação e número de camadas, por exemplo, que melhor satisfaz a classificação dos números.
"""
