.. _sec_scheduler:
Programação da taxa de aprendizagem
===================================
Até agora, focamos principalmente na otimização de *algoritmos* para
como atualizar os vetores de peso, em vez de na *taxa* na qual eles
estão sendo atualizados. No entanto, ajustar a taxa de aprendizagem é
frequentemente tão importante quanto o algoritmo real. Existem vários
aspectos a considerar:
- Obviamente, a *magnitude* da taxa de aprendizagem é importante. Se
for muito grande, a otimização diverge; se for muito pequena, leva
muito tempo para treinar ou terminamos com um resultado abaixo do
ideal. Vimos anteriormente que o número da condição do problema é
importante (consulte, por exemplo, :numref:`sec_momentum` para
obter detalhes). Intuitivamente, é a proporção da quantidade de
mudança na direção menos sensível em relação à mais sensível.
- Em segundo lugar, a taxa de degradação é tão importante. Se a taxa de
aprendizado permanecer alta, podemos simplesmente acabar saltando em
torno do mínimo e, portanto, não atingir a otimização.
:numref:`sec_minibatch_sgd` discutiu isso com alguns detalhes e
analisamos as garantias de desempenho em :numref:`sec_sgd`.
Resumindo, queremos que a taxa diminua, mas provavelmente mais
lentamente do que :math:`\mathcal{O}(t^{-\frac{1}{2}})`, o que seria
uma boa escolha para problemas convexos.
- Outro aspecto igualmente importante é a *inicialização*. Isso se
refere a como os parâmetros são definidos inicialmente (revise
:numref:`sec_numerical_stability` para detalhes) e também como eles
evoluem inicialmente. Isso tem o nome de *aquecimento*, ou seja, a
rapidez com que começamos a nos mover em direção à solução
inicialmente. Etapas grandes no início podem não ser benéficas, em
particular porque o conjunto inicial de parâmetros é aleatório. As
instruções iniciais de atualização também podem ser bastante
insignificantes.
- Por último, há uma série de variantes de otimização que realizam
ajustes de taxa de aprendizagem cíclica. Isso está além do escopo do
capítulo atual. Recomendamos que o leitor analise os detalhes em
:cite:`Izmailov.Podoprikhin.Garipov.ea.2018`, por exemplo, como
obter melhores soluções calculando a média de um caminho inteiro de
parâmetros.
Dado o fato de que são necessários muitos detalhes para gerenciar as
taxas de aprendizado, a maioria dos frameworks de aprendizado profundo
tem ferramentas para lidar com isso automaticamente. No capítulo atual,
revisaremos os efeitos que diferentes programações têm na precisão e
também mostraremos como isso pode ser gerenciado de forma eficiente por
meio de um *programador de taxa de aprendizagem*.
Problema Amostra
----------------
Começamos com um problema de brinquedo que é barato o suficiente para
ser computado facilmente, mas suficientemente não trivial para ilustrar
alguns dos principais aspectos. Para isso, escolhemos uma versão
ligeiramente modernizada do LeNet (``relu`` em vez de ativação
``sigmoid``, MaxPooling em vez de AveragePooling), aplicado ao
Fashion-MNIST. Além disso, hibridamos a rede para desempenho. Como a
maior parte do código é padrão, apenas apresentamos o básico sem uma
discussão mais detalhada. Veja :numref:`chap_cnn` para uma atualização
conforme necessário.
.. raw:: html
.. raw:: html
.. code:: python
%matplotlib inline
from mxnet import autograd, gluon, init, lr_scheduler, np, npx
from mxnet.gluon import nn
from d2l import mxnet as d2l
npx.set_np()
net = nn.HybridSequential()
net.add(nn.Conv2D(channels=6, kernel_size=5, padding=2, activation='relu'),
nn.MaxPool2D(pool_size=2, strides=2),
nn.Conv2D(channels=16, kernel_size=5, activation='relu'),
nn.MaxPool2D(pool_size=2, strides=2),
nn.Dense(120, activation='relu'),
nn.Dense(84, activation='relu'),
nn.Dense(10))
net.hybridize()
loss = gluon.loss.SoftmaxCrossEntropyLoss()
device = d2l.try_gpu()
batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size=batch_size)
# The code is almost identical to `d2l.train_ch6` defined in the
# lenet section of chapter convolutional neural networks
def train(net, train_iter, test_iter, num_epochs, loss, trainer, device):
net.initialize(force_reinit=True, ctx=device, init=init.Xavier())
animator = d2l.Animator(xlabel='epoch', xlim=[0, num_epochs],
legend=['train loss', 'train acc', 'test acc'])
for epoch in range(num_epochs):
metric = d2l.Accumulator(3) # train_loss, train_acc, num_examples
for i, (X, y) in enumerate(train_iter):
X, y = X.as_in_ctx(device), y.as_in_ctx(device)
with autograd.record():
y_hat = net(X)
l = loss(y_hat, y)
l.backward()
trainer.step(X.shape[0])
metric.add(l.sum(), d2l.accuracy(y_hat, y), X.shape[0])
train_loss = metric[0] / metric[2]
train_acc = metric[1] / metric[2]
if (i + 1) % 50 == 0:
animator.add(epoch + i / len(train_iter),
(train_loss, train_acc, None))
test_acc = d2l.evaluate_accuracy_gpu(net, test_iter)
animator.add(epoch + 1, (None, None, test_acc))
print(f'train loss {train_loss:.3f}, train acc {train_acc:.3f}, '
f'test acc {test_acc:.3f}')
.. raw:: html
.. raw:: html
.. code:: python
%matplotlib inline
import math
import torch
from torch import nn
from torch.optim import lr_scheduler
from d2l import torch as d2l
def net_fn():
class Reshape(nn.Module):
def forward(self, x):
return x.view(-1,1,28,28)
model = torch.nn.Sequential(
Reshape(),
nn.Conv2d(1, 6, kernel_size=5, padding=2), nn.ReLU(),
nn.MaxPool2d(kernel_size=2, stride=2),
nn.Conv2d(6, 16, kernel_size=5), nn.ReLU(),
nn.MaxPool2d(kernel_size=2, stride=2),
nn.Flatten(),
nn.Linear(16 * 5 * 5, 120), nn.ReLU(),
nn.Linear(120, 84), nn.ReLU(),
nn.Linear(84, 10))
return model
loss = nn.CrossEntropyLoss()
device = d2l.try_gpu()
batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size=batch_size)
# The code is almost identical to `d2l.train_ch6` defined in the
# lenet section of chapter convolutional neural networks
def train(net, train_iter, test_iter, num_epochs, loss, trainer, device,
scheduler=None):
net.to(device)
animator = d2l.Animator(xlabel='epoch', xlim=[0, num_epochs],
legend=['train loss', 'train acc', 'test acc'])
for epoch in range(num_epochs):
metric = d2l.Accumulator(3) # train_loss, train_acc, num_examples
for i, (X, y) in enumerate(train_iter):
net.train()
trainer.zero_grad()
X, y = X.to(device), y.to(device)
y_hat = net(X)
l = loss(y_hat, y)
l.backward()
trainer.step()
with torch.no_grad():
metric.add(l * X.shape[0], d2l.accuracy(y_hat, y), X.shape[0])
train_loss = metric[0] / metric[2]
train_acc = metric[1] / metric[2]
if (i + 1) % 50 == 0:
animator.add(epoch + i / len(train_iter),
(train_loss, train_acc, None))
test_acc = d2l.evaluate_accuracy_gpu(net, test_iter)
animator.add(epoch+1, (None, None, test_acc))
if scheduler:
if scheduler.__module__ == lr_scheduler.__name__:
# Using PyTorch In-Built scheduler
scheduler.step()
else:
# Using custom defined scheduler
for param_group in trainer.param_groups:
param_group['lr'] = scheduler(epoch)
print(f'train loss {train_loss:.3f}, train acc {train_acc:.3f}, '
f'test acc {test_acc:.3f}')
.. raw:: html
.. raw:: html
.. code:: python
%matplotlib inline
import math
import tensorflow as tf
from tensorflow.keras.callbacks import LearningRateScheduler
from d2l import tensorflow as d2l
def net():
return tf.keras.models.Sequential([
tf.keras.layers.Conv2D(filters=6, kernel_size=5, activation='relu',
padding='same'),
tf.keras.layers.AvgPool2D(pool_size=2, strides=2),
tf.keras.layers.Conv2D(filters=16, kernel_size=5,
activation='relu'),
tf.keras.layers.AvgPool2D(pool_size=2, strides=2),
tf.keras.layers.Flatten(),
tf.keras.layers.Dense(120, activation='relu'),
tf.keras.layers.Dense(84, activation='sigmoid'),
tf.keras.layers.Dense(10)])
batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size=batch_size)
# The code is almost identical to `d2l.train_ch6` defined in the
# lenet section of chapter convolutional neural networks
def train(net_fn, train_iter, test_iter, num_epochs, lr,
device=d2l.try_gpu(), custom_callback = False):
device_name = device._device_name
strategy = tf.distribute.OneDeviceStrategy(device_name)
with strategy.scope():
optimizer = tf.keras.optimizers.SGD(learning_rate=lr)
loss = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True)
net = net_fn()
net.compile(optimizer=optimizer, loss=loss, metrics=['accuracy'])
callback = d2l.TrainCallback(net, train_iter, test_iter, num_epochs,
device_name)
if custom_callback is False:
net.fit(train_iter, epochs=num_epochs, verbose=0,
callbacks=[callback])
else:
net.fit(train_iter, epochs=num_epochs, verbose=0,
callbacks=[callback, custom_callback])
return net
.. raw:: html
.. raw:: html
Vamos dar uma olhada no que acontece se invocarmos esse algoritmo com
configurações padrão, como uma taxa de aprendizado de :math:`0,3` e
treinar por :math:`30` iterações. Observe como a precisão do treinamento
continua aumentando enquanto o progresso em termos de precisão do teste
para além de um ponto. A lacuna entre as duas curvas indica sobreajuste.
.. raw:: html
.. raw:: html
.. code:: python
lr, num_epochs = 0.3, 30
net.initialize(force_reinit=True, ctx=device, init=init.Xavier())
trainer = gluon.Trainer(net.collect_params(), 'sgd', {'learning_rate': lr})
train(net, train_iter, test_iter, num_epochs, loss, trainer, device)
.. parsed-literal::
:class: output
train loss 0.156, train acc 0.939, test acc 0.878
.. figure:: output_lr-scheduler_1dfeb6_15_1.svg
.. raw:: html
.. raw:: html
.. code:: python
lr, num_epochs = 0.3, 30
net = net_fn()
trainer = torch.optim.SGD(net.parameters(), lr=lr)
train(net, train_iter, test_iter, num_epochs, loss, trainer, device)
.. parsed-literal::
:class: output
train loss 0.168, train acc 0.935, test acc 0.894
.. figure:: output_lr-scheduler_1dfeb6_18_1.svg
.. raw:: html
.. raw:: html
.. code:: python
lr, num_epochs = 0.3, 30
train(net, train_iter, test_iter, num_epochs, lr)
.. parsed-literal::
:class: output
loss 0.206, train acc 0.924, test acc 0.896
65831.8 examples/sec on /GPU:0
.. parsed-literal::
:class: output
.. figure:: output_lr-scheduler_1dfeb6_21_2.svg
.. raw:: html
.. raw:: html
Agendadores
-----------
Uma forma de ajustar a taxa de aprendizagem é defini-la explicitamente
em cada etapa. Isso é convenientemente alcançado pelo método
``set_learning_rate``. Poderíamos ajustá-lo para baixo após cada época
(ou mesmo após cada minibatch), por exemplo, de uma maneira dinâmica em
resposta a como a otimização está progredindo.
.. raw:: html
.. raw:: html
.. code:: python
trainer.set_learning_rate(0.1)
print(f'learning rate is now {trainer.learning_rate:.2f}')
.. parsed-literal::
:class: output
learning rate is now 0.10
.. raw:: html
.. raw:: html
.. code:: python
lr = 0.1
trainer.param_groups[0]["lr"] = lr
print(f'learning rate is now {trainer.param_groups[0]["lr"]:.2f}')
.. parsed-literal::
:class: output
learning rate is now 0.10
.. raw:: html
.. raw:: html
.. code:: python
lr = 0.1
dummy_model = tf.keras.models.Sequential([tf.keras.layers.Dense(10)])
dummy_model.compile(tf.keras.optimizers.SGD(learning_rate=lr), loss='mse')
print(f'learning rate is now ,', dummy_model.optimizer.lr.numpy())
.. parsed-literal::
:class: output
learning rate is now , 0.1
.. raw:: html
.. raw:: html
De maneira mais geral, queremos definir um planejador. Quando chamado
com o número de atualizações, ele retorna o valor apropriado da taxa de
aprendizado. Vamos definir um simples que define a taxa de aprendizagem
para :math:`\eta = \eta_0 (t + 1)^{-\frac{1}{2}}`.
.. code:: python
class SquareRootScheduler:
def __init__(self, lr=0.1):
self.lr = lr
def __call__(self, num_update):
return self.lr * pow(num_update + 1.0, -0.5)
Vamos representar graficamente seu comportamento em uma faixa de
valores.
.. raw:: html
.. raw:: html
.. code:: python
scheduler = SquareRootScheduler(lr=0.1)
d2l.plot(np.arange(num_epochs), [scheduler(t) for t in range(num_epochs)])
.. figure:: output_lr-scheduler_1dfeb6_41_0.svg
.. raw:: html
.. raw:: html
.. code:: python
scheduler = SquareRootScheduler(lr=0.1)
d2l.plot(torch.arange(num_epochs), [scheduler(t) for t in range(num_epochs)])
.. figure:: output_lr-scheduler_1dfeb6_44_0.svg
.. raw:: html
.. raw:: html
.. code:: python
scheduler = SquareRootScheduler(lr=0.1)
d2l.plot(tf.range(num_epochs), [scheduler(t) for t in range(num_epochs)])
.. figure:: output_lr-scheduler_1dfeb6_47_0.svg
.. raw:: html
.. raw:: html
Agora vamos ver como isso funciona para o treinamento no Fashion-MNIST.
Simplesmente fornecemos o escalonador como um argumento adicional para o
algoritmo de treinamento.
.. raw:: html
.. raw:: html
.. code:: python
trainer = gluon.Trainer(net.collect_params(), 'sgd',
{'lr_scheduler': scheduler})
train(net, train_iter, test_iter, num_epochs, loss, trainer, device)
.. parsed-literal::
:class: output
train loss 0.520, train acc 0.811, test acc 0.816
.. figure:: output_lr-scheduler_1dfeb6_53_1.svg
.. raw:: html
.. raw:: html
.. code:: python
net = net_fn()
trainer = torch.optim.SGD(net.parameters(), lr)
train(net, train_iter, test_iter, num_epochs, loss, trainer, device,
scheduler)
.. parsed-literal::
:class: output
train loss 0.273, train acc 0.901, test acc 0.876
.. figure:: output_lr-scheduler_1dfeb6_56_1.svg
.. raw:: html
.. raw:: html
.. code:: python
train(net, train_iter, test_iter, num_epochs, lr,
custom_callback=LearningRateScheduler(scheduler))
.. parsed-literal::
:class: output
loss 0.377, train acc 0.862, test acc 0.847
74628.4 examples/sec on /GPU:0
.. parsed-literal::
:class: output
.. figure:: output_lr-scheduler_1dfeb6_59_2.svg
.. raw:: html
.. raw:: html
Isso funcionou um pouco melhor do que antes. Duas coisas se destacam: a
curva era um pouco mais suave do que antes. Em segundo lugar, houve
menos ajuste excessivo. Infelizmente, não é uma questão bem resolvida
por que certas estratégias levam a menos ajustes excessivos em *teoria*.
Há algum argumento de que um tamanho de passo menor levará a parâmetros
mais próximos de zero e, portanto, mais simples. No entanto, isso não
explica o fenômeno inteiramente, uma vez que não paramos realmente cedo,
mas simplesmente reduzimos a taxa de aprendizagem suavemente.
Políticas
---------
Embora não possamos cobrir toda a variedade de programadores de taxa de
aprendizagem, tentamos fornecer uma breve visão geral das políticas
populares abaixo. As escolhas comuns são decaimento polinomial e
esquemas constantes por partes. Além disso, verificou-se que as
programações de taxa de aprendizado de cosseno funcionam bem
empiricamente em alguns problemas. Por último, em alguns problemas, é
benéfico aquecer o otimizador antes de usar altas taxas de aprendizado.
Planejador de Fator
~~~~~~~~~~~~~~~~~~~
Uma alternativa para um decaimento polinomial seria um multiplicativo,
que é :math:`\eta_{t+1} \leftarrow \eta_t \cdot \alpha` para
:math:`\alpha \in (0, 1)`. Para evitar que a taxa de aprendizagem
diminua além de um limite inferior razoável, a equação de atualização é
frequentemente modificada para
:math:`\eta_{t+1} \leftarrow \mathop{\mathrm{max}}(\eta_{\mathrm{min}}, \eta_t \cdot \alpha)`.
.. raw:: html
.. raw:: html
.. code:: python
class FactorScheduler:
def __init__(self, factor=1, stop_factor_lr=1e-7, base_lr=0.1):
self.factor = factor
self.stop_factor_lr = stop_factor_lr
self.base_lr = base_lr
def __call__(self, num_update):
self.base_lr = max(self.stop_factor_lr, self.base_lr * self.factor)
return self.base_lr
scheduler = FactorScheduler(factor=0.9, stop_factor_lr=1e-2, base_lr=2.0)
d2l.plot(np.arange(50), [scheduler(t) for t in range(50)])
.. figure:: output_lr-scheduler_1dfeb6_65_0.svg
.. raw:: html
.. raw:: html
.. code:: python
class FactorScheduler:
def __init__(self, factor=1, stop_factor_lr=1e-7, base_lr=0.1):
self.factor = factor
self.stop_factor_lr = stop_factor_lr
self.base_lr = base_lr
def __call__(self, num_update):
self.base_lr = max(self.stop_factor_lr, self.base_lr * self.factor)
return self.base_lr
scheduler = FactorScheduler(factor=0.9, stop_factor_lr=1e-2, base_lr=2.0)
d2l.plot(torch.arange(50), [scheduler(t) for t in range(50)])
.. figure:: output_lr-scheduler_1dfeb6_68_0.svg
.. raw:: html
.. raw:: html
.. code:: python
class FactorScheduler:
def __init__(self, factor=1, stop_factor_lr=1e-7, base_lr=0.1):
self.factor = factor
self.stop_factor_lr = stop_factor_lr
self.base_lr = base_lr
def __call__(self, num_update):
self.base_lr = max(self.stop_factor_lr, self.base_lr * self.factor)
return self.base_lr
scheduler = FactorScheduler(factor=0.9, stop_factor_lr=1e-2, base_lr=2.0)
d2l.plot(tf.range(50), [scheduler(t) for t in range(50)])
.. figure:: output_lr-scheduler_1dfeb6_71_0.svg
.. raw:: html
.. raw:: html
Isso também pode ser realizado por um agendador embutido no MXNet por
meio do objeto ``lr_scheduler.FactorScheduler``. Leva mais alguns
parâmetros, como período de aquecimento, modo de aquecimento (linear ou
constante), o número máximo de atualizações desejadas, etc .; No futuro,
usaremos os agendadores integrados conforme apropriado e apenas
explicaremos sua funcionalidade aqui. Conforme ilustrado, é bastante
simples construir seu próprio agendador, se necessário.
Planejador de Fatores Multiplos
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Uma estratégia comum para treinar redes profundas é manter a taxa de
aprendizado constante e diminuí-la em uma determinada quantidade de vez
em quando. Ou seja, dado um conjunto de vezes quando diminuir a taxa,
como :math:`s = \{5, 10, 20\}` diminuir
:math:`\eta_{t+1} \leftarrow \eta_t \cdot \alpha` sempre que
:math:`t \in s`. Supondo que os valores sejam reduzidos à metade em cada
etapa, podemos implementar isso da seguinte maneira.
.. raw:: html
.. raw:: html
.. code:: python
scheduler = lr_scheduler.MultiFactorScheduler(step=[15, 30], factor=0.5,
base_lr=0.5)
d2l.plot(np.arange(num_epochs), [scheduler(t) for t in range(num_epochs)])
.. figure:: output_lr-scheduler_1dfeb6_77_0.svg
.. raw:: html
.. raw:: html
.. code:: python
net = net_fn()
trainer = torch.optim.SGD(net.parameters(), lr=0.5)
scheduler = lr_scheduler.MultiStepLR(trainer, milestones=[15, 30], gamma=0.5)
def get_lr(trainer, scheduler):
lr = scheduler.get_last_lr()[0]
trainer.step()
scheduler.step()
return lr
d2l.plot(torch.arange(num_epochs), [get_lr(trainer, scheduler)
for t in range(num_epochs)])
.. figure:: output_lr-scheduler_1dfeb6_80_0.svg
.. raw:: html
.. raw:: html
.. code:: python
class MultiFactorScheduler:
def __init__(self, step, factor, base_lr):
self.step = step
self.factor = factor
self.base_lr = base_lr
def __call__(self, epoch):
if epoch in self.step:
self.base_lr = self.base_lr * self.factor
return self.base_lr
else:
return self.base_lr
scheduler = MultiFactorScheduler(step=[15, 30], factor=0.5, base_lr=0.5)
d2l.plot(tf.range(num_epochs), [scheduler(t) for t in range(num_epochs)])
.. figure:: output_lr-scheduler_1dfeb6_83_0.svg
.. raw:: html
.. raw:: html
A intuição por trás dessa programação de taxa de aprendizado constante
por partes é que se permite que a otimização prossiga até que um ponto
estacionário seja alcançado em termos de distribuição de vetores de
peso. Então (e somente então) diminuímos a taxa de forma a obter um
proxy de maior qualidade para um bom mínimo local. O exemplo abaixo
mostra como isso pode produzir soluções cada vez melhores.
.. raw:: html
.. raw:: html
.. code:: python
trainer = gluon.Trainer(net.collect_params(), 'sgd',
{'lr_scheduler': scheduler})
train(net, train_iter, test_iter, num_epochs, loss, trainer, device)
.. parsed-literal::
:class: output
train loss 0.199, train acc 0.924, test acc 0.879
.. figure:: output_lr-scheduler_1dfeb6_89_1.svg
.. raw:: html
.. raw:: html
.. code:: python
train(net, train_iter, test_iter, num_epochs, loss, trainer, device,
scheduler)
.. parsed-literal::
:class: output
train loss 0.194, train acc 0.926, test acc 0.901
.. figure:: output_lr-scheduler_1dfeb6_92_1.svg
.. raw:: html
.. raw:: html
.. code:: python
train(net, train_iter, test_iter, num_epochs, lr,
custom_callback=LearningRateScheduler(scheduler))
.. parsed-literal::
:class: output
loss 0.229, train acc 0.916, test acc 0.888
75661.4 examples/sec on /GPU:0
.. parsed-literal::
:class: output
.. figure:: output_lr-scheduler_1dfeb6_95_2.svg
.. raw:: html
.. raw:: html
Programador de Cosseno
~~~~~~~~~~~~~~~~~~~~~~
Uma heurística bastante desconcertante foi proposta por
:cite:`Loshchilov.Hutter.2016`. Baseia-se na observação de que podemos
não querer diminuir a taxa de aprendizado muito drasticamente no início
e, além disso, podemos querer “refinar” a solução no final usando uma
taxa de aprendizado muito pequena. Isso resulta em um esquema semelhante
ao cosseno com a seguinte forma funcional para taxas de aprendizado no
intervalo :math:`t \in [0, T]`.
.. math:: \eta_t = \eta_T + \frac{\eta_0 - \eta_T}{2} \left(1 + \cos(\pi t/T)\right)
Aqui :math:`\eta_0` é a taxa de aprendizado inicial, :math:`\eta_T` é a
taxa alvo no momento :math:`T`. Além disso, para :math:`t > T`
simplesmente fixamos o valor em :math:`\eta_T` sem aumentá-lo novamente.
No exemplo a seguir, definimos a etapa de atualização máxima
:math:`T = 20`.
.. raw:: html
.. raw:: html
.. code:: python
scheduler = lr_scheduler.CosineScheduler(max_update=20, base_lr=0.3,
final_lr=0.01)
d2l.plot(np.arange(num_epochs), [scheduler(t) for t in range(num_epochs)])
.. figure:: output_lr-scheduler_1dfeb6_101_0.svg
.. raw:: html
.. raw:: html
.. code:: python
class CosineScheduler:
def __init__(self, max_update, base_lr=0.01, final_lr=0,
warmup_steps=0, warmup_begin_lr=0):
self.base_lr_orig = base_lr
self.max_update = max_update
self.final_lr = final_lr
self.warmup_steps = warmup_steps
self.warmup_begin_lr = warmup_begin_lr
self.max_steps = self.max_update - self.warmup_steps
def get_warmup_lr(self, epoch):
increase = (self.base_lr_orig - self.warmup_begin_lr) \
* float(epoch) / float(self.warmup_steps)
return self.warmup_begin_lr + increase
def __call__(self, epoch):
if epoch < self.warmup_steps:
return self.get_warmup_lr(epoch)
if epoch <= self.max_update:
self.base_lr = self.final_lr + (
self.base_lr_orig - self.final_lr) * (1 + math.cos(
math.pi * (epoch - self.warmup_steps) / self.max_steps)) / 2
return self.base_lr
scheduler = CosineScheduler(max_update=20, base_lr=0.3, final_lr=0.01)
d2l.plot(torch.arange(num_epochs), [scheduler(t) for t in range(num_epochs)])
.. figure:: output_lr-scheduler_1dfeb6_104_0.svg
.. raw:: html
.. raw:: html
.. code:: python
class CosineScheduler:
def __init__(self, max_update, base_lr=0.01, final_lr=0,
warmup_steps=0, warmup_begin_lr=0):
self.base_lr_orig = base_lr
self.max_update = max_update
self.final_lr = final_lr
self.warmup_steps = warmup_steps
self.warmup_begin_lr = warmup_begin_lr
self.max_steps = self.max_update - self.warmup_steps
def get_warmup_lr(self, epoch):
increase = (self.base_lr_orig - self.warmup_begin_lr) \
* float(epoch) / float(self.warmup_steps)
return self.warmup_begin_lr + increase
def __call__(self, epoch):
if epoch < self.warmup_steps:
return self.get_warmup_lr(epoch)
if epoch <= self.max_update:
self.base_lr = self.final_lr + (
self.base_lr_orig - self.final_lr) * (1 + math.cos(
math.pi * (epoch - self.warmup_steps) / self.max_steps)) / 2
return self.base_lr
scheduler = CosineScheduler(max_update=20, base_lr=0.3, final_lr=0.01)
d2l.plot(tf.range(num_epochs), [scheduler(t) for t in range(num_epochs)])
.. figure:: output_lr-scheduler_1dfeb6_107_0.svg
.. raw:: html
.. raw:: html
No contexto da visão computacional, este cronograma *pode* levar a
melhores resultados. Observe, entretanto, que tais melhorias não são
garantidas (como pode ser visto abaixo).
.. raw:: html
.. raw:: html
.. code:: python
trainer = gluon.Trainer(net.collect_params(), 'sgd',
{'lr_scheduler': scheduler})
train(net, train_iter, test_iter, num_epochs, loss, trainer, device)
.. parsed-literal::
:class: output
train loss 0.344, train acc 0.878, test acc 0.868
.. figure:: output_lr-scheduler_1dfeb6_113_1.svg
.. raw:: html
.. raw:: html
.. code:: python
net = net_fn()
trainer = torch.optim.SGD(net.parameters(), lr=0.3)
train(net, train_iter, test_iter, num_epochs, loss, trainer, device,
scheduler)
.. parsed-literal::
:class: output
train loss 0.215, train acc 0.920, test acc 0.895
.. figure:: output_lr-scheduler_1dfeb6_116_1.svg
.. raw:: html
.. raw:: html
.. code:: python
train(net, train_iter, test_iter, num_epochs, lr,
custom_callback=LearningRateScheduler(scheduler))
.. parsed-literal::
:class: output
loss 0.266, train acc 0.903, test acc 0.881
75190.3 examples/sec on /GPU:0
.. parsed-literal::
:class: output
.. figure:: output_lr-scheduler_1dfeb6_119_2.svg
.. raw:: html
.. raw:: html
Aquecimento
~~~~~~~~~~~
Em alguns casos, inicializar os parâmetros não é suficiente para
garantir uma boa solução. Isso é particularmente um problema para alguns
projetos de rede avançados que podem levar a problemas de otimização
instáveis. Poderíamos resolver isso escolhendo uma taxa de aprendizado
suficientemente pequena para evitar divergências no início.
Infelizmente, isso significa que o progresso é lento. Por outro lado,
uma grande taxa de aprendizado inicialmente leva à divergência.
Uma solução bastante simples para esse dilema é usar um período de
aquecimento durante o qual a taxa de aprendizado *aumenta* até seu
máximo inicial e esfriar a taxa até o final do processo de otimização.
Para simplificar, normalmente usa-se um aumento linear para esse
propósito. Isso leva a uma programação do formulário indicado abaixo.
.. raw:: html
.. raw:: html
.. code:: python
scheduler = lr_scheduler.CosineScheduler(20, warmup_steps=5, base_lr=0.3,
final_lr=0.01)
d2l.plot(np.arange(num_epochs), [scheduler(t) for t in range(num_epochs)])
.. figure:: output_lr-scheduler_1dfeb6_125_0.svg
.. raw:: html
.. raw:: html
.. code:: python
scheduler = CosineScheduler(20, warmup_steps=5, base_lr=0.3, final_lr=0.01)
d2l.plot(torch.arange(num_epochs), [scheduler(t) for t in range(num_epochs)])
.. figure:: output_lr-scheduler_1dfeb6_128_0.svg
.. raw:: html
.. raw:: html
.. code:: python
scheduler = CosineScheduler(20, warmup_steps=5, base_lr=0.3, final_lr=0.01)
d2l.plot(tf.range(num_epochs), [scheduler(t) for t in range(num_epochs)])
.. figure:: output_lr-scheduler_1dfeb6_131_0.svg
.. raw:: html
.. raw:: html
Observe que a rede converge melhor inicialmente (em particular observe o
desempenho durante as primeiras 5 épocas).
.. raw:: html
.. raw:: html
.. code:: python
trainer = gluon.Trainer(net.collect_params(), 'sgd',
{'lr_scheduler': scheduler})
train(net, train_iter, test_iter, num_epochs, loss, trainer, device)
.. parsed-literal::
:class: output
train loss 0.345, train acc 0.875, test acc 0.847
.. figure:: output_lr-scheduler_1dfeb6_137_1.svg
.. raw:: html
.. raw:: html
.. code:: python
net = net_fn()
trainer = torch.optim.SGD(net.parameters(), lr=0.3)
train(net, train_iter, test_iter, num_epochs, loss, trainer, device,
scheduler)
.. parsed-literal::
:class: output
train loss 0.197, train acc 0.928, test acc 0.900
.. figure:: output_lr-scheduler_1dfeb6_140_1.svg
.. raw:: html
.. raw:: html
.. code:: python
train(net, train_iter, test_iter, num_epochs, lr,
custom_callback=LearningRateScheduler(scheduler))
.. parsed-literal::
:class: output
loss 0.279, train acc 0.898, test acc 0.880
74836.3 examples/sec on /GPU:0
.. parsed-literal::
:class: output
.. figure:: output_lr-scheduler_1dfeb6_143_2.svg
.. raw:: html
.. raw:: html
O aquecimento pode ser aplicado a qualquer agendador (não apenas
cosseno). Para uma discussão mais detalhada sobre cronogramas de taxas
de aprendizagem e muitos outros experimentos, consulte também
:cite:`Gotmare.Keskar.Xiong.ea.2018`. Em particular, eles descobrem
que uma fase de aquecimento limita a quantidade de divergência de
parâmetros em redes muito profundas. Isso faz sentido intuitivamente,
pois esperaríamos divergências significativas devido à inicialização
aleatória nas partes da rede que levam mais tempo para progredir no
início.
Sumário
-------
- Diminuir a taxa de aprendizado durante o treinamento pode levar a uma
maior precisão e (o que é mais desconcertante) à redução do ajuste
excessivo do modelo.
- Uma diminuição por partes da taxa de aprendizagem sempre que o
progresso atinge um platô é eficaz na prática. Essencialmente, isso
garante que convergiremos de forma eficiente para uma solução
adequada e só então reduziremos a variação inerente dos parâmetros,
reduzindo a taxa de aprendizagem.
- Os planejadores de cosseno são populares para alguns problemas de
visão computacional. Veja, por exemplo,
`GluonCV `__ para detalhes de tal
planejador.
- Um período de aquecimento antes da otimização pode evitar
divergências.
- A otimização serve a vários propósitos no aprendizado profundo. Além
de minimizar o objetivo do treinamento, diferentes escolhas de
algoritmos de otimização e programação da taxa de aprendizagem podem
levar a quantidades bastante diferentes de generalização e
overfitting no conjunto de teste (para a mesma quantidade de erro de
treinamento).
Exercícios
----------
1. Experimente o comportamento de otimização para uma determinada taxa
de aprendizagem fixa. Qual é o melhor modelo que você pode obter
dessa forma?
2. Como a convergência muda se você alterar o expoente da diminuição na
taxa de aprendizado? Use ``PolyScheduler`` para sua conveniência nos
experimentos.
3. Aplique o programador de cosseno a grandes problemas de visão
computacional, por exemplo, treinamento ImageNet. Como isso afeta o
desempenho em relação a outros agendadores?
4. Quanto tempo deve durar o aquecimento?
5. Você pode conectar otimização e amostragem? Comece usando os
resultados de :cite:`Welling.Teh.2011` em Stochastic Gradient
Langevin Dynamics.
.. raw:: html
.. raw:: html
`Discussão `__
.. raw:: html
.. raw:: html
`Discussão `__
.. raw:: html
.. raw:: html
`Discussão `__
.. raw:: html
.. raw:: html
.. raw:: html