Apache MXNet - Gluon

Outro pacote MXNet Python mais importante é o Gluon. Neste capítulo, iremos discutir este pacote. O Gluon fornece uma API clara, concisa e simples para projetos DL. Ele permite que o Apache MXNet crie protótipos, construa e treine modelos DL sem perder a velocidade de treinamento.

Blocos

Os blocos formam a base de projetos de rede mais complexos. Em uma rede neural, conforme a complexidade da rede neural aumenta, precisamos passar do projeto de camadas simples para camadas inteiras de neurônios. Por exemplo, o design NN como o ResNet-152 tem um grau muito razoável de regularidade ao consistir emblocks de camadas repetidas.

Exemplo

No exemplo abaixo, iremos escrever um bloco simples de código, ou seja, um bloco para um perceptron multicamadas.

from mxnet import nd
from mxnet.gluon import nn
x = nd.random.uniform(shape=(2, 20))
N_net = nn.Sequential()
N_net.add(nn.Dense(256, activation='relu'))
N_net.add(nn.Dense(10))
N_net.initialize()
N_net(x)

Output

Isso produz a seguinte saída:

[[ 0.09543004 0.04614332 -0.00286655 -0.07790346 -0.05130241 0.02942038
0.08696645 -0.0190793 -0.04122177 0.05088576]
[ 0.0769287 0.03099706 0.00856576 -0.044672 -0.06926838 0.09132431
0.06786592 -0.06187843 -0.03436674 0.04234696]]
<NDArray 2x10 @cpu(0)>

Etapas necessárias para ir desde a definição de camadas até a definição de blocos de uma ou mais camadas -

Step 1 - Bloco recebe os dados como entrada.

Step 2- Agora, os blocos irão armazenar o estado na forma de parâmetros. Por exemplo, no exemplo de codificação acima, o bloco contém duas camadas ocultas e precisamos de um local para armazenar seus parâmetros.

Step 3- O próximo bloco invocará a função direta para realizar a propagação direta. Também é chamado de computação direta. Como parte da primeira chamada de encaminhamento, os blocos inicializam os parâmetros de maneira preguiçosa.

Step 4- Por fim, os blocos invocarão a função de retrocesso e calcularão o gradiente com referência à sua entrada. Normalmente, esta etapa é executada automaticamente.

Bloco Sequencial

Um bloco sequencial é um tipo especial de bloco no qual os dados fluem por uma sequência de blocos. Neste, cada bloco aplicado à saída de um antes com o primeiro bloco sendo aplicado aos próprios dados de entrada.

Vamos ver como sequential trabalhos de classe -

from mxnet import nd
from mxnet.gluon import nn
class MySequential(nn.Block):
   def __init__(self, **kwargs):
      super(MySequential, self).__init__(**kwargs)

   def add(self, block):
      self._children[block.name] = block
   def forward(self, x):
   for block in self._children.values():
      x = block(x)
   return x
x = nd.random.uniform(shape=(2, 20))
N_net = MySequential()
N_net.add(nn.Dense(256, activation
='relu'))
N_net.add(nn.Dense(10))
N_net.initialize()
N_net(x)

Output

A saída é fornecida aqui -

[[ 0.09543004 0.04614332 -0.00286655 -0.07790346 -0.05130241 0.02942038
0.08696645 -0.0190793 -0.04122177 0.05088576]
[ 0.0769287 0.03099706 0.00856576 -0.044672 -0.06926838 0.09132431
0.06786592 -0.06187843 -0.03436674 0.04234696]]
<NDArray 2x10 @cpu(0)>

Bloco Personalizado

Podemos facilmente ir além da concatenação com bloco sequencial conforme definido acima. Mas, se quisermos fazer personalizações, oBlockclasse também nos fornece a funcionalidade necessária. A classe de bloco tem um construtor de modelo fornecido no módulo nn. Podemos herdar esse construtor de modelo para definir o modelo que queremos.

No exemplo a seguir, o MLP class substitui o __init__ e funções de encaminhamento da classe Block.

Vamos ver como funciona.

