Vai al contenuto principale

Algoritmo genetico: guida completa con implementazione in Python

Un algoritmo genetico รจ una tecnica di ricerca che imita la selezione naturale per trovare soluzioni ottimali affinando iterativamente una popolazione di soluzioni candidate.
ยท 15 min leggi

Scopri con l'IA

Immagina di dover ottimizzare i percorsi di consegna per dei camion. Ogni camion ha molti possibili percorsi, e hai molti camion con molte fermate. L'enorme numero di combinazioni possibili puรฒ essere scoraggiante, e trovare la soluzione migliore puรฒ sembrare come cercare un ago in un pagliaio.

Per risolvere problemi di questo tipo, possiamo prendere in prestito un concetto dalla biologia: l'evoluzione. In biologia, la selezione naturale รจ una forza che guida l'evoluzione. Il problema della natura รจ trovare le combinazioni ottimali di tratti che permettono a un organismo di sopravvivere e riprodursi. Usa la selezione naturale per mettere in competizione diversi insiemi di tratti e scegliere le combinazioni migliori.ย 

Allo stesso modo, possiamo usare questo concetto per creare un algoritmo che mette diverse soluzioni una contro l'altra e usa la selezione per โ€œevolverleโ€ fino alla soluzione migliore. Gli algoritmi che fanno questo si chiamano algoritmi genetici (GA).

Ispirati all'evoluzione naturale, i GA esplorano in modo efficiente lo spazio delle soluzioni per scoprire soluzioni ottimali o quasi ottimali, anche per problemi complessi con molte parti in gioco. Imitando il processo di selezione naturale, i GA possono far evolvere le soluzioni nel tempo, migliorandone iterativamente le prestazioni.

Fondamenti biologici degli algoritmi genetici

Gli algoritmi genetici sono stati ispirati dal processo di selezione naturale e dalla genetica. Imitano il modo in cui gli organismi viventi evolvono nel corso delle generazioni per adattarsi e sopravvivere nei loro ambienti. Capire i principi biologici di base alla base dei GA puรฒ aiutarci a comprendere come funzionano questi algoritmi e ad usarli in modo piรน strategico.

Selezione naturale

La selezione naturale รจ il processo per cui gli individui con combinazioni migliori di tratti favorevoli hanno maggiori probabilitร  di sopravvivere e riprodursi. Nel tempo, questi tratti vantaggiosi diventano piรน comuni nella popolazione.

Nel contesto dei GA, questo concetto si riflette selezionando per la riproduzione gli individui con punteggi di fitness piรน alti. In questo modo, trasmettono i loro tratti favorevoli, o "geni", alla generazione successiva.

Geni, crossover e mutazione

I tratti di un organismo biologico sono codificati dai suoi geni. I geni fanno parte del DNA, che รจ organizzato in cromosomi.

I GA prendono in prestito molti degli stessi concetti e della stessa terminologia. Le soluzioni potenziali a un problema sono rappresentate come "cromosomi", in genere codificati come stringhe o array. Ogni elemento del cromosoma corrisponde a un gene che determina uno specifico tratto della soluzione.

Il crossover, o ricombinazione, รจ un'operazione genetica che combina parti del materiale genetico di due cromosomi genitori in un nuovo cromosoma che contiene parti di ciascun genitore. Questo nuovo mix di cromosoma viene trasmesso al figlio. รˆ uno dei motivi per cui puoi avere il colore degli occhi di tua madre e il colore dei capelli di tuo padre.

Nei GA, il crossover consiste nello scambiare segmenti dei cromosomi dei genitori per creare nuovi individui. Questa operazione introduce variabilitร  e consente alla prole di ereditare tratti vantaggiosi da entrambi i genitori.

La mutazione introduce cambiamenti casuali nei geni di un organismo, che possono portare a nuovi tratti.

Spiegazione visiva di tre concetti di evoluzione: selezione, mutazione e crossover.

Negli algoritmi genetici, la mutazione consiste nell'alterare casualmente uno o piรน geni in un cromosoma. Questo aiuta a mantenere la diversitร  genetica all'interno della popolazione e consente all'algoritmo di esplorare nuove aree dello spazio delle soluzioni.

Valutazione della fitness

In natura, la fitness di un individuo รจ determinata dalla sua capacitร  di sopravvivere e riprodursi. Se muori prima di riprodurti, hai una fitness pari a zero. Se hai 12 nipoti la tua fitness sarร  piรน alta rispetto a qualcuno che ne ha 2.

Nei GA, la fitness รจ simile, ma un po' diversa. Una funzione di fitness valuta quanto bene una soluzione risolve il problema in questione. Le soluzioni con punteggi di fitness piรน elevati hanno maggiori probabilitร  di essere selezionate per la riproduzione, assicurando che soluzioni migliori si propaghino attraverso le generazioni.

Componenti di un algoritmo genetico

Gli algoritmi genetici sono stati ispirati dall'evoluzione. Di conseguenza, i componenti di un GA condividono nomi e funzioni simili a quelli delle loro controparti biologiche. Ognuno di questi componenti รจ di solito codificato come una propria funzione.

