La prima rete neurale: il Perceptron

La prima rete neurale: il Perceptron

Condividi con i tuoi amici...

Si parla così spesso negli ultimi anni di intelligenza artificiale che sembra trattarsi di una tecnologia dell’ultima ora, recentissima. Tuttavia non è così. Il primo passo verso l’intelligenza artificiale è stato fatto oltre 60 anni fa. Una forma semplice di rete neurale artificiale, detta Perceptron fu inventata nel 1958 da Frank Rosenblatt, uno psicologo e scienziato dell’informazione.

Il Perceptron è un modello matematico che emula il funzionamento del cervello umano. È costituito da uno o più neuroni artificiali, chiamati “perceptron”, che accettano input e producono una determinata uscita in base a una funzione di attivazione. Questa funzione di attivazione può essere binaria, cioè prendere solo i valori 0 o 1, o può essere una funzione lineare che restituisce un valore continuo.

Un aspetto chiave del Perceptron è che può essere addestrato in modo supervisionato, cioè fornendo algoritmicamente coppie di input e output desiderati. Questo addestramento permette al Perceptron di apprendere come classificare automaticamente nuovi input in base ai modelli precedentemente osservati. Se addestrato correttamente, il Perceptron può essere un potente classificatore.

Un esempio semplice può aiutare a capire meglio come funziona un Perceptron Immagina che tu voglia creare un Perceptron che possa distinguere tra mele mature e mele immature. Potresti addestrarlo fornendo immagini di mele mature e immagini di mele immature come input e dicendo al percettrone quale immagine rappresenta quale tipo di mela come output desiderato.

Dopo aver addestrato il Perceptron con un numero sufficientemente grande di esempi, potresti testarlo con nuove immagini di mele. Il Perceptron prenderebbe in input l’immagine della mela e darebbe in output una previsione su se la mela è matura o immatura.

Com’è fatto il Perceptron

l’input è rappresentabile mediante un vettore X(n) = (1, x1(n), x2(n), x3(n), … xm(n)). La componente j-esima rappresenta l’input proveniente dall’j-esimo nodo sorgente. Ad ogni connessione sinaptica del neurone proveniente dal nodo j-esimo è assegnato un peso wj. L’insieme di pesi è rappresentabile anch’esso come vettore (b, w1(n), w2(n), w3(n), … wm(n)).

n è un indice che indica il ciclo di iterazione.

Il neurone genera in uscita il seguente campo scalare v:

questo sarà l’ingresso della funzione di attivazione che può essere definita per esempio nel seguente modo:

fatt.(v) = 0 se v < 0

fatt.(v) = 1 se v >= 0

In uscita, la rete neurale fornirà valore y = fatt.(v) = 1 se la sommatoria è positiva o uguale a zero e valore y = fatt.(v) = 0 se è negativa

nello spazio multidimensionale l’equazione

è un iperpiano che fa da spartiacque tra due regioni di tale spazio nelle quali andranno collocati gli elementi di input che saranno, quindi classificati come appartenenti ad una regione (classe A) o appartenenti all’altra (classe B)

In uno spazio bidimensionale l’equazione è quella di una retta:

Il Perceptron risulta quindi essere un buon classificatore a patto che i dati da classificare siano linearmente separabili. In pratica per tali dati una volta rappresentati in uno spazio multidimensionale deve esistere una iperpiano che li separa in due classi o una retta nel caso di uno spazio a due dimensioni. Dati separabili mediante altri tipi di superfici o curve non vanno bene per addestrare il Perceptron. Questa è una delle limitazioni per questo tipo di rete neurale.

Addestramento del Perceptron

Nel seguito usiamo la lettera maiscola per indicare un vettore e quella minuscola per le sue componenti o uno scalare e consideriamo il caso con bias b = X0 = 0.

Inizialmente, abbiamo un set di p vettori di dimensionalità m rappresentanti i diversi input (dati di addestramento) e un vettore W dei pesi di dimensionalità m pari alla dimensionalità dei vettori di input. Le sue componenti sono i pesi da associare alle connessioni sinaptiche del neurone che lo uniscono ai nodi sorgente. I pesi inizialmente sono posti a zero: W(0) = (0,0,0,…0).

Trattandosi di una rete supervisionata, per ogni vettore di input Xi si associa un valore desiderato di output di che in partica ci dice in quale classe vogliamo collocare quel particolare elemento di input. Si assume valore 1 se vogliamo che Xi stia nella classe A o valore 0 se vogliamo che stia nella classe B.

In sintesi abbiamo i seguenti oggetti:
Dati di addestramento come lista di p vettori :

