Apache MXNet - NDArray

En este capítulo, discutiremos sobre el formato de matriz multidimensional de MXNet llamado ndarray.

Manejo de datos con NDArray

Primero, veremos cómo podemos manejar los datos con NDArray. Los siguientes son los requisitos previos para el mismo:

Prerrequisitos

Para comprender cómo podemos manejar los datos con este formato de matriz multidimensional, debemos cumplir con los siguientes requisitos previos:

  • MXNet instalado en un entorno Python

  • Python 2.7.xo Python 3.x

Ejemplo de implementación

Entendamos la funcionalidad básica con la ayuda de un ejemplo que se da a continuación:

Primero, necesitamos importar MXNet y ndarray desde MXNet de la siguiente manera:

import mxnet as mx
from mxnet import nd

Una vez que importemos las librerías necesarias, iremos con las siguientes funcionalidades básicas:

Una matriz 1-D simple con una lista de Python

Example

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

Output

El resultado es como se menciona a continuación:

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

Una matriz 2-D con una lista de 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 salida es la que se indica a continuación:

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

Creando un NDArray sin ninguna inicialización

Aquí, crearemos una matriz con 3 filas y 4 columnas usando .emptyfunción. También usaremos.full función, que tomará un operador adicional para el valor que desea completar en la matriz.

Example

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

Output

La salida se da a continuación:

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

Matriz de todos los ceros con la función .zeros

Example

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

Output

La salida es la siguiente:

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

Matriz de todos los que tienen la función .ones

Example

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

Output

La salida se menciona a continuación:

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

Creando una matriz cuyos valores se muestrean aleatoriamente

Example

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

Output

La salida se da a continuación:

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

Encontrar la dimensión de cada NDArray

Example

y.shape

Output

La salida es la siguiente:

(3, 4)

Encontrar el tamaño de cada NDArray

Example

y.size

Output

12

Encontrar el tipo de datos de cada NDArray

Example

y.dtype

Output

numpy.float32

Operaciones de NDArray

En esta sección, le presentaremos las operaciones de matriz de MXNet. NDArray admite una gran cantidad de operaciones matemáticas estándar e in situ.

Operaciones matemáticas estándar

Las siguientes son operaciones matemáticas estándar compatibles con NDArray:

Suma de elementos

Primero, necesitamos importar MXNet y ndarray desde MXNet de la siguiente manera:

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 salida se da a continuación:

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

Multiplicación por elementos

Example

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

Output

Verá la siguiente salida

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

Exponenciación

Example

nd.exp(x)

Output

Cuando ejecute el código, verá el siguiente resultado:

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

Transposición de matriz para calcular el producto matriz-matriz

Example

nd.dot(x, y.T)

Output

A continuación se muestra la salida del código:

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

Operaciones in situ

Cada vez que, en el ejemplo anterior, ejecutamos una operación, asignamos una nueva memoria para alojar su resultado.

Por ejemplo, si escribimos A = A + B, desreferenciaremos la matriz a la que A solía apuntar y en su lugar apuntará a la memoria recién asignada. Entendamos esto con el ejemplo que se da a continuación, usando la función 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

Tras la ejecución, recibirá el siguiente resultado:

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

De hecho, también podemos asignar el resultado a una matriz previamente asignada de la siguiente manera:

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 salida se muestra a continuación:

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

De la salida anterior, podemos ver que x + y todavía asignará un búfer temporal para almacenar el resultado antes de copiarlo en z. Entonces, ahora podemos realizar operaciones en el lugar para hacer un mejor uso de la memoria y evitar el búfer temporal. Para hacer esto, especificaremos el argumento de palabra clave out que cada operador admite de la siguiente manera:

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

Al ejecutar el programa anterior, obtendrá el siguiente resultado:

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

Contextos de NDArray

En Apache MXNet, cada matriz tiene un contexto y un contexto podría ser la CPU, mientras que otros contextos podrían ser varias GPU. Las cosas pueden empeorar aún más cuando implementamos el trabajo en varios servidores. Por eso, necesitamos asignar matrices a contextos de manera inteligente. Minimizará el tiempo dedicado a transferir datos entre dispositivos.

Por ejemplo, intente inicializar una matriz de la siguiente manera:

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

Output

Cuando ejecute el código anterior, debería ver el siguiente resultado:

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

Podemos copiar el NDArray dado de un contexto a otro utilizando el método copyto () de la siguiente manera:

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

Matriz NumPy frente a NDArray

Todos estamos familiarizados con las matrices NumPy, pero Apache MXNet ofrece su propia implementación de matriz llamada NDArray. En realidad, inicialmente fue diseñado para ser similar a NumPy, pero hay una diferencia clave:

La diferencia clave está en la forma en que se ejecutan los cálculos en NumPy y NDArray. Cada manipulación de NDArray en MXNet se realiza de forma asincrónica y sin bloqueo, lo que significa que, cuando escribimos código como c = a * b, la función se envía alExecution Engine, que iniciará el cálculo.

Aquí, a y b son NDArrays. El beneficio de usarlo es que la función regresa inmediatamente y el hilo del usuario puede continuar la ejecución a pesar de que el cálculo anterior puede que aún no se haya completado.

Funcionamiento del motor de ejecución

Si hablamos del funcionamiento del motor de ejecución, construye el gráfico de cálculo. El gráfico de cálculo puede reordenar o combinar algunos cálculos, pero siempre respeta el orden de dependencia.

Por ejemplo, si hay otra manipulación con 'X' realizada más adelante en el código de programación, el motor de ejecución comenzará a realizarlas una vez que el resultado de 'X' esté disponible. El motor de ejecución se encargará de algunos trabajos importantes para los usuarios, como la escritura de devoluciones de llamada para iniciar la ejecución del código posterior.

En Apache MXNet, con la ayuda de NDArray, para obtener el resultado del cómputo solo necesitamos acceder a la variable resultante. El flujo del código se bloqueará hasta que los resultados del cálculo se asignen a la variable resultante. De esta manera, aumenta el rendimiento del código sin dejar de admitir el modo de programación imperativo.

Conversión de NDArray a NumPy Array

Aprendamos cómo podemos convertir NDArray a NumPy Array en MXNet.

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

A veces, podemos ensamblar un operador de nivel superior utilizando los operadores existentes. Uno de los mejores ejemplos de esto es elnp.full_like()operador, que no está en la API de NDArray. Se puede reemplazar fácilmente con una combinación de operadores existentes de la siguiente manera:

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

Obtendremos una salida similar a la siguiente:

True

Finding similar operator with different name and/or signature

Entre todos los operadores, algunos de ellos tienen un nombre ligeramente diferente, pero son similares en términos de funcionalidad. Un ejemplo de esto esnd.ravel_index() con np.ravel()funciones. De la misma forma, algunos operadores pueden tener nombres similares, pero tienen firmas diferentes. Un ejemplo de esto esnp.split() y nd.split() son similares.

Entendamos con el siguiente ejemplo de programación:

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 salida se indica a continuación:

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

Minimizar el impacto del bloqueo de llamadas

En algunos de los casos, tenemos que usar .asnumpy() o .asscalar()métodos, pero esto obligará a MXNet a bloquear la ejecución, hasta que se pueda recuperar el resultado. Podemos minimizar el impacto de una llamada bloqueada llamando.asnumpy() o .asscalar() métodos en el momento, cuando pensamos que el cálculo de este valor ya está hecho.

Ejemplo de implementación

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

El resultado se cita a continuación:

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