Popolazione

La popolazione in un algoritmo genetico รจ un insieme di soluzioni candidate, spesso chiamate individui. Ogni individuo รจ rappresentato come un cromosoma, che puรฒ essere codificato come stringhe binarie, array o altre strutture dati.

Per esempio, ogni cromosoma potrebbe rappresentare un insieme di valori di input per una funzione che dobbiamo ottimizzare. Oppure ogni cromosoma potrebbe rappresentare un percorso per i camion delle consegne.

Funzione di fitness

La funzione di fitness valuta la capacitร  di ciascun individuo di risolvere il problema di nostro interesse. Assegna a ogni individuo un valore di fitness, con valori piรน alti per soluzioni migliori. La funzione di fitness guida l'algoritmo verso soluzioni migliori.

Ad esempio, nel nostro caso di ottimizzazione di una funzione matematica, la funzione di fitness potrebbe restituire il valore della funzione per gli input dati. Per il nostro esempio dei percorsi dei camion, la funzione di fitness potrebbe restituire la velocitร  di completamento delle consegne.

Funzione di selezione

La funzione di selezione sceglie gli individui della popolazione che si riprodurranno in base alla loro fitness. Gli individui con fitness piรน alta hanno maggiori probabilitร  di essere scelti per la riproduzione. Questo imita la selezione naturale, dove gli individui meglio adattati hanno piรน probabilitร  di sopravvivere e riprodursi.

Esistono diversi metodi di selezione. I metodi piรน comuni includono la selezione a ruota della roulette, la selezione a torneo e la selezione basata sul rango.

Nella selezione a ruota della roulette gli individui sono selezionati in base a una probabilitร  proporzionale alla loro fitness, in modo simile a far girare una roulette.

Nella selezione a torneo si sceglie casualmente un sottoinsieme di individui e si seleziona l'individuo con la fitness piรน alta in questo sottoinsieme.

La selezione basata sul rango classifica gli individui in base alla loro fitness. Poi le probabilitร  di selezione sono assegnate in base a questi ranghi invece che ai valori di fitness grezzi.

Ogni metodo ha i suoi vantaggi e andrebbe scelto in base alle esigenze specifiche del problema da affrontare.

Funzione di crossover

Il crossover combina le informazioni di due individui per creare discendenti. L'obiettivo รจ ereditare tratti vantaggiosi da entrambi i genitori. Le tecniche di crossover comuni includono il crossover a punto singolo, multi-punto, uniforme e blend.

Il crossover a punto singolo prevede la scelta di un punto casuale e lo scambio del materiale genetico di due genitori in quel punto per creare due figli. Il crossover multi-punto usa piรน punti per scambiare segmenti tra genitori, permettendo combinazioni genetiche piรน complesse.

Il crossover uniforme decide casualmente quale genitore contribuirร  con ciascun gene, fornendo un alto livello di diversitร  genetica. Il crossover blend genera discendenti mescolando i geni dei genitori usando un fattore di mescolamento casuale. La scelta della tecnica dovrebbe basarsi sulla complessitร  del problema e sul livello di diversitร  desiderato nella prole.

Funzione di mutazione

La mutazione introduce cambiamenti casuali nel materiale genetico dei discendenti. Questo aiuta a mantenere la diversitร  nella popolazione ed esplorare nuove aree dello spazio delle soluzioni. Le mutazioni possono essere semplici come invertire un bit in una stringa binaria o comportare cambiamenti piรน complessi a seconda dello schema di codifica.ย 

Il processo dell'algoritmo genetico

Un algoritmo genetico attraversa una serie di passaggi che imitano i processi evolutivi naturali per trovare soluzioni ottimali. Questi passaggi permettono alla popolazione di evolversi nel corso delle generazioni, migliorando la qualitร  delle soluzioni. Ecco una linea guida generale su come procede un algoritmo genetico:

Passo 1: Inizializzazione

Per prima cosa, generiamo una popolazione iniziale di individui casuali. Questo passaggio crea un insieme diversificato di possibili soluzioni per avviare l'algoritmo.ย 

Passo 2: Valutazione

Successivamente, dobbiamo calcolare la fitness di ciascun individuo nella popolazione. Qui usiamo la funzione di fitness per valutare quanto รจ buona ogni soluzione.

Passo 3: Selezione

Usando i criteri di selezione, selezioniamo gli individui per la riproduzione in base alla loro fitness. Questo passaggio determina chi saranno i genitori.

Passo 4: Crossover

Segue il crossover. Combinando il materiale genetico dei genitori selezionati, applichiamo tecniche di crossover per generare nuove soluzioni o discendenti.

Passo 5: Mutazione

Per mantenere la diversitร  nella nostra popolazione, dobbiamo introdurre mutazioni casuali nella prole.

Passo 6: Sostituzione

Poi sostituiamo parte o tutta la vecchia popolazione con la nuova prole, determinando quali individui passano alla generazione successiva.

