5.2. Gerenciamento de Parâmetros
Open the notebook in Colab
Open the notebook in Colab
Open the notebook in Colab
Open the notebook in SageMaker Studio Lab

Depois de escolher uma arquitetura e definir nossos hiperparâmetros, passamos para o ciclo de treinamento, onde nosso objetivo é encontrar valores de parâmetro que minimizam nossa função de perda. Após o treinamento, precisaremos desses parâmetros para fazer previsões futuras. Além disso, às vezes desejamos para extrair os parâmetros seja para reutilizá-los em algum outro contexto, para salvar nosso modelo em disco para que pode ser executado em outro software, ou para exame na esperança de ganhar compreensão científica.

Na maioria das vezes, seremos capazes de ignorar os detalhes essenciais de como os parâmetros são declarados e manipulado, contando com estruturas de Deep Learning para fazer o trabalho pesado. No entanto, quando nos afastamos de arquiteturas empilhadas com camadas padrão, às vezes precisaremos declarar e manipular parâmetros. Nesta seção, cobrimos o seguinte:

  • Parâmetros de acesso para depuração, diagnóstico e visualizações.

  • Inicialização de parâmetros.

  • Parâmetros de compartilhamento em diferentes componentes do modelo.

Começamos nos concentrando em um MLP com uma camada oculta.

from mxnet import init, np, npx
from mxnet.gluon import nn

npx.set_np()

net = nn.Sequential()
net.add(nn.Dense(8, activation='relu'))
net.add(nn.Dense(1))
net.initialize()  # Use o método de inicialização padrão

X = np.random.uniform(size=(2, 4))
net(X)  # Forward computation
array([[0.0054572 ],
       [0.00488594]])
import torch
from torch import nn

net = nn.Sequential(nn.Linear(4, 8), nn.ReLU(), nn.Linear(8, 1))
X = torch.rand(size=(2, 4))
net(X)
tensor([[0.2046],
        [0.4232]], grad_fn=<AddmmBackward>)
import tensorflow as tf

net = tf.keras.models.Sequential([
    tf.keras.layers.Flatten(),
    tf.keras.layers.Dense(4, activation=tf.nn.relu),
    tf.keras.layers.Dense(1),
])

X = tf.random.uniform((2, 4))
net(X)
<tf.Tensor: shape=(2, 1), dtype=float32, numpy=
array([[-0.08388045],
       [-0.040921  ]], dtype=float32)>

5.2.1. Acesso a Parâmetros

Vamos começar explicando como acessar os parâmetros dos modelos que você já conhece. Quando um modelo é definido por meio da classe Sequential, podemos primeiro acessar qualquer camada indexando no modelo como se fosse uma lista. Os parâmetros de cada camada são convenientemente localizado em seu atributo. Podemos inspecionar os parâmetros da segunda camada totalmente conectada da seguinte maneira.

print(net[1].params)
dense1_ (
  Parameter dense1_weight (shape=(1, 8), dtype=float32)
  Parameter dense1_bias (shape=(1,), dtype=float32)
)
print(net[2].state_dict())
OrderedDict([('weight', tensor([[ 0.2166, -0.3507, -0.1181,  0.2769, -0.1432,  0.2410,  0.0740,  0.2221]])), ('bias', tensor([0.3118]))])
print(net.layers[2].weights)
[<tf.Variable 'dense_1/kernel:0' shape=(4, 1) dtype=float32, numpy=
array([[-0.8849803 ],
       [ 0.28838134],
       [ 0.8427515 ],
       [ 0.29679787]], dtype=float32)>, <tf.Variable 'dense_1/bias:0' shape=(1,) dtype=float32, numpy=array([0.], dtype=float32)>]

A saída nos diz algumas coisas importantes. Primeiro, esta camada totalmente conectada contém dois parâmetros, correspondendo aos pesos e vieses, respectivamente. Ambos são armazenados como floats de precisão simples (float32). Observe que os nomes dos parâmetros nos permitem identificar de forma única parâmetros de cada camada, mesmo em uma rede contendo centenas de camadas.

5.2.1.1. Parâmetros Direcionados

Observe que cada parâmetro é representado como uma instância da classe de parâmetro. Para fazer algo útil com os parâmetros, primeiro precisamos acessar os valores numéricos subjacentes. Existem várias maneiras de fazer isso. Alguns são mais simples, enquanto outros são mais gerais. O código a seguir extrai o viés da segunda camada de rede neural, que retorna uma instância de classe de parâmetro, e acessa posteriormente o valor desse parâmetro.

