Apache MXNet - NDArray

Dans ce chapitre, nous discuterons du format de tableau multidimensionnel de MXNet appelé ndarray.

Gestion des données avec NDArray

Tout d'abord, nous allons voir comment nous pouvons gérer les données avec NDArray. Voici les conditions préalables pour le même -

Conditions préalables

Pour comprendre comment nous pouvons gérer les données avec ce format de tableau multidimensionnel, nous devons remplir les conditions préalables suivantes:

  • MXNet installé dans un environnement Python

  • Python 2.7.x ou Python 3.x

Exemple d'implémentation

Comprenons les fonctionnalités de base à l'aide d'un exemple donné ci-dessous -

Tout d'abord, nous devons importer MXNet et ndarray depuis MXNet comme suit -

import mxnet as mx
from mxnet import nd

Une fois que nous avons importé les bibliothèques nécessaires, nous utiliserons les fonctionnalités de base suivantes:

Un simple tableau 1-D avec une liste python

Example

x = nd.array([1,2,3,4,5,6,7,8,9,10])
print(x)

Output

La sortie est comme mentionné ci-dessous -

[ 1. 2. 3. 4. 5. 6. 7. 8. 9. 10.]
<NDArray 10 @cpu(0)>

Un tableau 2-D avec une liste python

Example

y = nd.array([[1,2,3,4,5,6,7,8,9,10], [1,2,3,4,5,6,7,8,9,10], [1,2,3,4,5,6,7,8,9,10]])
print(y)

Output

La sortie est comme indiqué ci-dessous -

[[ 1. 2. 3. 4. 5. 6. 7. 8. 9. 10.]
[ 1. 2. 3. 4. 5. 6. 7. 8. 9. 10.]
[ 1. 2. 3. 4. 5. 6. 7. 8. 9. 10.]]
<NDArray 3x10 @cpu(0)>

Créer un NDArray sans aucune initialisation

Ici, nous allons créer une matrice avec 3 lignes et 4 colonnes en utilisant .emptyfonction. Nous utiliserons également.full fonction, qui prendra un opérateur supplémentaire pour la valeur que vous voulez remplir dans le tableau.

Example

x = nd.empty((3, 4))
print(x)
x = nd.full((3,4), 8)
print(x)

Output

La sortie est donnée ci-dessous -

[[0.000e+00 0.000e+00 0.000e+00 0.000e+00]
 [0.000e+00 0.000e+00 2.887e-42 0.000e+00]
 [0.000e+00 0.000e+00 0.000e+00 0.000e+00]]
<NDArray 3x4 @cpu(0)>

[[8. 8. 8. 8.]
 [8. 8. 8. 8.]
 [8. 8. 8. 8.]]
<NDArray 3x4 @cpu(0)>

Matrice de tous les zéros avec la fonction .zeros

Example

x = nd.zeros((3, 8))
print(x)

Output

La sortie est la suivante -

[[0. 0. 0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0. 0. 0. 0.]]
<NDArray 3x8 @cpu(0)>

Matrice de tous avec la fonction .ones

Example

x = nd.ones((3, 8))
print(x)

Output

La sortie est mentionnée ci-dessous -

[[1. 1. 1. 1. 1. 1. 1. 1.]
   [1. 1. 1. 1. 1. 1. 1. 1.]
   [1. 1. 1. 1. 1. 1. 1. 1.]]
<NDArray 3x8 @cpu(0)>

Création d'un tableau dont les valeurs sont échantillonnées au hasard

Example

y = nd.random_normal(0, 1, shape=(3, 4))
print(y)

Output

La sortie est donnée ci-dessous -

[[ 1.2673576 -2.0345826 -0.32537818 -1.4583491 ]
 [-0.11176403 1.3606371 -0.7889914 -0.17639421]
 [-0.2532185 -0.42614475 -0.12548696 1.4022992 ]]
<NDArray 3x4 @cpu(0)>

Recherche de la dimension de chaque NDArray

Example

y.shape

Output

La sortie est la suivante -

(3, 4)

Recherche de la taille de chaque NDArray

Example

y.size

Output

12

Recherche du type de données de chaque NDArray

