O poder da evolução: Otimizando hiperparâmetros de modelos de IA com algoritmos genéticos

O enigma da performance oculta

Você já sentiu que seu modelo de Machine Learning tem um potencial escondido, esperando apenas a chave certa para ser destravado? É como ter um carro esportivo de última geração, mas não saber como ajustar a suspensão, o motor ou a aerodinâmica para que ele atinja sua velocidade máxima e estabilidade perfeita. Essa “chave” no mundo da Inteligência Artificial, meus caros entusiastas, são os hiperparâmetros.

Eles não são os parâmetros que o modelo “aprende” durante o treinamento (como os pesos de uma rede neural), mas sim as configurações externas que definem como o próprio processo de aprendizado ocorre. Pense neles como as “receitas” para o bolo do seu modelo: a quantidade de fermento, a temperatura do forno, o tempo de cozimento. Um ajuste errado pode transformar um bolo delicioso em algo… intragável.

A busca por essa combinação ideal de hiperparâmetros é um campo minado de tentativa e erro, intuição (às vezes, pura sorte!) e, claro, muito poder computacional. É um processo tedioso, repetitivo e, francamente, um tanto exaustivo. Mas e se eu dissesse que a Mãe Natureza, com sua sabedoria de bilhões de anos de evolução, nos deu a ferramenta perfeita para essa tarefa?

Prepare-se para embarcar em uma aventura onde o algoritmo genético – inspirado na seleção natural de Charles Darwin – não só resolve esse dilema, mas o faz de uma forma elegante, eficiente e surpreendentemente “inteligente”.

O campo minado dos hiperparâmetros: Por que é tão difícil?

Antes de pular para a solução, vamos entender a complexidade do problema. A otimização de hiperparâmetros é um desafio por diversas razões:

  • A Maldição da Dimensionalidade (The Curse of Dimensionality): Imagine que você tem apenas 5 hiperparâmetros, e cada um pode assumir 10 valores diferentes. Isso já são 10^5=100.000 combinações possíveis! Adicione mais hiperparâmetros ou mais opções para cada um, e o número de combinações explode exponencialmente. É como procurar uma agulha em um palheiro que cresce a cada segundo.
  • Espaço de Busca Complexo: A relação entre os hiperparâmetros e o desempenho do modelo não é linear, nem convexa. Isso significa que existem muitos “vales” (ótimos locais) onde um método de otimização ingênuo pode ficar preso, sem nunca encontrar o verdadeiro “pico” de performance (o ótimo global).
  • Custo Computacional: Cada combinação de hiperparâmetros geralmente exige o treinamento (e validação) do modelo do zero. Para modelos grandes e datasets volumosos, isso pode levar horas ou dias. Imagina 100.000 treinamentos! Seu bolso e sua paciência vão agradecer uma estratégia mais inteligente.
  • Tipos Variados de Hiperparâmetros: Alguns são contínuos (taxa de aprendizado), outros são inteiros (número de camadas), outros são categóricos (função de ativação: ReLU, Sigmoid, Tanh). Isso torna o processo de busca ainda mais complicado.

Métodos tradicionais como a Busca em Grade (Grid Search) testam todas as combinações (exaustivo, lento). A Busca Aleatória (Random Search) é mais eficiente, mas ainda assim pode perder combinações ótimas. É aqui que o Algoritmo Genético entra em cena, com sua abordagem mais “orgânica” e adaptativa.

Algoritmo Genético (AG): Uma dança evolutiva

O Algoritmo Genético é um tipo de algoritmo de otimização meta-heurístico inspirado na seleção natural e na genética. Em vez de testar soluções sistematicamente ou aleatoriamente, ele “evolui” soluções ao longo do tempo, como a vida evolui na Terra.