print(type(net[1].bias))
print(net[1].bias)
print(net[1].bias.data())
<class 'mxnet.gluon.parameter.Parameter'>
Parameter dense1_bias (shape=(1,), dtype=float32)
[0.]

Os parâmetros são objetos complexos, contendo valores, gradientes, e informações adicionais. É por isso que precisamos solicitar o valor explicitamente.

Além do valor, cada parâmetro também nos permite acessar o gradiente. Como ainda não invocamos a backpropagation para esta rede, ela está em seu estado inicial.

net[1].weight.grad()
array([[0., 0., 0., 0., 0., 0., 0., 0.]])
print(type(net[2].bias))
print(net[2].bias)
print(net[2].bias.data)
<class 'torch.nn.parameter.Parameter'>
Parameter containing:
tensor([0.3118], requires_grad=True)
tensor([0.3118])

Os parâmetros são objetos complexos, contendo valores, gradientes, e informações adicionais. É por isso que precisamos solicitar o valor explicitamente.

Além do valor, cada parâmetro também nos permite acessar o gradiente. Como ainda não invocamos a backpropagation para esta rede, ela está em seu estado inicial.

net[2].weight.grad == None
True
print(type(net.layers[2].weights[1]))
print(net.layers[2].weights[1])
print(tf.convert_to_tensor(net.layers[2].weights[1]))
<class 'tensorflow.python.ops.resource_variable_ops.ResourceVariable'>
<tf.Variable 'dense_1/bias:0' shape=(1,) dtype=float32, numpy=array([0.], dtype=float32)>
tf.Tensor([0.], shape=(1,), dtype=float32)

5.2.1.2. Todos os Parâmetros de Uma Vez

Quando precisamos realizar operações em todos os parâmetros, acessá-los um por um pode se tornar tedioso. A situação pode ficar especialmente complicada quando trabalhamos com blocos mais complexos (por exemplo, blocos aninhados), uma vez que precisaríamos voltar recursivamente através de toda a árvore para extrair parâmetros de cada sub-bloco. Abaixo, demonstramos como acessar os parâmetros da primeira camada totalmente conectada versus acessar todas as camadas.

print(net[0].collect_params())
print(net.collect_params())
dense0_ (
  Parameter dense0_weight (shape=(8, 4), dtype=float32)
  Parameter dense0_bias (shape=(8,), dtype=float32)
)
sequential0_ (
  Parameter dense0_weight (shape=(8, 4), dtype=float32)
  Parameter dense0_bias (shape=(8,), dtype=float32)
  Parameter dense1_weight (shape=(1, 8), dtype=float32)
  Parameter dense1_bias (shape=(1,), dtype=float32)
)
print(*[(name, param.shape) for name, param in net[0].named_parameters()])
print(*[(name, param.shape) for name, param in net.named_parameters()])
('weight', torch.Size([8, 4])) ('bias', torch.Size([8]))
('0.weight', torch.Size([8, 4])) ('0.bias', torch.Size([8])) ('2.weight', torch.Size([1, 8])) ('2.bias', torch.Size([1]))
print(net.layers[1].weights)
print(net.get_weights())
[<tf.Variable 'dense/kernel:0' shape=(4, 4) dtype=float32, numpy=
array([[-0.00696713,  0.06294048, -0.34609908,  0.28365868],
       [ 0.05357468, -0.3715149 , -0.21233797,  0.5667568 ],
       [ 0.75887805, -0.24776524,  0.58249944,  0.15801603],
       [-0.309555  , -0.46662605,  0.3352285 , -0.7561607 ]],
      dtype=float32)>, <tf.Variable 'dense/bias:0' shape=(4,) dtype=float32, numpy=array([0., 0., 0., 0.], dtype=float32)>]
[array([[-0.00696713,  0.06294048, -0.34609908,  0.28365868],
       [ 0.05357468, -0.3715149 , -0.21233797,  0.5667568 ],
       [ 0.75887805, -0.24776524,  0.58249944,  0.15801603],
       [-0.309555  , -0.46662605,  0.3352285 , -0.7561607 ]],
      dtype=float32), array([0., 0., 0., 0.], dtype=float32), array([[-0.8849803 ],
       [ 0.28838134],
       [ 0.8427515 ],
       [ 0.29679787]], dtype=float32), array([0.], dtype=float32)]

