Apache MXNet - Gluon

Un autre package MXNet Python le plus important est Gluon. Dans ce chapitre, nous discuterons de ce package. Gluon fournit une API claire, concise et simple pour les projets DL. Il permet à Apache MXNet de prototyper, de créer et de former des modèles DL sans perdre la vitesse de formation.

Blocs

Les blocs constituent la base de conceptions de réseaux plus complexes. Dans un réseau de neurones, à mesure que la complexité du réseau de neurones augmente, nous devons passer de la conception de couches uniques à des couches entières de neurones. Par exemple, la conception NN comme ResNet-152 a un très bon degré de régularité en se composant deblocks de couches répétées.

Exemple

Dans l'exemple donné ci-dessous, nous écrirons du code un bloc simple, à savoir un bloc pour un perceptron multicouche.

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

Cela produit la sortie suivante:

[[ 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)>

Étapes nécessaires pour passer de la définition des couches à la définition des blocs d'une ou plusieurs couches -

Step 1 - Bloquer prendre les données comme entrée.

Step 2- Désormais, les blocs stockent l'état sous forme de paramètres. Par exemple, dans l'exemple de codage ci-dessus, le bloc contient deux couches cachées et nous avons besoin d'un emplacement pour stocker les paramètres.

Step 3- Le bloc suivant invoquera la fonction avant pour effectuer la propagation avant. Il est également appelé calcul direct. Dans le cadre du premier appel de transfert, les blocs initialisent les paramètres de manière paresseuse.

Step 4- Enfin, les blocs invoqueront la fonction arrière et calculeront le gradient en référence à leur entrée. En règle générale, cette étape est effectuée automatiquement.

Bloc séquentiel

Un bloc séquentiel est un type particulier de bloc dans lequel les données circulent à travers une séquence de blocs. En cela, chaque bloc appliqué à la sortie d'un bloc précédent, le premier bloc étant appliqué aux données d'entrée elles-mêmes.

Voyons comment sequential travaux de 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

La sortie est donnée ici -

[[ 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)>

Bloc personnalisé

On peut facilement aller au-delà de la concaténation avec bloc séquentiel tel que défini ci-dessus. Mais, si nous voulons faire des personnalisations, alors leBlockclass nous fournit également les fonctionnalités requises. La classe Block a un constructeur de modèle fourni dans le module nn. Nous pouvons hériter de ce constructeur de modèle pour définir le modèle que nous voulons.

Dans l'exemple suivant, le MLP class remplace le __init__ et les fonctions avancées de la classe Block.

Voyons comment cela fonctionne.

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

Lorsque vous exécutez le code, vous verrez la sortie suivante:

[[ 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)>

Calques personnalisés

L'API Gluon d'Apache MXNet est fournie avec un nombre modeste de couches prédéfinies. Mais encore à un moment donné, nous pouvons constater qu'une nouvelle couche est nécessaire. Nous pouvons facilement ajouter une nouvelle couche dans l'API Gluon. Dans cette section, nous verrons comment créer un nouveau calque à partir de zéro.

Le calque personnalisé le plus simple

Pour créer une nouvelle couche dans l'API Gluon, nous devons créer une classe hérite de la classe Block qui fournit les fonctionnalités les plus basiques. Nous pouvons en hériter toutes les couches prédéfinies directement ou via d'autres sous-classes.

Pour créer la nouvelle couche, la seule méthode d'instance à implémenter est forward (self, x). Cette méthode définit ce que notre couche va faire exactement pendant la propagation vers l'avant. Comme indiqué précédemment, le passage de rétro-propagation des blocs sera effectué automatiquement par Apache MXNet lui-même.

Exemple

Dans l'exemple ci-dessous, nous allons définir un nouveau calque. Nous mettrons également en œuvreforward() méthode pour normaliser les données d'entrée en les ajustant dans une plage de [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

En exécutant le programme ci-dessus, vous obtiendrez le résultat suivant -

[[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)>

L'hybridation

Il peut être défini comme un processus utilisé par Apache MXNet pour créer un graphe symbolique d'un calcul direct. L'hybridation permet à MXNet d'augmenter les performances de calcul en optimisant le graphe symbolique de calcul. Plutôt que d'hériter directement deBlock, en fait, nous pouvons constater que lors de l'implémentation de couches existantes, un bloc hérite d'un HybridBlock.

Voici les raisons à cela -

  • Allows us to write custom layers: HybridBlock nous permet d'écrire des couches personnalisées qui peuvent ensuite être utilisées dans la programmation impérative et symbolique.

  • Increase computation performance- HybridBlock optimise le graphe symbolique de calcul qui permet à MXNet d'augmenter les performances de calcul.

Exemple

Dans cet exemple, nous réécrirons notre exemple de couche, créé ci-dessus, en utilisant 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

La sortie est indiquée ci-dessous:

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

L'hybridation n'a rien à voir avec le calcul sur GPU et on peut former des réseaux hybrides ou non hybrides sur CPU et GPU.

Différence entre Block et HybridBlock

Si nous comparons les Block Classe et HybridBlock, nous verrons que HybridBlock a déjà son forward() méthode mise en œuvre. HybridBlock définit un hybrid_forward()méthode qui doit être implémentée lors de la création des couches. L'argument F crée la principale différence entreforward() et hybrid_forward(). Dans la communauté MXNet, l'argument F est appelé backend. F peut désigner soitmxnet.ndarray API (utilisé pour la programmation impérative) ou mxnet.symbol API (utilisé pour la programmation symbolique).

Comment ajouter une couche personnalisée à un réseau?

Au lieu d'utiliser des calques personnalisés séparément, ces calques sont utilisés avec des calques prédéfinis. Nous pouvons utiliser soitSequential ou HybridSequentialconteneurs à partir d'un réseau neuronal séquentiel. Comme indiqué précédemment,Sequential conteneur hérite de Block et HybridSequential hériter de HybridBlock respectivement.

Exemple

Dans l'exemple ci-dessous, nous allons créer un réseau de neurones simple avec une couche personnalisée. La sortie deDense (5) couche sera l'entrée de NormalizationHybridLayer. La sortie deNormalizationHybridLayer deviendra l'entrée de Dense (1) couche.

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

Vous verrez la sortie suivante -

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

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

Paramètres de calque personnalisés

Dans un réseau neuronal, une couche est associée à un ensemble de paramètres. Nous les référons parfois comme des poids, qui est l'état interne d'une couche. Ces paramètres jouent différents rôles -

  • Parfois, ce sont ceux que nous voulons apprendre lors de l'étape de rétropropagation.

  • Parfois, ce ne sont que des constantes que nous voulons utiliser lors de la passe avant.

Si nous parlons du concept de programmation, ces paramètres (poids) d'un bloc sont stockés et accessibles via ParameterDict classe qui facilite leur initialisation, leur mise à jour, leur sauvegarde et leur chargement.

Exemple

Dans l'exemple ci-dessous, nous définirons deux ensembles de paramètres suivants -

  • Parameter weights- Ceci peut être entraîné et sa forme est inconnue pendant la phase de construction. Il sera déduit lors du premier passage de propagation vers l'avant.

  • Parameter scale- C'est une constante dont la valeur ne change pas. Contrairement aux poids des paramètres, sa forme est définie lors de la construction.

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