5.2. Gerenciamento de Parâmetros¶ 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:
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¶
Use o modelo
FancyMLP
definido em Section 5.1 e acesse os parâmetros das várias camadas.Observe o documento do módulo de inicialização para explorar diferentes inicializadores.
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.
Por que compartilhar parâmetros é uma boa ideia?