Passo 7: Ripeti

I passaggi 2-6 precedenti vengono ripetuti per un certo numero di generazioni o fino al verificarsi di una condizione di terminazione. Questo ciclo consente alla popolazione di evolversi nel tempo, auspicabilmente portando a una buona soluzione.

Esempio in Python

Ora che abbiamo una buona comprensione di cosa sono gli algoritmi genetici e in generale come funzionano, costruiamo il nostro algoritmo genetico per risolvere un semplice problema di ottimizzazione.

L'equazione y=ax2+bx+c, quando tracciata, crea una parabola. Useremo un algoritmo genetico per trovare la combinazione di valori di a, b e c che risulti nella parabola piรน piatta. Ecco un'anteprima di ciรฒ che otterremo:

Grafico delle equazioni con soluzioni dall'esempio codificato.

Importare le librerie

All'inizio della nostra funzione, importeremo le librerie necessarie. Useremo 'random' per generare valori casuali per la nostra popolazione iniziale. Numpy รจ in generale utile quando si fa matematica, secondo me.ย 

E sono fermamente convinta che dovresti sempre fare grafici per assicurarti che il tuo codice stia facendo ciรฒ che pensi stia facendo. Quindi importeremo 'matplotlib.pyplot' per creare alcuni grafici.

Mi piace anche stampare i risultati importanti di ogni generazione (supponendo un numero relativamente piccolo di generazioni) per capire meglio cosa รจ successo durante la simulazione. Quindi importeremo 'PrettyTable' per questo.

import random
import matplotlib.pyplot as plt
import numpy as np
from prettytable import PrettyTable

Definire la funzione di fitness

Successivamente dobbiamo definire la funzione di fitness che valuterร  la fitness di ogni soluzione. In questo caso, vogliamo trovare la forma a U piรน piatta tracciata. Quindi ho calcolato il valore y al vertice e a x=-1 e x=1.ย 

Poi ho calcolato la curvatura del grafico come differenza nel valore di y in questi tre punti. Poichรฉ vogliamo ottenere la curva piรน piatta, ho negato la curvatura per completare la funzione di fitness.

# Define the fitness function (objective is to create the flattest U-shape)
def fitness_function(params):
    a, b, c = params
    if a <= 0:
        return -float('inf')  # Penalize downward facing u-shapes heavily
    vertex_x = -b / (2 * a) #x value at vertex
    vertex_y = a * (vertex_x ** 2) + b * vertex_x + c #y value at vertex
    y_left = a * (-1) ** 2 + b * (-1) + c #y-coordinate at x = -1
    y_right = a * (1) ** 2 + b * (1) + c #y-coordinate at x = 1
    curviness = abs(y_left - vertex_y) + abs(y_right - vertex_y)
    return -curviness  # Negate to minimize curviness

Creare la popolazione iniziale

Useremo la libreria 'random' per generare una popolazione di soluzioni casuali. Ogni individuo in questa popolazione รจ un insieme di valori per a, b e c.

# Create the initial population
def create_initial_population(size, lower_bound, upper_bound):
    population = []
    for _ in range(size):
        individual = (random.uniform(lower_bound, upper_bound),
                      random.uniform(lower_bound, upper_bound),
                      random.uniform(lower_bound, upper_bound))
        population.append(individual)
    return population

Creare una funzione di selezione

La funzione di selezione determinerร  quali individui si riproducono per creare la prossima popolazione. In questo esempio, useremo un processo di selezione a torneo, in cui si prende un sottoinsieme casuale di individui della popolazione e si selezionano per la riproduzione gli individui con la fitness piรน alta all'interno di quel sottoinsieme.

# Selection function using tournament selection
def selection(population, fitnesses, tournament_size=3):
    selected = []
    for _ in range(len(population)):
        tournament = random.sample(list(zip(population, fitnesses)), tournament_size)
        winner = max(tournament, key=lambda x: x[1])[0]
        selected.append(winner)
    return selected

Creare una funzione di crossover

Ora scriveremo una rapida funzione per usare il crossover e creare nuove soluzioni a partire da soluzioni esistenti.

# Crossover function
def crossover(parent1, parent2):
    alpha = random.random()
    child1 = tuple(alpha * p1 + (1 - alpha) * p2 for p1, p2 in zip(parent1, parent2))
    child2 = tuple(alpha * p2 + (1 - alpha) * p1 for p1, p2 in zip(parent1, parent2))
    return child1, child2

Progettare una funzione di mutazione

Abbiamo anche bisogno di una funzione di mutazione per introdurre casualitร  nelle generazioni successive. Questo รจ importante per garantire che in ogni generazione ci sia abbastanza diversitร  da cui selezionare.

# Mutation function
def mutation(individual, mutation_rate, lower_bound, upper_bound):
    individual = list(individual)
    for i in range(len(individual)):
        if random.random() < mutation_rate:
            mutation_amount = random.uniform(-1, 1)
            individual[i] += mutation_amount
            # Ensure the individual stays within bounds
            individual[i] = max(min(individual[i], upper_bound), lower_bound)
    return tuple(individual)

