Apache MXNet - Gluon

Un altro pacchetto MXNet Python più importante è Gluon. In questo capitolo discuteremo di questo pacchetto. Gluon fornisce un'API chiara, concisa e semplice per i progetti DL. Consente ad Apache MXNet di prototipare, costruire e addestrare modelli DL senza perdere la velocità di addestramento.

Blocchi

I blocchi costituiscono la base di progetti di rete più complessi. In una rete neurale, con l'aumentare della complessità della rete neurale, dobbiamo passare dalla progettazione di singoli strati a interi livelli di neuroni. Ad esempio, il design NN come ResNet-152 ha un discreto grado di regolarità in quanto composto dablocks di strati ripetuti.

Esempio

Nell'esempio fornito di seguito, scriveremo codice un blocco semplice, vale a dire blocco per un perceptron multistrato.

from mxnet import nd
from mxnet.gluon import nn
x = nd.random.uniform(shape=(2, 20))
N_net = nn.Sequential()
N_net.add(nn.Dense(256, activation='relu'))
N_net.add(nn.Dense(10))
N_net.initialize()
N_net(x)

Output

Questo produce il seguente output:

[[ 0.09543004 0.04614332 -0.00286655 -0.07790346 -0.05130241 0.02942038
0.08696645 -0.0190793 -0.04122177 0.05088576]
[ 0.0769287 0.03099706 0.00856576 -0.044672 -0.06926838 0.09132431
0.06786592 -0.06187843 -0.03436674 0.04234696]]
<NDArray 2x10 @cpu(0)>

Passaggi necessari per passare dalla definizione dei livelli alla definizione dei blocchi di uno o più livelli -

Step 1 - Block accetta i dati come input.

Step 2- Ora, i blocchi memorizzeranno lo stato sotto forma di parametri. Ad esempio, nell'esempio di codifica sopra il blocco contiene due livelli nascosti e abbiamo bisogno di un posto per memorizzare i parametri per esso.

Step 3- Il blocco successivo richiamerà la funzione di inoltro per eseguire la propagazione in avanti. È anche chiamato calcolo in avanti. Come parte della prima chiamata di inoltro, i blocchi inizializzano i parametri in modo pigro.

Step 4- Infine i blocchi richiameranno la funzione di ritorno e calcoleranno il gradiente con riferimento al loro input. In genere, questo passaggio viene eseguito automaticamente.

Blocco sequenziale

Un blocco sequenziale è un tipo speciale di blocco in cui i dati fluiscono attraverso una sequenza di blocchi. In questo, ogni blocco applicato all'uscita di uno precedente con il primo blocco applicato ai dati di ingresso stessi.

Vediamo come sequential lavori di classe -

from mxnet import nd
from mxnet.gluon import nn
class MySequential(nn.Block):
   def __init__(self, **kwargs):
      super(MySequential, self).__init__(**kwargs)

   def add(self, block):
      self._children[block.name] = block
   def forward(self, x):
   for block in self._children.values():
      x = block(x)
   return x
x = nd.random.uniform(shape=(2, 20))
N_net = MySequential()
N_net.add(nn.Dense(256, activation
='relu'))
N_net.add(nn.Dense(10))
N_net.initialize()
N_net(x)

Output

L'output è fornito di seguito:

[[ 0.09543004 0.04614332 -0.00286655 -0.07790346 -0.05130241 0.02942038
0.08696645 -0.0190793 -0.04122177 0.05088576]
[ 0.0769287 0.03099706 0.00856576 -0.044672 -0.06926838 0.09132431
0.06786592 -0.06187843 -0.03436674 0.04234696]]
<NDArray 2x10 @cpu(0)>

Blocco personalizzato

Possiamo facilmente andare oltre la concatenazione con il blocco sequenziale come definito sopra. Ma, se vorremmo fare delle personalizzazioni, allora il fileBlockclass ci fornisce anche le funzionalità richieste. La classe Block ha un costruttore di modelli fornito nel modulo nn. Possiamo ereditare quel costruttore del modello per definire il modello che vogliamo.

Nell'esempio seguente, il MLP class sostituisce il __init__ e le funzioni forward della classe Block.

Vediamo come funziona.

