discesa del gradiente stocastica

Discesa del gradiente stocastica

Nel capitolo precedente abbiamo visto come poter minimizzare una funzione di costo, compiendo un passo nella direzione opposta rispetto a un gradiente che viene calcolato sulla base dell’intero insieme dei dati di addestramento.

Detto anche gradiente di Batch, non viene spesso utilizzato come primo algoritmo di minimizzazione, tutto ciò perchè il dataset contiene una grossa quantità di dati e comporta così un consumo elevato delle risorse computazionali.

Questo consumo dipende dalla rivalutazione costante dell’intero dataset di addestramento ogni volta che compiamo un passo verso il minimo globale.

Discesa del gradiente stocastica

Discesa del gradiente stocastica - la soluzione ?

L’algoritmo di discesa del gradiente stocastica permette di ovviare al problema dell’eccesivo utilizzo delle risorse. Esso infatti comporta l’aggiornamento dei pesi in modo incrementale e frequente per ogni campione di addestramento.

Tutto ciò comporta un migliore e più rapido raggiungimento della convergenza, che resta l’obbiettivo di ogni algoritmo di minimizzazione

La superficie di errore è più rumorosa rispetto alla discesa del gradiente poichè ciascun gradiente viene calcolato sulla base di un singolo esempio di addestramento.

Tutto ciò causa una possibilità di discesa migliore permettendo di sfuggire con maggiore facilità agli avvallamenti dei minimi globali.

Un compromesso fra la discesa del gradiente batch e la discesa del gradiente stocastica è l'apprendimento mini-batch. È una combinazione di discesa gradiente batch e discesa gradiente stocastica. Essa esegue un aggiornamento per una serie di osservazioni. È l’algoritmo di scelta per le reti neurali e le dimensioni dei lotti sono in genere da 50 a 256, in modo da raggiungere più velocemente la convergenza.

Appredimento mini-batch

Apprendimento online

Questo tipo di algoritmo di minimizzazione viene favorito dalla discesa del gradiente stocastica e permette l’apprendimento al “volo“, ovvero man mano che arrivano i nuovi dati di addestramento. Il sistema si adatta ai cambiamenti rapidamente e i dati di addestramento possono essere eliminati dop aver aggiornato il modello.

Algoritmo in Python della discesa del gradiente stocastica

Conosciamo già la regola di apprendimento Adaline utilizzando la discesa del gradiente. Nelle lezioni precedenti abbiamo infatti realizzato l’algoritmo in Python e analizzato le varie sezioni del codice in modo approfondito.

Modificando alcune parti di codice andiamo a realizzare l’algoritmo che permette di aggiornare i pesi tramite la discesa del gradiente stocastica.

All’interno del metodo fit, aggiorniamo i pesi  dopo ciascun campione di addestramento.

Implementiamo, invece, un nuovo partial_fit che servirà per non reinizializzare i pesi per l’apprendimento online

from numpy.random import seed

class AdalineSGD(object):
    """ADAptive LInear NEuron classifier.

    Parameters
    ------------
    eta : float
        Learning rate (between 0.0 and 1.0)
    n_iter : int
        Passes over the training dataset.

    Attributes
    -----------
    w_ : 1d-array
        Weights after fitting.
    cost_ : list
        Sum-of-squares cost function value averaged over all
        training samples in each epoch.
    shuffle : bool (default: True)
        Shuffles training data every epoch if True to prevent cycles.
    random_state : int (default: None)
        Set random state for shuffling and initializing the weights.
        
    """
    def __init__(self, eta=0.01, n_iter=10, shuffle=True, random_state=None):
        self.eta = eta
        self.n_iter = n_iter
        self.w_initialized = False
        self.shuffle = shuffle
        if random_state:
            seed(random_state)
        
    def fit(self, X, y):
        """ Fit training data.

        Parameters
        ----------
        X : {array-like}, shape = [n_samples, n_features]
            Training vectors, where n_samples is the number of samples and
            n_features is the number of features.
        y : array-like, shape = [n_samples]
            Target values.

        Returns
        -------
        self : object

        """
        self._initialize_weights(X.shape[1])
        self.cost_ = []
        for i in range(self.n_iter):
            if self.shuffle:
                X, y = self._shuffle(X, y)
            cost = []
            for xi, target in zip(X, y):
                cost.append(self._update_weights(xi, target))
            avg_cost = sum(cost) / len(y)
            self.cost_.append(avg_cost)
        return self

    def partial_fit(self, X, y):
        """Fit training data without reinitializing the weights"""
        if not self.w_initialized:
            self._initialize_weights(X.shape[1])
        if y.ravel().shape[0] > 1:
            for xi, target in zip(X, y):
                self._update_weights(xi, target)
        else:
            self._update_weights(X, y)
        return self

    def _shuffle(self, X, y):
        """Shuffle training data"""
        r = np.random.permutation(len(y))
        return X[r], y[r]
    
    def _initialize_weights(self, m):
        """Initialize weights to zeros"""
        self.w_ = np.zeros(1 + m)
        self.w_initialized = True
        
    def _update_weights(self, xi, target):
        """Apply Adaline learning rule to update the weights"""
        output = self.net_input(xi)
        error = (target - output)
        self.w_[1:] += self.eta * xi.dot(error)
        self.w_[0] += self.eta * error
        cost = 0.5 * error**2
        return cost
    
    def net_input(self, X):
        """Calculate net input"""
        return np.dot(X, self.w_[1:]) + self.w_[0]

    def activation(self, X):
        """Compute linear activation"""
        return self.net_input(X)

    def predict(self, X):
        """Return class label after unit step"""
        return np.where(self.activation(X) >= 0.0, 1, -1)

Lo scopo resta sempre quello di verificare se l’algoritmo converge dopo l’addestramento. Calcoliamo così il costo in termini di costo medio dei campioni di addestramento in ciascuna epoch.

Per evitare il nascere di cicli durante l’ottimizzazione della funzione di costo, mescoliamo i dati di addestramento prima di ogni epoch tramite la funzione shuffle.

Esso funziona nel modo seguente:

  • generiamo una sequenza random di numeri univici compresi tra 0 e 100 grazie alla libreria numpy.random i quali vengono utilizzati come indici per mescolare la matrice delle caratteristiche e il vettore delle etichette delle classi.

Tracciando il grafico verifichiamo che ...

ada = AdalineSGD(n_iter=15, eta=0.01, random_state=1)
ada.fit(X_std, y)

plot_decision_regions(X_std, y, classifier=ada)
plt.title('Adaline - Stochastic Gradient Descent')
plt.xlabel('sepal length [standardized]')
plt.ylabel('petal length [standardized]')
plt.legend(loc='upper left')

plt.tight_layout()
#plt.savefig('./adaline_4.png', dpi=300)
plt.show()

plt.plot(range(1, len(ada.cost_) + 1), ada.cost_, marker='o')
plt.xlabel('Epochs')
plt.ylabel('Average Cost')

plt.tight_layout()

plt.show()
discesa del gradiente stocastica
discesa del gradiente stocastica

Come possiamo vedere il costo medio si abbatte con buona rapidità e la decisione finale sul confine dopo quindici epoch ha lo stesso aspetto della discesa del gradiente batch.

Per aggiornare il modello ci basta richiamare il metodo partial_fit sui singoli campioni, come ada.partial_fit(X_std[0, :], y[0]).

Condividi il post

Condividi su facebook
Condividi su google
Condividi su twitter
Condividi su email
Condividi su whatsapp