{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[],"dockerImageVersionId":28755,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# TP4 - Transfer Learning e Interpretabilidade (TensorFlow)\n\n**Objetivos:**\n1. Leitura de dados nativa em TFRecord.\n2. Aplicar Aumentação de Dados via Keras Layers.\n3. Transfer-learning usando **ResNet50V2** (clássica) e **EfficientNetB0** (moderna).\n4. Aplicar **Grad-CAM** no melhor modelo.\n\n## Membros do grupo:\nAlane Benjamim dos Santos, 230457\nIsaac do Nascimento Oliveira, 247175\nJoão Vitor Gonçalves Oliveira, 237871\nPaula Maria da Fonseca, 138995\nPaulo Santos do Nascimento Filho, 186629","metadata":{}},{"cell_type":"markdown","source":"# 0. Imports necessários","metadata":{}},{"cell_type":"code","source":"import glob\nimport tensorflow as tf\nimport numpy as np\nimport matplotlib.pyplot as plt\nimport matplotlib.cm as cm\nfrom sklearn.metrics import f1_score\nfrom collections import Counter\nimport warnings\nwarnings.filterwarnings('ignore') # ignorar avisos para nao poluir o output","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# 1. Utilitários e ETL","metadata":{}},{"cell_type":"code","source":"strategy = tf.distribute.MirroredStrategy()\nprint(f'Número de GPUs sendo usadas: {strategy.num_replicas_in_sync}')\n\nBATCH_SIZE = 32 * strategy.num_replicas_in_sync \nIMAGE_SIZE = [331, 331] # tamanho padrao para todas as imagens\nNUM_CLASSES = 104 # total de classes diferentes no dataset\nEPOCHS = 4 # quantidade de epocas para o treinamento","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# pega todos os arquivos .tfrec de treino, validacao e teste\ntodos_arquivos_treino = glob.glob('/kaggle/input/**/train/*.tfrec', recursive=True)\ntodos_arquivos_val = glob.glob('/kaggle/input/**/val/*.tfrec', recursive=True)\ntodos_arquivos_test = glob.glob('/kaggle/input/**/test/*.tfrec', recursive=True)\n\n# filtra apenas os arquivos que tem resolucao 512x512\nTRAIN_FILENAMES = [f for f in todos_arquivos_treino if '331x331' in f]\nVAL_FILENAMES = [f for f in todos_arquivos_val if '331x331' in f]\nTEST_FILENAMES = [f for f in todos_arquivos_test if '331x331' in f]\n\n# decodifica os bytes da imagem para um tensor de pixels\ndef decode_image(image_data):\n    image = tf.image.decode_jpeg(image_data, channels=3) # decodifica o jpeg\n    image = tf.cast(image, tf.float32)  # converte os pixels para float32 na faixa de 0 a 255\n    image = tf.reshape(image, [*IMAGE_SIZE, 3]) # garante que o shape esta correto\n    return image\n\n# le um exemplo do arquivo tfrecord e extrai a imagem e a classe (com rotulo)\ndef read_labeled_tfrecord(example):\n    LABELED_TFREC_FORMAT = {\n        \"image\": tf.io.FixedLenFeature([], tf.string), # o campo image vem como string de bytes\n        \"class\": tf.io.FixedLenFeature([], tf.int64), # a classe vem como um inteiro\n    }\n    example = tf.io.parse_single_example(example, LABELED_TFREC_FORMAT) # faz o parse do exemplo\n    image = decode_image(example['image']) # transforma a string em uma imagem usavel\n    label = tf.cast(example['class'], tf.int32) # converte a classe para um inteiro menor\n    return image, label\n\n# le o arquivo tfrecord de teste que nao tem classe, apenas a imagem e o id\ndef read_unlabeled_tfrecord(example):\n    UNLABELED_TFREC_FORMAT = {\n        \"image\": tf.io.FixedLenFeature([], tf.string),\n        \"id\": tf.io.FixedLenFeature([], tf.string),\n    }\n    example = tf.io.parse_single_example(example, UNLABELED_TFREC_FORMAT)\n    image = decode_image(example['image'])\n    idnum = example['id']\n    return image, idnum\n\n# aplica transformacoes aleatorias para aumentar a variabilidade do treino\ndef data_augment(image, label):\n    image = tf.image.random_flip_left_right(image) # inverte horizontalmente de forma aleatoria\n    zoom_factor = tf.random.uniform([], 0.8, 1.0) # escolhe um fator de zoom aleatorio\n    new_h = tf.cast(IMAGE_SIZE[0] * zoom_factor, tf.int32)\n    new_w = tf.cast(IMAGE_SIZE[1] * zoom_factor, tf.int32)\n    image = tf.image.random_crop(image, size=[new_h, new_w, 3]) # faz o crop usando o novo tamanho\n    image = tf.image.resize(image, IMAGE_SIZE) # redimensiona de volta para 512x512\n    return image, label\n\n# pre-processamento especifico que a resnet precisa\ndef preprocess_resnet(image, label_or_id):\n    return tf.keras.applications.resnet_v2.preprocess_input(image), label_or_id\n\n# pre-processamento da efficientnet (ela ja faz isso internamente, entao so retorna)\ndef preprocess_efficientnet(image, label_or_id):\n    return image, label_or_id\n\n# constroi o pipeline de dados usando tf.data (para treino/val)\ndef build_dataset(filenames, training=False, augment=False, is_test=False):\n    ds = tf.data.TFRecordDataset(filenames, num_parallel_reads=tf.data.AUTOTUNE)\n    \n    # escolhe o parser dependendo se tem rotulo (treino/val) ou nao (teste)\n    if is_test:\n        ds = ds.map(read_unlabeled_tfrecord, num_parallel_calls=tf.data.AUTOTUNE)\n    else:\n        ds = ds.map(read_labeled_tfrecord, num_parallel_calls=tf.data.AUTOTUNE)\n        \n    if augment and not is_test:\n        ds = ds.map(data_augment, num_parallel_calls=tf.data.AUTOTUNE)\n        \n    if training:\n        ds = ds.shuffle(2048).batch(BATCH_SIZE).repeat().prefetch(tf.data.AUTOTUNE)\n    else:\n        ds = ds.batch(BATCH_SIZE).prefetch(tf.data.AUTOTUNE)\n    return ds","metadata":{},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# cria os datasets base sem augmentation\ntrain_ds_base = build_dataset(TRAIN_FILENAMES, training=True, augment=False)\nval_ds_base = build_dataset(VAL_FILENAMES, training=False, augment=False)\n\n# mapeia os processamentos de cada arquitetura para os datasets base\ntrain_dataset_resnet_base = train_ds_base.map(preprocess_resnet, num_parallel_calls=tf.data.AUTOTUNE)\nval_dataset_resnet_base = val_ds_base.map(preprocess_resnet, num_parallel_calls=tf.data.AUTOTUNE)\n\ntrain_dataset_effnet_base = train_ds_base.map(preprocess_efficientnet, num_parallel_calls=tf.data.AUTOTUNE)\nval_dataset_effnet_base = val_ds_base.map(preprocess_efficientnet, num_parallel_calls=tf.data.AUTOTUNE)\n\n# cria o dataset de treino com augmentation\ntrain_ds_aug = build_dataset(TRAIN_FILENAMES, training=True, augment=True)\n\n# mapeia os processamentos para os datasets com augmentation\ntrain_dataset_resnet_aug = train_ds_aug.map(preprocess_resnet, num_parallel_calls=tf.data.AUTOTUNE)\ntrain_dataset_effnet_aug = train_ds_aug.map(preprocess_efficientnet, num_parallel_calls=tf.data.AUTOTUNE)","metadata":{},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# importa o pandas aqui pra usar mais tarde no csv\nimport pandas as pd\n\n# cria o dataset de teste\ntest_ds_base = build_dataset(TEST_FILENAMES, training=False, augment=False, is_test=True)\n\n# dataset de teste com pre-processamento da resnet\ntest_dataset_resnet = test_ds_base.map(preprocess_resnet, num_parallel_calls=tf.data.AUTOTUNE)\n\n# dataset de teste com pre-processamento da efficientnet\ntest_dataset_effnet = test_ds_base.map(preprocess_efficientnet, num_parallel_calls=tf.data.AUTOTUNE)","metadata":{},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# 2. Análise Exploratória de Dados (EDA)","metadata":{}},{"cell_type":"code","source":"# mostra o balanceamento das classes para checar se alguma e muito mais frequente\ndef show_class_balance(dataset, num_samples=1000):\n    print(f\"analisando balanceamento de classes em uma amostra de {num_samples} exemplos...\")\n    labels = []\n    # tenta pegar os itens desembaralhados e avulsos se possivel\n    try:\n        ds_unbatched = dataset.unbatch().take(num_samples)\n    except ValueError:\n        ds_unbatched = dataset.take(num_samples)\n    \n    # extrai apenas o rotulo da amostra\n    for _, label in ds_unbatched:\n        labels.append(label.numpy())\n    \n    # conta a frequencia e pega os 20 mais comuns\n    counter = Counter(labels)\n    classes, counts = zip(*counter.most_common(20)) \n    \n    # plota o grafico de barras\n    plt.figure(figsize=(12, 5))\n    plt.bar(range(len(classes)), counts)\n    plt.xticks(range(len(classes)), classes, rotation=90)\n    plt.title('top 20 classes mais frequentes na amostra')\n    plt.xlabel('id da classe')\n    plt.ylabel('frequencia')\n    plt.show()\n\n# pega os dados puros sem processamento para mostrar na eda\nraw_ds = tf.data.TFRecordDataset(TRAIN_FILENAMES).map(read_labeled_tfrecord)\nshow_class_balance(raw_ds, num_samples=2000)","metadata":{},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# exibe as imagens originais em cima e as modificadas em baixo\ndef visualize_samples_and_augmentation(raw_ds):\n    sample_images = []\n    sample_labels = []\n    \n    # pega as primeiras 4 imagens e suas respectivas labels\n    for img, lbl in raw_ds.take(4):\n        sample_images.append(img)\n        sample_labels.append(lbl.numpy())\n        \n    fig, axes = plt.subplots(2, 4, figsize=(15, 8))\n    fig.suptitle('imagens originais (topo) vs com augmentation (base)', fontsize=16)\n    \n    for i in range(4):\n        # mostra a imagem original na primeira linha do plot\n        ax = axes[0, i]\n        img_orig = sample_images[i].numpy().astype('uint8') # converte pra inteiro pra conseguir plotar direito\n        ax.imshow(img_orig)\n        ax.set_title(f'classe: {sample_labels[i]} (original)')\n        ax.axis('off')\n        \n        # plota a imagem com augmentation na segunda linha\n        ax = axes[1, i]\n        img_aug, _ = data_augment(sample_images[i], sample_labels[i]) # aplica as modificacoes visuais nela\n        img_aug = img_aug.numpy().astype('uint8')\n        ax.imshow(img_aug)\n        ax.set_title(f'classe: {sample_labels[i]} (augmentada)')\n        ax.axis('off')\n        \n    plt.tight_layout() # ajusta o espacamento\n    plt.show()\n\nvisualize_samples_and_augmentation(raw_ds)","metadata":{},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# 3. Treinamento dos Modelos (4 Modelos)","metadata":{}},{"cell_type":"code","source":"# constroi a base de aprendizado usando as arquiteturas que passamos\ndef build_model(architecture_name):\n    # checa qual modelo de base nos queremos instanciar\n    if architecture_name == 'resnet':\n        base_model = tf.keras.applications.ResNet50V2(weights='imagenet', include_top=False, input_shape=[*IMAGE_SIZE, 3])\n    else:\n        base_model = tf.keras.applications.EfficientNetB0(weights='imagenet', include_top=False, input_shape=[*IMAGE_SIZE, 3])\n        \n    # congela a parte de baixo (base) pra nao treinar esses pesos ja muito bons da imagenet\n    base_model.trainable = False\n    \n    # monta a rede juntando a base com a nossa camada de saida nova\n    model = tf.keras.Sequential([\n        base_model,\n        tf.keras.layers.GlobalAveragePooling2D(), # comprime as features geograficas\n        tf.keras.layers.Dense(NUM_CLASSES, activation='softmax') # decide qual classe eh\n    ])\n    \n    # prepara o modelo para o treino escolhendo otimizador e funcao de perda\n    model.compile(optimizer='adam', loss='sparse_categorical_crossentropy', metrics=['sparse_categorical_accuracy'])\n    return model\n\n# calcula os passos baseados no tamanho do dataset\nSTEPS_PER_EPOCH = 12753 // BATCH_SIZE\n\nprint(\"construindo os 4 modelos...\")\nwith strategy.scope():\n    # instanciando os sem data augmentation\n    model_resnet_base = build_model('resnet')\n    model_effnet_base = build_model('efficientnet')\n    \n    # instanciando os que serao treinados com augmentation\n    model_resnet_aug = build_model('resnet')\n    model_effnet_aug = build_model('efficientnet')","metadata":{},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"\\n[1/4] treinando resnet (base - sem augmentation)...\")\n# roda o treino fornecendo o dataset e validando\nhistory_resnet_base = model_resnet_base.fit(train_dataset_resnet_base, steps_per_epoch=STEPS_PER_EPOCH,\n                               epochs=EPOCHS, validation_data=val_dataset_resnet_base)","metadata":{},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"\\n[2/4] treinando efficientnet (base - sem augmentation)...\")\n# roda o treino fornecendo o dataset e validando\nhistory_effnet_base = model_effnet_base.fit(train_dataset_effnet_base, steps_per_epoch=STEPS_PER_EPOCH,\n                               epochs=EPOCHS, validation_data=val_dataset_effnet_base)","metadata":{},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"\\n[3/4] treinando resnet (com augmentation)...\")\n# roda o treino fornecendo o dataset modificado\nhistory_resnet_aug = model_resnet_aug.fit(train_dataset_resnet_aug, steps_per_epoch=STEPS_PER_EPOCH,\n                               epochs=EPOCHS, validation_data=val_dataset_resnet_base)","metadata":{},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"\\n[4/4] treinando efficientnet (com augmentation)...\")\n# roda o treino fornecendo o dataset modificado\nhistory_effnet_aug = model_effnet_aug.fit(train_dataset_effnet_aug, steps_per_epoch=STEPS_PER_EPOCH,\n                               epochs=EPOCHS, validation_data=val_dataset_effnet_base)","metadata":{},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# usa o conjunto de validacao para predizer e calcular o score macro\ndef compute_macro_f1(model, val_dataset, nome):\n    y_true, y_pred = [], []\n    for images, labels in val_dataset:\n        preds = model.predict(images, verbose=0) # preve os resultados do batch\n        y_pred.extend(np.argmax(preds, axis=1)) # acha a classe com maior probabilidade\n        y_true.extend(labels.numpy()) # guarda o rotulo original\n        \n    f1 = f1_score(y_true, y_pred, average='macro') # avalia todos de uma vez\n    print(f\"{nome}: macro f1 = {f1:.4f}\")\n    return f1\n\nprint(\"calculando macro f1 para cada modelo usando os dados de validacao...\\n\")\nprint(\"(nota: usamos os dados de validacao para escolher o melhor modelo, pois os dados de teste no kaggle nao possuem rotulos para calcular a metrica localmente)\\n\")\nf1_resnet_base = compute_macro_f1(model_resnet_base, val_dataset_resnet_base, \"resnet (base)\")\nf1_effnet_base = compute_macro_f1(model_effnet_base, val_dataset_effnet_base, \"efficientnet (base)\")\nf1_resnet_aug  = compute_macro_f1(model_resnet_aug, val_dataset_resnet_base, \"resnet (com augmentation)\")\nf1_effnet_aug  = compute_macro_f1(model_effnet_aug, val_dataset_effnet_base, \"efficientnet (com augmentation)\")\n\n# dicionario para rastrear qual modelo teve melhor desempenho e sua respectiva camada convolucional\n# guardamos o dataset de validacao (para o grad-cam) e o de teste (para a submissao final)\nresults = {\n    \"resnet_base\": (f1_resnet_base, model_resnet_base, val_dataset_resnet_base, test_dataset_resnet, \"post_relu\"), \n    \"effnet_base\": (f1_effnet_base, model_effnet_base, val_dataset_effnet_base, test_dataset_effnet, \"top_activation\"), \n    \"resnet_aug\": (f1_resnet_aug, model_resnet_aug, val_dataset_resnet_base, test_dataset_resnet, \"post_relu\"),\n    \"effnet_aug\": (f1_effnet_aug, model_effnet_aug, val_dataset_effnet_base, test_dataset_effnet, \"top_activation\")\n}\n\n# checa quem teve o maior score\nbest_model_name = max(results, key=lambda k: results[k][0])\nbest_f1, best_model, best_val_dataset, best_test_dataset, best_conv_layer = results[best_model_name]\n\nprint(f\"\\no melhor modelo foi {best_model_name} com f1-score: {best_f1:.4f}\")","metadata":{},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Função para plotar as curvas de loss e acurácia de um modelo\ndef plot_historico_treinamento(history, nome_modelo):\n    fig, axes = plt.subplots(1, 2, figsize=(15, 5))\n    \n    # Plot da Função de Perda (Loss)\n    axes[0].plot(history.history['loss'], label='Treino', marker='o')\n    axes[0].plot(history.history['val_loss'], label='Validação', marker='o')\n    axes[0].set_title(f'Loss ao longo das épocas - {nome_modelo}')\n    axes[0].set_xlabel('Época')\n    axes[0].set_ylabel('Loss (Sparse Categorical Crossentropy)')\n    axes[0].legend()\n    axes[0].grid(True)\n    \n    # Plot da Acurácia\n    axes[1].plot(history.history['sparse_categorical_accuracy'], label='Treino', marker='o')\n    axes[1].plot(history.history['val_sparse_categorical_accuracy'], label='Validação', marker='o')\n    axes[1].set_title(f'Acurácia ao longo das épocas - {nome_modelo}')\n    axes[1].set_xlabel('Época')\n    axes[1].set_ylabel('Acurácia')\n    axes[1].legend()\n    axes[1].grid(True)\n    \n    plt.tight_layout()\n    plt.show()\n\nprint(\"Gerando gráficos de treinamento...\")\nplot_historico_treinamento(history_resnet_base, \"ResNet50V2 (Base)\")\nplot_historico_treinamento(history_effnet_base, \"EfficientNetB0 (Base)\")\nplot_historico_treinamento(history_resnet_aug, \"ResNet50V2 (Com Augmentation)\")\nplot_historico_treinamento(history_effnet_aug, \"EfficientNetB0 (Com Augmentation)\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# 4. Interpretabilidade: Grad-CAM no Melhor Modelo","metadata":{}},{"cell_type":"code","source":"# cria o mapa de calor olhando os gradientes que vem das conv layers\ndef make_gradcam_heatmap(img_array, model, last_conv_layer_name):\n    # o primeiro item do nosso sequential e a rede original (resnet ou efficientnet)\n    base_model = model.layers[0] \n    \n    # acha a camada convolucional alvo pelo nome\n    last_conv_layer = base_model.get_layer(last_conv_layer_name)\n    last_conv_layer_model = tf.keras.Model(base_model.inputs, last_conv_layer.output)\n\n    # cria um modelo falso so com a parte final de classificacao para conseguirmos o gradiente\n    classifier_input = tf.keras.Input(shape=last_conv_layer.output.shape[1:])\n    x = classifier_input\n    for layer in model.layers[1:]:\n        x = layer(x)\n    classifier_model = tf.keras.Model(classifier_input, x)\n\n    # observa as operacoes de modo interativo\n    with tf.GradientTape() as tape:\n        # passa a imagem na base para tirar os mapas de features\n        last_conv_layer_output = last_conv_layer_model(img_array)\n        tape.watch(last_conv_layer_output) # pede pro tape observar essa etapa\n        \n        # classifica usando o restinho\n        preds = classifier_model(last_conv_layer_output)\n        pred_index = tf.argmax(preds[0]) # acha qual class deu maior valor\n        class_channel = preds[:, pred_index]\n\n    # calcula o quanto cada mapa de feature impactou o resultado\n    grads = tape.gradient(class_channel, last_conv_layer_output)\n    pooled_grads = tf.reduce_mean(grads, axis=(0, 1, 2))\n\n    # multiplica o impacto no mapa de features gerado\n    last_conv_layer_output = last_conv_layer_output[0]\n    heatmap = last_conv_layer_output @ pooled_grads[..., tf.newaxis]\n    heatmap = tf.squeeze(heatmap) # enxuga a dimensao\n    \n    # limpa os negativos e normaliza\n    heatmap = tf.maximum(heatmap, 0) / tf.math.reduce_max(heatmap)\n    return heatmap.numpy()\n\n# funcao pra juntar a imagem com o mapa de calor e mostrar bonito\ndef save_and_display_gradcam(img_array, heatmap, is_resnet, alpha=0.4):\n    img = img_array[0].numpy()\n\n    # se for resnet precisamos desnormalizar de novo para valores entre 0 e 255\n    if is_resnet:\n        img = (img + 1.0) * 127.5\n    \n    # garante que os pixels fiquem cortados entre 0 e 255\n    img = np.clip(img, 0, 255).astype(np.uint8)\n\n    # cria mapa de cores usando a paleta jet padrao\n    heatmap = np.uint8(255 * heatmap)\n    jet = plt.get_cmap(\"jet\")\n    jet_colors = jet(np.arange(256))[:, :3]\n    jet_heatmap = jet_colors[heatmap]\n\n    # dimensiona os pixels gerados do heatmap pra ficar do tamanho da foto\n    jet_heatmap = tf.keras.preprocessing.image.array_to_img(jet_heatmap)\n    jet_heatmap = jet_heatmap.resize((img.shape[1], img.shape[0]))\n    jet_heatmap = tf.keras.preprocessing.image.img_to_array(jet_heatmap)\n    \n    # sobrepoe usando um fator alfa pra transparencia\n    superimposed_img = jet_heatmap * alpha + img\n    superimposed_img = tf.keras.preprocessing.image.array_to_img(superimposed_img)\n\n    # renderiza tudo\n    plt.figure(figsize=(10, 5))\n    plt.subplot(1, 2, 1)\n    plt.title(\"imagem original\")\n    plt.imshow(img)\n    plt.axis('off')\n    plt.subplot(1, 2, 2)\n    plt.title(\"grad-cam (atencao do modelo)\")\n    plt.imshow(superimposed_img)\n    plt.axis('off')\n    plt.show()","metadata":{},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# pega n imagens diferentes pra gente gerar a explicacao do grad-cam (usando dados de validacao para podermos ver a classe real)\ndef get_diverse_images(dataset, n=4):\n    imagens, rotulos = [], []\n    vistos = set() # armazena classes que ja coletamos\n    \n    # percorre uma por uma\n    for image, label in dataset.unbatch():\n        lbl = int(label.numpy())\n        # so aceita a imagem se a classe for nova pra nois\n        if lbl not in vistos:\n            imagens.append(image)\n            rotulos.append(lbl)\n            vistos.add(lbl)\n            \n        # quebra o laco quando tivermos imagens suficientes\n        if len(vistos) >= n:\n            break\n    return imagens, rotulos\n\nprint(f\"gerando interpretabilidade visual usando o melhor modelo ({best_model_name})...\")\nimagens, rotulos = get_diverse_images(best_val_dataset, n=4)\n\n# variavel pra dizer pro display se ele precisa desprocessar a imagem da resnet\nis_resnet = \"resnet\" in best_model_name\n\n# aplica os mapas nas amostras e exibe na tela\nfor img, rotulo in zip(imagens, rotulos):\n    img_array = tf.expand_dims(img, axis=0) # adiciona a dimensao de batch \n    heatmap = make_gradcam_heatmap(img_array, best_model, best_conv_layer) # roda aquele fluxo gradcam\n    print(f\"classe verdadeira: {rotulo}\")\n    save_and_display_gradcam(img_array, heatmap, is_resnet) # renderiza","metadata":{},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# 5. Submissão (Submission File)","metadata":{}},{"cell_type":"code","source":"print(f\"fazendo as predicoes no conjunto de teste usando {best_model_name}...\")\n\ntest_ids = []\ntest_preds = []\n\n# percorre o conjunto de teste para fazer predicao\nfor images, ids in best_test_dataset:\n    preds = best_model.predict(images, verbose=0)\n    pred_classes = np.argmax(preds, axis=1) # pega a classe que teve a maior prob\n    \n    # armazena os ids decodificados e as previsoes\n    test_ids.extend([id.numpy().decode('utf-8') for id in ids])\n    test_preds.extend(pred_classes)\n\n# monta o dataframe com o id e o label\nmy_submission = pd.DataFrame({'id': test_ids, 'label': test_preds})\n\n# salva no arquivo csv exigido pelo kaggle\nmy_submission.to_csv('submission.csv', index=False)\n\nprint(\"arquivo de submissao (submission.csv) criado com sucesso!\")\nmy_submission.head() # mostra os 5 primeiros pra gente checar","metadata":{},"outputs":[],"execution_count":null}]}