Ciclo principale

Ora dobbiamo creare il ciclo principale che metterร  insieme tutti questi pezzi per eseguire il nostro algoritmo. Poichรฉ dobbiamo anche tracciare i risultati, aggiungerรฒ in questa sezione il codice per i grafici, cosรฌ come il codice necessario per creare la nostra tabella finale.

# Main genetic algorithm function
def genetic_algorithm(population_size, lower_bound, upper_bound, generations, mutation_rate):
    population = create_initial_population(population_size, lower_bound, upper_bound)
    
    # Prepare for plotting
    fig, axs = plt.subplots(3, 1, figsize=(12, 18))  # 3 rows, 1 column for subplots
    best_performers = []
    all_populations = []

    # Prepare for table
    table = PrettyTable()
    table.field_names = ["Generation", "a", "b", "c", "Fitness"]

    for generation in range(generations):
        fitnesses = [fitness_function(ind) for ind in population]

        # Store the best performer of the current generation
        best_individual = max(population, key=fitness_function)
        best_fitness = fitness_function(best_individual)
        best_performers.append((best_individual, best_fitness))
        all_populations.append(population[:])
        table.add_row([generation + 1, best_individual[0], best_individual[1], best_individual[2], best_fitness])

        population = selection(population, fitnesses)

        next_population = []
        for i in range(0, len(population), 2):
            parent1 = population[i]
            parent2 = population[i + 1]

            child1, child2 = crossover(parent1, parent2)

            next_population.append(mutation(child1, mutation_rate, lower_bound, upper_bound))
            next_population.append(mutation(child2, mutation_rate, lower_bound, upper_bound))

        # Replace the old population with the new one, preserving the best individual
        next_population[0] = best_individual
        population = next_population

    # Print the table
    print(table)

    # Plot the population of one generation (last generation)
    final_population = all_populations[-1]
    final_fitnesses = [fitness_function(ind) for ind in final_population]

    axs[0].scatter(range(len(final_population)), [ind[0] for ind in final_population], color='blue', label='a')
    axs[0].scatter([final_population.index(best_individual)], [best_individual[0]], color='cyan', s=100, label='Best Individual a')
    axs[0].set_ylabel('a', color='blue')
    axs[0].legend(loc='upper left')
    
    axs[1].scatter(range(len(final_population)), [ind[1] for ind in final_population], color='green', label='b')
    axs[1].scatter([final_population.index(best_individual)], [best_individual[1]], color='magenta', s=100, label='Best Individual b')
    axs[1].set_ylabel('b', color='green')
    axs[1].legend(loc='upper left')
    
    axs[2].scatter(range(len(final_population)), [ind[2] for ind in final_population], color='red', label='c')
    axs[2].scatter([final_population.index(best_individual)], [best_individual[2]], color='yellow', s=100, label='Best Individual c')
    axs[2].set_ylabel('c', color='red')
    axs[2].set_xlabel('Individual Index')
    axs[2].legend(loc='upper left')
    
    axs[0].set_title(f'Final Generation ({generations}) Population Solutions')

    # Plot the values of a, b, and c over generations
    generations_list = range(1, len(best_performers) + 1)
    a_values = [ind[0][0] for ind in best_performers]
    b_values = [ind[0][1] for ind in best_performers]
    c_values = [ind[0][2] for ind in best_performers]
    fig, ax = plt.subplots()
    ax.plot(generations_list, a_values, label='a', color='blue')
    ax.plot(generations_list, b_values, label='b', color='green')
    ax.plot(generations_list, c_values, label='c', color='red')
    ax.set_xlabel('Generation')
    ax.set_ylabel('Parameter Values')
    ax.set_title('Parameter Values Over Generations')
    ax.legend()

    # Plot the fitness values over generations
    best_fitness_values = [fit[1] for fit in best_performers]
    min_fitness_values = [min([fitness_function(ind) for ind in population]) for population in all_populations]
    max_fitness_values = [max([fitness_function(ind) for ind in population]) for population in all_populations]
    fig, ax = plt.subplots()
    ax.plot(generations_list, best_fitness_values, label='Best Fitness', color='black')
    ax.fill_between(generations_list, min_fitness_values, max_fitness_values, color='gray', alpha=0.5, label='Fitness Range')
    ax.set_xlabel('Generation')
    ax.set_ylabel('Fitness')
    ax.set_title('Fitness Over Generations')
    ax.legend()

    # Plot the quadratic function for each generation
    fig, ax = plt.subplots()
    colors = plt.cm.viridis(np.linspace(0, 1, generations))
    for i, (best_ind, best_fit) in enumerate(best_performers):
        color = colors[i]
        a, b, c = best_ind
        x_range = np.linspace(lower_bound, upper_bound, 400)
        y_values = a * (x_range ** 2) + b * x_range + c
        ax.plot(x_range, y_values, color=color)

    ax.set_xlabel('x')
    ax.set_ylabel('y')
    ax.set_title('Quadratic Function')

    # Create a subplot for the colorbar
    cax = fig.add_axes([0.92, 0.2, 0.02, 0.6])  # [left, bottom, width, height]
    norm = plt.cm.colors.Normalize(vmin=0, vmax=generations)
    sm = plt.cm.ScalarMappable(cmap='viridis', norm=norm)
    sm.set_array([])
    fig.colorbar(sm, ax=ax, cax=cax, orientation='vertical', label='Generation')

    plt.show()

    return max(population, key=fitness_function)