Isso nos fornece outra maneira de acessar os parâmetros da rede como segue.

net.collect_params()['dense1_bias'].data()
array([0.])
net.state_dict()['2.bias'].data
tensor([0.3118])
net.get_weights()[1]
array([0., 0., 0., 0.], dtype=float32)

5.2.1.3. Coletando Parâmetros de Blocos Aninhados

Vamos ver como funcionam as convenções de nomenclatura de parâmetros se aninharmos vários blocos uns dentro dos outros. Para isso, primeiro definimos uma função que produz blocos (uma fábrica de blocos, por assim dizer) e então combine-os dentro de blocos ainda maiores.

def block1():
    net = nn.Sequential()
    net.add(nn.Dense(32, activation='relu'))
    net.add(nn.Dense(16, activation='relu'))
    return net

def block2():
    net = nn.Sequential()
    for _ in range(4):
        # Nested here
        net.add(block1())
    return net

rgnet = nn.Sequential()
rgnet.add(block2())
rgnet.add(nn.Dense(10))
rgnet.initialize()
rgnet(X)
array([[-6.3465846e-09, -1.1096752e-09,  6.4161787e-09,  6.6354140e-09,
        -1.1265507e-09,  1.3284951e-10,  9.3619388e-09,  3.2229084e-09,
         5.9429879e-09,  8.8181435e-09],
       [-8.6219423e-09, -7.5150686e-10,  8.3133251e-09,  8.9321128e-09,
        -1.6740003e-09,  3.2405989e-10,  1.2115976e-08,  4.4926449e-09,
         8.0741742e-09,  1.2075874e-08]])
def block1():
    return nn.Sequential(nn.Linear(4, 8), nn.ReLU(),
                         nn.Linear(8, 4), nn.ReLU())

def block2():
    net = nn.Sequential()
    for i in range(4):
        # Nested here
        net.add_module(f'block {i}', block1())
    return net

rgnet = nn.Sequential(block2(), nn.Linear(4, 1))
rgnet(X)
tensor([[0.1150],
        [0.1150]], grad_fn=<AddmmBackward>)
def block1(name):
    return tf.keras.Sequential([
        tf.keras.layers.Flatten(),
        tf.keras.layers.Dense(4, activation=tf.nn.relu)],
        name=name)

def block2():
    net = tf.keras.Sequential()
    for i in range(4):
        # Nested here
        net.add(block1(name=f'block-{i}'))
    return net

rgnet = tf.keras.Sequential()
rgnet.add(block2())
rgnet.add(tf.keras.layers.Dense(1))
rgnet(X)
<tf.Tensor: shape=(2, 1), dtype=float32, numpy=
array([[-0.04170988],
       [-0.21115464]], dtype=float32)>

Agora que projetamos a rede, vamos ver como está organizado.