Example

y.dtype

Output

numpy.float32

Opérations NDArray

Dans cette section, nous vous présenterons les opérations de matrice de MXNet. NDArray prend en charge un grand nombre d'opérations mathématiques standard ainsi que sur place.

Opérations mathématiques standard

Voici les opérations mathématiques standard prises en charge par NDArray -

Ajout par élément

Tout d'abord, nous devons importer MXNet et ndarray depuis MXNet comme suit:

import mxnet as mx
from mxnet import nd
x = nd.ones((3, 5))
y = nd.random_normal(0, 1, shape=(3, 5))
print('x=', x)
print('y=', y)
x = x + y
print('x = x + y, x=', x)

Output

La sortie est donnée ici -

x=
[[1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1.]]
<NDArray 3x5 @cpu(0)>
y=
[[-1.0554522 -1.3118273 -0.14674698 0.641493 -0.73820823]
[ 2.031364 0.5932667 0.10228804 1.179526 -0.5444829 ]
[-0.34249446 1.1086396 1.2756858 -1.8332436 -0.5289873 ]]
<NDArray 3x5 @cpu(0)>
x = x + y, x=
[[-0.05545223 -0.3118273 0.853253 1.6414931 0.26179177]
[ 3.031364 1.5932667 1.102288 2.1795259 0.4555171 ]
[ 0.6575055 2.1086397 2.2756858 -0.8332436 0.4710127 ]]
<NDArray 3x5 @cpu(0)>

Multiplication par élément

Example

x = nd.array([1, 2, 3, 4])
y = nd.array([2, 2, 2, 1])
x * y

Output

Vous verrez la sortie suivante -

[2. 4. 6. 4.]
<NDArray 4 @cpu(0)>

Exponentiation

Example

nd.exp(x)

Output

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

[ 2.7182817 7.389056 20.085537 54.59815 ]
<NDArray 4 @cpu(0)>

Matrice transposée pour calculer un produit matrice-matrice

Example

nd.dot(x, y.T)

Output

Ci-dessous est la sortie du code -

[16.]
<NDArray 1 @cpu(0)>

Opérations sur place

Chaque fois que, dans l'exemple ci-dessus, nous avons exécuté une opération, nous avons alloué une nouvelle mémoire pour héberger son résultat.

Par exemple, si nous écrivons A = A + B, nous déréférencerons la matrice vers laquelle A pointait et la pointerons plutôt vers la mémoire nouvellement allouée. Comprenons-le avec l'exemple donné ci-dessous, en utilisant la fonction id () de Python -

print('y=', y)
print('id(y):', id(y))
y = y + x
print('after y=y+x, y=', y)
print('id(y):', id(y))

Output

Lors de l'exécution, vous recevrez la sortie suivante -

y=
[2. 2. 2. 1.]
<NDArray 4 @cpu(0)>
id(y): 2438905634376
after y=y+x, y=
[3. 4. 5. 5.]
<NDArray 4 @cpu(0)>
id(y): 2438905685664

En fait, nous pouvons également affecter le résultat à un tableau précédemment alloué comme suit -

print('x=', x)
z = nd.zeros_like(x)
print('z is zeros_like x, z=', z)
print('id(z):', id(z))
print('y=', y)
z[:] = x + y
print('z[:] = x + y, z=', z)
print('id(z) is the same as before:', id(z))

Output

La sortie est indiquée ci-dessous -

x=
[1. 2. 3. 4.]
<NDArray 4 @cpu(0)>
z is zeros_like x, z=
[0. 0. 0. 0.]
<NDArray 4 @cpu(0)>
id(z): 2438905790760
y=
[3. 4. 5. 5.]
<NDArray 4 @cpu(0)>
z[:] = x + y, z=
[4. 6. 8. 9.]
<NDArray 4 @cpu(0)>
id(z) is the same as before: 2438905790760

À partir de la sortie ci-dessus, nous pouvons voir que x + y allouera toujours un tampon temporaire pour stocker le résultat avant de le copier dans z. Alors maintenant, nous pouvons effectuer des opérations sur place pour mieux utiliser la mémoire et éviter la mémoire tampon temporaire. Pour ce faire, nous allons spécifier l'argument de mot-clé out que chaque opérateur prend en charge comme suit -