Mettere tutto in esecuzione

Infine, dobbiamo impostare i parametri per il nostro algoritmo ed eseguirlo.

# Parameters for the genetic algorithm
population_size = 100
lower_bound = -50
upper_bound = 50
generations = 20
mutation_rate = 1

# Run the genetic algorithm
best_solution = genetic_algorithm(population_size, lower_bound, upper_bound, generations, mutation_rate)
print(f"Best solution found: a = {best_solution[0]}, b = {best_solution[1]}, c = {best_solution[2]}")

Condizione di terminazione (opzionale)

In questo esempio, abbiamo impostato un numero specifico di generazioni per l'esecuzione del nostro algoritmo. Tuttavia, avremmo potuto in alternativa impostare una condizione di terminazione che interrompesse il programma al raggiungimento di un valore di fitness target. Potrebbe essere qualcosa del genere:

def termination_condition(fitnesses, target_fitness):
    return max(fitnesses) >= target_fitness

Mettere tutto insieme

Ecco il codice completo tutto insieme.

import random
import matplotlib.pyplot as plt
import numpy as np
from prettytable import PrettyTable

# Define the fitness function (objective is to create the flattest U-shape)
def fitness_function(params):
    a, b, c = params
    if a <= 0:
        return -float('inf')  # Penalize downward facing u-shapes heavily
    vertex_x = -b / (2 * a) #x value at vertex
    vertex_y = a * (vertex_x ** 2) + b * vertex_x + c #y value at vertex
    y_left = a * (-1) ** 2 + b * (-1) + c #y-coordinate at x = -1
    y_right = a * (1) ** 2 + b * (1) + c #y-coordinate at x = 1
    curviness = abs(y_left - vertex_y) + abs(y_right - vertex_y)
    return -curviness  # Negate to minimize curviness

# Create the initial population
def create_initial_population(size, lower_bound, upper_bound):
    population = []
    for _ in range(size):
        individual = (random.uniform(lower_bound, upper_bound),
                      random.uniform(lower_bound, upper_bound),
                      random.uniform(lower_bound, upper_bound))
        population.append(individual)
    return population

# Selection function using tournament selection
def selection(population, fitnesses, tournament_size=3):
    selected = []
    for _ in range(len(population)):
        tournament = random.sample(list(zip(population, fitnesses)), tournament_size)
        winner = max(tournament, key=lambda x: x[1])[0]
        selected.append(winner)
    return selected

# Crossover function
def crossover(parent1, parent2):
    alpha = random.random()
    child1 = tuple(alpha * p1 + (1 - alpha) * p2 for p1, p2 in zip(parent1, parent2))
    child2 = tuple(alpha * p2 + (1 - alpha) * p1 for p1, p2 in zip(parent1, parent2))
    return child1, child2

# Mutation function
def mutation(individual, mutation_rate, lower_bound, upper_bound):
    individual = list(individual)
    for i in range(len(individual)):
        if random.random() < mutation_rate:
            mutation_amount = random.uniform(-1, 1)
            individual[i] += mutation_amount
            # Ensure the individual stays within bounds
            individual[i] = max(min(individual[i], upper_bound), lower_bound)
    return tuple(individual)