print(rgnet.collect_params)
print(rgnet.collect_params())
<bound method Block.collect_params of Sequential(
  (0): Sequential(
    (0): Sequential(
      (0): Dense(4 -> 32, Activation(relu))
      (1): Dense(32 -> 16, Activation(relu))
    )
    (1): Sequential(
      (0): Dense(16 -> 32, Activation(relu))
      (1): Dense(32 -> 16, Activation(relu))
    )
    (2): Sequential(
      (0): Dense(16 -> 32, Activation(relu))
      (1): Dense(32 -> 16, Activation(relu))
    )
    (3): Sequential(
      (0): Dense(16 -> 32, Activation(relu))
      (1): Dense(32 -> 16, Activation(relu))
    )
  )
  (1): Dense(16 -> 10, linear)
)>
sequential1_ (
  Parameter dense2_weight (shape=(32, 4), dtype=float32)
  Parameter dense2_bias (shape=(32,), dtype=float32)
  Parameter dense3_weight (shape=(16, 32), dtype=float32)
  Parameter dense3_bias (shape=(16,), dtype=float32)
  Parameter dense4_weight (shape=(32, 16), dtype=float32)
  Parameter dense4_bias (shape=(32,), dtype=float32)
  Parameter dense5_weight (shape=(16, 32), dtype=float32)
  Parameter dense5_bias (shape=(16,), dtype=float32)
  Parameter dense6_weight (shape=(32, 16), dtype=float32)
  Parameter dense6_bias (shape=(32,), dtype=float32)
  Parameter dense7_weight (shape=(16, 32), dtype=float32)
  Parameter dense7_bias (shape=(16,), dtype=float32)
  Parameter dense8_weight (shape=(32, 16), dtype=float32)
  Parameter dense8_bias (shape=(32,), dtype=float32)
  Parameter dense9_weight (shape=(16, 32), dtype=float32)
  Parameter dense9_bias (shape=(16,), dtype=float32)
  Parameter dense10_weight (shape=(10, 16), dtype=float32)
  Parameter dense10_bias (shape=(10,), dtype=float32)
)
print(rgnet)
Sequential(
  (0): Sequential(
    (block 0): Sequential(
      (0): Linear(in_features=4, out_features=8, bias=True)
      (1): ReLU()
      (2): Linear(in_features=8, out_features=4, bias=True)
      (3): ReLU()
    )
    (block 1): Sequential(
      (0): Linear(in_features=4, out_features=8, bias=True)
      (1): ReLU()
      (2): Linear(in_features=8, out_features=4, bias=True)
      (3): ReLU()
    )
    (block 2): Sequential(
      (0): Linear(in_features=4, out_features=8, bias=True)
      (1): ReLU()
      (2): Linear(in_features=8, out_features=4, bias=True)
      (3): ReLU()
    )
    (block 3): Sequential(
      (0): Linear(in_features=4, out_features=8, bias=True)
      (1): ReLU()
      (2): Linear(in_features=8, out_features=4, bias=True)
      (3): ReLU()
    )
  )
  (1): Linear(in_features=4, out_features=1, bias=True)
)
print(rgnet.summary())
Model: "sequential_1"
_________________________________________________________________
Layer (type)                 Output Shape              Param #
=================================================================
sequential_2 (Sequential)    (2, 4)                    80
_________________________________________________________________
dense_6 (Dense)              (2, 1)                    5
=================================================================
Total params: 85
Trainable params: 85
Non-trainable params: 0
_________________________________________________________________
None

Uma vez que as camadas são aninhadas hierarquicamente, também podemos acessá-los como se indexação por meio de listas aninhadas. Por exemplo, podemos acessar o primeiro bloco principal, dentro dele o segundo sub-bloco, e dentro disso o viés da primeira camada, com o seguinte.

rgnet[0][1][0].bias.data()
array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
       0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.])
rgnet[0][1][0].bias.data
tensor([ 0.3746,  0.1523, -0.2427, -0.0837, -0.3223, -0.3808, -0.1753,  0.4000])
rgnet.layers[0].layers[1].layers[1].weights[1]
<tf.Variable 'sequential_2/block-1/dense_3/bias:0' shape=(4,) dtype=float32, numpy=array([0., 0., 0., 0.], dtype=float32)>

5.2.2. Inicialização de Parâmetros

Agora que sabemos como acessar os parâmetros, vamos ver como inicializá-los corretamente. Discutimos a necessidade de inicialização adequada em Section 4.8. A estrutura de Deep Learning fornece inicializações aleatórias padrão para suas camadas. No entanto, muitas vezes queremos inicializar nossos pesos de acordo com vários outros protocolos. A estrutura fornece mais comumente protocolos usados e também permite criar um inicializador personalizado.

Por padrão, MXNet inicializa os parâmetros de peso ao desenhar aleatoriamente de uma distribuição uniforme \(U(-0.07, 0.07)\), limpar os parâmetros de polarização para zero. O módulo init do MXNet oferece uma variedade de métodos de inicialização predefinidos.

Por padrão, o PyTorch inicializa matrizes de ponderação e polarização uniformemente extraindo de um intervalo que é calculado de acordo com a dimensão de entrada e saída. O módulo nn.init do PyTorch oferece uma variedade de métodos de inicialização predefinidos.

Por padrão, Keras inicializa matrizes de ponderação uniformemente, tirando de um intervalo que é calculado de acordo com a dimensão de entrada e saída, e os parâmetros de polarização são todos definidos como zero. O TensorFlow oferece uma variedade de métodos de inicialização no módulo raiz e no módulo keras.initializers.

5.2.2.1. Inicialização Built-in

Vamos começar chamando inicializadores integrados. O código abaixo inicializa todos os parâmetros de peso como variáveis aleatórias gaussianas com desvio padrão de 0,01, enquanto os parâmetros de polarização são zerados.

