Apache MXNet - NDArray

In questo capitolo, discuteremo del formato di array multidimensionale di MXNet chiamato ndarray.

Gestione dei dati con NDArray

Innanzitutto, vedremo come gestire i dati con NDArray. Di seguito sono riportati i prerequisiti per lo stesso:

Prerequisiti

Per capire come possiamo gestire i dati con questo formato di array multidimensionale, dobbiamo soddisfare i seguenti prerequisiti:

  • MXNet installato in un ambiente Python

  • Python 2.7.x o Python 3.x

Esempio di implementazione

Comprendiamo le funzionalità di base con l'aiuto di un esempio fornito di seguito:

Innanzitutto, dobbiamo importare MXNet e ndarray da MXNet come segue:

import mxnet as mx
from mxnet import nd

Una volta importate le librerie necessarie, andremo con le seguenti funzionalità di base:

Un semplice array 1-D con un elenco di python

Example

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

Output

L'output è come indicato di seguito -

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

Un array 2-D con un elenco di 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

L'output è come indicato di seguito:

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

Creazione di un NDArray senza alcuna inizializzazione

Qui creeremo una matrice con 3 righe e 4 colonne utilizzando .emptyfunzione. Useremo anche.full funzione, che richiederà un operatore aggiuntivo per il valore che desideri inserire nella matrice.

Example

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

Output

L'output è fornito di seguito:

[[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 di tutti zeri con la funzione .zeros

Example

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

Output

L'output è il seguente:

[[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 di tutti quelli con la funzione .ones

Example

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

Output

L'output è menzionato di seguito:

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

Creazione di array i cui valori vengono campionati in modo casuale

Example

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

Output

L'output è fornito di seguito:

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

Trovare la dimensione di ogni NDArray

Example

y.shape

Output

L'output è il seguente:

(3, 4)

Trovare la dimensione di ogni NDArray

Example

y.size

Output

12

Trovare il tipo di dati di ogni NDArray

Example

y.dtype

Output

numpy.float32

Operazioni NDArray

In questa sezione, ti presenteremo le operazioni sugli array di MXNet. NDArray supporta un gran numero di operazioni matematiche e sul posto standard.

Operazioni matematiche standard

Di seguito sono riportate le operazioni matematiche standard supportate da NDArray:

Addizione basata sugli elementi

Innanzitutto, dobbiamo importare MXNet e ndarray da MXNet come segue:

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

L'output è fornito di seguito:

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

Moltiplicazione per elemento

Example

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

Output

Vedrai il seguente output -

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

Esponenziazione

Example

nd.exp(x)

Output

Quando esegui il codice, vedrai il seguente output:

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

Trasposizione della matrice per calcolare il prodotto matrice-matrice

Example

nd.dot(x, y.T)

Output

Di seguito è riportato l'output del codice:

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

Operazioni sul posto

Ogni volta, nell'esempio precedente, abbiamo eseguito un'operazione, abbiamo allocato una nuova memoria per ospitare il suo risultato.

Ad esempio, se scriviamo A = A + B, dereferenzieremo la matrice a cui A puntava e invece la indirizzeremo alla memoria appena allocata. Cerchiamo di capirlo con l'esempio fornito di seguito, utilizzando la funzione id () di 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

Dopo l'esecuzione, riceverai il seguente output:

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

In effetti, possiamo anche assegnare il risultato a un array precedentemente allocato come segue:

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

L'output è mostrato di seguito:

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

Dall'output di cui sopra, possiamo vedere che x + y allocherà ancora un buffer temporaneo per memorizzare il risultato prima di copiarlo su z. Quindi ora, possiamo eseguire operazioni sul posto per fare un uso migliore della memoria ed evitare il buffer temporaneo. Per fare ciò, specificheremo l'argomento della parola chiave out che ogni operatore supporta come segue:

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

Eseguendo il programma sopra, otterrai il seguente risultato:

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

Contesti NDArray

In Apache MXNet, ogni array ha un contesto e un contesto potrebbe essere la CPU, mentre altri contesti potrebbero essere più GPU. Le cose possono andare anche peggio, quando distribuiamo il lavoro su più server. Ecco perché, dobbiamo assegnare gli array ai contesti in modo intelligente. Ridurrà al minimo il tempo impiegato per il trasferimento dei dati tra i dispositivi.

Ad esempio, prova a inizializzare un array come segue:

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

Output

Quando esegui il codice sopra, dovresti vedere il seguente output:

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

Possiamo copiare il dato NDArray da un contesto a un altro contesto utilizzando il metodo copyto () come segue:

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

NumPy array vs. NDArray

Abbiamo tutti familiarità con gli array NumPy, ma Apache MXNet offre la propria implementazione di array denominata NDArray. In realtà, inizialmente è stato progettato per essere simile a NumPy, ma c'è una differenza fondamentale:

La differenza fondamentale è nel modo in cui i calcoli vengono eseguiti in NumPy e NDArray. Ogni manipolazione di NDArray in MXNet viene eseguita in modo asincrono e non bloccante, il che significa che, quando scriviamo codice come c = a * b, la funzione viene inviata alExecution Engine, che avvierà il calcolo.

Qui, aeb sono entrambi NDArrays. Il vantaggio di usarlo è che la funzione ritorna immediatamente indietro e il thread utente può continuare l'esecuzione nonostante il calcolo precedente potrebbe non essere stato ancora completato.

Funzionamento del motore di esecuzione

Se parliamo del funzionamento del motore di esecuzione, costruisce il grafico di calcolo. Il grafico di calcolo può riordinare o combinare alcuni calcoli, ma rispetta sempre l'ordine di dipendenza.

Ad esempio, se ci sono altre manipolazioni con "X" eseguite più avanti nel codice di programmazione, il motore di esecuzione inizierà a eseguirle una volta che il risultato di "X" sarà disponibile. Il motore di esecuzione gestirà alcuni lavori importanti per gli utenti, come la scrittura di callback per avviare l'esecuzione del codice successivo.

In Apache MXNet, con l'aiuto di NDArray, per ottenere il risultato del calcolo dobbiamo solo accedere alla variabile risultante. Il flusso del codice verrà bloccato finché i risultati del calcolo non verranno assegnati alla variabile risultante. In questo modo, aumenta le prestazioni del codice pur supportando la modalità di programmazione imperativa.

Conversione di NDArray in NumPy Array

Impariamo come possiamo convertire NDArray in NumPy Array in MXNet.

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

A volte, possiamo assemblare un operatore di livello superiore utilizzando gli operatori esistenti. Uno dei migliori esempi di questo è ilnp.full_like()operatore, che non è presente nell'API NDArray. Può essere facilmente sostituito con una combinazione di operatori esistenti come segue:

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

Otterremo l'output simile come segue:

True

Finding similar operator with different name and/or signature

Tra tutti gli operatori, alcuni hanno nomi leggermente diversi, ma sono simili in termini di funzionalità. Un esempio di questo ènd.ravel_index() con np.ravel()funzioni. Allo stesso modo, alcuni operatori possono avere nomi simili, ma hanno firme diverse. Un esempio di questo ènp.split() e nd.split() sono simili.

Comprendiamolo con il seguente esempio di programmazione:

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

L'output è indicato di seguito:

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

Riduzione al minimo dell'impatto del blocco delle chiamate

In alcuni casi, dobbiamo usare entrambi .asnumpy() o .asscalar()metodi, ma questo costringerà MXNet a bloccare l'esecuzione, fino a quando non sarà possibile recuperare il risultato. Possiamo ridurre al minimo l'impatto di una chiamata di blocco chiamando.asnumpy() o .asscalar() metodi nel momento in cui pensiamo che il calcolo di questo valore sia già stato fatto.

Esempio di implementazione

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

L'output è citato di seguito:

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