[X1, X2,…Xp] = [[x1(1), x2(1),..xm(1)], [x1(2), x2(2),…xm(2)],… [x1(p), x2(p),…,xm(p)]]
W = [w1, w2, ..wm] vettore di dimensionalità m pari a quella dei vettori di input
[d1,d2, ..,dp] lista di output desiderati, ognuno dei quali è associato al vettore di input corrispondente
[y1,y2,…,yp] lista di output del percettrone ognuno dei quali è calcolato sul vettore di input corrispondente

Illustriamo come avviene l’addestramento mediante un diagramma di flusso (clicca sul grafico per ingrandire)

(Cliccando qui poi visionare un diagramma di flusso equivalente generato mediante il plug in Show Me Diagram + GPT4)

Come si può osservare nell’operazione di correzione dei pesi se l’output coincide con il risultato desiderato d, il vettore dei pesi non verrà modificato essendo error = di – yi = 0 quindi Wi+1 = Wi , mentre se non coincide avverrà la correzione, infatti in quel caso error = di – yi è diverso da zero e Wi+1 del passo successivo assumerà un valore diverso da Wi attuale.

E’ possibile dimostrare la convergenza di una tale algoritmo, nel senso che se i dati sono linearmente separabili esiste sicuramente un numero finito di iterazioni N* al di sopra del quale i pesi non dovranno più essere corretti e il Perceptron risulterà addestrato.

La dimostrazione si trova su Neural Networks and Learning Machines di Haykin da cui abbiamo in parte tratto ispirazione per scrivere questo articolo.

Esempio con python

Traduciamo l’algoritmo del Perceptron in codice python in cui vengono forniti 5 vettori bidimensionali come esempio di dati di addestramento e i 5 corrispondenti valori di output desiderati. Il codice fornisce infine i pesi da assegnare alla rete neurale:


import numpy as np

# Funzione di attivazione (soglia)
def activation_fn(x):
    return 1 if x >= 0 else 0

# Funzione di addestramento del percettrone
def train_perceptron(X, d, learning_rate, num_epochs):
    
    num_features = X.shape[1]  #numero di componenti dei vettori di input
    num_samples = X.shape[0] #numero dei vettori di input
    
    # Inizializzazione dei pesi a zero
    weights = np.zeros(num_features) #vettore di input di dimensionalità num_features
    
    for epoch in range(num_epochs):
        for i in range(num_samples):
            # Calcolo dell'output attuale del percettrone
            output = activation_fn(np.dot(X[i], weights)) #funzione di attivazione sul prodotto scalare
            
            # Calcolo dell'errore di classificazione
            error = d[i] - output
            
            # Aggiornamento dei pesi
            weights += learning_rate * error * X[i]
    
    return weights

# Set di dati di addestramento
X = np.array([[1, 2], [2, 1], [-1, -3], [-2, -1], [1, -1]])
d = np.array([0, 0, 1, 1, 0])

# Parametri di addestramento
learning_rate = 0.1
num_epochs = 100

# Addestramento del percettrone
weights = train_perceptron(X, d, learning_rate, num_epochs)

# Stampa dei pesi addestrati
print("Pesi addestrati:", weights)

OUTPUT

Pesi addestrati: [-0.2 -0.1]

La retta che separa le classi sarà quindi
-0.2x1 -0.1x2 = 0

Visualizziamo i vettori di addestramento e la retta con un grafico

I punti rappresentano i dati di addestramento. La retta è la linea di separazione delle due classi. Ora, grazie ad essa sappiamo in quale classe di trova qualsiasi altro punto anche al di fuori di quelli impiegati nei dati di addestramento.

La realizzazione del grafico è stata fatta utilizzando la libreria Matplotlib di python. Ecco il codice sorgente:

import matplotlib.pyplot as plt
import numpy as np

# Definizione dei punti
points = np.array([(1, 2), (2, 1), (-1, -3), (-2, -1), (1, -1)])

# Definizione delle coordinate x e y dei punti
x = points[:, 0]
y = points[:, 1]

# Creazione del grafico
plt.scatter(x, y, color='red', label='Punti')  # Disegna i punti colorati di rosso
plt.xlabel('X')
plt.ylabel('Y')

# Definizione dei valori di x per la retta
x_values = np.linspace(-3, 3, 100)

# Calcolo dei valori di y corrispondenti alla retta x2 = -2x1
y_values = -2 * x_values

plt.plot(x_values, y_values, color='blue', label='x2 = -2x1')  # Disegna la retta colorata di blu
plt.legend()  # Mostra la legenda

# Mostra il grafico
plt.show()