# Main genetic algorithm function
def genetic_algorithm(population_size, lower_bound, upper_bound, generations, mutation_rate):
    population = create_initial_population(population_size, lower_bound, upper_bound)
    
    # Prepare for plotting
    fig, axs = plt.subplots(3, 1, figsize=(12, 18))  # 3 rows, 1 column for subplots
    best_performers = []
    all_populations = []

    # Prepare for table
    table = PrettyTable()
    table.field_names = ["Generation", "a", "b", "c", "Fitness"]

    for generation in range(generations):
        fitnesses = [fitness_function(ind) for ind in population]

        # Store the best performer of the current generation
        best_individual = max(population, key=fitness_function)
        best_fitness = fitness_function(best_individual)
        best_performers.append((best_individual, best_fitness))
        all_populations.append(population[:])
        table.add_row([generation + 1, best_individual[0], best_individual[1], best_individual[2], best_fitness])

        population = selection(population, fitnesses)

        next_population = []
        for i in range(0, len(population), 2):
            parent1 = population[i]
            parent2 = population[i + 1]

            child1, child2 = crossover(parent1, parent2)

            next_population.append(mutation(child1, mutation_rate, lower_bound, upper_bound))
            next_population.append(mutation(child2, mutation_rate, lower_bound, upper_bound))

        # Replace the old population with the new one, preserving the best individual
        next_population[0] = best_individual
        population = next_population

    # Print the table
    print(table)

    # Plot the population of one generation (last generation)
    final_population = all_populations[-1]
    final_fitnesses = [fitness_function(ind) for ind in final_population]

    axs[0].scatter(range(len(final_population)), [ind[0] for ind in final_population], color='blue', label='a')
    axs[0].scatter([final_population.index(best_individual)], [best_individual[0]], color='cyan', s=100, label='Best Individual a')
    axs[0].set_ylabel('a', color='blue')
    axs[0].legend(loc='upper left')
    
    axs[1].scatter(range(len(final_population)), [ind[1] for ind in final_population], color='green', label='b')
    axs[1].scatter([final_population.index(best_individual)], [best_individual[1]], color='magenta', s=100, label='Best Individual b')
    axs[1].set_ylabel('b', color='green')
    axs[1].legend(loc='upper left')
    
    axs[2].scatter(range(len(final_population)), [ind[2] for ind in final_population], color='red', label='c')
    axs[2].scatter([final_population.index(best_individual)], [best_individual[2]], color='yellow', s=100, label='Best Individual c')
    axs[2].set_ylabel('c', color='red')
    axs[2].set_xlabel('Individual Index')
    axs[2].legend(loc='upper left')
    
    axs[0].set_title(f'Final Generation ({generations}) Population Solutions')

    # Plot the values of a, b, and c over generations
    generations_list = range(1, len(best_performers) + 1)
    a_values = [ind[0][0] for ind in best_performers]
    b_values = [ind[0][1] for ind in best_performers]
    c_values = [ind[0][2] for ind in best_performers]
    fig, ax = plt.subplots()
    ax.plot(generations_list, a_values, label='a', color='blue')
    ax.plot(generations_list, b_values, label='b', color='green')
    ax.plot(generations_list, c_values, label='c', color='red')
    ax.set_xlabel('Generation')
    ax.set_ylabel('Parameter Values')
    ax.set_title('Parameter Values Over Generations')
    ax.legend()

    # Plot the fitness values over generations
    best_fitness_values = [fit[1] for fit in best_performers]
    min_fitness_values = [min([fitness_function(ind) for ind in population]) for population in all_populations]
    max_fitness_values = [max([fitness_function(ind) for ind in population]) for population in all_populations]
    fig, ax = plt.subplots()
    ax.plot(generations_list, best_fitness_values, label='Best Fitness', color='black')
    ax.fill_between(generations_list, min_fitness_values, max_fitness_values, color='gray', alpha=0.5, label='Fitness Range')
    ax.set_xlabel('Generation')
    ax.set_ylabel('Fitness')
    ax.set_title('Fitness Over Generations')
    ax.legend()

    # Plot the quadratic function for each generation
    fig, ax = plt.subplots()
    colors = plt.cm.viridis(np.linspace(0, 1, generations))
    for i, (best_ind, best_fit) in enumerate(best_performers):
        color = colors[i]
        a, b, c = best_ind
        x_range = np.linspace(lower_bound, upper_bound, 400)
        y_values = a * (x_range ** 2) + b * x_range + c
        ax.plot(x_range, y_values, color=color)

    ax.set_xlabel('x')
    ax.set_ylabel('y')
    ax.set_title('Quadratic Function')

    # Create a subplot for the colorbar
    cax = fig.add_axes([0.92, 0.2, 0.02, 0.6])  # [left, bottom, width, height]
    norm = plt.cm.colors.Normalize(vmin=0, vmax=generations)
    sm = plt.cm.ScalarMappable(cmap='viridis', norm=norm)
    sm.set_array([])
    fig.colorbar(sm, ax=ax, cax=cax, orientation='vertical', label='Generation')

    plt.show()

    return max(population, key=fitness_function)

# Parameters for the genetic algorithm
population_size = 100
lower_bound = -50
upper_bound = 50
generations = 20
mutation_rate = 1

# Run the genetic algorithm
best_solution = genetic_algorithm(population_size, lower_bound, upper_bound, generations, mutation_rate)
print(f"Best solution found: a = {best_solution[0]}, b = {best_solution[1]}, c = {best_solution[2]}")

Valutazione dei risultati

Usiamo i nostri output per vedere come si รจ comportata la nostra funzione.

Tabella con output grezzi dall'esempio codificato.

Per prima cosa, dalla tabella vediamo che abbiamo completato 20 generazioni e la fitness sembra essere aumentata da un valore relativamente negativo a uno meno negativo a ogni generazione. Ottimo!

Grafico della fitness sulle generazioni dall'esempio codificato.

Dal nostro grafico della fitness, possiamo confermare il sospetto che la fitness รจ migliorata nel tempo e sembra essersi appiattita un po' nelle generazioni successive. Ma possiamo anche vedere che in alcune delle prime generazioni ci sono state soluzioni davvero pessime che il nostro modello ha giustamente scartato.