Vamos revisitar os pilares do AG e como eles se aplicam à otimização de hiperparâmetros:

  1. População Inicial: Começamos com um conjunto de “indivíduos” – cada um representando uma combinação aleatória de hiperparâmetros para o nosso modelo. É como ter uma tribo de guerreiros, cada um com uma estratégia diferente.
  2. Função de Aptidão (Fitness Function): Esta é a métrica que avalia o “quão bom” é cada indivíduo (combinação de hiperparâmetros). Para nós, a aptidão será o desempenho do modelo de Machine Learning (acurácia, F1-score) ao ser treinado com aqueles hiperparâmetros específicos. Modelos com maior aptidão são mais “fortes” e têm mais chances de “sobreviver”.
  3. Seleção: Os indivíduos mais aptos (com melhor performance) são selecionados para “reproduzir”. É como escolher os atletas campeões para serem pais de futuras gerações de atletas.
  4. Crossover (Cruzamento): Os “cromossomos” (as combinações de hiperparâmetros) dos pais selecionados são combinados para criar novos “filhos” (novas combinações de hiperparâmetros). Isso permite a exploração de novas partes do espaço de busca combinando características de soluções bem-sucedidas.
  5. Mutação: Pequenas mudanças aleatórias são introduzidas nos cromossomos dos filhos. Isso garante diversidade na população e ajuda a evitar que o algoritmo fique preso em ótimos locais. É um toque de aleatoriedade que pode levar a descobertas inesperadas.
  6. Nova Geração: Os filhos formam a nova população, e o ciclo se repete por um número definido de “gerações” ou até que uma condição de parada seja atingida.

Ao longo das gerações, a população de hiperparâmetros “evolui”, convergindo para combinações que resultam em melhor desempenho do modelo.

Construindo um otimizador genético

Vamos criar um exemplo prático. Vamos otimizar os hiperparâmetros de uma Rede Neural Convolucional (CNN) simples para o famoso dataset MNIST (dígitos escritos à mão).

Nossos hiperparâmetros a serem otimizados serão:

  • learning_rate (taxa de aprendizado do otimizador)
  • num_dense_layers (número de camadas densas no final da CNN)
  • num_neurons_dense (número de neurônios na primeira camada densa)
  • dropout_rate (taxa de dropout para regularização)

1. Configuração inicial e importações

import random
import time

import tensorflow as tf
from tensorflow.keras.datasets import mnist
from tensorflow.keras.layers import Conv2D, Dense, Dropout, Flatten, Input, MaxPooling2D
from tensorflow.keras.models import Sequential
from tensorflow.keras.optimizers import Adam

# Garante que o TensorFlow use a GPU se disponível
physical_devices = tf.config.list_physical_devices('GPU')
if physical_devices:
    tf.config.experimental.set_memory_growth(physical_devices[0], True)
    print(f"GPU detectada: {physical_devices[0]}")
else:
    print("Nenhuma GPU detectada. Treinamento será na CPU.")

# Carregar e pré-processar o dataset MNIST
(x_train, y_train), (x_test, y_test) = mnist.load_data()
x_train = x_train.reshape(-1, 28, 28, 1).astype('float32') / 255.0
# Garante que y_train seja int32 desde o início
y_train = y_train.astype('int32')
x_test = x_test.reshape(-1, 28, 28, 1).astype('float32') / 255.0
# Garante que y_test seja int32 desde o início
y_test = y_test.astype('int32')

2. Definição dos hiperparâmetros e seus intervalos

Representar nosso “cromossomo”. Cada gene é um hiperparâmetro.

# Hiperparâmetros do Espaço de Busca
HYPERPARAMETERS_SPACE = {
    'learning_rate': [0.0001, 0.0005, 0.001, 0.005, 0.01],
    'num_dense_layers': [1, 2, 3],
    'num_neurons_dense': [32, 64, 128, 256],
    'dropout_rate': [0.0, 0.1, 0.2, 0.3, 0.4, 0.5]
}

# Parâmetros do Algoritmo Genético
POPULATION_SIZE = 10
NUM_GENERATIONS = 5
MUTATION_RATE = 0.1
CROSSOVER_RATE = 0.8
NUM_EPOCHS_PER_EVAL = 5 # Menor para exemplo, na prática pode ser 10-50
BATCH_SIZE = 64