# Aqui `force_reinit` garante que os parâmetros são inciados mesmo se
# eles já foram iniciados anteriormente
net.initialize(init=init.Normal(sigma=0.01), force_reinit=True)
net[0].weight.data()[0]
array([-0.00324057, -0.00895028, -0.00698632,  0.01030831])
def init_normal(m):
    if type(m) == nn.Linear:
        nn.init.normal_(m.weight, mean=0, std=0.01)
        nn.init.zeros_(m.bias)
net.apply(init_normal)
net[0].weight.data[0], net[0].bias.data[0]
(tensor([ 0.0045, -0.0082, -0.0063,  0.0008]), tensor(0.))
net = tf.keras.models.Sequential([
    tf.keras.layers.Flatten(),
    tf.keras.layers.Dense(
        4, activation=tf.nn.relu,
        kernel_initializer=tf.random_normal_initializer(mean=0, stddev=0.01),
        bias_initializer=tf.zeros_initializer()),
    tf.keras.layers.Dense(1)])

net(X)
net.weights[0], net.weights[1]
(<tf.Variable 'dense_7/kernel:0' shape=(4, 4) dtype=float32, numpy=
 array([[-0.00880642,  0.00698964, -0.00339042, -0.01238017],
        [-0.00936008,  0.00712868,  0.0120426 ,  0.01030472],
        [-0.00444559,  0.01566276, -0.00378464, -0.00585882],
        [-0.00496555,  0.00707446, -0.0006112 ,  0.00711826]],
       dtype=float32)>,
 <tf.Variable 'dense_7/bias:0' shape=(4,) dtype=float32, numpy=array([0., 0., 0., 0.], dtype=float32)>)

Também podemos inicializar todos os parâmetros a um determinado valor constante (digamos, 1).

net.initialize(init=init.Constant(1), force_reinit=True)
net[0].weight.data()[0]
array([1., 1., 1., 1.])
def init_constant(m):
    if type(m) == nn.Linear:
        nn.init.constant_(m.weight, 1)
        nn.init.zeros_(m.bias)
net.apply(init_constant)
net[0].weight.data[0], net[0].bias.data[0]
(tensor([1., 1., 1., 1.]), tensor(0.))
net = tf.keras.models.Sequential([
    tf.keras.layers.Flatten(),
    tf.keras.layers.Dense(
        4, activation=tf.nn.relu,
        kernel_initializer=tf.keras.initializers.Constant(1),
        bias_initializer=tf.zeros_initializer()),
    tf.keras.layers.Dense(1),
])

net(X)
net.weights[0], net.weights[1]
(<tf.Variable 'dense_9/kernel:0' shape=(4, 4) dtype=float32, numpy=
 array([[1., 1., 1., 1.],
        [1., 1., 1., 1.],
        [1., 1., 1., 1.],
        [1., 1., 1., 1.]], dtype=float32)>,
 <tf.Variable 'dense_9/bias:0' shape=(4,) dtype=float32, numpy=array([0., 0., 0., 0.], dtype=float32)>)

Também podemos aplicar inicializadores diferentes para certos blocos. Por exemplo, abaixo inicializamos a primeira camada com o inicializador Xavier e inicializar a segunda camada para um valor constante de 42.

net[0].weight.initialize(init=init.Xavier(), force_reinit=True)
net[1].initialize(init=init.Constant(42), force_reinit=True)
print(net[0].weight.data()[0])
print(net[1].weight.data())
[-0.17594433  0.02314097 -0.1992535   0.09509248]
[[42. 42. 42. 42. 42. 42. 42. 42.]]
def xavier(m):
    if type(m) == nn.Linear:
        nn.init.xavier_uniform_(m.weight)
def init_42(m):
    if type(m) == nn.Linear:
        nn.init.constant_(m.weight, 42)

net[0].apply(xavier)
net[2].apply(init_42)
print(net[0].weight.data[0])
print(net[2].weight.data)
tensor([ 0.5433, -0.5930, -0.6287, -0.6148])
tensor([[42., 42., 42., 42., 42., 42., 42., 42.]])
net = tf.keras.models.Sequential([
    tf.keras.layers.Flatten(),
    tf.keras.layers.Dense(
        4,
        activation=tf.nn.relu,
        kernel_initializer=tf.keras.initializers.GlorotUniform()),
    tf.keras.layers.Dense(
        1, kernel_initializer=tf.keras.initializers.Constant(1)),
])