Possiamo anche notare che in diverse generazioni non c'era molta diversitร  nell'intervallo di fitness. Anche se non รจ necessariamente un male, รจ qualcosa da tenere d'occhio. In alcuni casi, questo puรฒ essere un indicatore di convergenza prematura, un problema che si verifica quando non c'รจ abbastanza diversitร  nella popolazione.

Successivamente, tracciamo le nostre equazioni di ogni generazione per vedere se le abbiamo davvero appiattite.

Grafico delle equazioni con soluzioni dall'esempio codificato.

Possiamo vedere che le soluzioni delle prime generazioni sono molto curve, il che ha senso. Questa equazione produce un grafico a forma di U. Ma possiamo anche vedere che le generazioni successive sono diventate via via piรน piatte. รˆ esattamente ciรฒ che stavamo cercando!

Vale la pena notare che stiamo esplicitamente guardando solo entro i nostri limiti dichiarati di -50 - 50. Se ingrandissimo questo grafico, mostrerebbe che anche queste linee apparentemente piatte sono ancora forme a U, solo con basi piรน ampie.

Questo esempio evidenzia l'importanza di essere cauti con le nostre assunzioni nelle analisi per assicurarci che supportino le nostre previsioni. Il nostro modello ha fatto esattamente ciรฒ che volevamo, ma solo entro i limiti che gli abbiamo dato. Se volessimo trovare una combinazione di variabili che crei una forma piatta piรน lontano, dovremmo rieseguire il modello con limiti piรน ampi.

Grafico di come ciascun parametro รจ cambiato a ogni generazione nell'esempio codificato.

Possiamo tracciare ciascuna delle variabili per vedere quanto sono cambiate attraverso le generazioni. Possiamo vedere che in questa esecuzione della simulazione, c รจ variata moltissimo all'inizio mentre a e b sono cambiati molto meno. Questo grafico avrร  un aspetto diverso ogni volta che eseguiamo questa simulazione, anche se non cambiamo alcun parametro, perchรฉ la nostra popolazione iniziale รจ casuale.

Tre sottografi che mostrano l'intera popolazione di individui nell'ultima generazione dell'esempio codificato.

L'ultimo grafico che voglio guardare รจ solo un'istantanea dell'intera popolazione in una generazione: l'ultima generazione. Poichรฉ ogni individuo nella popolazione ha tre parametri, ho deciso di separarli in tre sottografi invece di mettermi a trafficare con grafici tridimensionali.ย 

Quello che voglio davvero vedere da questi grafici รจ se c'รจ una buona diversitร  all'interno di questa generazione. Senza una buona quantitร  di diversitร , potremmo finire in una situazione in cui le uniche soluzioni disponibili sono quelle con valori di fitness bassi, con conseguente convergenza prematura. Ma sembra che questi grafici mostrino una buona quantitร  di diversitร , quindi sono soddisfatta di questo risultato.

Considerazioni e consigli

Gli algoritmi genetici possono essere usati in un'ampia varietร  di situazioni. Ma ci sono alcune considerazioni da tenere a mente.

Tuning dei parametri

Come per qualsiasi modello, le prestazioni di un algoritmo genetico dipendono da vari parametri, in particolare dimensione della popolazione, tasso di crossover, tasso di mutazione e parametri di delimitazione. Cambiare questi parametri cambierร  le prestazioni del tuo modello.

Come regola generale, dimensioni maggiori della popolazione ti aiuteranno a trovare piรน rapidamente la soluzione ottimale perchรฉ ci sono piรน opzioni tra cui scegliere. Tuttavia, popolazioni piรน grandi richiedono anche piรน tempo e risorse per l'esecuzione.

Un tasso di crossover piรน alto puรฒ portare a una convergenza piรน rapida combinando piรน frequentemente tratti vantaggiosi di individui diversi. Tuttavia, un tasso di crossover troppo alto potrebbe disturbare la struttura della popolazione, portando a convergenza prematura.

Un tasso di mutazione piรน alto aiuta a mantenere la diversitร  genetica, impedendo all'algoritmo di bloccarsi in un ottimo locale. Tuttavia, se il tasso di mutazione รจ troppo alto, puรฒ disturbare il processo di convergenza introducendo troppa casualitร , rendendo difficile per l'algoritmo affinare le soluzioni.

I parametri di delimitazione definiscono l'intervallo entro cui l'algoritmo cerca le soluzioni. รˆ importante sintonizzarli sul tuo specifico problema di business. Un'area di delimitazione troppo stretta puรฒ far perdere soluzioni ottimali al tuo problema. Un'area di delimitazione troppo ampia richiederร  piรน tempo e risorse per l'esecuzione. Ma ci sono anche altre considerazioni.ย 

