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