class MLP(nn.Block):

   def __init__(self, **kwargs):
      super(MLP, self).__init__(**kwargs)
      self.hidden = nn.Dense(256, activation='relu') # Hidden layer
      self.output = nn.Dense(10) # Output layer


   def forward(self, x):
      hidden_out = self.hidden(x)
      return self.output(hidden_out)
x = nd.random.uniform(shape=(2, 20))
N_net = MLP()
N_net.initialize()
N_net(x)

Output

Quando esegui il codice, vedrai il seguente output:

[[ 0.07787763 0.00216403 0.01682201 0.03059879 -0.00702019 0.01668715
0.04822846 0.0039432 -0.09300035 -0.04494302]
[ 0.08891078 -0.00625484 -0.01619131 0.0380718 -0.01451489 0.02006172
0.0303478 0.02463485 -0.07605448 -0.04389168]]
<NDArray 2x10 @cpu(0)>

Livelli personalizzati

L'API Gluon di Apache MXNet viene fornita con un numero modesto di livelli predefiniti. Tuttavia, a un certo punto, potremmo scoprire che è necessario un nuovo livello. Possiamo facilmente aggiungere un nuovo livello nell'API Gluon. In questa sezione vedremo come creare un nuovo livello da zero.

Il livello personalizzato più semplice

Per creare un nuovo livello nell'API Gluon, dobbiamo creare una classe ereditata dalla classe Block che fornisce le funzionalità di base. Possiamo ereditare tutti i livelli predefiniti da esso direttamente o tramite altre sottoclassi.

Per creare il nuovo livello, l'unico metodo di istanza da implementare è forward (self, x). Questo metodo definisce cosa farà esattamente il nostro livello durante la propagazione in avanti. Come discusso in precedenza, il passaggio di back-propagation per i blocchi verrà eseguito automaticamente da Apache MXNet stesso.

Esempio

Nell'esempio seguente, definiremo un nuovo livello. Lo implementeremo ancheforward() metodo per normalizzare i dati di input inserendoli in un intervallo di [0, 1].

from __future__ import print_function
import mxnet as mx
from mxnet import nd, gluon, autograd
from mxnet.gluon.nn import Dense
mx.random.seed(1)
class NormalizationLayer(gluon.Block):
   def __init__(self):
      super(NormalizationLayer, self).__init__()

   def forward(self, x):
      return (x - nd.min(x)) / (nd.max(x) - nd.min(x))
x = nd.random.uniform(shape=(2, 20))
N_net = NormalizationLayer()
N_net.initialize()
N_net(x)

Output

Eseguendo il programma sopra, otterrai il seguente risultato:

[[0.5216355 0.03835821 0.02284337 0.5945146 0.17334817 0.69329053
0.7782702 1. 0.5508242 0. 0.07058554 0.3677264
0.4366546 0.44362497 0.7192635 0.37616986 0.6728799 0.7032008

 0.46907538 0.63514024]
[0.9157533 0.7667402 0.08980197   0.03593295 0.16176797 0.27679572
 0.07331014 0.3905285 0.6513384 0.02713427 0.05523694 0.12147208
 0.45582628 0.8139887 0.91629887 0.36665893 0.07873632 0.78268915
 0.63404864 0.46638715]]
 <NDArray 2x20 @cpu(0)>

Ibridazione

Può essere definito come un processo utilizzato da Apache MXNet per creare un grafico simbolico di un calcolo in avanti. L'ibridazione consente a MXNet di aumentare le prestazioni di calcolo ottimizzando il grafico simbolico computazionale. Piuttosto che ereditare direttamente daBlock, infatti, potremmo scoprire che durante l'implementazione di livelli esistenti un blocco eredita da un file HybridBlock.

Di seguito sono riportate le ragioni per questo:

  • Allows us to write custom layers: HybridBlock ci consente di scrivere livelli personalizzati che possono essere ulteriormente utilizzati sia nella programmazione imperativa che in quella simbolica.

  • Increase computation performance- HybridBlock ottimizza il grafico simbolico computazionale che consente a MXNet di aumentare le prestazioni di calcolo.

Esempio

In questo esempio, riscriveremo il nostro layer di esempio, creato sopra, utilizzando HybridBlock:

class NormalizationHybridLayer(gluon.HybridBlock):
   def __init__(self):
      super(NormalizationHybridLayer, self).__init__()

   def hybrid_forward(self, F, x):
      return F.broadcast_div(F.broadcast_sub(x, F.min(x)), (F.broadcast_sub(F.max(x), F.min(x))))