Per esempio, nel nostro esempio codificato sopra, quell'equazione teoricamente non ha limiti. Ma praticamente, non possiamo chiedere al computer di trovare l'arco piรน piatto in un grafico infinitamente grande. Quindi, i confini sono necessari. Ma cambiare quei confini cambierร  anche la risposta ottimale. A mio avviso, stabilire confini appropriati per il tuo caso d'uso specifico รจ fondamentale con qualsiasi modello.

Potrebbero esserci altri parametri importanti da sintonizzare nel tuo modello specifico. Sperimenta con valori diversi per trovare le impostazioni migliori per il tuo problema. Per imparare a fare tuning dei GA nello specifico, dai un'occhiata a Metodi informati: algoritmi genetici.

Encoding

L'encoding รจ un passaggio importante nell'impostazione di un algoritmo genetico. Comporta la conversione delle soluzioni potenziali in un formato che l'algoritmo puรฒ elaborare. Per esempio, supponiamo di progettare un algoritmo genetico per ottimizzare i percorsi di consegna dei nostri camion. Come trasformi un elenco di luoghi di consegna in un cromosoma da inserire in un GA? L'encoding trasforma questo elenco in una sequenza che l'algoritmo puรฒ manipolare, come un elenco ordinato di luoghi che rappresenta un possibile percorso di consegna.

Un encoding appropriato garantisce che l'algoritmo genetico possa esplorare efficacemente lo spazio delle soluzioni e combinare o mutare le soluzioni in modi significativi. Senza di esso, l'algoritmo non sarebbe in grado di gestire dati complessi. Dati come luoghi di consegna, problemi di pianificazione o interazioni con i clienti sono resi accessibili ai computer solo tramite encoding.ย 

Esistono diversi schemi di encoding tra cui scegliere. Puoi saperne di piรน per decidere quale potrebbe essere il migliore per la tua situazione in Lavorare con dati categorici in Python o Come convertire stringhe in byte in Python.

Convergenza prematura

La convergenza prematura รจ un problema che si verifica quando la popolazione diventa troppo simile. Questo puรฒ portare l'algoritmo a bloccarsi in un ottimo locale invece di trovare l'ottimo globale. In sostanza, se non c'รจ abbastanza diversitร  nella popolazione, le soluzioni diventano โ€œconsanguineeโ€ e non ottieni la soluzione migliore.

Ci sono alcune tecniche che puoi provare per evitare la convergenza prematura. Un tasso di mutazione piรน alto puรฒ introdurre piรน diversitร  nella popolazione, aiutando a esplorare nuove aree dello spazio delle soluzioni. Puoi anche provare a partire con una popolazione iniziale piรน diversificata per garantire un'ampia esplorazione dello spazio delle soluzioni fin dall'inizio.

Se hai ancora a che fare con la convergenza prematura, puoi provare ad adattare dinamicamente parametri come tasso di mutazione e tasso di crossover in base all'avanzamento dell'algoritmo. In alternativa, puoi usare piรน popolazioni (pensale come isole) che occasionalmente si scambiano individui per mantenere la diversitร .

Puoi saperne di piรน sulla convergenza prematura e su altri passi falsi nel machine learning nel corso Monitoring Machine Learning Concepts.

Elitismo

D'altra parte, un crossover e una mutazione forti o qualsiasi casualitร  nella selezione possono far sรฌ che ottime soluzioni non finiscano per riprodursi. รˆ qui che puรฒ entrare in gioco l'elitismo. L'elitismo รจ una tecnica in cui i migliori individui vengono trasferiti direttamente alla generazione successiva per preservare le buone soluzioni. Questo aiuta a garantire che le migliori soluzioni non vadano perse durante il processo evolutivo.

Tuttavia, fai attenzione se usi l'elitismo. Se l'elitismo รจ usato senza una diversitร  della popolazione sufficientemente alta, potresti finire invece con una convergenza prematura. Se il tuo problema di business richiede tecniche di elitismo, assicurati di abbinarle a funzioni di crossover e mutazione abbastanza forti e a una popolazione numerosa. Questo approccio aiuta a mantenere un sano equilibrio tra lo sfruttamento delle soluzioni note e l'esplorazione di nuove possibilitร .

Conclusione

Gli algoritmi genetici sono un ottimo esempio di data science che trae ispirazione dal mondo naturale. Offrono un metodo potente per risolvere problemi complessi di ottimizzazione imitando il processo di selezione naturale.

Se ti interessano altri modelli ispirati alla biologia, scopri le reti neurali e altre tecniche di deep learning in Introduzione al deep learning in Python. Potrebbero anche interessarti il corso sui concetti di AI generativa di DataCamp e AI Ethics.

Se sei ispiratษ™ a fare un progetto con dati biologici, dai un'occhiata a Analizzare dati genomici in R o Analisi di immagini biomediche in Python.

Amberle McKee

Autore

Amberle McKee

Sono un PhD con 13 anni di esperienza nel lavoro con i dati in un contesto di ricerca biologica. Creo software in diversi linguaggi di programmazione, tra cui Python, MATLAB e R. Sono appassionato di condividere con il mondo il mio amore per lโ€™apprendimento.

Argomenti

Correlato