Ero al mare e guardavo un amico che aveva portato i suoi cani a fare il bagno e li vedevo lanciarsi in acqua e nuotare verso la pallina lanciata dal padrone, mi sono chiesto: “ma chi ha insegnato loro a nuotare?”
La risposta è l’istinto naturale, ma l’istinto è qualcosa che non si impara, è qualcosa che è scritto in “hard coding” nel nostro DNA, insomma son cose che sappiamo fare senza esser stati addestrati da esperienze accadute nella nostra vita.
Quindi ho pensato all’intelligenza artificiale, dove tutto è frutto di addestramento, ma si potrebbe simulare un qualcosa di simile all’istinto da inglobare in un’AI che simula un cane?
Se si dovesse costruire un cane robotico, lo si addestrerebbe a fare il cane, quindi a riportare la palla, ad obbedire agli ordini del padrone, ad abbaiare in caso di pericolo, ecc. ecc.. Tutte cose che possono essere insegnate col Deep Learning.
Ma le azioni “naturali”? Nuotare, camminare ed altro possono essere insegnate, ma in una simulazione reale, il cane robotico dovrebbe saperle fare “istintivamente”, quindi con una conoscenza insita nella sua stessa natura.
Sarebbe possibile fornire questo pre-addestramento all’AI del cane robotico? Una cultura che deriva dall’evoluzione?
La differenza tra un’AI addestrata e un’AI con istinto
La differenza tra un’AI addestrata e un’AI con istinto si basa principalmente sul concetto di apprendimento vs. comportamento innato.
AI addestrata
- Definizione: un’AI addestrata impara dall’esperienza attraverso grandi quantità di dati. Il suo comportamento è il risultato di un processo di apprendimento supervisionato, non supervisionato o per rinforzo.
- Caratteristiche: richiede un ciclo di apprendimento continuo basato su feedback. L’AI è altamente adattabile ma non ha conoscenza iniziale predefinita, parte da un “foglio bianco”.
- Esempi: reti neurali che apprendono a riconoscere immagini, agenti che imparano a giocare a giochi complessi.
AI con istinto:
- Definizione: un’AI con istinto simula comportamenti “innati”, ovvero non appresi, ma integrati nel sistema sin dall’inizio. Questi comportamenti di base permettono all’AI di affrontare situazioni iniziali senza dati di addestramento specifici.
- Caratteristiche: l’istinto implica che l’AI abbia una serie di regole o modelli predefiniti che guidano il suo comportamento in modo automatico e senza bisogno di addestramento, al fine di simulare l’istinto ci può essere una sorta di pre-addestramento o comportamento “evolutivo” incorporato, basato su principi generali.
- Esempi: algoritmi genetici che evolvono comportamenti di base, come quelli simulati nel nuoto del cane, possono essere visti come AI che sviluppano una forma di “istinto”.
In sintesi, mentre un’AI addestrata apprende dall’esperienza, un’AI con istinto è progettata per avere una serie di comportamenti iniziali, che possono evolvere o essere raffinati, ma che non richiedono apprendimento dall’inizio.
Simulare l’istinto tramite un algoritmo di intelligenza artificiale è una sfida complessa, però possiamo immaginare alcuni approcci teorici che potrebbero avvicinarsi a questo concetto.
Programmazione di bias innati (euristica)
Uno degli approcci più semplici sarebbe programmare degli “istinti” o comportamenti predefiniti direttamente nell’algoritmo, in questo caso, l’IA non avrebbe bisogno di apprendere da esperienze o dati per eseguire determinate azioni.
Ad esempio:
- Riconoscimento di modelli semplici: un sistema di IA potrebbe essere programmato per reagire a determinati stimoli o condizioni (come evitare pericoli o cercare risorse) senza averle mai apprese, ma basandosi su euristiche codificate.
- Regole evolutive: simili agli istinti di sopravvivenza degli animali, l’IA potrebbe essere pre-programmata con una serie di regole basilari, come evitare situazioni rischiose o massimizzare la ricerca di risorse.
Evoluzione simulata (algoritmi genetici)
Un altro modo per simulare l’istinto in un’IA è tramite l’evoluzione simulata, grazie all’utilizzo degli algoritmi genetici o evolutivi, sono tecniche in cui un sistema cerca soluzioni ottimali a problemi complessi tramite un processo di selezione naturale simulato. Ecco come potrebbe funzionare:
- Si parte da genitori casuali con un patrimonio genetico casuale, essi genereranno figli che, tramite cross-over e mutazioni, giungeranno a produrre l’oggetto target.
- Dopo numerose generazioni, emergono comportamenti ottimali che potrebbero essere paragonati a una forma di “istinto” artificiale, poiché non derivano direttamente dall’apprendimento ma dalla selezione di pattern di comportamento che risultano vincenti.
In questo contesto, l’IA potrebbe sviluppare comportamenti che non ha mai appreso in senso classico, ma che emergono dalla simulazione di un processo evolutivo.
Reti neurali con regole basali o modelli iniziali pre-addestrati
Le reti neurali spesso richiedono un lungo processo di addestramento basato su grandi quantità di dati, ma si potrebbe cercare di simulare l’istinto fornendo all’IA delle connessioni pre-configurate o modelli pre-addestrati.
Questo potrebbe avvenire in due modi:
- Modelli innati: configurare la rete neurale con pattern iniziali che riflettano determinate regole di base, come il riconoscimento di pericoli o la ricerca di cibo. Questo comportamento sarebbe “innato” nel sistema, perché impostato in modo che l’algoritmo lo realizzi senza apprenderlo esplicitamente.
- Reti neurali ricorrenti: le reti neurali ricorrenti (RNN) e altre forme di modelli a memoria possono essere utilizzate per simulare reazioni a input nuovi e non previsti, basandosi su esperienze precedenti o su configurazioni di base non apprese.
Un modello può essere predisposto con delle “reazioni preconfigurate” a determinati stimoli, che appaiono come istintive.
Ambienti simulati e reinforcement learning con funzioni di ricompensa
Il reinforcement learning (apprendimento per rinforzo) è un metodo in cui un agente impara a prendere decisioni esplorando un ambiente e ricevendo ricompense o punizioni. Tuttavia, se si limitano le regole e le funzioni di ricompensa a principi molto semplici (come la sopravvivenza, la ricerca di risorse o l’evitare pericoli), l’agente potrebbe sviluppare comportamenti “istintivi”.
Questi comportamenti potrebbero sembrare simili all’istinto perché non derivano da esperienze specifiche apprese in modo convenzionale, ma piuttosto dalla continua ottimizzazione di azioni basate su regole semplici e ricompense immediate.
Integrazione di sensori e risposte immediate
Un’ulteriore strategia potrebbe essere quella di collegare un algoritmo a una serie di sensori fisici che generano input immediati dall’ambiente.
Ad esempio, un robot con sensori di temperatura, pressione o rilevamento di oggetti potrebbe rispondere immediatamente a certi stimoli in modo automatico, senza aver bisogno di apprendere come fare, questo simulerebbe un comportamento istintivo, in quanto la risposta è una reazione automatica all’input sensoriale, simile a come molti organismi rispondono agli stimoli esterni senza riflettere.
L’esperimento
In questo esperimento userò gli algoritmi genetici per simulare l’evoluzione biologica che porta alla realizzazione dell’istinto, quindi quanto di più vicino alla Natura, rispetto ad altre soluzioni magari anche più performanti.
Scriviamo il programma
# -*- coding: utf-8 -*-
“””
Created on Tue Sep 10 16:39:30 2024
@author: nannib
“””
import numpy as np
import matplotlib.pyplot as plt
import random
import json
# Definisci la funzione di fitness basata su distanza + coordinazione
def swimming_efficiency(leg_movements):
“””
Il fitness è determinato da quanta distanza in direzione x
il quadrupede può percorrere in base ai movimenti delle sue gambe
e quanto sono coordinati i movimenti.
“””
num_legs = 4
body_center = np.array([0, 0]) # Centro del corpo del quadrupede all’origine
leg_positions = np.zeros((num_legs, 2)) # Memorizza le posizioni finali (x, y) di ogni gamba
# Punti di partenza fissi per ogni gamba (punti di attacco sul corpo)
start_positions = [
np.array([-1, 1]), # Gamba anteriore sinistra
np.array([1, 1]), # Gamba anteriore destra
np.array([-1, -1]), # Gamba posteriore sinistra
np.array([1, -1]) # Gamba posteriore destra
]
distances = np.zeros(num_legs) # Distanza che ogni gamba si sposta in direzione x
total_distance = 0
coordination_score = 0
# Calcola le posizioni finali delle gambe in base all’ampiezza (lunghezza) e all’angolo
for i in range(num_legs):
length, angle = leg_movements[i]
leg_positions[i] = start_positions[i] + np.array([length * np.cos(angle), length * np.sin(angle)])
# Calcola la distanza percorsa da ogni gamba in direzione x
distances[i] = leg_positions[i][0] – start_positions[i][0]
# Calcola la coordinazione (ricompensa i movimenti alternati: simmetria sinistra-destra)
coordination_score = np.abs(distances[0] – distances[1]) + np.abs(distances[2] – distances[3])
# Distanza totale percorsa in avanti
total_distance = np.sum(distances)
# Combina distanza e coordinazione in un punteggio di fitness
fitness = total_distance – 0.5 * coordination_score # Penalizziamo la scarsa coordinazione
return fitness
# Genera una popolazione iniziale di movimenti delle gambe casuali
def initialize_population(pop_size):
population = []
for _ in range(pop_size):
individual = []
for _ in range(4): # Quattro gambe
length = random.uniform(0.5, 2.0) # Lunghezza tra 0.5 e 2.0
angle = random.uniform(0, 2 * np.pi) # Angolo tra 0 e 2*pi
individual.append([length, angle])
population.append(individual)
return population
# Funzione di selezione (roulette wheel)
def select_parents(population, fitnesses):
# Assicurati che tutti i valori di fitness siano non negativi aggiungendo una costante se necessario
min_fitness = min(fitnesses)
if min_fitness < 0:
fitnesses = [f – min_fitness for f in fitnesses] # Sposta tutti i fitness per renderli non negativi
total_fitness = sum(fitnesses)
if total_fitness == 0:
# Se tutti i fitness sono zero, seleziona i genitori casualmente
selection_probs = [1 / len(fitnesses)] * len(fitnesses)
else:
# Normalizza i valori di fitness per creare le probabilità di selezione
selection_probs = [f / total_fitness for f in fitnesses]
# Scegli due genitori in base alle probabilità di selezione
parent_indices = np.random.choice(len(population), size=2, p=selection_probs)
parent1, parent2 = population[parent_indices[0]], population[parent_indices[1]]
return parent1, parent2
# Crossover (crossover blend tra due genitori)
def crossover(parent1, parent2):
child = []
alpha = random.uniform(0, 1)
for leg in range(4):
leg_movement = [
alpha * parent1[leg][0] + (1 – alpha) * parent2[leg][0], # Lunghezza
alpha * parent1[leg][1] + (1 – alpha) * parent2[leg][1] # Angolo
]
child.append(leg_movement)
return child
# Mutazione (perturba casualmente il movimento di una gamba)
def mutate(individual, mutation_rate=0.1):
for leg in range(4):
if random.random() < mutation_rate:
individual[leg][0] += random.uniform(-0.1, 0.1) # Mutazione sulla lunghezza
individual[leg][1] += random.uniform(-0.1, 0.1) # Mutazione sull’angolo
return individual
# Salva la migliore soluzione in un file
def save_solution(solution, filename=”best_swimming_solution.json”):
with open(filename, “w”) as f:
json.dump(solution, f)
# Traccia i movimenti delle gambe come segmenti
def plot_leg_movements(leg_movements, generation):
# Punti di partenza fissi per ogni gamba
start_positions = [
np.array([-1, 1]), # Gamba anteriore sinistra
np.array([1, 1]), # Gamba anteriore destra
np.array([-1, -1]), # Gamba posteriore sinistra
np.array([1, -1]) # Gamba posteriore destra
]
plt.figure()
# Traccia il corpo come un punto all’origine
plt.plot(0, 0, ‘ro’, label=’Centro del corpo’)
# Traccia ogni gamba come un segmento
for i, (length, angle) in enumerate(leg_movements):
start = start_positions[i]
end = start + np.array([length * np.cos(angle), length * np.sin(angle)])
plt.plot([start[0], end[0]], [start[1], end[1]], label=f’Gamba {i+1}’)
plt.title(f’Movimenti delle Gambe (Segmenti) – Generazione {generation}’)
plt.xlabel(‘X’)
plt.ylabel(‘Y’)
plt.xlim(-3, 3)
plt.ylim(-3, 3)
plt.axhline(0, color=’gray’, linestyle=’–‘)
plt.axvline(0, color=’gray’, linestyle=’–‘)
plt.legend()
plt.grid(True)
plt.show()
# Algoritmo genetico
def genetic_algorithm(pop_size=50, generations=100, mutation_rate=0.1, fitness_threshold=10.0):
population = initialize_population(pop_size)
best_individual = None
best_fitness = float(‘-inf’) # Inizializziamo la migliore fitness al valore minimo possibile
for generation in range(generations):
fitnesses = [swimming_efficiency(individual) for individual in population]
current_best_fitness = max(fitnesses)
current_best_individual = population[fitnesses.index(current_best_fitness)]
# Aggiorna la migliore soluzione trovata se quella attuale è migliore
if current_best_fitness > best_fitness:
best_fitness = current_best_fitness
best_individual = current_best_individual
print(f”Generazione {generation}: Miglior Fitness = {best_fitness:.4f}”)
# Visualizza i movimenti delle gambe per il miglior individuo della generazione corrente
plot_leg_movements(current_best_individual, generation)
# Controlla se abbiamo raggiunto la soglia di fitness
if best_fitness >= fitness_threshold:
print(f”Obiettivo raggiunto alla generazione {generation} con fitness {best_fitness:.4f}”)
save_solution(best_individual) # Salva la soluzione migliore
break
# Genera la prossima generazione
next_population = []
while len(next_population) < pop_size:
parent1, parent2 = select_parents(population, fitnesses)
child = crossover(parent1, parent2)
child = mutate(child, mutation_rate)
next_population.append(child)
population = next_population
# Se il ciclo termina senza aver raggiunto la soglia di fitness, salva comunque la migliore soluzione trovata
if best_fitness < fitness_threshold:
print(f”Massimo numero di generazioni raggiunto. Miglior fitness: {best_fitness:.4f}”)
save_solution(best_individual) # Salva la soluzione migliore anche se non ha raggiunto la soglia
print(“Evoluzione completata.”)
# Esegui l’algoritmo genetico genetic_algorithm()
Spiegazione del funzionamento del programma
- Inizializzazione: il programma parte creando una popolazione casuale iniziale di movimenti delle gambe.
- Calcolo del fitness: a ogni generazione, la funzione swimming_efficiency calcola quanto bene ogni quadrupede si muove, considerando la distanza che riesce a coprire in avanti e la coordinazione tra le gambe.
- Selezione, crossover e mutazione: gli individui migliori (con il miglior fitness) vengono scelti per creare la generazione successiva tramite il crossover e, occasionalmente, mutazioni casuali nei movimenti delle gambe.
- Obiettivo finale: l’algoritmo evolve la popolazione fino a trovare una configurazione di movimento delle gambe, che si muove con sufficiente efficienza (copre abbastanza distanza in modo coordinato).
Una volta raggiunto l’obiettivo i movimenti delle gambe ottimali vengono salvati per essere usati come pre-addestramento per altri algoritmi di AI.
Spiegazione dettagliata
- Funzione swimming_efficiency: viene utilizzata per calcolare il fitness di ogni individuo. Il fitness è basato su quanto in avanti (direzione X) riesce a spostarsi l’individuo grazie ai movimenti delle sue gambe e quanto sono coordinati questi movimenti.
La distanza in avanti è sommata per tutte e quattro le gambe, mentre la coordinazione (simmetria tra le gambe anteriori e posteriori) è penalizzata nel fitness se non è buona.
- Funzione initialize_population: questa funzione crea una popolazione iniziale di movimenti delle gambe, dove ciascun movimento è descritto da una lunghezza (tra 0.5 e 2.0) e da un angolo (tra 0 e 2π).
- Funzione select_parents: implementa la selezione dei genitori tramite la roulette wheel selection, che seleziona genitori proporzionalmente ai loro valori di fitness.
- Funzione crossover: effettua un crossover tra due genitori combinando casualmente le loro lunghezze e angoli per creare un figlio.
- Funzione mutate: introduce casualmente delle mutazioni nei movimenti delle gambe per aggiungere variazione alla popolazione.
- Funzione save_solution: salva la soluzione migliore trovata in un file JSON.
- Funzione plot_leg_movements: crea un grafico dei movimenti delle gambe dell’individuo migliore per ogni generazione, mostrando come le gambe si muovono nel piano cartesiano.
- Funzione genetic_algorithm: è la funzione principale che esegue l’algoritmo genetico per diverse generazioni, evolvendo la popolazione per migliorare il fitness dei movimenti delle gambe.
L’algoritmo termina quando viene raggiunto un fitness threshold specificato o al termine delle generazioni prestabilite.
Nel nostro caso la fitness threshold è impostata a 10, quindi l’algoritmo si fermerà quando la configurazione di tutte e quattro le gambe (individual) raggiunge la fitness di 10 oppure quando le generazioni arrivano a 100.
def genetic_algorithm(pop_size=50, generations=100, mutation_rate=0.1, fitness_threshold=10.0): |
Ed ecco il grafico della prima generazione (ponendo le generazioni a 1400):
Ed il grafico dell’ultima generazione, la numero 1399: Miglior Fitness = 4.6649:
Il grafico rappresenta il movimento delle gambe di un quadrupede simulato, dove ogni gamba è visualizzata come un segmento che si estende dal punto di partenza (posizione iniziale della gamba sul corpo) verso la sua posizione finale.
I segmenti sono disegnati su un piano cartesiano, con l’origine (0, 0) che rappresenta il centro del corpo dell’animale ed ogni gamba è etichettata e mostrata con un colore diverso:
Descrizione delle gambe:
- Gamba 1 (blu): Questa gamba si trova nel quadrante negativo delle x e positivo delle y, quindi è orientata verso sinistra e in alto. La sua lunghezza è piuttosto significativa, e l’angolo è tale da far sì che si estenda diagonalmente in quella direzione. Questo movimento suggerisce che la gamba potrebbe spingere il corpo verso destra o mantenere il bilanciamento in quella direzione.
- Gamba 2 (arancione): Questa gamba si trova principalmente nel quadrante positivo delle x e delle y; quindi, è diretta verso destra e in alto. Non è perfettamente orizzontale, ma ha un angolo inclinato verso l’alto rispetto all’asse x, è più corta della gamba 1, ma probabilmente lavora insieme ad essa per bilanciare i movimenti anteriori del robot.
- Gamba 3 (verde): Questa gamba si estende nel quadrante negativo delle x e positivo delle y per una piccola porzione, mentre è principalmente inclinata verso il basso, essa si trova in una posizione simile alla gamba 1 ma nel quadrante opposto rispetto all’asse y ed è più corta rispetto alla gamba 1 e 2, il che suggerisce che potrebbe contribuire al bilanciamento o alla stabilizzazione del corpo piuttosto che alla propulsione.
- Gamba 4 (rossa): Questa gamba si estende nel quadrante positivo delle x e negativo delle y; quindi, è orientata verso destra e in basso, a differenza della gamba 2, ha una lunghezza maggiore ed è inclinata verso il basso. Questa gamba sembra essere una delle più lunghe e, con la sua direzione, potrebbe contribuire alla propulsione in avanti e al bilanciamento del robot.
Analisi corretta dei movimenti:
- Gambe anteriori:
- Gamba 1 (blu) e Gamba 2 (arancione) lavorano in opposizione per stabilizzare il corpo. La Gamba 1 si sposta a sinistra, mentre la Gamba 2 si sposta a destra, entrambe sembrano contribuire al bilanciamento del robot, con la Gamba 1 che esercita una forza maggiore (per via della lunghezza e dell’orientamento) verso sinistra.
- Gambe posteriori:
- Gamba 3 (verde) si estende principalmente in basso e verso sinistra, ma la sua lunghezza ridotta potrebbe indicare un ruolo più di stabilizzazione che di propulsione.
- Gamba 4 (rossa) si estende più a lungo verso destra e in basso, indicando che probabilmente ha un ruolo significativo nel movimento e nella spinta del robot verso destra.
Il grafico mostra una configurazione delle gambe in cui il robot si sta bilanciando tra movimenti laterali (sinistra e destra) e propulsione. Le gambe 1 e 2 sembrano stabilizzarsi tra loro, mentre le gambe 3 e 4 giocano un ruolo nel bilanciamento e nella spinta.
Questi movimenti influenzano la capacità dell’organismo di “nuotare” simulato, dove la coordinazione dei movimenti determina l’efficacia del movimento in avanti.
Guardiamo l’istinto creato
Ed ecco l’istinto artificiale (contenuto del file best_swimming_solution.json)
[[1.5173303454091152, 0.7888139669570793], [1.4168472516657826, 0.7690200517454989], [1.650141244069472, 0.6784042528992451], [1.7604518724106133, 0.6952795573128941]] |
Interpretazione dei valori:
Ogni coppia di numeri nel file JSON corrisponde alla configurazione di una gamba, contenente due valori, che rappresentano:
Lunghezza della gamba (raggio): la distanza che la gamba si estende dal punto di attacco al corpo del robot.
Angolo (in radianti): l’angolo rispetto all’asse X, misurato in radianti, che indica la direzione in cui la gamba si estende.
I dati salvati in questo file JSON rappresentano una configurazione pre-addestrata o “istinto” per il nuoto, che può essere caricato e utilizzato come parte dello stato iniziale di una rete neurale (ad esempio, pesi) in un modello di deep learning, che potrebbe sfruttare queste informazioni come conoscenza preliminare quando si impara a nuotare, accelerando il processo di apprendimento partendo da una soluzione quasi ottimale invece che da un’inizializzazione casuale.
Conclusioni
Questo articolo non ha la pretesa di aver creato un perfetto istinto natatorio canino, anzi genera solo una posizione iniziale delle gambe, ma serve a dare un’idea di come “iniettare” l’istinto nell’IA, ma poi vuoi mettere il gusto di poter visualizzare un qualcosa di così impalpabile?