layer_hybd = NormalizationHybridLayer()
layer_hybd(nd.array([1, 2, 3, 4, 5, 6], ctx=mx.cpu()))

Output

L'output è indicato di seguito:

[0. 0.2 0.4 0.6 0.8 1. ]
<NDArray 6 @cpu(0)>

L'ibridazione non ha nulla a che fare con il calcolo su GPU e si possono addestrare reti ibridate e non ibridate sia su CPU che su GPU.

Differenza tra Block e HybridBlock

Se confrontiamo il file Block Classe e HybridBlock, lo vedremo HybridBlock ha già il suo forward() metodo implementato. HybridBlock definisce a hybrid_forward()metodo che deve essere implementato durante la creazione dei livelli. L'argomento F crea la differenza principale traforward() e hybrid_forward(). Nella comunità MXNet, l'argomento F viene indicato come backend. F può riferirsi amxnet.ndarray API (utilizzato per la programmazione imperativa) o mxnet.symbol API (utilizzato per la programmazione simbolica).

Come aggiungere un livello personalizzato a una rete?

Invece di utilizzare i layer personalizzati separatamente, questi layer vengono utilizzati con layer predefiniti. Possiamo usare entrambiSequential o HybridSequentialcontenitori da una rete neurale sequenziale. Come discusso in precedenza anche,Sequential container eredita da Block e HybridSequential ereditare da HybridBlock rispettivamente.

Esempio

Nell'esempio seguente, creeremo una semplice rete neurale con un livello personalizzato. L'uscita daDense (5) layer sarà l'input di NormalizationHybridLayer. L'output diNormalizationHybridLayer diventerà l'input di Dense (1) strato.

net = gluon.nn.HybridSequential()
with net.name_scope():
net.add(Dense(5))
net.add(NormalizationHybridLayer())
net.add(Dense(1))
net.initialize(mx.init.Xavier(magnitude=2.24))
net.hybridize()
input = nd.random_uniform(low=-10, high=10, shape=(10, 2))
net(input)

Output

Vedrai il seguente output:

[[-1.1272651]
 [-1.2299833]
 [-1.0662932]
 [-1.1805027]
 [-1.3382034]
 [-1.2081106]
 [-1.1263978]
 [-1.2524893]
 
 [-1.1044774]

 [-1.316593 ]]
<NDArray 10x1 @cpu(0)>

Parametri layer personalizzati

In una rete neurale, un livello ha una serie di parametri ad esso associati. A volte li chiamiamo pesi, che è lo stato interno di un livello. Questi parametri svolgono ruoli diversi:

  • A volte questi sono quelli che vogliamo imparare durante la fase di backpropagation.

  • A volte queste sono solo costanti che vogliamo usare durante il passaggio in avanti.

Se parliamo del concetto di programmazione, questi parametri (pesi) di un blocco vengono memorizzati e accessibili tramite ParameterDict classe che aiuta nell'inizializzazione, aggiornamento, salvataggio e caricamento di loro.

Esempio

Nell'esempio seguente, definiremo due seguenti set di parametri:

  • Parameter weights- Questo è addestrabile e la sua forma è sconosciuta durante la fase di costruzione. Verrà dedotto alla prima esecuzione della propagazione in avanti.

  • Parameter scale- Questa è una costante il cui valore non cambia. A differenza dei pesi dei parametri, la sua forma viene definita durante la costruzione.

class NormalizationHybridLayer(gluon.HybridBlock):
   def __init__(self, hidden_units, scales):
      super(NormalizationHybridLayer, self).__init__()
      with self.name_scope():
      self.weights = self.params.get('weights',
      shape=(hidden_units, 0),
      allow_deferred_init=True)
      self.scales = self.params.get('scales',
         shape=scales.shape,
         init=mx.init.Constant(scales.asnumpy()),
         differentiable=False)
      def hybrid_forward(self, F, x, weights, scales):
         normalized_data = F.broadcast_div(F.broadcast_sub(x, F.min(x)),
         (F.broadcast_sub(F.max(x), F.min(x))))
         weighted_data = F.FullyConnected(normalized_data, weights, num_hidden=self.weights.shape[0], no_bias=True)
         scaled_data = F.broadcast_mul(scales, weighted_data)
return scaled_data