class MLP(nn.Block):

   def __init__(self, **kwargs):
      super(MLP, self).__init__(**kwargs)
      self.hidden = nn.Dense(256, activation='relu') # Hidden layer
      self.output = nn.Dense(10) # Output layer


   def forward(self, x):
      hidden_out = self.hidden(x)
      return self.output(hidden_out)
x = nd.random.uniform(shape=(2, 20))
N_net = MLP()
N_net.initialize()
N_net(x)

Output

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

[[ 0.07787763 0.00216403 0.01682201 0.03059879 -0.00702019 0.01668715
0.04822846 0.0039432 -0.09300035 -0.04494302]
[ 0.08891078 -0.00625484 -0.01619131 0.0380718 -0.01451489 0.02006172
0.0303478 0.02463485 -0.07605448 -0.04389168]]
<NDArray 2x10 @cpu(0)>

Camadas Personalizadas

A API Gluon do Apache MXNet vem com um número modesto de camadas predefinidas. Mas ainda em algum ponto, podemos descobrir que uma nova camada é necessária. Podemos facilmente adicionar uma nova camada na API Gluon. Nesta seção, veremos como podemos criar uma nova camada do zero.

A Camada Personalizada Mais Simples

Para criar uma nova camada na API Gluon, devemos criar uma classe herda da classe Block que fornece a funcionalidade mais básica. Podemos herdar todas as camadas predefinidas diretamente ou por meio de outras subclasses.

Para criar a nova camada, o único método de instância necessário para ser implementado é forward (self, x). Este método define o que exatamente nossa camada fará durante a propagação direta. Conforme discutido anteriormente, a passagem de propagação reversa para blocos será feita pelo próprio Apache MXNet automaticamente.

Exemplo

No exemplo abaixo, estaremos definindo uma nova camada. Também implementaremosforward() método para normalizar os dados de entrada ajustando-os em um intervalo de [0, 1].

from __future__ import print_function
import mxnet as mx
from mxnet import nd, gluon, autograd
from mxnet.gluon.nn import Dense
mx.random.seed(1)
class NormalizationLayer(gluon.Block):
   def __init__(self):
      super(NormalizationLayer, self).__init__()

   def forward(self, x):
      return (x - nd.min(x)) / (nd.max(x) - nd.min(x))
x = nd.random.uniform(shape=(2, 20))
N_net = NormalizationLayer()
N_net.initialize()
N_net(x)

Output

Ao executar o programa acima, você obterá o seguinte resultado -

[[0.5216355 0.03835821 0.02284337 0.5945146 0.17334817 0.69329053
0.7782702 1. 0.5508242 0. 0.07058554 0.3677264
0.4366546 0.44362497 0.7192635 0.37616986 0.6728799 0.7032008

 0.46907538 0.63514024]
[0.9157533 0.7667402 0.08980197   0.03593295 0.16176797 0.27679572
 0.07331014 0.3905285 0.6513384 0.02713427 0.05523694 0.12147208
 0.45582628 0.8139887 0.91629887 0.36665893 0.07873632 0.78268915
 0.63404864 0.46638715]]
 <NDArray 2x20 @cpu(0)>

Hibridação

Pode ser definido como um processo usado pelo Apache MXNet para criar um gráfico simbólico de uma computação direta. A hibridização permite que o MXNet aumente o desempenho de computação otimizando o gráfico simbólico computacional. Em vez de herdar diretamente deBlock, na verdade, podemos descobrir que, ao implementar as camadas existentes, um bloco herda de um HybridBlock.

A seguir estão as razões para isso -

  • Allows us to write custom layers: HybridBlock nos permite escrever camadas personalizadas que podem ser usadas posteriormente na programação imperativa e simbólica.

  • Increase computation performance- HybridBlock otimiza o gráfico simbólico computacional que permite ao MXNet aumentar o desempenho computacional.

Exemplo

Neste exemplo, estaremos reescrevendo nossa camada de exemplo, criada acima, usando HybridBlock:

class NormalizationHybridLayer(gluon.HybridBlock):
   def __init__(self):
      super(NormalizationHybridLayer, self).__init__()

   def hybrid_forward(self, F, x):
      return F.broadcast_div(F.broadcast_sub(x, F.min(x)), (F.broadcast_sub(F.max(x), F.min(x))))