net(X)
print(net.layers[1].weights[0])
print(net.layers[2].weights[0])
<tf.Variable 'dense_11/kernel:0' shape=(4, 4) dtype=float32, numpy=
array([[-0.2608738 , -0.22718716,  0.11381751, -0.08536631],
       [-0.32224733,  0.3555506 ,  0.52300423, -0.6358117 ],
       [ 0.52879506,  0.32641047, -0.37077838, -0.72854525],
       [-0.29767555, -0.3362043 , -0.71178615, -0.1830396 ]],
      dtype=float32)>
<tf.Variable 'dense_12/kernel:0' shape=(4, 1) dtype=float32, numpy=
array([[1.],
       [1.],
       [1.],
       [1.]], dtype=float32)>

5.2.2.2. Inicialização Customizada

Às vezes, os métodos de inicialização de que precisamos não são fornecidos pela estrutura de Deep Learning. No exemplo abaixo, definimos um inicializador para qualquer parâmetro de peso \(w\) usando a seguinte distribuição estranha:

(5.2.1)\[\begin{split}\begin{aligned} w \sim \begin{cases} U(5, 10) & \text{ with probability } \frac{1}{4} \\ 0 & \text{ with probability } \frac{1}{2} \\ U(-10, -5) & \text{ with probability } \frac{1}{4} \end{cases} \end{aligned}\end{split}\]

Aqui definimos uma subclasse da classe Initializer. Normalmente, só precisamos implementar a função _init_weight que leva um argumento tensor (data) e atribui a ele os valores inicializados desejados.

class MyInit(init.Initializer):
    def _init_weight(self, name, data):
        print('Init', name, data.shape)
        data[:] = np.random.uniform(-10, 10, data.shape)
        data *= np.abs(data) >= 5

net.initialize(MyInit(), force_reinit=True)
net[0].weight.data()[:2]
Init dense0_weight (8, 4)
Init dense1_weight (1, 8)
array([[ 0.       , -0.       , -0.       ,  8.522827 ],
       [ 0.       , -8.828651 , -0.       , -5.6012006]])

Novamente, implementamos uma função my_init para aplicar anet.

def my_init(m):
    if type(m) == nn.Linear:
        print("Init", *[(name, param.shape)
                        for name, param in m.named_parameters()][0])
        nn.init.uniform_(m.weight, -10, 10)
        m.weight.data *= m.weight.data.abs() >= 5

net.apply(my_init)
net[0].weight[:2]
Init weight torch.Size([8, 4])
Init weight torch.Size([1, 8])
tensor([[ 6.2500,  9.0335,  6.4244, -6.6561],
        [-5.9088,  9.8715,  7.8404,  0.0000]], grad_fn=<SliceBackward>)

Aqui nós definimos uma subclasse de Initializer e implementamos o__call__ função que retorna um tensor desejado de acordo com a forma e o tipo de dados.

class MyInit(tf.keras.initializers.Initializer):
    def __call__(self, shape, dtype=None):
        data=tf.random.uniform(shape, -10, 10, dtype=dtype)
        factor=(tf.abs(data) >= 5)
        factor=tf.cast(factor, tf.float32)
        return data * factor

net = tf.keras.models.Sequential([
    tf.keras.layers.Flatten(),
    tf.keras.layers.Dense(
        4,
        activation=tf.nn.relu,
        kernel_initializer=MyInit()),
    tf.keras.layers.Dense(1),
])

net(X)
print(net.layers[1].weights[0])
<tf.Variable 'dense_13/kernel:0' shape=(4, 4) dtype=float32, numpy=
array([[ 5.46612  , -0.       ,  8.619307 ,  0.       ],
       [ 7.9447002,  9.824312 ,  6.20899  , -6.143732 ],
       [ 8.9025135, -6.1618114, -6.355505 , -6.9544387],
       [ 0.       , -0.       ,  5.6121492, -5.8810377]], dtype=float32)>

Observe que sempre temos a opção de definir parâmetros diretamente.

net[0].weight.data()[:] += 1
net[0].weight.data()[0, 0] = 42
net[0].weight.data()[0]
array([42.      ,  1.      ,  1.      ,  9.522827])

Uma observação para usuários avançados: se você quiser ajustar os parâmetros dentro de um escopo autograd, você precisa usar set_data para evitar confundir a mecânica de diferenciação automática.