3. Classe para avaliação do modelo

Criamos uma classe para gerenciar o treinamento e a avaliação do modelo. Desta forma, podemos encapsular a lógica de treinamento e avaliação em grafos do Tensowflow com @tf.function, melhorando o desempenho.

# Classe Auxiliar para Construção e Avaliação do Modelo
class ModelEvaluator:
    def __init__(self, learning_rate, num_dense_layers, num_neurons_dense, dropout_rate):
        self.learning_rate = learning_rate
        self.num_dense_layers = num_dense_layers
        self.num_neurons_dense = num_neurons_dense
        self.dropout_rate = dropout_rate

        self.model = self.build_model()
        self.optimizer = Adam(learning_rate=self.learning_rate)

        # Para métricas dentro de @tf.function, é melhor usar tf.keras.metrics
        self.accuracy_metric = tf.keras.metrics.SparseCategoricalAccuracy()

    def build_model(self):
        model = Sequential()
        model.add(Input((28, 28, 1)))
        model.add(Conv2D(32, (3, 3), activation='relu', input_shape=(28, 28, 1)))
        model.add(MaxPooling2D((2, 2)))
        model.add(Conv2D(64, (3, 3), activation='relu'))
        model.add(MaxPooling2D((2, 2)))
        model.add(Flatten())

        for _ in range(self.num_dense_layers):
            model.add(Dense(self.num_neurons_dense, activation='relu'))
            model.add(Dropout(self.dropout_rate))

        # 10 classes para MNIST
        model.add(Dense(10))
        return model

    @tf.function
    def train_step(self, x_batch, y_batch):
        with tf.GradientTape() as tape:
            logits = self.model(x_batch, training=True)
            loss_value = tf.keras.losses.sparse_categorical_crossentropy(y_batch, logits, from_logits=True)
        grads = tape.gradient(loss_value, self.model.trainable_variables)
        self.optimizer.apply_gradients(zip(grads, self.model.trainable_variables))
        return loss_value

    @tf.function
    def test_step(self, x_batch, y_batch):
        logits = self.model(x_batch, training=False)
        # Atualiza a métrica de acurácia
        self.accuracy_metric.update_state(y_batch, logits)
        # Não precisa do retorno para a acurácia, apenas para o loop
        return logits

    def evaluate_model(self, x_train_data, y_train_data, x_test_data, y_test_data, num_epochs):
        train_dataset = tf.data.Dataset.from_tensor_slices((x_train_data, y_train_data)).shuffle(10000).batch(BATCH_SIZE)
        test_dataset = tf.data.Dataset.from_tensor_slices((x_test_data, y_test_data)).batch(BATCH_SIZE)

        # Loop de Treinamento
        for epoch in range(num_epochs):
            for x_batch_train, y_batch_train in train_dataset:
                self.train_step(x_batch_train, y_batch_train)

        # Loop de Avaliação
        self.accuracy_metric.reset_state()
        for x_batch_test, y_batch_test in test_dataset:
            self.test_step(x_batch_test, y_batch_test)

        # Obtém o resultado final da acurácia
        accuracy = self.accuracy_metric.result().numpy()
        return accuracy

4. Funções específicas do algoritmo genético

Agora, adicionamos as funções para gerenciar a população e realizar a seleção natural dos indivíduos:

# Função para criar um indivíduo (combinação de hiperparâmetros)
def create_individual():
    individual = {
        'learning_rate': random.choice(HYPERPARAMETERS_SPACE['learning_rate']),
        'num_dense_layers': random.choice(HYPERPARAMETERS_SPACE['num_dense_layers']),
        'num_neurons_dense': random.choice(HYPERPARAMETERS_SPACE['num_neurons_dense']),
        'dropout_rate': random.choice(HYPERPARAMETERS_SPACE['dropout_rate'])
    }
    return individual