print('x=', x, 'is in id(x):', id(x))
print('y=', y, 'is in id(y):', id(y))
print('z=', z, 'is in id(z):', id(z))
nd.elemwise_add(x, y, out=z)
print('after nd.elemwise_add(x, y, out=z), x=', x, 'is in id(x):', id(x))
print('after nd.elemwise_add(x, y, out=z), y=', y, 'is in id(y):', id(y))
print('after nd.elemwise_add(x, y, out=z), z=', z, 'is in id(z):', id(z))

Output

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

x=
[1. 2. 3. 4.]
<NDArray 4 @cpu(0)> is in id(x): 2438905791152
y=
[3. 4. 5. 5.]
<NDArray 4 @cpu(0)> is in id(y): 2438905685664
z=
[4. 6. 8. 9.]
<NDArray 4 @cpu(0)> is in id(z): 2438905790760
after nd.elemwise_add(x, y, out=z), x=
[1. 2. 3. 4.]
<NDArray 4 @cpu(0)> is in id(x): 2438905791152
after nd.elemwise_add(x, y, out=z), y=
[3. 4. 5. 5.]
<NDArray 4 @cpu(0)> is in id(y): 2438905685664
after nd.elemwise_add(x, y, out=z), z=
[4. 6. 8. 9.]
<NDArray 4 @cpu(0)> is in id(z): 2438905790760

Contextes NDArray

Dans Apache MXNet, chaque tableau a un contexte et un contexte peut être le processeur, tandis que d'autres contextes peuvent être plusieurs GPU. Les choses peuvent être encore pires lorsque nous déployons le travail sur plusieurs serveurs. C'est pourquoi, nous devons attribuer intelligemment des tableaux aux contextes. Cela minimisera le temps passé à transférer des données entre les appareils.

Par exemple, essayez d'initialiser un tableau comme suit -

from mxnet import nd
z = nd.ones(shape=(3,3), ctx=mx.cpu(0))
print(z)

Output

Lorsque vous exécutez le code ci-dessus, vous devriez voir la sortie suivante -

[[1. 1. 1.]
 [1. 1. 1.]
 [1. 1. 1.]]
<NDArray 3x3 @cpu(0)>

Nous pouvons copier le NDArray donné d'un contexte vers un autre contexte en utilisant la méthode copyto () comme suit -

x_gpu = x.copyto(gpu(0))
print(x_gpu)

Tableau NumPy et NDArray

Nous sommes tous familiers avec les tableaux NumPy, mais Apache MXNet propose sa propre implémentation de tableau nommée NDArray. En fait, il a été initialement conçu pour être similaire à NumPy mais il y a une différence clé -

La principale différence réside dans la manière dont les calculs sont exécutés dans NumPy et NDArray. Chaque manipulation de NDArray dans MXNet est effectuée de manière asynchrone et non bloquante, ce qui signifie que, lorsque nous écrivons du code comme c = a * b, la fonction est poussée vers leExecution Engine, qui lancera le calcul.

Ici, a et b sont tous deux des NDArrays. L'avantage de son utilisation est que la fonction revient immédiatement et que le thread utilisateur peut continuer son exécution malgré le fait que le calcul précédent n'est peut-être pas encore terminé.

Fonctionnement du moteur d'exécution

Si nous parlons du fonctionnement du moteur d'exécution, il construit le graphe de calcul. Le graphe de calcul peut réorganiser ou combiner certains calculs, mais il respecte toujours l'ordre des dépendances.

Par exemple, s'il y a d'autres manipulations avec 'X' effectuées plus tard dans le code de programmation, le moteur d'exécution commencera à les faire une fois que le résultat de 'X' sera disponible. Le moteur d'exécution gérera certains travaux importants pour les utilisateurs, tels que l'écriture de rappels pour démarrer l'exécution du code suivant.

Dans Apache MXNet, avec l'aide de NDArray, pour obtenir le résultat du calcul, il suffit d'accéder à la variable résultante. Le flux du code sera bloqué jusqu'à ce que les résultats du calcul soient affectés à la variable résultante. De cette façon, il augmente les performances du code tout en prenant en charge le mode de programmation impératif.

