4.8. Estabilidade Numérica e Inicialização¶ Open the notebook in SageMaker Studio Lab
Até agora, todos os modelos que implementamos exigiram que inicializássemos seus parâmetros de acordo com alguma distribuição pré-especificada. Até agora, considerávamos o esquema de inicialização garantido, encobrindo os detalhes de como essas escolhas são feitas. Você pode até ter ficado com a impressão de que essas escolhas não são especialmente importantes. Pelo contrário, a escolha do esquema de inicialização desempenha um papel significativo na aprendizagem da rede neural, e pode ser crucial para manter a estabilidade numérica. Além disso, essas escolhas podem ser amarradas de maneiras interessantes com a escolha da função de ativação não linear. Qual função escolhemos e como inicializamos os parâmetros pode determinar a rapidez com que nosso algoritmo de otimização converge. Escolhas ruins aqui podem nos fazer encontrar explosões ou desaparecimento de gradientes durante o treinamento. Nesta seção, nos aprofundamos nesses tópicos com mais detalhes e discutimos algumas heurísticas úteis que você achará útil ao longo de sua carreira em deep learning.
4.8.1. Explosão e Desaparecimento de Gradientes¶
Considere uma rede profunda com $ L $ camadas, entrada $ mathbf {x} $ e saída \(\mathbf{o}\). Com cada camada \(l\) definida por uma transformação \(f_l\) parametrizada por pesos \(\mathbf{W}^{(l)}\), cuja variável oculta é \(\mathbf{h}^{(l)}\) (com \(\mathbf{h}^{(0)} = \mathbf{x}\)), nossa rede pode ser expressa como:
Se todas as variáveis ocultas e a entrada forem vetores, podemos escrever o gradiente de \(\mathbf{o}\) em relação a qualquer conjunto de parâmetros \(\mathbf{W}^{(l)}\) da seguinte forma:
Em outras palavras, este gradiente é o produto das matrizes \(L-l\) \(\mathbf{M}^{(L)} \cdot \ldots \cdot \mathbf{M}^{(l+1)}\) e o vetor gradiente \(\mathbf{v}^{(l)}\). Assim, somos suscetíveis aos mesmos problemas de underflow numérico que muitas vezes surgem ao multiplicar muitas probabilidades. Ao lidar com probabilidades, um truque comum é mudar para o espaço de registro, ou seja, mudar pressão da mantissa para o expoente da representação numérica. Infelizmente, nosso problema acima é mais sério: inicialmente as matrizes \(\mathbf{M}^{(l)}\) podem ter uma grande variedade de autovalores. Eles podem ser pequenos ou grandes e seu produto pode ser muito grande ou muito pequeno.
Os riscos apresentados por gradientes instáveis vão além da representação numérica. Gradientes de magnitude imprevisível também ameaçam a estabilidade de nossos algoritmos de otimização. Podemos estar enfrentando atualizações de parâmetros que são (i) excessivamente grandes, destruindo nosso modelo (o problema da explosão do gradiente); ou (ii) excessivamente pequeno (o problema do desaparecimento do gradiente), tornando a aprendizagem impossível como parâmetros dificilmente se move a cada atualização.
4.8.1.1. Desaparecimento do Gradiente¶
Um culpado frequente que causa o problema do desaparecimento de gradiente é a escolha da função de ativação \(\sigma\) que é anexada após as operações lineares de cada camada. Historicamente, a função sigmóide \(1/(1 + \exp(-x))\) (introduzida em Section 4.1) era popular porque se assemelha a uma função de limiar. Como as primeiras redes neurais artificiais foram inspiradas por redes neurais biológicas, a ideia de neurônios que disparam totalmente ou nem um pouco (como neurônios biológicos) parecia atraente. Vamos dar uma olhada mais de perto no sigmóide para ver por que isso pode causar desaparecimento de gradientes.
%matplotlib inline
from mxnet import autograd, np, npx
from d2l import mxnet as d2l
npx.set_np()
x = np.arange(-8.0, 8.0, 0.1)
x.attach_grad()
with autograd.record():
y = npx.sigmoid(x)
y.backward()
d2l.plot(x, [y, x.grad], legend=['sigmoid', 'gradient'], figsize=(4.5, 2.5))
[03:39:50] src/base.cc:49: GPU context requested, but no GPUs found.
%matplotlib inline
import torch
from d2l import torch as d2l
x = torch.arange(-8.0, 8.0, 0.1, requires_grad=True)
y = torch.sigmoid(x)
y.backward(torch.ones_like(x))
d2l.plot(x.detach().numpy(), [y.detach().numpy(), x.grad.numpy()],
legend=['sigmoid', 'gradient'], figsize=(4.5, 2.5))
%matplotlib inline
import tensorflow as tf
from d2l import tensorflow as d2l
x = tf.Variable(tf.range(-8.0, 8.0, 0.1))
with tf.GradientTape() as t:
y = tf.nn.sigmoid(x)
d2l.plot(x.numpy(), [y.numpy(), t.gradient(y, x).numpy()],
legend=['sigmoid', 'gradient'], figsize=(4.5, 2.5))
Como você pode ver, o gradiente do sigmóide desaparece tanto quando suas entradas são grandes quanto quando são pequenas. Além disso, ao retropropagar através de muitas camadas, a menos que estejamos na zona Cachinhos Dourados, onde as entradas para muitos dos sigmóides são próximas de zero, os gradientes do produto geral podem desaparecer. Quando nossa rede possui muitas camadas, a menos que tenhamos cuidado, o gradiente provavelmente será cortado em alguma camada. Na verdade, esse problema costumava atormentar o treinamento profundo da rede. Consequentemente, ReLUs, que são mais estáveis (mas menos neuralmente plausíveis), surgiram como a escolha padrão para os profissionais.
4.8.1.2. Explosão de Gradiente¶
O problema oposto, quando os gradientes explodem, pode ser igualmente irritante. Para ilustrar isso um pouco melhor, desenhamos 100 matrizes aleatórias Gaussianas e multiplicamos-nas com alguma matriz inicial. Para a escala que escolhemos (a escolha da variação \(\sigma^2=1\)), o produto da matriz explode. Quando isso acontece devido à inicialização de uma rede profunda, não temos chance de obter um otimizador de gradiente descendente capaz de convergir.
M = np.random.normal(size=(4, 4))
print('a single matrix', M)
for i in range(100):
M = np.dot(M, np.random.normal(size=(4, 4)))
print('after multiplying 100 matrices', M)
a single matrix [[ 2.2122064 1.1630787 0.7740038 0.4838046 ]
[ 1.0434405 0.29956347 1.1839255 0.15302546]
[ 1.8917114 -1.1688148 -1.2347414 1.5580711 ]
[-1.771029 -0.5459446 -0.45138445 -2.3556297 ]]
after multiplying 100 matrices [[ 3.4459714e+23 -7.8040680e+23 5.9973287e+23 4.5229990e+23]
[ 2.5275089e+23 -5.7240326e+23 4.3988473e+23 3.3174740e+23]
[ 1.3731286e+24 -3.1097155e+24 2.3897773e+24 1.8022959e+24]
[-4.4951040e+23 1.0180033e+24 -7.8232281e+23 -5.9000354e+23]]
M = torch.normal(0, 1, size=(4,4))
print('a single matrix \n',M)
for i in range(100):
M = torch.mm(M,torch.normal(0, 1, size=(4, 4)))
print('after multiplying 100 matrices\n', M)
a single matrix
tensor([[ 1.4391, 0.5853, 0.3140, -0.8884],
[ 1.4385, 1.3793, -1.2473, 0.6873],
[ 0.1406, -0.1450, -0.5697, -0.2563],
[-0.8765, -0.1916, 0.1412, 0.3991]])
after multiplying 100 matrices
tensor([[-1.5826e+23, 6.3882e+22, -2.6931e+23, -5.2050e+23],
[-3.4426e+23, 1.3896e+23, -5.8582e+23, -1.1322e+24],
[-2.7453e+22, 1.1081e+22, -4.6717e+22, -9.0290e+22],
[ 1.1260e+23, -4.5449e+22, 1.9160e+23, 3.7031e+23]])
M = tf.random.normal((4, 4))
print('a single matrix \n', M)
for i in range(100):
M = tf.matmul(M, tf.random.normal((4, 4)))
print('after multiplying 100 matrices\n', M.numpy())
a single matrix
tf.Tensor(
[[ 0.2855358 0.98259926 -0.41519606 -0.05112769]
[-0.66470706 1.2891607 -0.809949 -1.0318425 ]
[ 0.07409963 -0.7304153 0.5636679 0.12928967]
[-0.32265195 0.11378457 0.18844195 -0.71320873]], shape=(4, 4), dtype=float32)
after multiplying 100 matrices
[[-1.1830594e+25 -1.1770829e+26 -3.7617978e+24 -1.3166611e+24]
[-4.7745583e+25 -4.7504369e+26 -1.5181753e+25 -5.3137622e+24]
[ 2.0201246e+25 2.0099183e+26 6.4234272e+24 2.2482776e+24]
[-6.2446662e+24 -6.2131161e+25 -1.9856277e+24 -6.9499405e+23]]
4.8.1.3. Quebrando a Simetria¶
Outro problema no projeto de rede neural é a simetria inerente à sua parametrização. Suponha que temos um MLP simples com uma camada oculta e duas unidades. Neste caso, poderíamos permutar os pesos \(\mathbf{W}^{(1)}\) da primeira camada e da mesma forma permutar os pesos da camada de saída para obter a mesma função. Não há nada especial em diferenciar a primeira unidade oculta vs. a segunda unidade oculta. Em outras palavras, temos simetria de permutação entre as unidades ocultas de cada camada.
Isso é mais do que apenas um incômodo teórico. Considere o já mencionado MLP de uma camada oculta com duas unidades ocultas. Para ilustração, suponha que a camada de saída transforme as duas unidades ocultas em apenas uma unidade de saída. Imagine o que aconteceria se inicializássemos todos os parâmetros da camada oculta como \(\mathbf{W}^{(1)} = c\) para alguma constante \(c\). Neste caso, durante a propagação direta qualquer unidade oculta leva as mesmas entradas e parâmetros a produzir a mesma ativação, que é alimentada para a unidade de saída. Durante a retropropagação, diferenciar a unidade de saída com respeito aos parâmetros \(\mathbf{W}^{(1)}\) dá um gradiente cujos elementos tomam o mesmo valor. Assim, após a iteração baseada em gradiente (por exemplo, gradiente descendente estocástico de minibatch), todos os elementos de \(\mathbf{W}^{(1)}\) ainda têm o mesmo valor. Essas iterações nunca iriam quebrar a simetria por conta própria e podemos nunca ser capazes de perceber o poder expressivo da rede. A camada oculta se comportaria como se tivesse apenas uma unidade. Observe que, embora o gradiente descendente estocástico de minibatch não quebrasse essa simetria, a regularização do dropout iria!
4.8.2. Inicialização de Parâmetros¶
Uma forma de abordar — ou pelo menos mitigar — os problemas levantados acima é através de inicialização cuidadosa. Cuidado adicional durante a otimização e a regularização adequada pode aumentar ainda mais a estabilidade.
4.8.2.1. Inicialização Padrão¶
Nas seções anteriores, por exemplo, em Section 3.3, nós usamos uma distribuição normal para inicializar os valores de nossos pesos. Se não especificarmos o método de inicialização, o framework irá usar um método de inicialização aleatória padrão, que geralmente funciona bem na prática para tamanhos moderados de problemas.
4.8.2.2. Inicialização de Xavier¶
Vejamos a distribuição da escala de uma saída (por exemplo, uma variável oculta) \(o_{i}\) para alguma camada totalmente conectada sem não linearidades. Com \(n_\mathrm{in}\), entradas \(x_j\) e seus pesos associados \(w_{ij}\) para esta camada, uma saída é dada por
Os pesos \(w_{ij}\) estão todos sorteados independentemente da mesma distribuição. Além disso, vamos supor que esta distribuição tem média zero e variância \(\sigma^2\). Observe que isso não significa que a distribuição deve ser gaussiana, apenas que a média e a variância precisam existir. Por enquanto, vamos supor que as entradas para a camada \(x_j\) também têm média zero e variância \(\gamma^2\) e que elas são independentes de \(w_{ij}\) e independentes uma da outra. Nesse caso, podemos calcular a média e a variância de \(o_i\) da seguinte forma:
Uma maneira de manter a variância fixa é definir \(n_\mathrm{in} \sigma^2 = 1\). Agora, considere a retropropagação. Lá nós enfrentamos um problema semelhante, embora com gradientes sendo propagados das camadas mais próximas da saída. Usando o mesmo raciocínio da propagação direta, vemos que a variância dos gradientes pode explodir a menos que \(n_\mathrm{out} \sigma^2 = 1\), onde \(n_\mathrm{out}\) é o número de saídas desta camada. Isso nos deixa em um dilema: não podemos satisfazer ambas as condições simultaneamente. Em vez disso, simplesmente tentamos satisfazer:
Este é o raciocínio subjacente à agora padrão e praticamente benéfica inicialização de Xavier, em homenagem ao primeiro autor de seus criadores [Glorot & Bengio, 2010]. Normalmente, a inicialização de Xavier amostra pesos de uma distribuição gaussiana com média e variância zero \(\sigma^2 = \frac{2}{n_\mathrm{in} + n_\mathrm{out}}\). Também podemos adaptar a intuição de Xavier para escolher a variância ao amostrar os pesos de uma distribuição uniforme. Observe que a distribuição uniforme \(U(-a, a)\) tem variância \(\frac{a^2}{3}\). Conectar \(\frac{a^2}{3}\) em nossa condição em \(\sigma^2\) produz a sugestão de inicializar de acordo com
Embora a suposição de inexistência de não linearidades no raciocínio matemático acima pode ser facilmente violada em redes neurais, o método de inicialização de Xavier acaba funcionando bem na prática.
4.8.2.3. Além¶
O raciocínio acima mal arranha a superfície de abordagens modernas para inicialização de parâmetros. Uma estrutura de deep learning geralmente implementa mais de uma dúzia de heurísticas diferentes. Além disso, a inicialização do parâmetro continua a ser uma área quente de pesquisa fundamental em deep learning. Entre elas estão heurísticas especializadas para parâmetros vinculados (compartilhados), super-resolução, modelos de sequência e outras situações. Por exemplo, Xiao et al. demonstraram a possibilidade de treinar Redes neurais de 10.000 camadas sem truques arquitetônicos usando um método de inicialização cuidadosamente projetado [Xiao et al., 2018].
Se o assunto interessar a você, sugerimos um mergulho profundo nas ofertas deste módulo, lendo os artigos que propuseram e analisaram cada heurística, e explorando as publicações mais recentes sobre o assunto. Talvez você tropece ou até invente uma ideia inteligente e contribuir com uma implementação para estruturas de deep learning.
4.8.3. Resumo¶
Desaparecimento e explosão de gradientes são problemas comuns em redes profundas. É necessário muito cuidado na inicialização dos parâmetros para garantir que gradientes e parâmetros permaneçam bem controlados.
As heurísticas de inicialização são necessárias para garantir que os gradientes iniciais não sejam nem muito grandes nem muito pequenos.
As funções de ativação ReLU atenuam o problema do desaparecimento de gradiente. Isso pode acelerar a convergência.
A inicialização aleatória é a chave para garantir que a simetria seja quebrada antes da otimização.
A inicialização de Xavier sugere que, para cada camada, a variação de qualquer saída não é afetada pelo número de entradas e a variação de qualquer gradiente não é afetada pelo número de saídas.
4.8.4. Exercícios¶
Você pode projetar outros casos em que uma rede neural pode exibir simetria exigindo quebra, além da simetria de permutação nas camadas de um MLP?
Podemos inicializar todos os parâmetros de peso na regressão linear ou na regressão softmax para o mesmo valor?
Procure limites analíticos nos autovalores do produto de duas matrizes. O que isso diz a você sobre como garantir que os gradientes sejam bem condicionados?
Se sabemos que alguns termos divergem, podemos consertar isso após o fato? Veja o artigo sobre escalonamento de taxa adaptável em camadas para se inspirar [You et al., 2017].