layer_hybd = NormalizationHybridLayer()
layer_hybd(nd.array([1, 2, 3, 4, 5, 6], ctx=mx.cpu()))

Output

O resultado é declarado abaixo:

[0. 0.2 0.4 0.6 0.8 1. ]
<NDArray 6 @cpu(0)>

A hibridização não tem nada a ver com computação na GPU e pode-se treinar redes hibridizadas ou não hibridizadas na CPU e GPU.

Diferença entre Block e HybridBlock

Se compararmos o Block Classe e HybridBlock, nós veremos isso HybridBlock já tem o seu forward() método implementado. HybridBlock define um hybrid_forward()método que precisa ser implementado durante a criação das camadas. O argumento F cria a principal diferença entreforward() e hybrid_forward(). Na comunidade MXNet, o argumento F é conhecido como back-end. F pode se referir amxnet.ndarray API (usado para programação imperativa) ou mxnet.symbol API (usado para programação simbólica).

Como adicionar uma camada personalizada a uma rede?

Em vez de usar camadas personalizadas separadamente, essas camadas são usadas com camadas predefinidas. Podemos usar qualquer umSequential ou HybridSequentialcontêineres de uma rede neural sequencial. Conforme discutido anteriormente também,Sequential recipiente herda de Bloco e HybridSequential herdar de HybridBlock respectivamente.

Exemplo

No exemplo abaixo, estaremos criando uma rede neural simples com uma camada personalizada. A saída deDense (5) camada será a entrada de NormalizationHybridLayer. A saída deNormalizationHybridLayer se tornará a entrada de Dense (1) camada.

net = gluon.nn.HybridSequential()
with net.name_scope():
net.add(Dense(5))
net.add(NormalizationHybridLayer())
net.add(Dense(1))
net.initialize(mx.init.Xavier(magnitude=2.24))
net.hybridize()
input = nd.random_uniform(low=-10, high=10, shape=(10, 2))
net(input)

Output

Você verá a seguinte saída -

[[-1.1272651]
 [-1.2299833]
 [-1.0662932]
 [-1.1805027]
 [-1.3382034]
 [-1.2081106]
 [-1.1263978]
 [-1.2524893]
 
 [-1.1044774]

 [-1.316593 ]]
<NDArray 10x1 @cpu(0)>

Parâmetros de camada personalizados

Em uma rede neural, uma camada possui um conjunto de parâmetros associados a ela. Às vezes, nos referimos a eles como pesos, que é o estado interno de uma camada. Esses parâmetros desempenham papéis diferentes -

  • Às vezes, esses são os que queremos aprender durante a etapa de retropropagação.

  • Às vezes, essas são apenas constantes que queremos usar durante o passe para frente.

Se falamos sobre o conceito de programação, esses parâmetros (pesos) de um bloco são armazenados e acessados ​​via ParameterDict classe que ajuda na inicialização, atualização, salvamento e carregamento deles.

Exemplo

No exemplo abaixo, iremos definir dois conjuntos de parâmetros a seguir -

  • Parameter weights- É treinável e sua forma é desconhecida durante a fase de construção. Será inferido na primeira execução da propagação direta.

  • Parameter scale- Esta é uma constante cujo valor não muda. Ao contrário dos pesos dos parâmetros, sua forma é definida durante a construção.

class NormalizationHybridLayer(gluon.HybridBlock):
   def __init__(self, hidden_units, scales):
      super(NormalizationHybridLayer, self).__init__()
      with self.name_scope():
      self.weights = self.params.get('weights',
      shape=(hidden_units, 0),
      allow_deferred_init=True)
      self.scales = self.params.get('scales',
         shape=scales.shape,
         init=mx.init.Constant(scales.asnumpy()),
         differentiable=False)
      def hybrid_forward(self, F, x, weights, scales):
         normalized_data = F.broadcast_div(F.broadcast_sub(x, F.min(x)),
         (F.broadcast_sub(F.max(x), F.min(x))))
         weighted_data = F.FullyConnected(normalized_data, weights, num_hidden=self.weights.shape[0], no_bias=True)
         scaled_data = F.broadcast_mul(scales, weighted_data)
return scaled_data