Apache MXNet - NDArray

Neste capítulo, iremos discutir sobre o formato de array multidimensional do MXNet chamado ndarray.

Tratamento de dados com NDArray

Primeiro, veremos como podemos lidar com os dados com NDArray. A seguir estão os pré-requisitos para o mesmo -

Pré-requisitos

Para entender como podemos lidar com dados com este formato de matriz multidimensional, precisamos cumprir os seguintes pré-requisitos:

  • MXNet instalado em um ambiente Python

  • Python 2.7.x ou Python 3.x

Exemplo de Implementação

Vamos entender a funcionalidade básica com a ajuda de um exemplo dado abaixo -

Primeiro, precisamos importar MXNet e ndarray do MXNet da seguinte forma -

import mxnet as mx
from mxnet import nd

Assim que importarmos as bibliotecas necessárias, iremos com as seguintes funcionalidades básicas:

Uma matriz 1-D simples com uma lista python

Example

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

Output

O resultado é como mencionado abaixo -

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

Uma matriz 2-D com uma lista 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

A saída é conforme indicado abaixo -

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

Criação de um NDArray sem qualquer inicialização

Aqui, criaremos uma matriz com 3 linhas e 4 colunas usando .emptyfunção. Também vamos usar.full , que terá um operador adicional para o valor que você deseja preencher na matriz.

Example

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

Output

O resultado é dado abaixo -

[[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 os zeros com a função .zeros

Example

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

Output

O resultado é o seguinte -

[[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 com a função .ones

Example

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

Output

O resultado é mencionado abaixo -

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

Criação de matriz cujos valores são amostrados aleatoriamente

Example

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

Output

O resultado é dado abaixo -

[[ 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 a dimensão de cada NDArray

Example

y.shape

Output

O resultado é o seguinte -

(3, 4)

Encontrar o tamanho de cada NDArray

Example

y.size

Output

12

Encontrar o tipo de dados de cada NDArray

Example

y.dtype

Output

numpy.float32

Operações NDArray

Nesta seção, apresentaremos as operações de array do MXNet. O NDArray oferece suporte a um grande número de operações matemáticas padrão e também no local.

Operações Matemáticas Padrão

A seguir estão as operações matemáticas padrão suportadas pelo NDArray -

Adição elementar

Primeiro, precisamos importar MXNet e ndarray do MXNet da seguinte maneira:

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

A saída é fornecida aqui -

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

Multiplicação elementar

Example

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

Output

Você verá a seguinte saída−

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

Exponenciação

Example

nd.exp(x)

Output

Ao executar o código, você verá a seguinte saída:

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

Matriz transposta para calcular o produto matriz-matriz

Example

nd.dot(x, y.T)

Output

A seguir está a saída do código -

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

Operações no local

Cada vez que, no exemplo acima, executamos uma operação, alocamos uma nova memória para hospedar seu resultado.

Por exemplo, se escrevermos A = A + B, vamos desreferenciar a matriz para a qual A costumava apontar e, em vez disso, apontá-la para a memória recém-alocada. Vamos entender isso com o exemplo dado abaixo, usando a função id () do 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

Após a execução, você receberá a seguinte saída -

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

Na verdade, também podemos atribuir o resultado a uma matriz previamente alocada da seguinte maneira -

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

O resultado é mostrado abaixo -

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

Pela saída acima, podemos ver que x + y ainda alocará um buffer temporário para armazenar o resultado antes de copiá-lo para z. Portanto, agora podemos realizar operações no local para fazer melhor uso da memória e evitar buffer temporário. Para fazer isso, especificaremos o argumento de palavra-chave out que cada operador suporta da seguinte maneira -

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

Ao executar o programa acima, você obterá o seguinte 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 NDArray

No Apache MXNet, cada array tem um contexto e um contexto pode ser a CPU, enquanto outros contextos podem ser várias GPUs. As coisas podem ficar ainda piores, quando implantamos o trabalho em vários servidores. É por isso que precisamos atribuir matrizes a contextos de forma inteligente. Isso irá minimizar o tempo gasto na transferência de dados entre dispositivos.

Por exemplo, tente inicializar uma matriz da seguinte maneira -

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

Output

Ao executar o código acima, você verá a seguinte saída -

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

Podemos copiar o NDArray fornecido de um contexto para outro, usando o método copyto () da seguinte maneira -

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

Matriz NumPy vs. NDArray

Todos nós estamos familiarizados com arrays NumPy, mas o Apache MXNet oferece sua própria implementação de array chamada NDArray. Na verdade, ele foi inicialmente projetado para ser semelhante ao NumPy, mas há uma diferença fundamental -

A principal diferença está na maneira como os cálculos são executados em NumPy e NDArray. Cada manipulação de NDArray em MXNet é feita de forma assíncrona e não bloqueadora, o que significa que, quando escrevemos código como c = a * b, a função é enviada para oExecution Engine, que iniciará o cálculo.

Aqui, aeb são NDArrays. A vantagem de usá-lo é que a função retorna imediatamente e o thread do usuário pode continuar a execução, apesar do cálculo anterior ainda não ter sido concluído.

Funcionamento do mecanismo de execução

Se falamos sobre o funcionamento do mecanismo de execução, ele constrói o gráfico de computação. O gráfico de computação pode reordenar ou combinar alguns cálculos, mas sempre respeita a ordem de dependência.

Por exemplo, se houver outra manipulação com 'X' feita posteriormente no código de programação, o Execution Engine começará a fazê-las assim que o resultado de 'X' estiver disponível. O mecanismo de execução tratará de alguns trabalhos importantes para os usuários, como escrever retornos de chamada para iniciar a execução do código subsequente.

No Apache MXNet, com a ajuda de NDArray, para obter o resultado da computação, precisamos apenas acessar a variável resultante. O fluxo do código será bloqueado até que os resultados do cálculo sejam atribuídos à variável resultante. Dessa forma, ele aumenta o desempenho do código e, ao mesmo tempo, dá suporte ao modo de programação imperativo.

Convertendo NDArray em NumPy Array

Vamos aprender como podemos converter NDArray em NumPy Array em MXNet.

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

Às vezes, podemos montar um operador de nível superior usando os operadores existentes. Um dos melhores exemplos disso é onp.full_like()operador, que não existe na API NDArray. Ele pode ser facilmente substituído por uma combinação de operadores existentes da seguinte maneira:

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

Obteremos uma saída semelhante à seguinte -

True

Finding similar operator with different name and/or signature

Entre todos os operadores, alguns deles têm nomes ligeiramente diferentes, mas são semelhantes em termos de funcionalidade. Um exemplo disso énd.ravel_index() com np.ravel()funções. Da mesma forma, alguns operadores podem ter nomes semelhantes, mas com assinaturas diferentes. Um exemplo disso énp.split() e nd.split() são similares.

Vamos entender isso com o seguinte exemplo de programação:

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

O resultado é declarado abaixo -

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

Minimizando o impacto do bloqueio de chamadas

Em alguns casos, temos que usar .asnumpy() ou .asscalar()métodos, mas isso forçará o MXNet a bloquear a execução, até que o resultado possa ser recuperado. Podemos minimizar o impacto de uma chamada de bloqueio chamando.asnumpy() ou .asscalar() métodos no momento, quando pensamos que o cálculo deste valor já está feito.

Exemplo de Implementação

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

O resultado é citado abaixo:

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