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