Mostrar códigos
!pip install optunaRedes Neurais Profundas | Prof. Dr. Guilherme Souza Rodrigues
December 6, 2025
Primeiramente, escolhi utilizar Python para esta tarefa, com a biblioteca principal Pytorch e seus utilitários, além de outras bibliotecas auxiliares para tarefas intermediárias, como o optuna para estudo e seleção de hiperparâmetros.
Esta é uma tarefa relativamente simples, que pode ser executada em computador pessoal, ainda que sem GPU. Entretanto, pensando em aprimorar a velocidade de treinamento dos modelos, utilizei a infraestrutura gratuita do google colab para execução dos códigos na GPU gratuita fornecida. No código, permiti que selecionasse GPU ou CPU, podendo portanto executar em qualquer máquina disponível, com prioridade para GPU, se existente. Também, trouxe a possibilidade de um modo de fast debug, principalmente pensando na execução em CPU, tal que o código pudesse ser ajustado e aprimorado com poucas iterações nas etapas intermediárias, e ao final desligar este modo para execução completa da atividade.
Por se tratar de um problema de classificação de imagens, podemos adotar diversas estratégias para melhorar o nosso dado de entrada, e utilizar destas melhorias também para fazer o aumentamento dos dados. Por se tratarem de poucos dados, e ainda por cima desbalanceados — existem muito mais imagens referentes a impressões digitais masculinas que imagens referentes a impressões digitais femininas —, executei as seguintes tarefas de aumentamento dos dados:
Replicar as imagens aleatoriamente com alterações de contraste, saturação e brilho;
Aplicar desfoque gaussiano aleatório para gerar réplicas de cada imagem com leves desfoques;
Realizar rotações nas imagens aleatoriamente;
Reduzir as imagens (“cropar”);
Realizar transformações afins nas imagens, mantendo o centro delas invariante.
Com estas estratégias, podemos inserir para nosso modelo uma quantidade aumentada de dados, visto o cenário de poucos exemplos de treinamento, que em geral levam a baixo ajuste da rede.
Optei por utilizar redes residuais pré treinadas disponíveis no Pytorch, ao invés de declarar minha própria rede. Para isso, observei que estas redes foram treinadas com imagens no tamanho 224x224, enquanto as imagens disponíveis nesta atividade tinham tamanho 96x103. Por isso, resolvi alterar o tamanho das imagens para 224x224, visto que os modelos foram treinados com este tamanho, e a utilização de outro tamanho poderia comprometer o funcionamento dos filtros aprendidos pela rede, por exemplo.
Além disso, fiz a normalização dos tensores também considerando os pesos de normalização padrão das redes da família ResNet do Pytorch, com objetivo análogo ao citado anteriormente.
Também declarei as imagens como se tivessem camadas de cor. Apesar de serem imagens em escala de cinza, estas redes do Pytorch foram treinadas com imagens de três canais de cor (RGB). Para tentar garantir um melhor funcionamento destas redes, repliquei a imagem nestes três canais, como se RGB fossem.
Dividi os dados do conjunto de treino em 80% treino e 20% validação, para estudo de hiperparâmetros. Esta divisão foi estratificada, considerando o desbalanceamento das classes.
Além disso, apliquei pesos para os registros de treinamento, tal que as imagens de impressões digitais femininas tivessem um peso maior que as impressões digitais masculinas, com objetivo de resolver ou ao menos minorar a questão do desbalancemanto de classes no conjunto de dados.
Como utilizei de redes pré treinadas, as considerei como hiperparâmetros. Isto é, qual rede usar é um dos hiperparâmetros do estudo. As redes que considerei como possibilidade foram: “resnet18”; “resnet50”; “mobilenet_v2”; “efficientnet_b0”; “densenet121”. Pela documentação do Pytorch, todas estas redes foram treinadas com imagens 224x224, servem para classificação, e possuem pesos similares, o que favorece as escolhas anteriores de pré processamento.
É possível alterar diversos aspectos destas redes no estudo de hiperparâmetros, como a taxa de aprendizado, camadas da rede, otimizador (Adam, SGD, …), etc. Entretanto, fiz alguns testes considerando estudos maiores de hiperparâmetros, e pelas limitações de infraestrutura (quanto mais parâmetros decidirmos ajustar, ou teremos menos combinações a fazer considerando a mesma quantidade de iterações optuna, ou teremos de aumentar exponencialmente a quantidade de iterações, aumentando assim conjuntamente o tempo de seleção e uso de computação), notei que não conseguia ganhos significativos por buscar combinações mais detalhadas de hiperparâmetros, além de perder muito em tempo de execução. Desta forma, optei por ajustar apenas o hiperparâmetro de taxa de aprendizagem (para além de qual rede utilizar), e acredito ter obtido resultados satisfatórios para esta tarefa.
Os valores possíveis para a taxa de aprendizado eram entre 0,001 e 0,00001. Fixei o otimizador Adam convencional, e utilizei como função de perda a “BCEWithLogitsLoss”, que é uma combinação do Pytorch de uma camada Sigmoid com a perda de Entropia Cruzada Binária (BCELoss).
Defini ainda a utilização de poda pelo optuna, bem como algumas questões fixadas como a paciência, a quantidade de épocas e de iterações, que também foram fixas.
O estudo optuna reportou a rede resnet18, com taxa de aprendizado \(\approx 0,001\) como o modelo com maior escore F1 (\(\approx 0,5225\)). Fixado estes hiperparâmetros, re-treinei a rede combinando os conjuntos de treino e validação, onde obtive um escore F1 \(\approx 0,51\), o que parece aceitável para esta atividade.
Portanto, utilizou-se deste modelo treinado sobre todo o conjunto de treino para realizar as predições para o conjunto de teste, as quais seguem em anexo no formato solicitado pela atividade, num arquivo csv de duas colunas.
| Técnicas utilizadas: | Argumentos |
|---|---|
| ColorJitter | brightness = 0.1, contrast = 0.1, prob = 0.2 |
| GaussianBlur | kernel_size = 3, sigma = (0.1, 0.5), prob = 0.1 |
| RandomHorizontalFlip | prob = 0.5 |
| RandomRotation | degrees = (-3, 3) |
| RandomResizedCrop | size = (224, 224), scale = (0.95, 1.0) |
| RandomAffine | degrees=0, translate=(0.03, 0.03) |
| Hiperparâmetro | Valores possíveis |
|---|---|
| Rede | “resnet18”; “resnet50”; “mobilenet_v2”; “efficientnet_b0”; “densenet121” |
| Learning rate | [0,001;0,00001] |
| Rede | Learnign rate |
|---|---|
| resnet18 | \(\approx 0,0001\) |
Performance desta rede:
Durante estudo optuna (80% treino, 20% validação):
Treino final (100% treino):
Os resultados da metodologia supracitada e seus respectivos resultados estão listados abaixo na execução do notebook python.
Instalar optuna no Colab (único módulo dos que não usarei que não está pré instalado)
Importando módulos
from google.colab import drive
import os
import glob
import random
import numpy as np
from PIL import Image
from tqdm import tqdm
import pandas as pd
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import Dataset, DataLoader, WeightedRandomSampler
import torchvision.transforms as T
import torchvision.models as models
from sklearn.model_selection import train_test_split
from sklearn.metrics import f1_score, precision_score, recall_score, confusion_matrix
import optunaCriando conexão com o meu drive, onde subi as imagens
Definindo o caminho (remoto) dos dados
Definindo questões operacionais: Modo fast debug para rodar rapidinho e verificar erros e problemas, desligar para rodar de verdade
fixando o tamanho da imagem para upscale - como estou usando modelos pré treinados para 224x224, a recomendação da bibliografia é (neste caso) aumentar o tamanho das imagens de 96x103 para 224x224, para garantir o correto funcionamento dos filtros kernel de imagem da forma que foram concebidos nos modelos originais.
Garantindo possibilidades: rodar na GPU se possível, no processador, caso não tenha.
DEVICE: cuda
FAST_DEBUG: False
Trazer os dados para o python
Preparando os dados pré-separação em treino e validação
class FingerprintDataset(Dataset):
def __init__(self, files_list, transform=None):
self.files = list(files_list)
self.transform = transform
self.labels = []
for f in self.files:
name = os.path.basename(f)
first = name[0].upper() if len(name) > 0 else ""
if first == "F":
self.labels.append(1)
elif first == "M":
self.labels.append(0)
else:
base = name.split("_")[0].upper() if "_" in name else first
if base == "F":
self.labels.append(1)
elif base == "M":
self.labels.append(0)
else:
raise ValueError(f"Nome de arquivo não tem F/M na frente: {name}")
def __len__(self):
return len(self.files)
def __getitem__(self, idx):
path = self.files[idx]
img = Image.open(path)
if self.transform:
img = self.transform(img)
label = self.labels[idx]
return img, labelCriando os tensores - data augmentation sendo feito nesta etapa
obs: a normalização está sendo feita com os pesos dos modelos pré treinados, em detrimento das estatísticas dos meus dados.
train_tf = T.Compose([
T.Resize((IMAGE_HEIGHT, IMAGE_WIDTH)),
T.Grayscale(num_output_channels=3),
T.RandomApply([T.ColorJitter(brightness=0.1, contrast=0.1)], p=0.2),
T.RandomApply([T.GaussianBlur(kernel_size=3, sigma=(0.1, 0.5))], p=0.1),
T.RandomHorizontalFlip(p=0.5),
T.RandomRotation(degrees=(-3, 3)),
T.RandomResizedCrop(size=(IMAGE_HEIGHT, IMAGE_WIDTH), scale=(0.95, 1.0)),
T.RandomAffine(degrees=0, translate=(0.03, 0.03)),
T.ToTensor(),
T.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
])
val_tf = T.Compose([
T.Resize((IMAGE_HEIGHT, IMAGE_WIDTH)),
T.Grayscale(num_output_channels=3),
T.ToTensor(),
T.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
])Split treino/validação (80/20)
labels_all = []
for f in all_files:
name = os.path.basename(f)
first = name[0].upper() if len(name) > 0 else ""
if first == "F":
labels_all.append(1)
elif first == "M":
labels_all.append(0)
else:
base = name.split("_")[0].upper() if "_" in name else first
labels_all.append(1 if base == "F" else 0)
labels_all = np.array(labels_all)
train_idx, val_idx = train_test_split(
np.arange(len(all_files)),
test_size=0.2,
stratify=labels_all,
random_state=42
)
train_files = [all_files[i] for i in train_idx]
val_files = [all_files[i] for i in val_idx]
train_dataset = FingerprintDataset(train_files, transform=train_tf)
val_dataset = FingerprintDataset(val_files, transform=val_tf)Aplicando pesos para a classe com menor frequência (impressões digitais de mulheres)
train_labels = np.array(train_dataset.labels)
class_counts = np.bincount(train_labels)
class_weights = 1.0 / (class_counts)
sample_weights = class_weights[train_labels]
sampler = WeightedRandomSampler(
weights=sample_weights.tolist(),
num_samples=len(sample_weights),
replacement=True
)
train_loader = DataLoader(train_dataset, batch_size=BATCH_SIZE, sampler=sampler, num_workers=0, pin_memory=False)
val_loader = DataLoader(val_dataset, batch_size=BATCH_SIZE, shuffle=False, num_workers=0, pin_memory=False)Definindo modelos: Ao invés de montar uma arquitetura própria, trouxe algumas sugestões de arquiteturas pré-treinadas para classificação de imagens
def create_model(trial):
model_name = trial.suggest_categorical(
"model_type",
["resnet18", "resnet50", "mobilenet_v2", "efficientnet_b0", "densenet121"]
)
if model_name == "resnet18":
model = models.resnet18(weights="IMAGENET1K_V1")
model.fc = nn.Linear(model.fc.in_features, 1)
elif model_name == "resnet50":
model = models.resnet50(weights="IMAGENET1K_V1")
model.fc = nn.Linear(model.fc.in_features, 1)
elif model_name == "mobilenet_v2":
model = models.mobilenet_v2(weights="IMAGENET1K_V1")
model.classifier[1] = nn.Linear(model.classifier[1].in_features, 1)
elif model_name == "efficientnet_b0":
model = models.efficientnet_b0(weights="IMAGENET1K_V1")
model.classifier[1] = nn.Linear(model.classifier[1].in_features, 1)
elif model_name == "densenet121":
model = models.densenet121(weights="IMAGENET1K_V1")
model.classifier = nn.Linear(model.classifier.in_features, 1)
return model.to(device)Treino e avaliação dos modelos
def train_and_evaluate(model, trial=None, save_path=None):
lr = trial.suggest_float("lr", 1e-5, 1e-3, log=True) if trial is not None else 1e-4
optimizer = optim.Adam(model.parameters(), lr=lr)
criterion = nn.BCEWithLogitsLoss()
best_f1 = 0.0
best_state = None
patience = 3
no_improve = 0
for epoch in range(N_EPOCHS):
model.train()
running_loss = 0.0
for imgs, labels in train_loader:
imgs = imgs.to(device)
labels = labels.float().to(device)
optimizer.zero_grad()
logits = model(imgs).squeeze(1)
loss = criterion(logits, labels)
loss.backward()
optimizer.step()
running_loss += loss.item() * imgs.size(0)
model.eval()
preds = []
trues = []
with torch.no_grad():
for imgs, labels in val_loader:
imgs = imgs.to(device)
labels = labels.to(device)
logits = model(imgs).squeeze(1)
probs = torch.sigmoid(logits)
pred_bin = (probs > 0.5).long().cpu().numpy()
preds.extend(pred_bin.tolist())
trues.extend(labels.cpu().numpy().tolist())
f1 = f1_score(trues, preds, zero_division=0)
prec = precision_score(trues, preds, zero_division=0)
rec = recall_score(trues, preds, zero_division=0)
print(f"Epoch {epoch+1}/{N_EPOCHS} - loss: {running_loss/len(train_dataset):.4f} - F1: {f1:.4f} - P: {prec:.4f} - R: {rec:.4f}")
if f1 > best_f1:
best_f1 = f1
no_improve = 0
best_state = model.state_dict()
if save_path is not None:
torch.save(best_state, save_path)
else:
no_improve += 1
if no_improve >= patience:
print("Early stopping.")
break
if trial is not None:
trial.report(best_f1, epoch)
if trial.should_prune():
raise optuna.exceptions.TrialPruned()
return best_f1, best_stateFunção objetivo do optuna
Rodar optuna
Coletando melhor modelo do estudo de hiperparâmetros
best_params = study.best_trial.params
class Dummy:
def __init__(self, params):
self.params = params
def suggest_float(self, *args, **kwargs):
return self.params.get("lr", 1e-4)
def suggest_categorical(self, name, choices):
return self.params.get("model_type", choices[0])
dummy_trial = Dummy(best_params)
final_model = create_model(dummy_trial)
best_model_path = "best_model_final.pth"
best_f1, best_state = train_and_evaluate(final_model, trial=None, save_path=best_model_path)
print("Final F1 (val):", best_f1)
print("Best model saved to:", best_model_path)Epoch 1/20 - loss: 0.5652 - F1: 0.4317 - P: 0.3053 - R: 0.7365
Epoch 2/20 - loss: 0.4471 - F1: 0.5226 - P: 0.4029 - R: 0.7432
Epoch 3/20 - loss: 0.3927 - F1: 0.4959 - P: 0.4155 - R: 0.6149
Epoch 4/20 - loss: 0.3358 - F1: 0.4978 - P: 0.3709 - R: 0.7568
Epoch 5/20 - loss: 0.2652 - F1: 0.5101 - P: 0.5067 - R: 0.5135
Early stopping.
Final F1 (val): 0.5225653206650831
Best model saved to: best_model_final.pth
Avaliação “final”
if best_state is not None:
final_model.load_state_dict(best_state)
final_model.eval()
preds = []
trues = []
with torch.no_grad():
for imgs, labels in val_loader:
imgs = imgs.to(device)
labels = labels.to(device)
logits = final_model(imgs).squeeze(1)
probs = torch.sigmoid(logits)
pred_bin = (probs > 0.5).long().cpu().numpy()
preds.extend(pred_bin.tolist())
trues.extend(labels.cpu().numpy().tolist())
print("Precision:", precision_score(trues, preds, zero_division=0))
print("Recall:", recall_score(trues, preds, zero_division=0))
print("F1 Score:", f1_score(trues, preds, zero_division=0))
print("Confusion Matrix:\n", confusion_matrix(trues, preds))Precision: 0.5066666666666667
Recall: 0.5135135135135135
F1 Score: 0.5100671140939598
Confusion Matrix:
[[578 74]
[ 72 76]]
Utilizando agora o modelo selecionado para classificar as imagens no conjunto de teste
TEST_ROOT = '/content/drive/MyDrive/colab_data/rna1/lista5/Teste'
def collect_image_files(root):
exts = ("*.bmp", "*.BMP")
files = []
for e in exts:
files.extend(glob.glob(os.path.join(root, e)))
files = sorted(files)
return files
test_files = collect_image_files(TEST_ROOT)
if best_state is None and os.path.exists("best_model_final.pth"):
final_model.load_state_dict(torch.load("best_model_final.pth", map_location=device))
elif best_state is not None:
final_model.load_state_dict(best_state)
class TestDataset(Dataset):
def __init__(self, files_list, transform=None):
self.files = files_list
self.transform = transform
def __len__(self):
return len(self.files)
def __getitem__(self, idx):
path = self.files[idx]
img = Image.open(path)
file_id = os.path.basename(path).split('.')[0]
if self.transform:
img = self.transform(img)
return img, file_id
test_dataset = TestDataset(test_files, transform=val_tf)
test_loader = DataLoader(test_dataset, batch_size=BATCH_SIZE, shuffle=False, num_workers=0)
final_model.eval()
final_model.to(device)
predictions = []
with torch.no_grad():
for imgs, file_ids in tqdm(test_loader, desc="Classificando imagens de teste"):
imgs = imgs.to(device)
logits = final_model(imgs).squeeze(1)
probs = torch.sigmoid(logits)
pred_bin = (probs > 0.5).long().cpu().numpy()
for file_id, pred in zip(file_ids, pred_bin):
label_str = "M" if pred == 0 else "F"
predictions.append([file_id, label_str])
df_predictions = pd.DataFrame(predictions, columns=["ID", "PREDIT"])Salvando os resultados em CSV