{"cells":[{"metadata":{"id":"nPtzZfeclfLs"},"cell_type":"markdown","source":"# SIIM-ISIC Melanoma Classification\n\n\n","execution_count":null},{"metadata":{"id":"kRNS-y6lni6Y"},"cell_type":"markdown","source":"O [câncer de pele](https://saude.gov.br/saude-de-a-z/cancer-de-pele) é um grave problema de saúde, sendo o tipo de câncer mais comum no Brasil e no mundo. Dentro das categorias de câncer de pele, o Melanoma é o tipo mais perigoso por sua alta possibilidade de ocasionar metástase e se espalhar para outros órgãos.\n\nA competição [SIIM-ISIC Melanoma Classification](https://www.kaggle.com/c/siim-isic-melanoma-classification) tem o objetivo de classificar em benigna e maligna várias imagens suspeitas de possuir câncer de pele Melanoma. A submissão deve conter um arquivo .csv com uma coluna de dados referente ao nome das imagens e outra com as respectivas probabilidades de cada imagem conter uma amostra maligna.\n\nA base é composta por 33126 imagens de treinamento e 10982 imagens de teste. E também por dois arquivos .csv com algumas informações sobre os pacientes de cada imagem.","execution_count":null},{"metadata":{"id":"AzYnjm99ntQy"},"cell_type":"markdown","source":"## Análise da base de dados","execution_count":null},{"metadata":{"id":"pz43JPyeWdZp"},"cell_type":"markdown","source":"Para todos os exemplos testados nesse notebook, admitimos que os arquivos train.csv, test.csv e as pastas /train e /test com as imagens .jpg estão no mesmo diretório que esse código.","execution_count":null},{"metadata":{"id":"c5gcWraNoKXP"},"cell_type":"markdown","source":"Primeiramente, devemos ler os arquivos .csv com as informações da base.","execution_count":null},{"metadata":{"id":"DCxTfHM6n7Zt","trusted":false},"cell_type":"code","source":"import pandas as pd\n\n# Leitura dos dataframes dos arquivos csv\ntrain_data = pd.read_csv('train.csv')\ntest_data = pd.read_csv('test.csv')","execution_count":null,"outputs":[]},{"metadata":{"id":"CGz6BS3HpC5J"},"cell_type":"markdown","source":"Características do arquivo de treinamento.","execution_count":null},{"metadata":{"id":"35Rf643FpHpd","outputId":"68fb04f4-058b-44ea-dd13-11cfe270482e","trusted":false},"cell_type":"code","source":"train_data.head()","execution_count":null,"outputs":[]},{"metadata":{"id":"7EJI6Q2fpLwK"},"cell_type":"markdown","source":"Vemos que esse dataset possui as informações:\n\n* image_name: Nome da imagem referente a esse exemplo de treinamento.\n* patient_id: ID do paciente.\n* sex: Sexo do paciente.\n* age_approx: Idade aproximada do paciente\n* anatom_site_general_challenge: Parte do corpo onde a foto foi tirada.\n* diagnosis: Dianóstico.\n* benign_malignant: Classificação da mancha em maligna ou benigna.\n* target: 0 (benigno), 1 (maligno).","execution_count":null},{"metadata":{"id":"UR5KKzHvq0VA"},"cell_type":"markdown","source":"Características do arquivo de teste.","execution_count":null},{"metadata":{"id":"IZeabc25qwgW","outputId":"8a11ef8e-15ca-46e4-a785-574b013eb05a","trusted":false},"cell_type":"code","source":"test_data.head()","execution_count":null,"outputs":[]},{"metadata":{"id":"6kMVnFzGq48P"},"cell_type":"markdown","source":"O arquivo de dados de teste possui quase as mesmas informações que o de treinamento, exceto as últimas colunas (diagnosis, benign_malignant, target), as quais queremos inferir.","execution_count":null},{"metadata":{"id":"jWowG8Bxrbr9"},"cell_type":"markdown","source":"Um dado interessante a ser observado em qualquer base é a distribuição dos exemplos em cada classe.","execution_count":null},{"metadata":{"id":"wl4NBHsyTRwq","outputId":"b2ac10d6-ee05-47b3-8e3b-0fd902abc0f8","trusted":false},"cell_type":"code","source":"import matplotlib.pyplot as plt\n\nlabels=train_data['benign_malignant'].value_counts().index\nvalues=train_data['benign_malignant'].value_counts().values\n\nplt.title('Número de exemplos de cada classe')\nnclasses = plt.bar(labels, values)\nnclasses[0].set_color('tab:orange')\nplt.show()\n\nprint(\"Número de exemplos benignos: {}\".format(values[0]))\nprint(\"Número de exemplos malignos: {}\".format(values[1]))","execution_count":null,"outputs":[]},{"metadata":{"id":"5GPlUDGlUABt"},"cell_type":"markdown","source":"Vemos que essa base possui um forte desbalanceamento, essa característica é muito importante e irá influenciar a maneira como os modelos de redes neurais serão treinados.","execution_count":null},{"metadata":{"id":"Nt17syw96Y6O"},"cell_type":"markdown","source":"Também é interessante visualizar algumas das imagens de ambas as classes para verificar as características de manchas que são Melanoma e manchas que não são Melanomas.","execution_count":null},{"metadata":{"id":"SPyHY54G6Y6O","outputId":"4ea274f3-aa52-4b92-f990-0616776e9aaa","trusted":false},"cell_type":"code","source":"import cv2\nimport numpy as np\n\nbenign_samples = train_data[train_data['benign_malignant']=='benign'].sample(20)\n\nfig, ax = plt.subplots(5,4, figsize=(10,8))\n\nfor i in range(len(benign_samples)):\n    img=cv2.imread(str(\"train/\" + benign_samples['image_name'].iloc[i]+'.jpg'))\n    img = cv2.resize(img, (224,224))\n    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n    img = img.astype(np.float32)/255.\n    ax[i//4, i%4].imshow(img)\n    ax[i//4, i%4].axis('off')\n\nfig.suptitle('Imagens de amostras benignas', fontsize=22)       \nplt.show()","execution_count":null,"outputs":[]},{"metadata":{"id":"Oi3xyYUV6Y6T","outputId":"f4f31b51-59ba-4530-bcbd-0f9dec11df77","trusted":false},"cell_type":"code","source":"malignant_samples = train_data[train_data['benign_malignant']=='benign'].sample(20)\nfig, ax = plt.subplots(5,4, figsize=(10,8))\n\nfor i in range(len(malignant_samples)):\n    img=cv2.imread(str(\"train/\" + malignant_samples['image_name'].iloc[i]+'.jpg'))\n    img = cv2.resize(img, (224,224))\n    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n    img = img.astype(np.float32)/255.\n    ax[i//4, i%4].imshow(img)\n    ax[i//4, i%4].axis('off')\n        \nfig.suptitle('Imagens de amostras malignas', fontsize=22)       \nplt.show()","execution_count":null,"outputs":[]},{"metadata":{"id":"q9DKFm3r6Y6X"},"cell_type":"markdown","source":"Para uma pessoa sem treinamento proficional, identificar se uma amostra é maligna ou benigna é bastante difícil.","execution_count":null},{"metadata":{"id":"aejAYBkRtn_p"},"cell_type":"markdown","source":"## Avaliação e score\n\nQuando treinamos um modelo que será usado para fazer inferências é essencial estabelecer quais métricas serão usadas para avaliá-lo.\n\nO score oficial usado para testar as submissões na competição é a área abaixo da curva ROC das probabilidades preditas e os valores esperados.\n\n### Curva ROC e AUC\n\nA curva ROC (Curva Característica de Operação do Receptor) é uma forma de representar os resultados de desempenho de um classificador baseado em parâmetros de taxa de resultados \"Verdadeiro Positivo\" e \"Falso Positivo\".\n\nTemos que:\n\n>$Taxa De Verdadeiros Positivos = \\frac{Verdadeiros Positivos}{Verdadeiros Positivos + Falsos Negativos}$\n\n>$Taxa De Falsos Positivos = \\frac{Falsos Positivos}{Falsos Positivos + Verdadeiros Negativos}$\n\nPara representação gráfica, são consideradas essas taxas referentes à diferentes valores de threshold do classificador, aumentando e/ou diminuindo os valores esperados como positivos ou negativos.\n\nComo ilustrado na imagem abaixo, temos a curva ROC representada a partir de diferentes pontos de treshhold do classificador (pontos A, B e C).\n\n<img src=\"https://drive.google.com/uc?id=1mA6SJEf27PR9VJVfonuCqKHED4yMknjP\">\n\nEsse tipo de representação é extremamente útil para avaliação de aplicações que trabalham em um ambiente com uma grande desproporção entre classes, possibilitando a consideração de um \"custo/benefício\" para os erros/acertos do classificador.\n\nE com o intuito de simplificar e quantizar o desempenho de um classificador representado no gráfico de curva ROC, é utilizado a área abaixo da curva ROC, chamada de AUC, como métrica. Seu valor pode variar entre 0 e 1, sendo que quanto maior for seu valor, melhor será o classificador.\n\n","execution_count":null},{"metadata":{"id":"EfsW8PqkrlKd"},"cell_type":"markdown","source":"## Construindo um simples modelo\n\nA primeira submissão para a competição tinha o objetivo de testar um modelo simples e verificar aproximadamente quanto tempo demoraria para a sua execução, com isso avaliaríamos a viabilidade de lidar com essa base tão extensa.","execution_count":null},{"metadata":{"id":"_EDHt1rfzGWW","trusted":false},"cell_type":"code","source":"# Imports\nimport pandas as pd\nimport numpy as np\n\nfrom tensorflow.keras.preprocessing.image import ImageDataGenerator\n\nfrom tensorflow.keras.models import Sequential\nfrom tensorflow.keras.layers import Conv2D\nfrom tensorflow.keras.layers import MaxPooling2D\nfrom tensorflow.keras.layers import Flatten\nfrom tensorflow.keras.layers import Dense\n\n\nfrom tensorflow.keras.metrics import TruePositives\nfrom tensorflow.keras.metrics import FalsePositives\nfrom tensorflow.keras.metrics import TrueNegatives\nfrom tensorflow.keras.metrics import FalseNegatives\nfrom tensorflow.keras.metrics import BinaryAccuracy\nfrom tensorflow.keras.metrics import Precision\nfrom tensorflow.keras.metrics import Recall\nfrom tensorflow.keras.metrics import AUC\n\nfrom tensorflow.keras.callbacks import EarlyStopping\n\nfrom sklearn.metrics import roc_auc_score","execution_count":null,"outputs":[]},{"metadata":{"id":"3Ij-uNYisqgj"},"cell_type":"markdown","source":"Primeiramente carregamos os aquivos de informações e adicionamos a extensão de formato das imagens às colunas image_name.","execution_count":null},{"metadata":{"id":"E5C6daLAs6PO","trusted":false},"cell_type":"code","source":"# Leitura dos dataframes dos arquivos csv\ntrain_data = pd.read_csv('train.csv')\ntest_data = pd.read_csv('test.csv')\n\n# Adicionando a extensão aos nomes das imagens\ntrain_data['image_name'] = train_data['image_name'].astype(str)+\".jpg\"\ntest_data['image_name'] = test_data['image_name'].astype(str)+\".jpg\"","execution_count":null,"outputs":[]},{"metadata":{"id":"lQ7XYAmVs-Kq"},"cell_type":"markdown","source":"Para lidar com imagens, o Keras possui a ferramenta [ImageDataGenerator](https://keras.io/api/preprocessing/image/). Essa classe foi criada para lidar com grandes bases de dados de imagens e permite um carregamento em pacotes de imagens para gerar menos impacto na memória RAM do computador. Além disso, ainda faz o pré-processamento das imagens, colocando-as em um determinado tamanho e podendo até mesmo aplicar filtros e outros métodos de visão computacional.\n\nPara facilitar ainda mais, essa classe possui o método [flow_from_dataframe](https://keras.io/api/preprocessing/image/#flowfromdataframe-method), que faz o carregamento das imagens por meio de dados de um dataframe pandas.\n\nNessa primeira submissão, escolhemos um tamanho de imagem pequeno de 32x32 pixels.","execution_count":null},{"metadata":{"id":"OfeCO0haurOl","outputId":"81ec1bef-cfbe-4670-aea7-eb2f8384553a","trusted":false},"cell_type":"code","source":"image_height = 32\nimage_width = 32\nbatch_size = 32\n\n# Carregando as imagens de treinamento com ImageDataGenerator\nimage_datagen = ImageDataGenerator(rescale = 1./255,          # normaliza valores dos pixels da imagem entre 0-1\n                                   validation_split = 0.3)    # divide os dados do dataset em uma proporção de treinamento e validação\n\n\ntrain_generator = image_datagen.flow_from_dataframe(dataframe=train_data,                   # dataframe com dados da imagem\n                                                    directory=\"train\",                      # diretório com as imagens\n                                                    x_col=\"image_name\",                     # nome da coluna do dataframe com os nomes das imagens\n                                                    y_col=\"benign_malignant\",               # nome da coluna do dataframe com a especificação das classes\n                                                    class_mode=\"binary\",                    # modo binário, irá selecionar as imagens em duas classes\n                                                    target_size=(image_height,image_width), # tamanho das imagens de treinamento\n                                                    batch_size=batch_size,                  # quantidade de imagens por pacote\n                                                    subset=\"training\",                      # subset de treinamento ou validação\n                                                    color_mode=\"rgb\")                       # modo de carregamento da imagem em 3 canais RGB\n\nvalidation_generator = image_datagen.flow_from_dataframe(dataframe=train_data,\n                                                         directory=\"train\",\n                                                         x_col=\"image_name\",\n                                                         y_col=\"benign_malignant\",\n                                                         class_mode=\"binary\",\n                                                         target_size=(image_height,image_width),\n                                                         batch_size=batch_size,\n                                                         subset=\"validation\",\n                                                         color_mode=\"rgb\")\n\n","execution_count":null,"outputs":[]},{"metadata":{"id":"BXEkRwN8xyIJ"},"cell_type":"markdown","source":"Em seguida, construimos um modelo simples, com apenas uma camada de convolução usando a biblioteca Keras. ","execution_count":null},{"metadata":{"id":"T0g1km-nyORE","outputId":"7facf7b5-42a6-4de1-80d1-bc541b86ec2c","trusted":false},"cell_type":"code","source":"model = Sequential()\nmodel.add(Conv2D(filters=32, kernel_size=(7,7), input_shape=(32,32,3), activation='relu'))\nmodel.add(MaxPooling2D(pool_size = (2, 2)))\nmodel.add(Flatten())\nmodel.add(Dense(units = 100, activation = 'relu'))\nmodel.add(Dense(units = 1, activation = 'sigmoid'))\nmodel.summary()","execution_count":null,"outputs":[]},{"metadata":{"id":"ytfXXz_9z0vb"},"cell_type":"markdown","source":"Para compilar o nosso modelo, escolhemos o otimizador [Adam](https://machinelearningmastery.com/adam-optimization-algorithm-for-deep-learning/) e a funcão de loss [binary_crossentropy](https://keras.io/api/losses/probabilistic_losses/#binarycrossentropy-class).\n\nPara avaliar o nosso modelo durante o treinamento, iremos observar:\n\n* True Positives: amostras com cancer corretamente classificadas.\n* False Positives: amostras sem cancer erroneamente classificadas.\n* True Negatives: amostras sem cancer corretamente classificadas.\n* False Negatives: amostras com cancer erroneamente classificadas.\n* Binary Accuracy: Acurácia do modelo.\n* Precision: Precisão do modelo.\n* Recall: Revocação do modelo.\n* AUC: Área aproximada abaixo da curva ROC","execution_count":null},{"metadata":{"id":"UyG33q7XnBzz","trusted":false},"cell_type":"code","source":"METRICS = [\n      TruePositives(name='tp'),\n      FalsePositives(name='fp'),\n      TrueNegatives(name='tn'),\n      FalseNegatives(name='fn'), \n      BinaryAccuracy(name='accuracy'),\n      Precision(name='precision'),\n      Recall(name='recall'),\n      AUC(name='auc'),\n]","execution_count":null,"outputs":[]},{"metadata":{"id":"HzXMjHTGzw1l","trusted":false},"cell_type":"code","source":"model.compile(optimizer = 'adam', loss = 'binary_crossentropy', metrics = METRICS)","execution_count":null,"outputs":[]},{"metadata":{"id":"Mamxadka6mrH"},"cell_type":"markdown","source":"Para fazer o treinamento, é útil usar uma função de callback para parar esse treinamento quando o modelo não está mais evoluindo. Por isso, utilizaremos a função [EarlyStopping](https://keras.io/api/callbacks/early_stopping/), que irá acompanhar a medida AUC do dataset de validação ('val_auc').","execution_count":null},{"metadata":{"id":"XnncNLUs6kN3","trusted":false},"cell_type":"code","source":"early_stopping = EarlyStopping(\n    monitor='val_auc', \n    verbose=1,\n    patience=3,\n    mode='max',\n    restore_best_weights=True)","execution_count":null,"outputs":[]},{"metadata":{"id":"6KQW5Plgyc0K"},"cell_type":"markdown","source":"Para fazer o treinamento, usamos o método fit com as imagens de treinamento e validação. O parâmetro workers faz paralelização do processamento quando múltiplos cores de processamento estão disponíveis.","execution_count":null},{"metadata":{"id":"MrZbFiySy59S","outputId":"df4c4fc2-3585-4e5c-d59f-d45a933eb3d6","trusted":false},"cell_type":"code","source":"# Fazer o Treinamento\nSTEP_SIZE_TRAIN = train_generator.n//train_generator.batch_size\nSTEP_SIZE_VALID = validation_generator.n//validation_generator.batch_size\n\nmodel.fit(train_generator,\n          steps_per_epoch=STEP_SIZE_TRAIN,\n          validation_data=validation_generator,\n          validation_steps=STEP_SIZE_VALID,\n          epochs=50,\n          callbacks=early_stopping,\n          workers = 16)\n\nmodel.save_weights('simple_model_weights.h5')","execution_count":null,"outputs":[]},{"metadata":{"id":"6b922Y8wztv8"},"cell_type":"markdown","source":"Antes de fazer a inferência dos dados de teste, vamos analisar o resultado nos dados de validação.","execution_count":null},{"metadata":{"id":"OcE0HavTz8ak","outputId":"38bea391-e86c-4d25-b7a8-4aec55f6875c","trusted":false},"cell_type":"code","source":"validation_predictions = model.predict(validation_generator,\n                                       verbose=1,\n                                       workers=16)","execution_count":null,"outputs":[]},{"metadata":{"id":"W8BGI4rO86m3"},"cell_type":"markdown","source":"Usamos o cálculo do score AUC da biblioteca scikitLearn para obter um resultado mais fiel dessa estatística.","execution_count":null},{"metadata":{"id":"AB6km99T85E5","outputId":"79804a92-f2ff-4b50-9327-1693ff4158f7","trusted":false},"cell_type":"code","source":"val_auc = roc_auc_score(y_true = validation_generator.classes, y_score=validation_predictions)\nprint(val_auc)","execution_count":null,"outputs":[]},{"metadata":{"id":"UCHzfNF89R7P"},"cell_type":"markdown","source":"Para visualizar graficamente, construimos a matriz de confusão das predições de validação.","execution_count":null},{"metadata":{"id":"RtDvHmcqaqpF","trusted":false},"cell_type":"code","source":"from sklearn.metrics import confusion_matrix\nfrom sklearn.metrics import accuracy_score\nimport matplotlib.pyplot as plt\nimport seaborn as sns\n\ndef plot_confusion_matrix(y_true, y_pred, classes, title):\n    acc = accuracy_score(y_true, y_pred)\n    title = title + \" (Acurácia: \" + str(\"{:10.4f}\".format(acc)) + \")\"\n\n    cm = confusion_matrix(y_true, y_pred, normalize='true')\n    cm_df = pd.DataFrame(cm, index = classes, columns = classes)\n    plt.figure(figsize=(5.5,4))\n    sns.heatmap(cm_df, annot=True, cmap=\"YlGnBu\")\n    plt.title(title)\n    plt.ylabel('Label verdadeira')\n    plt.xlabel('Label predita')\n    plt.show()\n","execution_count":null,"outputs":[]},{"metadata":{"id":"sL_wnfUg9QdV","outputId":"121528ce-86e6-46e7-f69f-f55328e17ee8","trusted":false},"cell_type":"code","source":"# Plota a matriz de confusão dos dados de validação\nplot_confusion_matrix(validation_generator.classes,\n                      (validation_predictions>0.5).astype(int),\n                      ['benign','malignant'], \"Matriz de confusão\")","execution_count":null,"outputs":[]},{"metadata":{"id":"QkH40LusbIPP"},"cell_type":"markdown","source":"Como podemos ver, apesar de apresentar uma alta acurácia, o modelo não classificou nenhuma imagem como melanoma. Isso provavelmente se deve a simplicidade do modelo, redução massiva do tamanho das imagens e ao grande desbalanceamento da base.","execution_count":null},{"metadata":{"id":"9Ap8J_5LbiwF"},"cell_type":"markdown","source":"Para fazer a primeira submissão, inferimos os dados de teste.","execution_count":null},{"metadata":{"id":"RKhqrqtfbxNX","outputId":"d07d58e4-4204-4709-b8c1-a6cce707f60d","trusted":false},"cell_type":"code","source":"test_datagen = ImageDataGenerator(rescale = 1./255)\ntest_generator = test_datagen.flow_from_dataframe(dataframe=test_data,\n                                                  directory=\"test\",\n                                                  x_col=\"image_name\",\n                                                  y_col=None,\n                                                  class_mode=None,\n                                                  target_size=(image_height,image_width),\n                                                  batch_size=batch_size)\n\n# Realiza predição\npredictions = model.predict(test_generator,\n                            verbose=1,\n                            workers=16)","execution_count":null,"outputs":[]},{"metadata":{"id":"yBAzFLzBega-","trusted":false},"cell_type":"code","source":"submission = pd.DataFrame(test_data['image_name'].str.replace(r'.jpg', ''))\nsubmission['target']=predictions\n\n# Cria arquivo de submissão\nsubmission.to_csv('simple_model_submission.csv',index=False)","execution_count":null,"outputs":[]},{"metadata":{"id":"HLCBwrW87GHW"},"cell_type":"markdown","source":"O score oficial dessa submissão foi 0.506.","execution_count":null},{"metadata":{"id":"0bS-Q1vQ7O5Q"},"cell_type":"markdown","source":"## Modelo de rede neural VGG\n\nA segunda submissão tinha como objetivo implementar um modelo de classificação de imagem já testado em bases de classificação famosas, uma rede neural [VGG](https://becominghuman.ai/what-is-the-vgg-neural-network-a590caa72643).\n\nNossa implementação possui uma arquitetura semelhante a VGG Network, usando várias camadas de convolução seguidas de camadas de pooling, criando uma rede neural convolucional profunda.\n\nEssa rede terá como entrada imagens de 224x224 pixels.","execution_count":null},{"metadata":{"id":"tummWV6-AGMy","outputId":"ef5b0cad-078a-404d-fddb-999bc2acad06","trusted":false},"cell_type":"code","source":"# Carregamento das imagens\n\ntrain_data = pd.read_csv('train.csv')\ntest_data = pd.read_csv('test.csv')\n\ntrain_data['image_name'] = train_data['image_name'].astype(str)+\".jpg\"\ntest_data['image_name'] = test_data['image_name'].astype(str)+\".jpg\"\n\nbatch_size = 16       # Tamanho do pacote\nimage_height = 224    # Altura da imagem\nimage_width = 224     # Largura da imagem\n\n# Realiza as augmentações nas imagens\nimage_datagen = ImageDataGenerator(rescale = 1./255,\n                                   validation_split = 0.3)\n\ntrain_generator = image_datagen.flow_from_dataframe(dataframe=train_data,\n                                                    directory=\"train\",\n                                                    x_col=\"image_name\",\n                                                    y_col=\"benign_malignant\",\n                                                    class_mode=\"binary\",\n                                                    target_size=(image_height,image_width),\n                                                    batch_size=batch_size,\n                                                    subset=\"training\",\n                                                    color_mode=\"rgb\")\n\nvalidation_generator = image_datagen.flow_from_dataframe(dataframe=train_data,\n                                                         directory=\"train\",\n                                                         x_col=\"image_name\",\n                                                         y_col=\"benign_malignant\",\n                                                         class_mode=\"binary\",\n                                                         target_size=(image_height,image_width),\n                                                         batch_size=batch_size,\n                                                         subset=\"validation\",\n                                                         color_mode=\"rgb\")","execution_count":null,"outputs":[]},{"metadata":{"id":"sY79uCVGBAAv","outputId":"32da8186-3754-4acb-e82a-2125743cc94f","trusted":false},"cell_type":"code","source":"# Criação do modelo\n\nmodel = Sequential()\nmodel.add(Conv2D(input_shape=(image_height,image_width,3),filters=64,kernel_size=(3,3),padding=\"same\", activation=\"relu\"))\nmodel.add(Conv2D(filters=64,kernel_size=(3,3),padding=\"same\", activation=\"relu\"))\nmodel.add(MaxPooling2D(pool_size=(2,2),strides=(2,2)))\nmodel.add(Conv2D(filters=128, kernel_size=(3,3), padding=\"same\", activation=\"relu\"))\nmodel.add(Conv2D(filters=128, kernel_size=(3,3), padding=\"same\", activation=\"relu\"))\nmodel.add(MaxPooling2D(pool_size=(2,2),strides=(2,2)))\nmodel.add(Conv2D(filters=256, kernel_size=(3,3), padding=\"same\", activation=\"relu\"))\nmodel.add(Conv2D(filters=256, kernel_size=(3,3), padding=\"same\", activation=\"relu\"))\nmodel.add(Conv2D(filters=256, kernel_size=(3,3), padding=\"same\", activation=\"relu\"))\nmodel.add(MaxPooling2D(pool_size=(2,2),strides=(2,2)))\nmodel.add(Conv2D(filters=512, kernel_size=(3,3), padding=\"same\", activation=\"relu\"))\nmodel.add(Conv2D(filters=512, kernel_size=(3,3), padding=\"same\", activation=\"relu\"))\nmodel.add(Conv2D(filters=512, kernel_size=(3,3), padding=\"same\", activation=\"relu\"))\nmodel.add(MaxPooling2D(pool_size=(2,2),strides=(2,2)))\nmodel.add(Conv2D(filters=512, kernel_size=(3,3), padding=\"same\", activation=\"relu\"))\nmodel.add(Conv2D(filters=512, kernel_size=(3,3), padding=\"same\", activation=\"relu\"))\nmodel.add(Conv2D(filters=512, kernel_size=(3,3), padding=\"same\", activation=\"relu\"))\nmodel.add(MaxPooling2D(pool_size=(2,2),strides=(2,2)))\nmodel.add(Flatten())\nmodel.add(Dense(units=4096,activation=\"relu\"))\nmodel.add(Dense(units=4096,activation=\"relu\"))\nmodel.add(Dense(units=1, activation=\"softmax\"))\nmodel.summary()\n\nmodel.compile(optimizer = 'adam', loss = 'binary_crossentropy', metrics = METRICS)","execution_count":null,"outputs":[]},{"metadata":{"id":"sAoyCfMDKM0f","outputId":"b24d46a7-14d0-4a22-ab6f-c626a25a6225","trusted":false},"cell_type":"code","source":"# Realiza treinamento\n\nSTEP_SIZE_TRAIN = train_generator.n//train_generator.batch_size\nSTEP_SIZE_VALID = validation_generator.n//validation_generator.batch_size\n\nmodel.fit(train_generator,\n          steps_per_epoch=STEP_SIZE_TRAIN,\n          validation_data=validation_generator,\n          validation_steps=STEP_SIZE_VALID,\n          epochs=50,\n          callbacks=early_stopping,\n          workers = 16)\n\nmodel.save_weights('vgg_model_weights.h5')","execution_count":null,"outputs":[]},{"metadata":{"id":"4-Z06N3z6Y7j"},"cell_type":"markdown","source":"Como podemos perceber pela saída do método fit, as métricas de avaliação do modelo não mudaram com o passar das épocas. Isso indica que o modelo não convergiu, isso pode ser ter sido causado pela alta quantidade de camadas, pelo grande desbalanceamento da base, pela função de loss. \n\nPelos testes realizados, mudar parâmetros como função de loss, tamanho do batch das imagens e função de saída da última camada densa não foram suficientes para evitar a não convergência.","execution_count":null},{"metadata":{"id":"d0CzCvTxYrD3","outputId":"39647e63-e20c-4159-9bd2-9803cf231ca5","trusted":false},"cell_type":"code","source":"validation_predictions = model.predict(validation_generator,\n                                       verbose=1,\n                                       workers=16)","execution_count":null,"outputs":[]},{"metadata":{"id":"STbHsr2B_9cY","outputId":"52c6e144-d7c9-4d7a-8bba-4372a5d79e79","trusted":false},"cell_type":"code","source":"val_auc = roc_auc_score(y_true = validation_generator.classes, y_score=validation_predictions)\nprint(val_auc)","execution_count":null,"outputs":[]},{"metadata":{"id":"znAML0eu6Y7p"},"cell_type":"markdown","source":"A medida AUC dos dados de validação deu exatamente 0.5, indicando que o modelo pode estar classificando todos os exemplos em apenas uma classe.\n","execution_count":null},{"metadata":{"id":"-P8A1NqrYsyy","outputId":"19470f8a-c6d2-4107-c0c5-1629fac21513","trusted":false},"cell_type":"code","source":"# Plota a matriz de confusão dos dados de validação\nplot_confusion_matrix(validation_generator.classes,\n                      (validation_predictions>0.5).astype(int),\n                      ['benign','malignant'], \"Matriz de confusão\")","execution_count":null,"outputs":[]},{"metadata":{"id":"teqF5l4H6Y7s"},"cell_type":"markdown","source":"Nesse caso, estranhamente todos os elementos foram classificados como malignos, confirmando a não convergência do modelo.","execution_count":null},{"metadata":{"id":"BaqxvjI8uziK","outputId":"e65105dc-e222-42f1-b1e9-3de19148c64f","trusted":false},"cell_type":"code","source":"test_datagen = ImageDataGenerator(rescale = 1./255)\ntest_generator = test_datagen.flow_from_dataframe(dataframe=test_data,\n                                                  directory=\"test\",\n                                                  x_col=\"image_name\",\n                                                  y_col=None,\n                                                  class_mode=None,\n                                                  target_size=(image_height,image_width),\n                                                  batch_size=batch_size)\n\npredictions = model.predict(test_generator,\n                            verbose=1,\n                            workers=16)\n\n\nsubmission = pd.DataFrame(test_data['image_name'].str.replace(r'.jpg', ''))\nsubmission['target'] = predictions\n\nsubmission.to_csv('vgg_model_submission.csv',index=False)","execution_count":null,"outputs":[]},{"metadata":{"id":"rORWAL02BCGL"},"cell_type":"markdown","source":"O score oficial dessa submissão foi 0.500.","execution_count":null},{"metadata":{"id":"W0uO7yY5vL4m"},"cell_type":"markdown","source":"## Tentativa de realizar o pré-processamento da base e reduzir o desbalanceamento\n\nVimos que aumentar a complexidade do modelo não alterou a eficácia em predizer os pacientes com tumor maligno.\n\nUm palpide de porque o modelo está enviesado é o grande desbalanceamento da base, que tentaremos corrigir utilizando pesos para as classes. Também tentaremos melhorar o pré-processamento das bases usando [augmentations](https://www.pyimagesearch.com/2019/07/08/keras-imagedatagenerator-and-data-augmentation/). Dessa forma a cada época as imagens de treinamento e validação serão um pouco diferentes das anteriores, adicionando uma certa aleatoriedade ao processo de treinamento.\n\n","execution_count":null},{"metadata":{"id":"EFuL75osSVkb","outputId":"56002428-9085-42c8-fe38-65f0b1c15c7b","trusted":false},"cell_type":"code","source":"# Carregamento das imagens\ntrain_data = pd.read_csv('train.csv')\ntest_data = pd.read_csv('test.csv')\n\ntrain_data['image_name'] = train_data['image_name'].astype(str)+\".jpg\"\ntest_data['image_name'] = test_data['image_name'].astype(str)+\".jpg\"\n\n\nbatch_size = 16       # Tamanho do pacote\nimage_height = 224    # Altura da imagem\nimage_width = 224     # Largura da imagem\n\n# Realiza as augmentações nas imagens\nimage_datagen = ImageDataGenerator(rescale = 1./255,\n                                   validation_split = 0.3,\n                                   rotation_range=30,      # Rotaciona aleatóriamente imagens em 30 graus\n                                   zoom_range=0.15,        # Zoom aleatório\n                                   width_shift_range=0.2,  # Shift de largura aleatório\n                                   height_shift_range=0.2, # Shift de altura aleatório\n                                   horizontal_flip=True)   # Flip horizontal aleatório\n\ntrain_generator = image_datagen.flow_from_dataframe(dataframe=train_data,\n                                                    directory=\"train\",\n                                                    x_col=\"image_name\",\n                                                    y_col=\"benign_malignant\",\n                                                    class_mode=\"binary\",\n                                                    target_size=(image_height,image_width),\n                                                    batch_size=batch_size,\n                                                    subset=\"training\",\n                                                    color_mode=\"rgb\")\n\nvalidation_generator = image_datagen.flow_from_dataframe(dataframe=train_data,\n                                                         directory=\"train\",\n                                                         x_col=\"image_name\",\n                                                         y_col=\"benign_malignant\",\n                                                         class_mode=\"binary\",\n                                                         target_size=(image_height,image_width),\n                                                         batch_size=batch_size,\n                                                         subset=\"validation\",\n                                                         color_mode=\"rgb\")","execution_count":null,"outputs":[]},{"metadata":{"id":"LaGb4uD8bF6b"},"cell_type":"markdown","source":"Para esse teste usaremos uma outra arquitetura de rede famosa, a [ResNet](https://towardsdatascience.com/an-overview-of-resnet-and-its-variants-5281e2f56035). A ela, adicionamos uma camada densa com um neurônio que dará a probabilidade em estudo.","execution_count":null},{"metadata":{"id":"QAUMeuOOeMyK","outputId":"fd3cfe8e-dcc1-43f9-958d-5639961a1e64","trusted":false},"cell_type":"code","source":"# Criação do modelo\n\nfrom tensorflow.keras.applications.resnet50 import ResNet50\n# load model\n\nmodel = Sequential()\nmodel.add(ResNet50(input_shape=(image_height, image_width, 3), classes=2,include_top=False, weights=None))\nmodel.add(Flatten())\nmodel.add(Dense(units=1, activation=\"sigmoid\"))\n\n\n# summarize the model\nmodel.summary()","execution_count":null,"outputs":[]},{"metadata":{"id":"OEzJyDM5FMX2","trusted":false},"cell_type":"code","source":"model.compile(optimizer = 'adam', loss = 'binary_crossentropy', metrics = METRICS)","execution_count":null,"outputs":[]},{"metadata":{"id":"3GMQu5LHEVg8"},"cell_type":"markdown","source":"Calcula os pesos de cada classe.","execution_count":null},{"metadata":{"id":"BxkcocFMfrO6","outputId":"74e2dab3-798a-4571-ec88-2cc43e62b708","trusted":false},"cell_type":"code","source":"from sklearn.utils import class_weight\n\nclass_weights = class_weight.compute_class_weight('balanced',\n                                                 np.unique(train_generator.classes),\n                                                 train_generator.classes)\n\nclass_weights = dict(zip(np.unique(train_generator.classes), class_weights))\nprint(class_weights)","execution_count":null,"outputs":[]},{"metadata":{"id":"w_kOi6qv6Y79","outputId":"c292af9b-ed2a-44c8-dd6e-4557890a77e0","trusted":false},"cell_type":"code","source":"STEP_SIZE_TRAIN = train_generator.n//train_generator.batch_size\nSTEP_SIZE_VALID = validation_generator.n//validation_generator.batch_size\n\nmodel.fit(train_generator,\n          steps_per_epoch=STEP_SIZE_TRAIN,\n          validation_data=validation_generator,\n          validation_steps=STEP_SIZE_VALID,\n          epochs=50,\n          callbacks=early_stopping,\n          workers = 16,\n          class_weight=class_weights)\n\nmodel.save_weights('class_weights_balanced_weights.h5')","execution_count":null,"outputs":[]},{"metadata":{"id":"sm87TH6g6Y7_","outputId":"2b4ba477-f2d8-4ffa-d503-72eeba9b8e86","trusted":false},"cell_type":"code","source":"validation_predictions = model.predict(validation_generator,\n                                       verbose=1,\n                                       workers=16)","execution_count":null,"outputs":[]},{"metadata":{"id":"Fejr_71A6Y8B","outputId":"efb65bd9-131f-41f0-d528-d51502a80012","trusted":false},"cell_type":"code","source":"val_auc = roc_auc_score(y_true = validation_generator.classes, y_score=validation_predictions)\nprint(val_auc)","execution_count":null,"outputs":[]},{"metadata":{"id":"QDrPkpdx6Y8D","outputId":"5abf4e06-e66b-4c69-80e5-ac2799a40c9d","trusted":false},"cell_type":"code","source":"# Plota a matriz de confusão dos dados de validação\nplot_confusion_matrix(validation_generator.classes,\n                      (validation_predictions>0.5).astype(int),\n                      ['benign','malignant'], \"Matriz de confusão\")","execution_count":null,"outputs":[]},{"metadata":{"id":"y4cVKct8cEot","outputId":"8f50489e-e380-41ed-934e-f14f6126963c","trusted":false},"cell_type":"code","source":"test_datagen = ImageDataGenerator(rescale = 1./255)\ntest_generator = test_datagen.flow_from_dataframe(dataframe=test_data,\n                                                  directory=\"test\",\n                                                  x_col=\"image_name\",\n                                                  y_col=None,\n                                                  class_mode=None,\n                                                  target_size=(image_height,image_width),\n                                                  batch_size=batch_size)\n\npredictions = model.predict(test_generator,\n                            verbose=1,\n                            workers=16)\n\n\nsubmission = pd.DataFrame(test_data['image_name'].str.replace(r'.jpg', ''))\nsubmission['target'] = predictions\n\nsubmission.to_csv('class_weight_balanced_submission.csv',index=False)","execution_count":null,"outputs":[]},{"metadata":{"id":"i733VnASBHv3"},"cell_type":"markdown","source":"O score oficial dessa submissão foi 0.489.","execution_count":null},{"metadata":{"id":"T-EB831a38ux"},"cell_type":"markdown","source":"## Usando um modelo pré-treinado como extrator de features","execution_count":null},{"metadata":{"id":"BglT86QlAbqH"},"cell_type":"markdown","source":"Uma estratégia comum nas aplicações de redes neurais ultimamente é a transferência de aprendizado ou 'transfer learning'. Nessa estratégia, um modelo treinado para uma finalidade é utilizado para tentar inferir exemplos de problemas diferentes. Em outras palavras, consiste em utilizar recursos aprendidos em um problema prévio e aproveitá-lo em um novo problema semelhante.\n\nA rede treinada já entrou em contato com várias imagens de várias classes e seus pesos já foram usados em alguma tarefa e trouxeram resultados satisfatórios. As camadas de convolução dessa rede já estão especialistas em encontrar certos tipos de features das imagens.\n\nSendo assim, podemos utilizar tais pesos para treinar um dataset que, a principio, tem poucos dados para que seja feito um treinamento do zero ou, simplesmente, com o intuito de poupar tempo/custo de processamento.\n\nPara fazer a transferência de aprendizado, usa-se apenas as camadas convolucionais da rede já treinada e adiciona a esse modelo um classificador. \n\nA sua base de dados irá passar pelas camadas convolucionais da rede treinada e na prática ocorrerá a extração de features das suas imagens. O seu classificador irá usar essas features e tentar acertar a a classe correta.\n\nA partir dos diversos tipos de redes pré-treinadas, é possível verificar o desempenho alcançado por cada rede em comparativos relacionados às suas taxas de acurácia, como ilustrado abaixo. \n\n![texto alternativo](https://drive.google.com/uc?id=10wTfPtfhae6DGINgY-zTzTYhGP3qNM1w)\n[Fonte](https://github.com/tensorflow/tpu/tree/master/models/official/efficientnet)","execution_count":null},{"metadata":{"id":"xopTkqJL4ptP","trusted":false},"cell_type":"code","source":"# Leitura dos dataframes dos arquivos csv\ntrain_data = pd.read_csv('train.csv')\ntest_data = pd.read_csv('test.csv')\n\n# Adicionando a extensão aos nomes das imagens\ntrain_data['image_name'] = train_data['image_name'].astype(str)+\".jpg\"\ntest_data['image_name'] = test_data['image_name'].astype(str)+\".jpg\"\n","execution_count":null,"outputs":[]},{"metadata":{"id":"4-WhnjVU6Oye","outputId":"3bd353cb-ef13-4661-8867-0528673bbd05","trusted":false},"cell_type":"code","source":"# Carregamento das imagens\ntrain_data = pd.read_csv('train.csv')\ntest_data = pd.read_csv('test.csv')\n\ntrain_data['image_name'] = train_data['image_name'].astype(str)+\".jpg\"\ntest_data['image_name'] = test_data['image_name'].astype(str)+\".jpg\"\n\n\nbatch_size = 32       # Tamanho do pacote\nimage_height = 224    # Altura da imagem\nimage_width = 224     # Largura da imagem\n\n# Realiza as augmentações nas imagens\nimage_datagen = ImageDataGenerator(rescale = 1./255,\n                                   rotation_range=30,      \n                                   zoom_range=0.15,        \n                                   width_shift_range=0.2,  \n                                   height_shift_range=0.2, \n                                   horizontal_flip=True)   \n\n# Nesse treinamento, todas as imagens serão usadas como dataset de treinamento\ntrain_generator = image_datagen.flow_from_dataframe(dataframe=train_data,\n                                                    directory=\"train\",\n                                                    x_col=\"image_name\",\n                                                    y_col=\"benign_malignant\",\n                                                    class_mode=\"binary\",\n                                                    target_size=(image_height,image_width),\n                                                    batch_size=batch_size, subset=\"training\",\n                                                    color_mode=\"rgb\")","execution_count":null,"outputs":[]},{"metadata":{"id":"8qWlYNsv6Y8Z","outputId":"90cd4ac4-59c9-443e-851c-36b6a5fdcda0","trusted":false},"cell_type":"code","source":"import efficientnet.tfkeras as efn\n# load model\n\nmodel = Sequential([\n        efn.EfficientNetB0(\n            input_shape=(image_height, image_width, 3),\n            weights='imagenet',\n            include_top=False\n        ),\n        GlobalAveragePooling2D(),\n        Dense(1, activation='sigmoid')\n    ])\n\n\n\n# summarize the model\nmodel.summary()","execution_count":null,"outputs":[]},{"metadata":{"id":"GJXVc4OD6Y8c","trusted":false},"cell_type":"code","source":"model.compile(optimizer = 'adam', loss = 'binary_crossentropy', metrics = METRICS)","execution_count":null,"outputs":[]},{"metadata":{"id":"aLXMeJtE6Y8e","trusted":false},"cell_type":"code","source":"early_stopping = EarlyStopping(\n    monitor='auc', \n    verbose=1,\n    patience=3,\n    mode='max',\n    restore_best_weights=True)","execution_count":null,"outputs":[]},{"metadata":{"id":"2xTbiBiE6Y8g","outputId":"f0cf1305-744c-4444-8ed3-224d11dd38fa","trusted":false},"cell_type":"code","source":"STEP_SIZE_TRAIN = train_generator.n//train_generator.batch_size\n\nmodel.fit(train_generator,\n          steps_per_epoch=STEP_SIZE_TRAIN,\n          epochs=50,\n          callbacks=early_stopping,\n          workers = 16)","execution_count":null,"outputs":[]},{"metadata":{"id":"qgaHBNxc6Y8j","trusted":false},"cell_type":"code","source":"model.save_weights('tranfer_learning_weights.h5')","execution_count":null,"outputs":[]},{"metadata":{"id":"Yu9IR5-H6Y8l","outputId":"bc362a8b-f256-46a2-bd0f-406c4aba4d4a","trusted":false},"cell_type":"code","source":"test_datagen = ImageDataGenerator(rescale = 1./255)\ntest_generator = test_datagen.flow_from_dataframe(dataframe=test_data,\n                                                  directory=\"test\",\n                                                  x_col=\"image_name\",\n                                                  y_col=None,\n                                                  class_mode=None,\n                                                  target_size=(image_height,image_width),\n                                                  batch_size=batch_size)\n\npredictions = model.predict(test_generator,\n                            verbose=1,\n                            workers=16)\n\n\nsubmission = pd.DataFrame(test_data['image_name'].str.replace(r'.jpg', ''))\nsubmission['target'] = predictions\n\nsubmission.to_csv('effnet3_model_submission.csv',index=False)","execution_count":null,"outputs":[]},{"metadata":{"id":"qIBLmQMcBZSd"},"cell_type":"markdown","source":"O score oficial dessa submissão foi 0.523.","execution_count":null},{"metadata":{"id":"QLZBTNHiTt4V"},"cell_type":"markdown","source":"## Rede neural multi-input\n\nSabemos que essa base de dados, além das imagens, possui um dataframe com informações sobre o paciente referente a cada imagem de melanoma. Assim, é natural imaginar um método que utilize os dois tipos de informação para a classificação. Dessa forma, foi criada uma rede neural que recebe duas entradas, imagem e dataframe, para gerar a classificação.","execution_count":null},{"metadata":{"id":"Ap2yhU1sqiLl","trusted":false},"cell_type":"code","source":"# Imports\nfrom pathlib import Path\n\nfrom tensorflow.data import Dataset\nfrom tensorflow.keras import utils\nfrom tensorflow.keras.models import Model\nfrom tensorflow.keras.layers import Input\nfrom tensorflow.keras.layers import concatenate\n\nfrom tensorflow.keras.preprocessing.image import ImageDataGenerator, load_img, img_to_array","execution_count":null,"outputs":[]},{"metadata":{"id":"4pN1sNkusUWr"},"cell_type":"markdown","source":"Para que a rede neural receba duas entradas diferentes, essas entradas precisam estar em um formato compatível entre si. Assim, foi criada a classe MixedDataGenerator para gerar a entrada mista da rede neural.\n\nEssa classe permite que os dados sejam carregados na memória em pequenos pacotes e os pacotes são carregados iterativamente durante os processos de treinamento, validação e predição. Dessa forma, não é necessário carregar os dados todos de uma única vez, gerando menos impacto na memória RAM do computador.","execution_count":null},{"metadata":{"id":"fNtsb_bdr4aD","trusted":false},"cell_type":"code","source":"class MixedDataGenerator(utils.Sequence):\n\n    def __init__(self, values: pd.DataFrame,\n                images: pd.Series, directory: str, labels: pd.Series=None,\n                target_size: tuple=(32,32), batch_size: int=32, shuffle: bool=True):\n        'Inicialização'\n\n        self.values = values\n        self.labels = labels\n        self.images = images\n        self.directory = Path(directory)\n        self.target_size = target_size\n        self.batch_size = batch_size\n        self.shuffle = shuffle\n        self.num_files = images.count()\n        self.columns = self.values.columns\n        self.indices = None\n        self.values_gen = ImageDataGenerator()\n        self.labels_indices = None\n        if labels is not None:\n            self.labels_indices = list(self.labels.unique())\n\n        self.on_epoch_end()\n\n    \n    def __len__(self):\n        'Indica a quantidades de pacotes por épocas'\n\n        return (self.num_files + self.batch_size - 1) // self.batch_size\n\n    \n    def __getitem__(self, index):\n        'Gera um pacote de dados'\n\n        # Gera os índices do pacote\n        if index >= len(self):\n            raise ValueError('Asked to retrieve element {index}, '\n                             'but the Sequence '\n                             'has length {length}'.format(index=index,\n                                                          length=len(self)))\n        indices = self.indices[index * self.batch_size : (index + 1) * self.batch_size]\n        x_images = [self.__process_image(image) for image in self.images.loc[indices]]\n        images = np.array(x_images)\n        x_values = self.values.loc[indices]\n        values = np.array(x_values)\n        if self.labels is not None:\n            y_labels = [self.labels_indices.index(label) for label in self.labels.loc[indices]]\n            labels = np.eye(len(self.labels_indices))[y_labels]\n        # Gera os dados\n        if self.labels is not None:\n            return [images, values], labels\n        else:\n            return [images, values]\n\n\n    def on_epoch_end(self):\n        'Atualiza os índices após cada época'\n\n        self.indices = np.arange(len(self.values))\n        if self.shuffle:\n            np.random.shuffle(self.indices)\n\n    \n    def __process_image(self, image_name):\n        'Lê a imagem da memória'\n\n        image = load_img(Path(self.directory, image_name), target_size=self.target_size)\n        image = self.values_gen.apply_transform(image, dict(rescale=1./255))            # normaliza valores dos pixels da imagem entre 0-1\n        array = img_to_array(image)\n        image.close()\n\n        return array","execution_count":null,"outputs":[]},{"metadata":{"id":"ITjAqNbv1Xb4"},"cell_type":"markdown","source":"Alguns exemplos do dataframe possuem dados faltantes nas colunas 'sex', 'age_approx' e 'anatom_site_general_challenge'. Assim, foi feita uma função para tratar esses dados. \n\nComo os exemplos com dados faltante nas colunas 'sex' e 'age_approx' eram poucos, e estes só existiam no conjunto de treinamento, foram simplesmente removidos. \n\nCom relação ao atributo 'anatom_site_general_challenge', foi criada uma nova categoria chamada 'unknown' (desconhecido), e os dados faltantes foram substituídos por essa categoria.","execution_count":null},{"metadata":{"id":"KG3V84f11W2f","trusted":false},"cell_type":"code","source":"def treatNan(data):\n    # remove nan na coluna 'sex'\n    data = data[data.sex == data.sex]\n    # remove nan na coluna 'age_approx'\n    data = data[data.age_approx == data.age_approx]\n\n    # substitui nan na coluna 'anatom_site_general_challenge' pelo atributo 'unknown'\n    data.anatom_site_general_challenge.fillna(value='unknown', inplace=True)\n\n    return data","execution_count":null,"outputs":[]},{"metadata":{"id":"JxMxeMIM27hV"},"cell_type":"markdown","source":"### Leitura dos dados","execution_count":null},{"metadata":{"id":"A-os8Ckq3Cks","outputId":"d147a599-33a1-47c8-ae9e-946afc86c5c1","trusted":false},"cell_type":"code","source":"# Leitura dos dataframes dos arquivos csv\ntrain_data = pd.read_csv('train.csv')\ntest_data = pd.read_csv('test.csv')\n\n# Adicionando a extensão aos nomes das imagens\ntrain_data['image_name'] = train_data['image_name'].astype(str)+\".jpg\"\ntest_data['image_name'] = test_data['image_name'].astype(str)+\".jpg\"\n\ntrain_data = treatNan(train_data)\ntest_data = treatNan(test_data)\n\ntrain_data.head()","execution_count":null,"outputs":[]},{"metadata":{"id":"8moVpLJm7W-A","outputId":"2edead6a-3f9d-4d9a-ebb6-f5cf63816801","trusted":false},"cell_type":"code","source":"test_data.head()","execution_count":null,"outputs":[]},{"metadata":{"id":"BFIwd_J63sgc"},"cell_type":"markdown","source":"Os dados provenientes do arquivo 'train.csv' foram separados em treinamento e validação de forma a manter a proporção de amostras das classes 'benign' e 'malignant' nos dois conjuntos.","execution_count":null},{"metadata":{"id":"UcaTrPhT3xXS","trusted":false},"cell_type":"code","source":"# Separa os dados em treinamento e validação mantendo a proporção entre classes 'benign' e 'malignant'\ntrain_data_ben = train_data[train_data.benign_malignant == 'benign']\ntrain_data_mal = train_data[train_data.benign_malignant == 'malignant']\n\nvalid_data_ben = train_data_ben.sample(frac=0.3, random_state=1)\nvalid_data_mal = train_data_mal.sample(frac=0.3, random_state=1)\n\nvalid_data = valid_data_ben.append(valid_data_mal)\n\ntrain_data.drop(index=valid_data.index)\n\nvalid_data = valid_data.reindex(np.random.permutation(valid_data.index))\nvalid_data.reset_index(drop=True, inplace=True)\ntrain_data.reset_index(drop=True, inplace=True)","execution_count":null,"outputs":[]},{"metadata":{"id":"6IFMHPRx6VTX"},"cell_type":"markdown","source":"As colunas de rótulos e atributos foram separadas. Foram selecioandos apenas os atributos que geram informação para o modelo, ou seja, excluímos as colunas 'image_name' e 'patient_id', e que também constam nos dados de teste. Assim, apenas as colunas 'sex', 'age_approx' e 'anatom_site_general_challenge' foram selecionadas.","execution_count":null},{"metadata":{"id":"YXxnynne4kd4","trusted":false},"cell_type":"code","source":"# Rótulos dos dados\ny_train = train_data.benign_malignant\ny_valid = valid_data.benign_malignant\n\n# Dados com atributos para o treinamento\nX_train = train_data[['sex', 'age_approx', 'anatom_site_general_challenge']]\nX_valid = valid_data[['sex', 'age_approx', 'anatom_site_general_challenge']]","execution_count":null,"outputs":[]},{"metadata":{"id":"nOjhav1I-0Wo"},"cell_type":"markdown","source":"Essa base de dados possui dados categóricos, contudo a rede neural não aceita esse tipo de dado. Assim, usamos o método [get_dummies()](https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.Series.str.get_dummies.html) para transformar os dados categórico em numéricos.","execution_count":null},{"metadata":{"id":"_jKOJvDh_qcA","trusted":false},"cell_type":"code","source":"train_sex = X_train['sex'].str.get_dummies()\ntrain_anatom = X_train['anatom_site_general_challenge'].str.get_dummies()\n\nvalid_sex = X_valid['sex'].str.get_dummies()\nvalid_anatom = X_valid['anatom_site_general_challenge'].str.get_dummies()\n\nX_train = X_train.join([train_sex, train_anatom])\nX_valid = X_valid.join([valid_sex, valid_anatom])\n\nX_train.drop(columns=['sex', 'anatom_site_general_challenge'], inplace=True)\nX_valid.drop(columns=['sex', 'anatom_site_general_challenge'], inplace=True)\n\ntrain_columns = X_train.columns\nvalid_columns = X_valid.columns\n\nfor column in valid_columns:\n    if column not in train_columns:\n        X_train[column] = 0\n\nfor column in train_columns:\n    if column not in valid_columns:\n        X_valid[column] = 0","execution_count":null,"outputs":[]},{"metadata":{"id":"vxlBDD-e_5ai"},"cell_type":"markdown","source":"Finalmente, as imagens de treinamento e validação forma lidas e os dados mistos forma criados.","execution_count":null},{"metadata":{"id":"Fj4ZrdRtADs_","trusted":false},"cell_type":"code","source":"gen_train = MixedDataGenerator(values=X_train,\n                            images=train_data['image_name'],\n                            directory='train',\n                            labels=y_train,\n                            target_size=(32, 32),\n                            batch_size=32)\ngen_valid = MixedDataGenerator(values=X_valid,\n                            images=valid_data['image_name'],\n                            directory='train',\n                            labels=y_valid,\n                            target_size=(32, 32),\n                            batch_size=32)","execution_count":null,"outputs":[]},{"metadata":{"id":"6vZycFrvAJHd"},"cell_type":"markdown","source":"### Criação da rede neural multi-input\n\nA rede neural multi-input consiste na concatenação de duas, ou mais, redes neurais para gerar uma única saída. Nesse caso, a nossa rede neural é uma concatenação de um Multi Layer Perceptron (MLP) e uma Rede Convolucional (CNN) como é mostrado na figura a seguir.\n\n\n<img src=\"https://drive.google.com/uc?id=122h0V2nbJCg-UUfUNfXGFSl5aUmwHZqL\" alt=\"drawing\" width=\"400\"/>\n\n\n","execution_count":null},{"metadata":{"id":"FR41sjFxnPOD"},"cell_type":"markdown","source":"Para criar a rede, primeiramente foram criados modelos de MLP e CNN usando as classes [Sequential()](https://keras.io/api/models/sequential/#sequential-class) e [Model()](https://keras.io/api/models/model/#model-class) do Keras, respectivamente. É importante ressaltar que os modelos só serão compilados após a concatenação.","execution_count":null},{"metadata":{"id":"29UmK2zAo1cF","trusted":false},"cell_type":"code","source":"def createMLP(dimension):\n    model = Sequential()\n    model.add(Dense(units=8, input_dim=dimension, activation=\"relu\"))\n    model.add(Dense(units=4, activation=\"relu\"))\n\n    return model\n\n\ndef createCNN(shape):\n    inputImage = Input(shape=shape)\n    x = inputImage\n    x = Conv2D(filters=32, kernel_size=(7,7), activation='relu')(x)\n    x = MaxPooling2D(pool_size = (2, 2))(x)\n    x = Conv2D(filters=20, kernel_size=(3, 3), activation = 'relu')(x)\n    x = MaxPooling2D(pool_size = (2, 2))(x)\n\n    x = Flatten()(x)\n    x = Dense(units = 10, activation = 'relu')(x)\n    x = Dense(units = 4, activation = 'relu')(x)\n\n    model = Model(inputImage, x)\n\n    return model\n\n\n#Criação do modelo multi-Input\nmlp = createMLP(dimension=len(X_train.columns))\ncnn = createCNN((32,32,3))","execution_count":null,"outputs":[]},{"metadata":{"id":"VEm8qi3bqoJi"},"cell_type":"markdown","source":"Em seguida, foi criado um modelo concatenando as saídas do MLP e da CNN. Foram adicionadas uma camada oculta e, por fim, uma de saída. E, finalmente, para compilar o nosso modelo, escolhemos o otimizador [Adam](https://machinelearningmastery.com/adam-optimization-algorithm-for-deep-learning/) e a funcão de loss [binary_crossentropy](https://keras.io/api/losses/probabilistic_losses/#binarycrossentropy-class).","execution_count":null},{"metadata":{"id":"XlgNjrwDq4gt","outputId":"79d63e00-c203-426e-f953-b61901d4364b","trusted":false},"cell_type":"code","source":"combinedInput = concatenate([cnn.output, mlp.output])\n\nx = Dense(units=4, activation=\"relu\")(combinedInput)\nx = Dense(units=2, activation=\"sigmoid\")(x)\n\nmodel = Model(inputs=[cnn.input, mlp.input], outputs=x)\n\nmodel.compile(optimizer = 'adam', loss = 'binary_crossentropy', metrics = ['accuracy'])\n\nmodel.summary()","execution_count":null,"outputs":[]},{"metadata":{"id":"iNib-LjasAxw"},"cell_type":"markdown","source":"Para fazer o treinamento, usamos o método fit com as imagens de treinamento e validação. O parâmetro workers faz paralelização do processamento quando múltiplos cores de processamento estão disponíveis.","execution_count":null},{"metadata":{"id":"siwF1z7XsG78","outputId":"85c6aa61-4089-4be8-8815-0ded9f0283f6","trusted":false},"cell_type":"code","source":"# Treinamento\n\nmodel.fit(gen_train,\n          epochs=4,\n          validation_data=gen_valid,\n          workers=16)\nmodel.save_weights('cnn_melanoma.h5')","execution_count":null,"outputs":[]},{"metadata":{"id":"Wn5bfEgGsc4a"},"cell_type":"markdown","source":"Antes de fazer a inferência dos dados de teste, vamos analisar o resultado nos dados de validação.","execution_count":null},{"metadata":{"id":"SJYhf8_Uslwa","outputId":"d90251c5-8ce3-41e2-b25b-1a33ab8f1d38","trusted":false},"cell_type":"code","source":"# Validação\n\nvalidation_predictions = model.predict(gen_valid,\n                                       verbose=1,\n                                       workers=16)\nvalidation_predictions = (validation_predictions > 0.5)","execution_count":null,"outputs":[]},{"metadata":{"id":"S-N00olatSKJ"},"cell_type":"markdown","source":"Para obter alguns dados de avaliação, utilizamos a função [classification_report](https://scikit-learn.org/stable/modules/generated/sklearn.metrics.classification_report.html?highlight=classification_report#sklearn.metrics.classification_report) da biblioteca ScikitLearn.","execution_count":null},{"metadata":{"id":"75TOsYxitrq_","outputId":"aa92aba0-9deb-4b15-dc37-aa7d2f762014","trusted":false},"cell_type":"code","source":"# Faz o classification report do modelo aplicado nos dados de validação\nfrom sklearn.metrics import classification_report\nreport = classification_report(pd.get_dummies(y_valid),\n                               validation_predictions)\nprint(report)","execution_count":null,"outputs":[]},{"metadata":{"id":"woXYesN-s0S4"},"cell_type":"markdown","source":"Para podermos visualizar o resultado, contruímos a matriz de confusão para os dados de validação","execution_count":null},{"metadata":{"id":"Oj4ELpant-0e","outputId":"0d720973-613b-4b00-a1a5-7ef68ec517ce","trusted":false},"cell_type":"code","source":"# Mostra a matriz de confusão dos dados de validação\nplot_confusion_matrix(pd.get_dummies(y_valid).values.argmax(axis=1),\n                      validation_predictions.argmax(axis=1),['benign', 'malignant'], \"Matriz de confusão\")","execution_count":null,"outputs":[]},{"metadata":{"id":"6mLyj-jD_N4W"},"cell_type":"markdown","source":"É possível observar que o modelo não classificou nenhum exemplo como 'malignant'. Isso provavelmente se deve ao tamanho bastante reduzido das imagens, o uso da acurácia como métrica do modelo em uma base desbalanceada e, principalmente, ao enorme desbalanceamento da base de dados em si.","execution_count":null},{"metadata":{"id":"fHih9gY-_2WU"},"cell_type":"markdown","source":"Para fazer a submissão, inferimos os dados de teste.","execution_count":null},{"metadata":{"id":"zdVDRLlSzl92","outputId":"f6dab559-2b0b-4399-9e8f-8665ad054ee3","trusted":false},"cell_type":"code","source":"# Teste\n\nX_test = test_data.drop(columns=['image_name', 'patient_id'])\n\ntest_sex = X_test['sex'].str.get_dummies()\ntest_anatom = X_test['anatom_site_general_challenge'].str.get_dummies()\n\nX_test = X_test.join([test_sex, test_anatom])\n\nX_test.drop(columns=['sex', 'anatom_site_general_challenge'], inplace=True)\n\ntest_columns = X_test.columns\n\nfor column in train_columns:\n    if column not in test_columns:\n        X_test[column] = 0\n\n\ngen_test = MixedDataGenerator(values=X_test,\n                            images=test_data['image_name'],\n                            directory='test',\n                            target_size=(32, 32),\n                            batch_size=32)\n\n\npredictions = model.predict(gen_test,\n                            verbose=1,\n                            workers=16)\n\n\n# predictions é um vetor em que cada elemento é um vetor de dois valores, já que temos duas classes (benigno e maligno).\n# Pega-se então a probabilidade do tumor ser maligno\nsubmission = pd.DataFrame(test_data['image_name'].str.replace(r'.jpg', ''))\nsubmission['target']=predictions[:,1]\n\n# Cria arquivo de submissão\nsubmission.to_csv('multi_input_submission.csv',index=False)","execution_count":null,"outputs":[]},{"metadata":{"id":"b2emHw0H-GtF"},"cell_type":"markdown","source":"O score oficial dessa submissão foi 0.499.","execution_count":null},{"metadata":{"id":"BsX-aA1hAmvg"},"cell_type":"markdown","source":"### Segunda submissão\n\nPara uma nova submissão, algumas mudanças foram feitas a fim de tentar melhorar a classificação do modelo. ","execution_count":null},{"metadata":{"id":"PV7G4-hLODF7"},"cell_type":"markdown","source":"Primeiramente, as imagens foram carregadas com uma resolução de 128x128 pixels","execution_count":null},{"metadata":{"id":"GBPmbQDwNg38","trusted":false},"cell_type":"code","source":"# Leitura dos dataframes dos arquivos csv\ntrain_data = pd.read_csv('train.csv')\ntest_data = pd.read_csv('test.csv')\n\n# Adicionando a extensão aos nomes das imagens\ntrain_data['image_name'] = train_data['image_name'].astype(str)+\".jpg\"\ntest_data['image_name'] = test_data['image_name'].astype(str)+\".jpg\"\n\ntrain_data = treatNan(train_data)\ntest_data = treatNan(test_data)\n\n\n# Separa os dados em treinamento e validação mantendo a proporção entre classes 'benign' e 'malignant'\ntrain_data_ben = train_data[train_data.benign_malignant == 'benign']\ntrain_data_mal = train_data[train_data.benign_malignant == 'malignant']\n\nvalid_data_ben = train_data_ben.sample(frac=0.3, random_state=1)\nvalid_data_mal = train_data_mal.sample(frac=0.3, random_state=1)\n\nvalid_data = valid_data_ben.append(valid_data_mal)\n\ntrain_data.drop(index=valid_data.index)\n\nvalid_data = valid_data.reindex(np.random.permutation(valid_data.index))\nvalid_data.reset_index(drop=True, inplace=True)\ntrain_data.reset_index(drop=True, inplace=True)\n\n\n# Rótulos dos dados\ny_train = train_data.benign_malignant\ny_valid = valid_data.benign_malignant\n\n# Dados com atributos para o treinamento\nX_train = train_data[['sex', 'age_approx', 'anatom_site_general_challenge']]\nX_valid = valid_data[['sex', 'age_approx', 'anatom_site_general_challenge']]\n\n\ntrain_sex = X_train['sex'].str.get_dummies()\ntrain_anatom = X_train['anatom_site_general_challenge'].str.get_dummies()\n\nvalid_sex = X_valid['sex'].str.get_dummies()\nvalid_anatom = X_valid['anatom_site_general_challenge'].str.get_dummies()\n\nX_train = X_train.join([train_sex, train_anatom])\nX_valid = X_valid.join([valid_sex, valid_anatom])\n\nX_train.drop(columns=['sex', 'anatom_site_general_challenge'], inplace=True)\nX_valid.drop(columns=['sex', 'anatom_site_general_challenge'], inplace=True)\n\ntrain_columns = X_train.columns\nvalid_columns = X_valid.columns\n\nfor column in valid_columns:\n    if column not in train_columns:\n        X_train[column] = 0\n\nfor column in train_columns:\n    if column not in valid_columns:\n        X_valid[column] = 0\n\ngen_train = MixedDataGenerator(values=X_train,\n                            images=train_data['image_name'],\n                            directory='train',\n                            labels=y_train,\n                            target_size=(128,128),\n                            batch_size=32)\ngen_valid = MixedDataGenerator(values=X_valid,\n                            images=valid_data['image_name'],\n                            directory='train',\n                            labels=y_valid,\n                            target_size=(128,128),\n                            batch_size=32)","execution_count":null,"outputs":[]},{"metadata":{"id":"AQP2RlRVOxSQ"},"cell_type":"markdown","source":"Nesse modelo, foi usada a área abaixo da curva ROC como métrica de avaliação em vez da acurácia.","execution_count":null},{"metadata":{"id":"dt1B3BA3OzUH","trusted":false},"cell_type":"code","source":"#Criação do modelo multi-Input\n\nmlp = createMLP(dimension=len(X_train.columns))\ncnn = createCNN((128,128,3))\n\ncombinedInput = concatenate([cnn.output, mlp.output])\n\nx = Dense(units=4, activation=\"relu\")(combinedInput)\nx = Dense(units=2, activation=\"sigmoid\")(x)\n\nmodel = Model(inputs=[cnn.input, mlp.input], outputs=x)\n\nmodel.compile(optimizer = 'adam', loss = 'categorical_crossentropy', metrics = [AUC()])","execution_count":null,"outputs":[]},{"metadata":{"id":"Rh5d_xfZPF0M"},"cell_type":"markdown","source":"Além disso, como uma tentativa de compensar o desbalanceamento da base, foram definidos pesos para cada classe, utilizando a função [compute_class_weight()](https://scikit-learn.org/stable/modules/generated/sklearn.utils.class_weight.compute_class_weight.html?highlight=class_weight#sklearn.utils.class_weight.compute_class_weight) do ScikitLearn.","execution_count":null},{"metadata":{"id":"S2tVnVtNQpk0","trusted":false},"cell_type":"code","source":"from sklearn.utils import class_weight\n\nclass_weights = class_weight.compute_class_weight('balanced',\n                                                 np.unique(y_train),\n                                                 y_train)\n\nclass_weights = dict(zip([0,1], class_weights))","execution_count":null,"outputs":[]},{"metadata":{"id":"5mFar7ED6Y9Z","outputId":"d91f95e5-baa3-4cf9-bae7-d7422ee86558","trusted":false},"cell_type":"code","source":"# Treinamento\nmodel.fit(gen_train,\n          epochs=4,\n          validation_data=gen_valid,\n          class_weight=class_weights,\n          workers=16)\nmodel.save_weights('cnn_melanoma.h5')","execution_count":null,"outputs":[]},{"metadata":{"id":"I2cOSS3l6Y9c","outputId":"2ce3c69e-c09b-4c66-9fcf-8f9e8bbef0b6","trusted":false},"cell_type":"code","source":"# Validação\n\nvalidation_predictions = model.predict(gen_valid,\n                                       verbose=1,\n                                       workers=16)","execution_count":null,"outputs":[]},{"metadata":{"id":"STgdRBtEQvny","outputId":"443b660c-255e-40fb-c1b9-8caf0de33dc1","trusted":false},"cell_type":"code","source":"validation_predictions = (validation_predictions > 0.5)\n\n\n# class_labels = list(validation_generator.class_indices.keys())\n# validation_true = validation_generator.classes\n\n# Faz o classification report do modelo aplicado nos dados de validação\nfrom sklearn.metrics import classification_report\nreport = classification_report(pd.get_dummies(y_valid),\n                               validation_predictions)\nprint(report)\n\n\n# Plota a matriz de confusão dos dados de validação\nplot_confusion_matrix(pd.get_dummies(y_valid).values.argmax(axis=1),\n                      validation_predictions.argmax(axis=1),['benign', 'malignant'], \"Matriz de confusão\")","execution_count":null,"outputs":[]},{"metadata":{"id":"AT0w8w3B6Y9f","outputId":"fd22d4b7-e7f6-40ec-92af-5cd07075f331","trusted":false},"cell_type":"code","source":"# Teste\n\nX_test = test_data.drop(columns=['image_name', 'patient_id'])\n\ntest_sex = X_test['sex'].str.get_dummies()\ntest_anatom = X_test['anatom_site_general_challenge'].str.get_dummies()\n\nX_test = X_test.join([test_sex, test_anatom])\n\nX_test.drop(columns=['sex', 'anatom_site_general_challenge'], inplace=True)\n\ntest_columns = X_test.columns\n\nfor column in train_columns:\n    if column not in test_columns:\n        X_test[column] = 0\n\n\ngen_test = MixedDataGenerator(values=X_test,\n                            images=test_data['image_name'],\n                            directory='test',\n                            target_size=(128, 128),\n                            batch_size=32)\n\n\npredictions = model.predict(gen_test,\n                            verbose=1,\n                            workers=16)\n\n\n# predictions é um vetor em que cada elemento é um vetor de dois valores, já que temos duas classes (benigno e maligno).\n# Pega-se então a probabilidade do tumor ser maligno\nsubmission = pd.DataFrame(test_data['image_name'].str.replace(r'.jpg', ''))\nsubmission['target']=predictions[:,1]\n\n# Cria arquivo de submissão\nsubmission.to_csv('multi_input_submission2.csv',index=False)","execution_count":null,"outputs":[]},{"metadata":{"id":"IurCUUiQ-l8F"},"cell_type":"markdown","source":"O score oficial dessa submissão foi 0.500.","execution_count":null},{"metadata":{"id":"ffkLUbCU-pJH"},"cell_type":"markdown","source":"Após a submissão dos resultados, o score obtido foi ligeiramente maior. Contudo, a diferença não foi significativa, o que nos faz perceber que o uso de pesos para as classes ainda não foi suficiente para compensar o grande desbalanceamento da base de dados.\n\nAlém disso, comparando o modelo multi-input com a CNN, observamos que utilizar as características dos pacientes contidas no dataframe não gerou um ganho de informação suficiente a ponto de melhorar significativamente a capacidade de classificação do modelo para essa base de dados.","execution_count":null}],"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"pygments_lexer":"ipython3","nbconvert_exporter":"python","version":"3.6.4","file_extension":".py","codemirror_mode":{"name":"ipython","version":3},"name":"python","mimetype":"text/x-python"}},"nbformat":4,"nbformat_minor":4}