Simulazione di un Quadricottero con Pygame usando l’apprendimento con rinforzo in PyTorch
Questa volta, voglio scrivere un articolo che nasce dall’unione tra un vecchio progetto e una grande passione: la costruzione di droni.
Per l’estate mi sono dato come obiettivo di costruire un drone autopilotato, ovvero un UAV, applicando principi di intelligenza artificiale, in un tentativo di portare queste due affascinanti discipline insieme. Da tempo, mi dedico alla costruzione di droni, una passione che adesso ho deciso di arricchire con un tocco di AI. Di seguito per nostalgia riporto il video della costruzione del mio primo drone:
Il progetto che presenterò in questo articolo rappresenta un esempio di come questa sinergia possa prendere forma.
A tal proposito ho sviluppato un codice di esempio che voglio condividervi, in grado di creare una simulazione intuitiva di un quadricottero, il quale sfrutta l’apprendimento per rinforzo per navigare in uno spazio tridimensionale. L’obiettivo che ho perseguito è stato quello di consentire ad un UAV di apprendere come raggiungere un determinato obiettivo.
La simulazione è stata realizzata usando Pygame per la parte grafica e PyTorch per implementare la rete neurale che governa l’apprendimento. Troverete il codice e le relative spiegazioni qui di seguito, e sono disponibili anche su GitHub: https://github.com/domsoria/droneSimRL/tree/main.
Per approfondimenti sulle nozioni di apprendimento per rinforzo e sulla funzione di Q-learning utilizzata, vi rimando a un mio precedente articolo: https://www.domsoria.com/2018/12/reinforcement-learning-apprendimento-con-rinforzo-prima-parte/
Ricordate, questo è solo un esempio di ciò che è possibile realizzare: l’obiettivo finale è ben più complesso e proverò a condividervi via via dei pezzi del progetto.
import pygame from pygame.locals import * from math import cos, sin, pi import pygame.display from OpenGL.GLUT import * from OpenGL.GL import * from OpenGL.GLUT import * from OpenGL.GLU import * import numpy as np import torch import torch.nn as nn import torch.optim as optim class QNetwork(nn.Module): def __init__(self, state_size, action_size): super(QNetwork, self).__init__() self.fc1 = nn.Linear(state_size, 64) self.fc2 = nn.Linear(64, 64) self.fc3 = nn.Linear(64, action_size) def forward(self, state): x = torch.relu(self.fc1(state)) x = torch.relu(self.fc2(x)) return self.fc3(x) action_mapping = {'up': 0, 'down': 1, 'left': 2, 'right': 3,'forward' : 4, 'backward': 5} # Update this mapping based on your specific actions quadcopter_pos = [0, 0, 0] # Questo elenco memorizza le posizioni passate del drone quadcopter_path = [] # Definizione dei componenti che rappresentano il quadricottero components = { "body": { "size": [0.3, 0.1, 0.3], "color": [1, 0, 0] }, "arm1": { "size": [0.8, 0.05, 0.05], "color": [0, 0, 1], "position": [0.2, 0, 0.2], "rotation": 45 }, "arm2": { "size": [0.8, 0.05, 0.05], "color": [0, 0, 1], "position": [-0.2, 0, -0.2], "rotation": 45 }, "arm3": { "size": [0.8, 0.05, 0.05], "color": [0, 0, 1], "position": [0.2, 0, -0.2], "rotation": -45 }, "arm4": { "size": [0.8, 0.05, 0.05], "color": [0, 0, 1], "position": [-0.2, 0, 0.2], "rotation": -45 }, "propeller1": { "size": [0.2, 0.01, 0.2], "color": [0, 1, 0], "position": [0.6, 0, 0.6] }, "propeller2": { "size": [0.2, 0.01, 0.2], "color": [0, 1, 0], "position": [-0.6, 0, -0.6] }, "propeller3": { "size": [0.2, 0.01, 0.2], "color": [0, 1, 0], "position": [0.6, 0, -0.6] }, "propeller4": { "size": [0.2, 0.01, 0.2], "color": [0, 1, 0], "position": [-0.6, 0, 0.6] } } def Quadricottero(): glPushMatrix() # Save current matrix glTranslatef(quadcopter_pos[0], quadcopter_pos[1], quadcopter_pos[2]) # Move quadcopter for component, attributes in components.items(): glPushMatrix() # Push Matrix on stack glRotatef(attributes.get("rotation", 0), 0, 1, 0) # Rotate the Matrix drawCube(attributes["size"], attributes["color"], attributes.get("position", [0, 0, 0])) glPopMatrix() # Pop Matrix from stack glPopMatrix() # Restore matrix to previous state (before quadcopter translation) def drawCube(size, color, position): vertices = [ [size[0]/2, -size[1]/2, -size[2]/2], [size[0]/2, size[1]/2, -size[2]/2], [-size[0]/2, size[1]/2, -size[2]/2], [-size[0]/2, -size[1]/2, -size[2]/2], [size[0]/2, -size[1]/2, size[2]/2], [size[0]/2, size[1]/2, size[2]/2], [-size[0]/2, -size[1]/2, size[2]/2], [-size[0]/2, size[1]/2, size[2]/2] ] vertices = [[v[0]+position[0], v[1]+position[1], v[2]+position[2]] for v in vertices] edges = [ (0,1), (0,3), (0,4), (2,1), (2,3), (2,7), (6,3), (6,4), (6,7), (5,1), (5,4), (5,7) ] glBegin(GL_LINES) for edge in edges: for vertex in edge: glColor3fv(color) glVertex3fv(vertices[vertex]) glEnd() # Funzione per muovere il drone basata sull'azione selezionata def move_quadcopter(action_index): global quadcopter_pos action = action_space[action_index] if action == 'forward': quadcopter_pos[2] -= 0.1 elif action == 'backward': quadcopter_pos[2] += 0.1 elif action == 'up': quadcopter_pos[1] += 0.1 elif action == 'down': quadcopter_pos[1] -= 0.1 elif action == 'left': quadcopter_pos[0] -= 0.1 elif action == 'right': quadcopter_pos[0] += 0.1 def draw_target(position, radius, color): glPushMatrix() glTranslatef(position[0], position[1], position[2]) glColor3fv(color) # Draw a "circle" in 3D glBegin(GL_POLYGON) for i in range(100): glVertex3f(radius * cos(2*pi*i/100), radius * sin(2*pi*i/100), 0.0) glEnd() glPopMatrix() # Funzione per verificare se il drone si trova nella stessa posizione di un altro oggetto def is_colliding(pos1, pos2, tolerance=0.2): return abs(pos1[0] - pos2[0]) < tolerance and abs(pos1[1] - pos2[1]) < tolerance and abs(pos1[2] - pos2[2]) < tolerance # Variabili globali per lo spazio delle azioni e lo spazio degli stati action_space = ['up', 'down', 'left', 'right','forward', 'backward'] #state_space = list(np.arange(-5, 5.1, 0.1)) # Posizioni da -5 a 5 con incrementi di 0.1 state_space = list(np.round(np.arange(-5, 5.1, 0.1), 1)) # Dimensione dello stato e delle azioni state_size = 3 # x, y, z coordinate action_size = len(action_space) # Creare e ottimizzare la rete neurale qnetwork = QNetwork(state_size, action_size) optimizer = optim.Adam(qnetwork.parameters(), lr=0.001) #q_table = np.zeros((len(state_space), len(state_space), len(state_space), len(action_space))) # Funzione di ricompensa # Funzione di ricompensa def reward(old_state, new_state, target): old_distance = np.sqrt(np.sum((np.array(old_state) - np.array(target))**2)) new_distance = np.sqrt(np.sum((np.array(new_state) - np.array(target))**2)) if is_colliding(new_state, target): return 100 # Ricompensa grande per aver raggiunto l'obiettivo elif new_distance < old_distance: return 1 # Ricompensa positiva per avvicinarsi all'obiettivo else: return -1 # Penalità per allontanarsi dall'obiettivo # Funzione di scelta dell'azione def choose_action(state, epsilon): state = torch.tensor(state, dtype=torch.float32) if np.random.uniform(0, 1) < epsilon: action_index = np.random.choice(len(action_space)) else: with torch.no_grad(): action_index = torch.argmax(qnetwork(state)).item() return action_space[action_index], action_index def update_q_network(old_state, action_index, reward, new_state): old_state = torch.tensor(old_state, dtype=torch.float32) new_state = torch.tensor(new_state, dtype=torch.float32) reward = torch.tensor(reward, dtype=torch.float32) # Calcola la previsione corrente (Q(s, a)) current_q = qnetwork(old_state)[action_index] # Calcola l'obiettivo (r + γ max Q(s', a')) with torch.no_grad(): max_new_q = torch.max(qnetwork(new_state)).item() target_q = reward + 0.95 * max_new_q # 0.95 è il fattore di sconto γ # Calcola la perdita loss = torch.square(target_q - current_q) # Backpropagation optimizer.zero_grad() loss.backward() optimizer.step() def main(): global quadcopter_pos, quadcopter_path, q_table, action_space pygame.init() display = (1024*1.3,768*1.3) screen = pygame.display.set_mode(display, DOUBLEBUF | OPENGL) gluPerspective(45, (display[0]/display[1]), 0.1, 50.0) # Initial camera position glTranslatef(0.0, -1.0, -5) target_position = [np.random.uniform(-2, 2) for _ in range(3)] print(target_position) epsilon = 0.3 # Probabilità di scelta casuale dell'azione episodes = 1000 # Numero di episodi per l'apprendimento # Creare e ottimizzare la rete neurale qnetwork = QNetwork(state_size, action_size) optimizer = optim.Adam(qnetwork.parameters(), lr=0.001) # Carica i pesi del modello se esistono if os.path.exists('qnetwork_weights_episode.pth'): qnetwork.load_state_dict(torch.load('qnetwork_weights_episode.pth')) for episode in range(episodes): done = False while not done: for event in pygame.event.get(): if event.type == pygame.QUIT: done = True exit() elif event.type == pygame.VIDEORESIZE: # Handle window resizing here if necessary pass action, action_index = choose_action(quadcopter_pos, epsilon) old_quadcopter_pos = quadcopter_pos[:] move_quadcopter(action_index) r = reward(old_quadcopter_pos, quadcopter_pos, target_position) update_q_network(old_quadcopter_pos, action_index, r, quadcopter_pos) quadcopter_path.append(quadcopter_pos[:]) if is_colliding(quadcopter_pos, target_position): print("Episodio {} completato!".format(episode)) print("Il drone ha colliso con l'obiettivo!") # Imposta il font e le dimensioni del testo # Stampa il numero di episodi completati print(f'Episodio {episode} completato!') # Stampa la posizione finale del drone print(f'Posizione finale: {quadcopter_pos}') # Stampa la posizione finale dell'obiettivo print(f'Posizione obiettivo: {target_position}') # Stampa il percorso del drone #print(f'Percorso: {quadcopter_path}') # Stampa la tabella Q #print(f'Tabella Q: {q_table}') done = True # Genera una nuova posizione target casualmente nel range -5 a 5 per ogni dimensione target_position = [np.random.uniform(-2, 2) for _ in range(3)] glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT) Quadricottero() draw_target(target_position, 0.2, (1,0,0)) # Save the current matrix # Reset perspective projection pygame.display.flip() pygame.time.wait(10) # Salva i pesi del modello dopo ogni episodio torch.save(qnetwork.state_dict(), f'qnetwork_weights_episode.pth') main()
Per lo sviluppo del progetto, ho impiegato le librerie Pygame e PyOpenGL. La prima è una libreria Python dedicata alla creazione di videogiochi, che ho utilizzato, insieme a PyOpenGL, per costruire una simulazione tridimensionale del quadricottero. All’interno del codice, la funzione “Quadricottero” sfrutta le funzioni di OpenGL per generare una rappresentazione grafica del quadricottero basata su componenti definiti nel dizionario “components”. Questa rappresentazione viene traslata e ruotata nello spazio tridimensionale, rispecchiando così la posizione corrente del quadricottero.
Per permettere al quadricottero di imparare a raggiungere l’obiettivo, ho utilizzato una rete neurale e un metodo di apprendimento per rinforzo chiamato Q-learning. Nel mio codice, la classe QNetwork definisce la rete neurale utilizzata, una rete neurale feed-forward con tre strati completamente connessi. Questa prende in input lo stato corrente del quadricottero e restituisce un vettore di valori Q per ogni azione possibile.
Il Q-learning opera assegnando un valore Q a ogni possibile coppia stato-azione, rappresentando l’attesa di ricompensa futura per quell’azione in quel stato. Durante ogni passaggio, l’algoritmo sceglie un’azione (in base alla policy corrente, che è una combinazione di esplorazione casuale e scelta dell’azione con il massimo valore Q), la esegue e riceve una ricompensa. Successivamente, aggiorna il valore Q per la coppia stato-azione precedente, basandosi sulla ricompensa ricevuta e sul massimo valore Q tra le possibili azioni nel nuovo stato.
Il movimento del quadricottero è gestito dalla funzione “move_quadcopter”, che modifica la posizione corrente del quadricottero a seconda dell’azione scelta, mentre la funzione “reward” calcola la ricompensa ricevuta dal quadricottero in base al suo movimento; la logica decisa fornisce una ricompensa positiva quando il drone prova ad avvicinarsi all’obiettivo e una penalità negativa quando si allontana, ottenendo una grossa ricompensa quando il quadricottero entra in collisione con l’obiettivo (come determinato dalla funzione “is_colliding”); in tal caso l’episodio corrente termina e un nuovo obiettivo viene generato casualmente.
Al cuore del codice, abbiamo una semplice rete neurale, `QNetwork`, utilizzata per apprendere la funzione di valore dell’azione-Q nel contesto dell’apprendimento per rinforzo. Questa rete ha un solo strato nascosto e viene utilizzata per mappare stati in valutazioni di azioni.
class QNetwork(nn.Module): def __init__(self, state_size, action_size): super(QNetwork, self).__init__() self.fc1 = nn.Linear(state_size, 64) self.fc2 = nn.Linear(64, 64) self.fc3 = nn.Linear(64, action_size) def forward(self, state): x = torch.relu(self.fc1(state)) x = torch.relu(self.fc2(x)) return self.fc3(x)
Il codice definisce, inoltre, una serie di funzioni per la visualizzazione e il controllo del drone nel suo ambiente. La funzione `Quadricottero()`, ad esempio, disegna il drone nel suo ambiente corrente, e la funzione `drawCube()` disegna un componente specifico del drone.
def Quadricottero(): glPushMatrix() # Save current matrix glTranslatef(quadcopter_pos[0], quadcopter_pos[1], quadcopter_pos[2]) # Move quadcopter for component, attributes in components.items(): glPushMatrix() # Push Matrix on stack glRotatef(attributes.get("rotation", 0), 0, 1, 0) # Rotate the Matrix drawCube(attributes["size"], attributes["color"], attributes.get("position", [0, 0, 0])) glPopMatrix() # Pop Matrix from stack glPopMatrix() # Restore matrix to previous state (before quadcopter translation)
La funzione `move_quadcopter()` controlla, invece, come il drone si muove in risposta a una determinata azione.
move_quadcopter()
Il codice stabilisce, inoltre, come il drone dovrebbe scegliere e apprendere dalle sue azioni; La funzione `choose_action()` determina quale azione il drone dovrebbe eseguire, in base al suo stato attuale e a un parametro epsilon che governa la sua propensione a esplorare azioni casuali. La funzione `update_q_network()` aggiorna la rete neurale, basandosi sulla differenza tra la previsione corrente della rete neurale e l’obiettivo calcolato, implementando quindi un algoritmo di apprendimento Q-learning.
Infine, il ciclo principale del programma controlla il flusso generale dell’apprendimento e dell’azione.
def main(): global quadcopter_pos, quadcopter_path, q_table, action_space pygame.init() display = (1024*1.3,768*1.3) screen = pygame.display.set_mode(display, DOUBLEBUF | OPENGL) gluPerspective(45, (display[0]/display[1]), 0.1, 50.0) # Initial camera position glTranslatef(0.0, -1.0, -5) target_position = [np.random.uniform(-2, 2) for _ in range(3)] print(target_position) epsilon = 0.3 # Probabilità di scelta casuale dell'azione episodes = 1000 # Numero di episodi per l'apprendimento # Creare e ottimizzare la rete neurale qnetwork = QNetwork(state_size, action_size) optimizer = optim.Adam(qnetwork.parameters(), lr=0.001) # Carica i pesi del modello se esistono if os.path.exists('qnetwork_weights_episode.pth'): qnetwork.load_state_dict(torch.load('qnetwork_weights_episode.pth')) for episode in range(episodes): done = False while not done: for event in pygame.event.get(): if event.type == pygame.QUIT: done = True exit() elif event.type == pygame.VIDEORESIZE: # Handle window resizing here if necessary pass action, action_index = choose_action(quadcopter_pos, epsilon) old_quadcopter_pos = quadcopter_pos[:] move_quadcopter(action_index) r = reward(old_quadcopter_pos, quadcopter_pos, target_position) update_q_network(old_quadcopter_pos, action_index, r, quadcopter_pos) quadcopter_path.append(quadcopter_pos[:]) if is_colliding(quadcopter_pos, target_position): print("Episodio {} completato!".format(episode)) print("Il drone ha colliso con l'obiettivo!") # Imposta il font e le dimensioni del testo # Stampa il numero di episodi completati print(f'Episodio {episode} completato!') # Stampa la posizione finale del drone print(f'Posizione finale: {quadcopter_pos}') # Stampa la posizione finale dell'obiettivo print(f'Posizione obiettivo: {target_position}') # Stampa il percorso del drone #print(f'Percorso: {quadcopter_path}') # Stampa la tabella Q #print(f'Tabella Q: {q_table}') done = True # Genera una nuova posizione target casualmente nel range -5 a 5 per ogni dimensione target_position = [np.random.uniform(-2, 2) for _ in range(3)] glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT) Quadricottero() draw_target(target_position, 0.2, (1,0,0)) # Save the current matrix # Reset perspective projection pygame.display.flip() pygame.time.wait(10) # Salva i pesi del modello dopo ogni episodio torch.save(qnetwork.state_dict(), f'qnetwork_weights_episode.pth')
Sebbene l’esempio sia molto semplificato, può essere utile per iniziare a comprendere come impostare un sistema di Q-learning. Spero che possa essere un elemento didattico utile per chi desidera creare qualcosa di più sofisticato.
Sono amante della tecnologia e delle tante sfumature del mondo IT, ho partecipato, sin dai primi anni di università ad importanti progetti in ambito Internet proseguendo, negli anni, allo startup, sviluppo e direzione di diverse aziende; Nei primi anni di carriera ho lavorato come consulente nel mondo dell’IT italiano, partecipando attivamente a progetti nazionali ed internazionali per realtà quali Ericsson, Telecom, Tin.it, Accenture, Tiscali, CNR. Dal 2010 mi occupo di startup mediante una delle mie società techintouch S.r.l che grazie alla collaborazione con la Digital Magics SpA, di cui sono Partner la Campania, mi occupo di supportare ed accelerare aziende del territorio .
Attualmente ricopro le cariche di :
– CTO MareGroup
– CTO Innoida
– Co-CEO in Techintouch s.r.l.
– Board member in StepFund GP SA
Manager ed imprenditore dal 2000 sono stato,
CEO e founder di Eclettica S.r.l. , Società specializzata in sviluppo software e System Integration
Partner per la Campania di Digital Magics S.p.A.
CTO e co-founder di Nexsoft S.p.A, società specializzata nella Consulenza di Servizi in ambito Informatico e sviluppo di soluzioni di System Integration, CTO della ITsys S.r.l. Società specializzata nella gestione di sistemi IT per la quale ho partecipato attivamente alla fase di startup.
Sognatore da sempre, curioso di novità ed alla ricerca di “nuovi mondi da esplorare“.
Comments