net[0].weight.data[:] += 1
net[0].weight.data[0, 0] = 42
net[0].weight.data[0]
tensor([42.0000, 10.0335,  7.4244, -5.6561])
net.layers[1].weights[0][:].assign(net.layers[1].weights[0] + 1)
net.layers[1].weights[0][0, 0].assign(42)
net.layers[1].weights[0]
<tf.Variable 'dense_13/kernel:0' shape=(4, 4) dtype=float32, numpy=
array([[42.       ,  1.       ,  9.619307 ,  1.       ],
       [ 8.9447   , 10.824312 ,  7.20899  , -5.143732 ],
       [ 9.9025135, -5.1618114, -5.355505 , -5.9544387],
       [ 1.       ,  1.       ,  6.6121492, -4.8810377]], dtype=float32)>

5.2.3. Parâmetros Tied

Frequentemente, queremos compartilhar parâmetros em várias camadas. Vamos ver como fazer isso com elegância. A seguir, alocamos uma camada densa e usar seus parâmetros especificamente para definir os de outra camada.

net = nn.Sequential()
# Precisamos dar as camadas compartilhadas um nome
# para que possamos referenciar seus parâmetros
shared = nn.Dense(8, activation='relu')
net.add(nn.Dense(8, activation='relu'),
        shared,
        nn.Dense(8, activation='relu', params=shared.params),
        nn.Dense(10))
net.initialize()

X = np.random.uniform(size=(2, 20))
net(X)

# Checar se são os mesmos parâmetros
print(net[1].weight.data()[0] == net[2].weight.data()[0])
net[1].weight.data()[0, 0] = 100
# Garantindo que são o mesmo objeto ao invés de ter
# apenas o mesmo valor

print(net[1].weight.data()[0] == net[2].weight.data()[0])
[ True  True  True  True  True  True  True  True]
[ True  True  True  True  True  True  True  True]

Este exemplo mostra que os parâmetros da segunda e terceira camadas são amarrados. Eles não são apenas iguais, eles são representado pelo mesmo tensor exato. Assim, se mudarmos um dos parâmetros, o outro também muda. Você pode se perguntar, quando os parâmetros são amarrados o que acontece com os gradientes? Uma vez que os parâmetros do modelo contêm gradientes, os gradientes da segunda camada oculta e a terceira camada oculta são adicionadas juntas durante a retropropagação.

# Precisamos dar as camadas compartilhadas um nome
# para que possamos referenciar seus parâmetros

#
shared = nn.Linear(8, 8)
net = nn.Sequential(nn.Linear(4, 8), nn.ReLU(),
                    shared, nn.ReLU(),
                    shared, nn.ReLU(),
                    nn.Linear(8, 1))
net(X)
# Checar se são os mesmos parâmetros
print(net[2].weight.data[0] == net[4].weight.data[0])
net[2].weight.data[0, 0] = 100
# Garantindo que são o mesmo objeto ao invés de ter
# apenas o mesmo valor
print(net[2].weight.data[0] == net[4].weight.data[0])
tensor([True, True, True, True, True, True, True, True])
tensor([True, True, True, True, True, True, True, True])

Este exemplo mostra que os parâmetros da segunda e terceira camadas são amarrados. Eles não são apenas iguais, eles são representado pelo mesmo tensor exato. Assim, se mudarmos um dos parâmetros, o outro também muda. Você pode se perguntar, quando os parâmetros são amarrados o que acontece com os gradientes? Uma vez que os parâmetros do modelo contêm gradientes, os gradientes da segunda camada oculta e a terceira camada oculta são adicionadas juntas durante a retropropagação.

# tf.keras behaves a bit differently. It removes the duplicate layer
# automatically
shared = tf.keras.layers.Dense(4, activation=tf.nn.relu)
net = tf.keras.models.Sequential([
    tf.keras.layers.Flatten(),
    shared,
    shared,
    tf.keras.layers.Dense(1),
])

net(X)
# Checando se os parâmetros são diferentes
print(len(net.layers) == 3)
True

5.2.4. Sumário

  • Temos várias maneiras de acessar, inicializar e vincular os parâmetros do modelo.

  • Podemos usar inicialização personalizada.

5.2.5. Exercícios

  1. Use o modelo FancyMLP definido em Section 5.1 e acesse os parâmetros das várias camadas.

  2. Observe o documento do módulo de inicialização para explorar diferentes inicializadores.

  3. Construa um MLP contendo uma camada de parâmetros compartilhados e treine-o. Durante o processo de treinamento, observe os parâmetros do modelo e gradientes de cada camada.

  4. Por que compartilhar parâmetros é uma boa ideia?