Conversion de NDArray en NumPy Array

Apprenons comment convertir NDArray en NumPy Array dans MXNet.

Combining higher-level operator with the help of few lower-level operators

Parfois, nous pouvons assembler un opérateur de niveau supérieur en utilisant les opérateurs existants. L'un des meilleurs exemples en est lenp.full_like()opérateur, qui n'est pas présent dans l'API NDArray. Il peut facilement être remplacé par une combinaison d'opérateurs existants comme suit:

from mxnet import nd
import numpy as np
np_x = np.full_like(a=np.arange(7, dtype=int), fill_value=15)
nd_x = nd.ones(shape=(7,)) * 15
np.array_equal(np_x, nd_x.asnumpy())

Output

Nous obtiendrons la sortie similaire comme suit -

True

Finding similar operator with different name and/or signature

Parmi tous les opérateurs, certains d'entre eux ont un nom légèrement différent, mais ils sont similaires en termes de fonctionnalités. Un exemple de ceci estnd.ravel_index() avec np.ravel()les fonctions. De la même manière, certains opérateurs peuvent avoir des noms similaires, mais ils ont des signatures différentes. Un exemple de ceci estnp.split() et nd.split() sont similaires.

Comprenons-le avec l'exemple de programmation suivant:

def pad_array123(data, max_length):
data_expanded = data.reshape(1, 1, 1, data.shape[0])
data_padded = nd.pad(data_expanded,
mode='constant',
pad_width=[0, 0, 0, 0, 0, 0, 0, max_length - data.shape[0]],
constant_value=0)
data_reshaped_back = data_padded.reshape(max_length)
return data_reshaped_back
pad_array123(nd.array([1, 2, 3]), max_length=10)

Output

La sortie est indiquée ci-dessous -

[1. 2. 3. 0. 0. 0. 0. 0. 0. 0.]
<NDArray 10 @cpu(0)>

Minimiser l'impact du blocage des appels

Dans certains cas, nous devons utiliser soit .asnumpy() ou .asscalar()méthodes, mais cela forcera MXNet à bloquer l'exécution, jusqu'à ce que le résultat puisse être récupéré. Nous pouvons minimiser l'impact d'un blocage d'appel en appelant.asnumpy() ou .asscalar() méthodes dans le moment, quand on pense que le calcul de cette valeur est déjà fait.

Exemple d'implémentation

Example

from __future__ import print_function
import mxnet as mx
from mxnet import gluon, nd, autograd
from mxnet.ndarray import NDArray
from mxnet.gluon import HybridBlock
import numpy as np

class LossBuffer(object):
   """
   Simple buffer for storing loss value
   """
   
   def __init__(self):
      self._loss = None

   def new_loss(self, loss):
      ret = self._loss
      self._loss = loss
      return ret

      @property
      def loss(self):
         return self._loss

net = gluon.nn.Dense(10)
ce = gluon.loss.SoftmaxCELoss()
net.initialize()
data = nd.random.uniform(shape=(1024, 100))
label = nd.array(np.random.randint(0, 10, (1024,)), dtype='int32')
train_dataset = gluon.data.ArrayDataset(data, label)
train_data = gluon.data.DataLoader(train_dataset, batch_size=128, shuffle=True, num_workers=2)
trainer = gluon.Trainer(net.collect_params(), optimizer='sgd')
loss_buffer = LossBuffer()
for data, label in train_data:
   with autograd.record():
      out = net(data)
      # This call saves new loss and returns previous loss
      prev_loss = loss_buffer.new_loss(ce(out, label))
   loss_buffer.loss.backward()
   trainer.step(data.shape[0])
   if prev_loss is not None:
      print("Loss: {}".format(np.mean(prev_loss.asnumpy())))

Output

Le résultat est cité ci-dessous:

Loss: 2.3373236656188965
Loss: 2.3656985759735107
Loss: 2.3613128662109375
Loss: 2.3197104930877686
Loss: 2.3054862022399902
Loss: 2.329197406768799
Loss: 2.318927526473999