# A função de aptidão principal do AG
def calculate_fitness(individual, x_train, y_train, x_test, y_test):
    print(f"Avaliando indivíduo: {individual}")

    # Cria uma nova instância de ModelEvaluator para cada indivíduo
    evaluator = ModelEvaluator(
        individual['learning_rate'],
        individual['num_dense_layers'],
        individual['num_neurons_dense'],
        individual['dropout_rate']
    )

    # Avalia o modelo
    accuracy = evaluator.evaluate_model(x_train, y_train, x_test, y_test, NUM_EPOCHS_PER_EVAL)
    print(f"  Acurácia de validação: {accuracy:.4f}")

    # Limpa a sessão Keras para evitar acúmulo de grafos/variáveis de modelos anteriores
    # Isso é crucial quando se cria muitos modelos Sequential em loops externos
    tf.keras.backend.clear_session()

    return accuracy

# Operadores genéticos: seleção, crossover e mutação (sem alterações)
def select_parents(population_with_fitness, num_parents):
    parents = []
    for _ in range(num_parents):
        tournament_candidates = random.sample(population_with_fitness, min(5, len(population_with_fitness)))
        best_candidate = max(tournament_candidates, key=lambda x: x['fitness'])
        parents.append(best_candidate['individual'])
    return parents

def crossover(parent1, parent2):
    child1 = {}
    child2 = {}

    for hp_name in HYPERPARAMETERS_SPACE.keys():
        if random.random() < CROSSOVER_RATE:
            child1[hp_name] = parent2[hp_name]
            child2[hp_name] = parent1[hp_name]
        else:
            child1[hp_name] = parent1[hp_name]
            child2[hp_name] = parent2[hp_name]

    return child1, child2

def mutate(individual):
    mutated_individual = individual.copy()
    for hp_name, hp_values in HYPERPARAMETERS_SPACE.items():
        if random.random() < MUTATION_RATE:
            mutated_individual[hp_name] = random.choice(hp_values)
    return mutated_individual

5. Loop principal

Por fim, adicionamos o loop principal que controla o fluxo pelo número de gerações definidas anteriormente:

# O loop principal do algoritmo genético
def genetic_algorithm_optimizer(x_train, y_train, x_test, y_test):
    population = []
    for _ in range(POPULATION_SIZE):
        population.append(create_individual())

    best_individual_overall = None
    best_fitness_overall = -1.0

    for generation in range(NUM_GENERATIONS):
        print(f"\n--- Geração {generation + 1}/{NUM_GENERATIONS} ---")
        population_with_fitness = []

        for individual in population:
            fitness = calculate_fitness(individual, x_train, y_train, x_test, y_test)
            population_with_fitness.append({'individual': individual, 'fitness': fitness})

        population_with_fitness.sort(key=lambda x: x['fitness'], reverse=True)

        current_best_individual = population_with_fitness[0]['individual']
        current_best_fitness = population_with_fitness[0]['fitness']

        print(f"\nMelhor indivíduo da Geração {generation + 1}: {current_best_individual} com acurácia: {current_best_fitness:.4f}")

        if current_best_fitness > best_fitness_overall:
            best_fitness_overall = current_best_fitness
            best_individual_overall = current_best_individual.copy()

        parents = select_parents(population_with_fitness, POPULATION_SIZE // 2)

        next_population = []
        for i in range(0, len(parents), 2):
            if i + 1 < len(parents):
                p1 = parents[i]
                p2 = parents[i+1]

                child1, child2 = crossover(p1, p2)

                next_population.append(mutate(child1))
                next_population.append(mutate(child2))
            else:
                next_population.append(mutate(parents[i]))

        if len(next_population) < POPULATION_SIZE:
            for i in range(POPULATION_SIZE - len(next_population)):
                next_population.append(population_with_fitness[i]['individual'].copy())
        elif len(next_population) > POPULATION_SIZE:
            next_population = next_population[:POPULATION_SIZE]

        population = next_population

    print("\n--- Otimização Concluída ---")
    print(f"Melhor combinação de hiperparâmetros encontrada: {best_individual_overall}")
    print(f"Acurácia máxima alcançada: {best_fitness_overall:.4f}")
    return best_individual_overall, best_fitness_overall

# Executar o otimizador
if __name__ == "__main__":
    start_time = time.time()
    best_hp, best_acc = genetic_algorithm_optimizer(x_train, y_train, x_test, y_test)
    end_time = time.time()
    print(f"\nTempo total de execução: {(end_time - start_time):.2f} segundos")

Ao executar o código, você pode acompanhar o processo de avaliação do algoritmo genético, e ao final, a melhor combinação encontrada durante o processo:

--- Geração 1/5 ---
Avaliando indivíduo: {'learning_rate': 0.0001, 'num_dense_layers': 2, 'num_neurons_dense': 64, 'dropout_rate': 0.4}
  Acurácia de validação: 0.9791
Avaliando indivíduo: {'learning_rate': 0.0005, 'num_dense_layers': 2, 'num_neurons_dense': 256, 'dropout_rate': 0.3}
  Acurácia de validação: 0.9907
Avaliando indivíduo: {'learning_rate': 0.01, 'num_dense_layers': 1, 'num_neurons_dense': 32, 'dropout_rate': 0.0}
  Acurácia de validação: 0.9859
Avaliando indivíduo: {'learning_rate': 0.001, 'num_dense_layers': 2, 'num_neurons_dense': 128, 'dropout_rate': 0.2}
  Acurácia de validação: 0.9887
Avaliando indivíduo: {'learning_rate': 0.01, 'num_dense_layers': 1, 'num_neurons_dense': 256, 'dropout_rate': 0.5}
  Acurácia de validação: 0.9821
Avaliando indivíduo: {'learning_rate': 0.005, 'num_dense_layers': 2, 'num_neurons_dense': 64, 'dropout_rate': 0.4}
  Acurácia de validação: 0.9855
Avaliando indivíduo: {'learning_rate': 0.0001, 'num_dense_layers': 3, 'num_neurons_dense': 64, 'dropout_rate': 0.1}
  Acurácia de validação: 0.9803
Avaliando indivíduo: {'learning_rate': 0.0001, 'num_dense_layers': 1, 'num_neurons_dense': 32, 'dropout_rate': 0.1}
  Acurácia de validação: 0.9769
Avaliando indivíduo: {'learning_rate': 0.0005, 'num_dense_layers': 1, 'num_neurons_dense': 256, 'dropout_rate': 0.2}
  Acurácia de validação: 0.9906
Avaliando indivíduo: {'learning_rate': 0.005, 'num_dense_layers': 2, 'num_neurons_dense': 64, 'dropout_rate': 0.1}
  Acurácia de validação: 0.9869

Melhor indivíduo da Geração 1: {'learning_rate': 0.0005, 'num_dense_layers': 2, 'num_neurons_dense': 256, 'dropout_rate': 0.3} com acurácia: 0.9907

--- Geração 2/5 ---
Avaliando indivíduo: {'learning_rate': 0.01, 'num_dense_layers': 1, 'num_neurons_dense': 32, 'dropout_rate': 0.0}
  Acurácia de validação: 0.9824
Avaliando indivíduo: {'learning_rate': 0.0001, 'num_dense_layers': 1, 'num_neurons_dense': 256, 'dropout_rate': 0.2}
  Acurácia de validação: 0.9833
Avaliando indivíduo: {'learning_rate': 0.0005, 'num_dense_layers': 1, 'num_neurons_dense': 256, 'dropout_rate': 0.1}
  Acurácia de validação: 0.9885
Avaliando indivíduo: {'learning_rate': 0.0005, 'num_dense_layers': 2, 'num_neurons_dense': 256, 'dropout_rate': 0.0}
  Acurácia de validação: 0.9912
Avaliando indivíduo: {'learning_rate': 0.001, 'num_dense_layers': 2, 'num_neurons_dense': 32, 'dropout_rate': 0.2}
  Acurácia de validação: 0.9890
Avaliando indivíduo: {'learning_rate': 0.0005, 'num_dense_layers': 2, 'num_neurons_dense': 256, 'dropout_rate': 0.3}
  Acurácia de validação: 0.9907
Avaliando indivíduo: {'learning_rate': 0.0005, 'num_dense_layers': 1, 'num_neurons_dense': 256, 'dropout_rate': 0.2}
  Acurácia de validação: 0.9894
Avaliando indivíduo: {'learning_rate': 0.001, 'num_dense_layers': 2, 'num_neurons_dense': 128, 'dropout_rate': 0.2}
  Acurácia de validação: 0.9913
Avaliando indivíduo: {'learning_rate': 0.005, 'num_dense_layers': 2, 'num_neurons_dense': 64, 'dropout_rate': 0.1}
  Acurácia de validação: 0.9886
Avaliando indivíduo: {'learning_rate': 0.01, 'num_dense_layers': 1, 'num_neurons_dense': 32, 'dropout_rate': 0.0}
  Acurácia de validação: 0.9824

Melhor indivíduo da Geração 2: {'learning_rate': 0.001, 'num_dense_layers': 2, 'num_neurons_dense': 128, 'dropout_rate': 0.2} com acurácia: 0.9913

--- Geração 3/5 ---
Avaliando indivíduo: {'learning_rate': 0.0005, 'num_dense_layers': 2, 'num_neurons_dense': 256, 'dropout_rate': 0.0}
  Acurácia de validação: 0.9902
Avaliando indivíduo: {'learning_rate': 0.0005, 'num_dense_layers': 1, 'num_neurons_dense': 256, 'dropout_rate': 0.2}
  Acurácia de validação: 0.9918
Avaliando indivíduo: {'learning_rate': 0.0005, 'num_dense_layers': 2, 'num_neurons_dense': 256, 'dropout_rate': 0.0}
  Acurácia de validação: 0.9913
Avaliando indivíduo: {'learning_rate': 0.0005, 'num_dense_layers': 2, 'num_neurons_dense': 256, 'dropout_rate': 0.3}
  Acurácia de validação: 0.9907
Avaliando indivíduo: {'learning_rate': 0.001, 'num_dense_layers': 2, 'num_neurons_dense': 128, 'dropout_rate': 0.2}
  Acurácia de validação: 0.9910
Avaliando indivíduo: {'learning_rate': 0.001, 'num_dense_layers': 2, 'num_neurons_dense': 128, 'dropout_rate': 0.2}
  Acurácia de validação: 0.9909
Avaliando indivíduo: {'learning_rate': 0.0005, 'num_dense_layers': 2, 'num_neurons_dense': 256, 'dropout_rate': 0.0}
  Acurácia de validação: 0.9889
Avaliando indivíduo: {'learning_rate': 0.0005, 'num_dense_layers': 2, 'num_neurons_dense': 256, 'dropout_rate': 0.3}
  Acurácia de validação: 0.9916
Avaliando indivíduo: {'learning_rate': 0.0005, 'num_dense_layers': 1, 'num_neurons_dense': 256, 'dropout_rate': 0.2}
  Acurácia de validação: 0.9915
Avaliando indivíduo: {'learning_rate': 0.001, 'num_dense_layers': 2, 'num_neurons_dense': 32, 'dropout_rate': 0.2}
  Acurácia de validação: 0.9879

Melhor indivíduo da Geração 3: {'learning_rate': 0.0005, 'num_dense_layers': 1, 'num_neurons_dense': 256, 'dropout_rate': 0.2} com acurácia: 0.9918

--- Geração 4/5 ---
Avaliando indivíduo: {'learning_rate': 0.0005, 'num_dense_layers': 1, 'num_neurons_dense': 256, 'dropout_rate': 0.2}
  Acurácia de validação: 0.9911
Avaliando indivíduo: {'learning_rate': 0.0005, 'num_dense_layers': 1, 'num_neurons_dense': 256, 'dropout_rate': 0.2}
  Acurácia de validação: 0.9909
Avaliando indivíduo: {'learning_rate': 0.01, 'num_dense_layers': 1, 'num_neurons_dense': 256, 'dropout_rate': 0.2}
  Acurácia de validação: 0.9839
Avaliando indivíduo: {'learning_rate': 0.0005, 'num_dense_layers': 2, 'num_neurons_dense': 256, 'dropout_rate': 0.2}
  Acurácia de validação: 0.9908
Avaliando indivíduo: {'learning_rate': 0.0005, 'num_dense_layers': 1, 'num_neurons_dense': 256, 'dropout_rate': 0.2}
  Acurácia de validação: 0.9917
Avaliando indivíduo: {'learning_rate': 0.0005, 'num_dense_layers': 1, 'num_neurons_dense': 256, 'dropout_rate': 0.2}
  Acurácia de validação: 0.9907
Avaliando indivíduo: {'learning_rate': 0.0005, 'num_dense_layers': 2, 'num_neurons_dense': 256, 'dropout_rate': 0.3}
  Acurácia de validação: 0.9915
Avaliando indivíduo: {'learning_rate': 0.0005, 'num_dense_layers': 1, 'num_neurons_dense': 256, 'dropout_rate': 0.2}
  Acurácia de validação: 0.9913
Avaliando indivíduo: {'learning_rate': 0.0005, 'num_dense_layers': 2, 'num_neurons_dense': 256, 'dropout_rate': 0.0}
  Acurácia de validação: 0.9899
Avaliando indivíduo: {'learning_rate': 0.001, 'num_dense_layers': 2, 'num_neurons_dense': 128, 'dropout_rate': 0.2}
  Acurácia de validação: 0.9907

Melhor indivíduo da Geração 4: {'learning_rate': 0.0005, 'num_dense_layers': 1, 'num_neurons_dense': 256, 'dropout_rate': 0.2} com acurácia: 0.9917

--- Geração 5/5 ---
Avaliando indivíduo: {'learning_rate': 0.0005, 'num_dense_layers': 2, 'num_neurons_dense': 256, 'dropout_rate': 0.2}
  Acurácia de validação: 0.9898
Avaliando indivíduo: {'learning_rate': 0.0005, 'num_dense_layers': 1, 'num_neurons_dense': 256, 'dropout_rate': 0.2}
  Acurácia de validação: 0.9899
Avaliando indivíduo: {'learning_rate': 0.0005, 'num_dense_layers': 2, 'num_neurons_dense': 256, 'dropout_rate': 0.3}
  Acurácia de validação: 0.9920
Avaliando indivíduo: {'learning_rate': 0.0005, 'num_dense_layers': 1, 'num_neurons_dense': 256, 'dropout_rate': 0.2}
  Acurácia de validação: 0.9906
Avaliando indivíduo: {'learning_rate': 0.0005, 'num_dense_layers': 1, 'num_neurons_dense': 256, 'dropout_rate': 0.2}
  Acurácia de validação: 0.9903
Avaliando indivíduo: {'learning_rate': 0.0005, 'num_dense_layers': 1, 'num_neurons_dense': 256, 'dropout_rate': 0.2}
  Acurácia de validação: 0.9890
Avaliando indivíduo: {'learning_rate': 0.0005, 'num_dense_layers': 2, 'num_neurons_dense': 256, 'dropout_rate': 0.3}
  Acurácia de validação: 0.9885
Avaliando indivíduo: {'learning_rate': 0.0005, 'num_dense_layers': 1, 'num_neurons_dense': 256, 'dropout_rate': 0.2}
  Acurácia de validação: 0.9894
Avaliando indivíduo: {'learning_rate': 0.0005, 'num_dense_layers': 1, 'num_neurons_dense': 256, 'dropout_rate': 0.2}
  Acurácia de validação: 0.9919
Avaliando indivíduo: {'learning_rate': 0.0005, 'num_dense_layers': 1, 'num_neurons_dense': 256, 'dropout_rate': 0.2}
  Acurácia de validação: 0.9890

Melhor indivíduo da Geração 5: {'learning_rate': 0.0005, 'num_dense_layers': 2, 'num_neurons_dense': 256, 'dropout_rate': 0.3} com acurácia: 0.9920

--- Otimização Concluída ---
Melhor combinação de hiperparâmetros encontrada: {'learning_rate': 0.0005, 'num_dense_layers': 2, 'num_neurons_dense': 256, 'dropout_rate': 0.3}
Acurácia máxima alcançada: 0.9920

Tempo total de execução: 710.42 segundos
Ver mais

Nem tudo são flores: limitações do Algoritmo Genético na otimização de hiperparâmetros

Embora os algoritmos genéticos sejam versáteis e muitas vezes surpreendentemente eficazes, nem tudo é tão darwinianamente perfeito assim. Eles carregam algumas limitações importantes, especialmente no contexto da otimização de hiperparâmetros:

1. Custo computacional ainda é alto

Por mais que o AG evite testar todas as combinações como no Grid Search, ele ainda exige o treinamento do modelo diversas vezes — o que pode ser um fardo considerável quando se lida com redes neurais complexas ou grandes volumes de dados. Se cada avaliação levar uma hora, um AG com 10 indivíduos e 5 gerações já vai te custar… 50 horas de processamento. A Mãe Natureza pode ser paciente, mas seu orçamento na nuvem talvez não seja.

2. Sensibilidade aos parâmetros do próprio AG

Ironicamente, o AG também tem seus próprios “hiperparâmetros”: tamanho da população, taxa de mutação, taxa de crossover, número de gerações… e adivinha só? Eles também influenciam bastante o resultado final. Uma configuração inadequada pode levar a uma convergência precoce (ficar preso em um ótimo local) ou a uma busca excessivamente aleatória, que nunca estabiliza.

3. Pode demorar para convergir

AGs são exploradores natos. Isso é ótimo para escapar de ótimos locais, mas também significa que eles podem ser mais lentos para chegar perto de soluções ótimas, especialmente se comparados a métodos mais direcionados como Otimização Bayesiana (que aprende com as avaliações anteriores para escolher melhor os próximos testes).

4. Pouco aproveitamento do histórico

Enquanto algoritmos como Gradient Descent e Bayesian Optimization aprendem ativamente com os resultados anteriores para afinar sua busca, o AG trata cada nova geração como um experimento genético semi-aleatório. Ele não constrói um modelo preditivo do espaço de busca, o que pode desperdiçar informação valiosa, especialmente quando cada avaliação tem um custo alto.

5. Escalabilidade limitada com hiperparâmetros contínuos

Embora o AG consiga lidar com hiperparâmetros categóricos com naturalidade, ele pode não ser tão preciso em refinar hiperparâmetros contínuos (como a taxa de aprendizado) quanto métodos mais direcionados. Isso acontece porque sua busca depende de mutações e cruzamentos discretos — e não de gradientes ou aproximações suaves.

O veredito final

A otimização de hiperparâmetros não precisa mais ser um poço de frustração. Ao adotar a beleza da seleção natural, o Algoritmo Genético oferece uma rota eficiente e elegante para desbloquear o desempenho máximo dos seus modelos de Inteligência Artificial. Com a capacidade de aproveitar o poder das GPUs via TensorFlow e tf.function de forma inteligente, você pode acelerar esse processo e passar mais tempo inovando e menos tempo ajustando manualmente.

O universo da IA é vasto e cheio de ferramentas incríveis. O AG é apenas uma delas, mas uma das mais robustas e fascinantes. Espero que este guia inspire você a experimentar, a otimizar e a deixar seus modelos “evoluírem” para patamares que você talvez nem imaginava ser possível. O futuro da IA está em otimizar cada detalhe, e agora você tem mais uma ferramenta poderosa em seu arsenal.

Qual sua próxima combinação de hiperparâmetros a ser evoluída? Compartilhe nos comentários e vamos acelerar o futuro juntos!


Veja também

Deixe um comentário