As muitas aplicações da descida de gradiente no TensorFlow

Publicados: 2022-03-11

O TensorFlow do Google é uma das principais ferramentas para treinamento e implantação de modelos de aprendizado profundo. Ele é capaz de otimizar arquiteturas de rede neural extremamente complexas com centenas de milhões de parâmetros e vem com uma ampla gama de ferramentas para aceleração de hardware, treinamento distribuído e fluxos de trabalho de produção. Esses recursos poderosos podem parecer intimidantes e desnecessários fora do domínio do aprendizado profundo.

Mas o TensorFlow pode ser acessível e utilizável para problemas mais simples não diretamente relacionados ao treinamento de modelos de aprendizado profundo. Em sua essência, o TensorFlow é apenas uma biblioteca otimizada para operações de tensor (vetores, matrizes, etc.) e as operações de cálculo usadas para realizar descida de gradiente em sequências arbitrárias de cálculos. Cientistas de dados experientes reconhecerão a “descida de gradiente” como uma ferramenta fundamental para a matemática computacional, mas geralmente requer a implementação de códigos e equações específicos do aplicativo. Como veremos, é aí que entra a arquitetura moderna de “diferenciação automática” do TensorFlow.

Casos de uso do TensorFlow

  • Exemplo 1: regressão linear com descida de gradiente no TensorFlow 2.0
    • O que é descida de gradiente?
  • Exemplo 2: Vetores de Unidade de Distribuição Máxima
  • Exemplo 3: Gerando Entradas Adversarial AI
  • Considerações Finais: Otimização de Descida de Gradiente
  • Descida de gradiente no TensorFlow: de encontrar mínimos a atacar sistemas de IA

Exemplo 1: regressão linear com descida de gradiente no TensorFlow 2.0

Exemplo 1 Caderno

Antes de acessar o código do TensorFlow, é importante estar familiarizado com a descida de gradiente e a regressão linear.

O que é descida de gradiente?

Em termos mais simples, é uma técnica numérica para encontrar as entradas para um sistema de equações que minimizam sua saída. No contexto de aprendizado de máquina, esse sistema de equações é nosso modelo , as entradas são os parâmetros desconhecidos do modelo e a saída é uma função de perda a ser minimizada, que representa quanto erro existe entre o modelo e nossos dados. Para alguns problemas (como regressão linear), existem equações para calcular diretamente os parâmetros que minimizam nosso erro, mas para a maioria das aplicações práticas, precisamos de técnicas numéricas como gradiente descendente para chegar a uma solução satisfatória.

O ponto mais importante deste artigo é que o gradiente descendente geralmente requer a apresentação de nossas equações e o uso de cálculo para derivar a relação entre nossa função de perda e nossos parâmetros. Com o TensorFlow (e qualquer ferramenta moderna de autodiferenciação), o cálculo é feito para nós, para que possamos nos concentrar no design da solução e não perder tempo com sua implementação.

Aqui está o que parece em um problema de regressão linear simples. Temos uma amostra das alturas (h) e pesos (w) de 150 machos adultos e começamos com uma estimativa imperfeita da inclinação e do desvio padrão dessa linha. Após cerca de 15 iterações de gradiente descendente, chegamos a uma solução quase ótima.

Duas animações sincronizadas. O lado esquerdo mostra um gráfico de dispersão de altura-peso, com uma linha ajustada que começa longe dos dados, depois se move rapidamente em direção a eles, diminuindo a velocidade antes de encontrar o ajuste final. O tamanho certo mostra um gráfico de perda versus iteração, com cada quadro adicionando uma nova iteração ao gráfico. A perda começa acima do topo do gráfico em 2.000, mas rapidamente se aproxima da linha de perda mínima dentro de algumas iterações no que parece ser uma curva logarítmica.

Vamos ver como produzimos a solução acima usando o TensorFlow 2.0.

Para regressão linear, dizemos que os pesos podem ser previstos por uma equação linear de alturas.

w-subscript-i,pred é igual ao produto escalar alfa h-subscript-i mais beta.

Queremos encontrar os parâmetros α e β (inclinação e interceptação) que minimizem o erro quadrático médio (perda) entre as previsões e os valores verdadeiros. Portanto, nossa função de perda (neste caso, o “erro quadrático médio” ou MSE) se parece com isso:

MSE é igual a um sobre N vezes a soma de i é igual a um a N do quadrado da diferença entre w-subscript-i,true e w-subscript-i,pred.

Podemos ver como o erro quadrático médio se parece com algumas linhas imperfeitas e, em seguida, com a solução exata (α=6,04, β=-230,5).

Três cópias do mesmo gráfico de dispersão altura-peso, cada uma com uma linha ajustada diferente. A primeira tem w = 4,00 * h + -120,0 e uma perda de 1057,0; a linha está abaixo dos dados e menos íngreme do que ela. A segunda tem w = 2,00 * h + 70,0 e uma perda de 720,8; a linha está perto da parte superior dos pontos de dados e ainda menos íngreme. O terceiro tem w = 60,4 * h + -230,5 e uma perda de 127,1; a linha passa pelos pontos de dados de forma que eles apareçam uniformemente agrupados em torno dela.

Vamos colocar essa ideia em prática com o TensorFlow. A primeira coisa a fazer é codificar a função de perda usando tensores e funções tf.* .

 def calc_mean_sq_error(heights, weights, slope, intercept): predicted_wgts = slope * heights + intercept errors = predicted_wgts - weights mse = tf.reduce_mean(errors**2) return mse

Isso parece bem direto. Todos os operadores algébricos padrão estão sobrecarregados para tensores, então só temos que ter certeza de que as variáveis ​​que estamos otimizando são tensores, e usamos métodos tf.* para qualquer outra coisa.

Então, tudo o que temos a fazer é colocar isso em um loop de gradiente descendente:

 def run_gradient_descent(heights, weights, init_slope, init_icept, learning_rate): # Any values to be part of gradient calcs need to be vars/tensors tf_slope = tf.Variable(init_slope, dtype='float32') tf_icept = tf.Variable(init_icept, dtype='float32') # Hardcoding 25 iterations of gradient descent for i in range(25): # Do all calculations under a "GradientTape" which tracks all gradients with tf.GradientTape() as tape: tape.watch((tf_slope, tf_icept)) # This is the same mean-squared-error calculation as before predictions = tf_slope * heights + tf_icept errors = predictions - weights loss = tf.reduce_mean(errors**2) # Auto-diff magic! Calcs gradients between loss calc and params dloss_dparams = tape.gradient(loss, [tf_slope, tf_icept]) # Gradients point towards +loss, so subtract to "descend" tf_slope = tf_slope - learning_rate * dloss_dparams[0] tf_icept = tf_icept - learning_rate * dloss_dparams[1]

Vamos ter um momento para apreciar o quão legal isso é. A descida do gradiente requer o cálculo de derivadas da função de perda em relação a todas as variáveis ​​que estamos tentando otimizar. O cálculo deveria estar envolvido, mas na verdade não fizemos nada disso. A mágica está no fato de que:

  1. O TensorFlow cria um gráfico de computação de cada cálculo feito em um tf.GradientTape() .
  2. O TensorFlow sabe calcular as derivadas (gradientes) de cada operação, para que possa determinar como qualquer variável no gráfico de computação afeta qualquer outra variável.

Como é o processo a partir de diferentes pontos de partida?

Os mesmos gráficos sincronizados de antes, mas também sincronizados com um par semelhante de gráficos abaixo deles para comparação. O gráfico de iteração de perda do par inferior é semelhante, mas parece convergir mais rapidamente; sua linha ajustada correspondente começa acima dos pontos de dados e não abaixo, e mais perto de seu local de descanso final.

A descida do gradiente fica notavelmente próxima do MSE ideal, mas na verdade converge para uma inclinação e interceptação substancialmente diferentes do ótimo em ambos os exemplos. Em alguns casos, isso é simplesmente gradiente descendente convergindo para o mínimo local, o que é um desafio inerente aos algoritmos de gradiente descendente. Mas a regressão linear provavelmente tem apenas um mínimo global. Então, como acabamos na inclinação errada e interceptamos?

Nesse caso, o problema é que simplificamos demais o código para fins de demonstração. Não normalizamos nossos dados e o parâmetro slope tem uma característica diferente do parâmetro intercept. Pequenas mudanças na inclinação podem produzir grandes mudanças na perda, enquanto pequenas mudanças na interceptação têm muito pouco efeito. Essa enorme diferença na escala dos parâmetros treináveis ​​leva a inclinação a dominar os cálculos do gradiente, com o parâmetro de interceptação sendo quase ignorado.

Assim, a descida do gradiente encontra efetivamente a melhor inclinação muito próxima da estimativa de interceptação inicial. E como o erro está tão próximo do ótimo, os gradientes ao redor dele são minúsculos, então cada iteração sucessiva se move apenas um pouquinho. Normalizar nossos dados primeiro teria melhorado drasticamente esse fenômeno, mas não o eliminaria.

Este foi um exemplo relativamente simples, mas veremos nas próximas seções que esse recurso de “autodiferenciação” pode lidar com algumas coisas bastante complexas.

Exemplo 2: Vetores de Unidade de Distribuição Máxima

Exemplo 2 Caderno

Este próximo exemplo é baseado em um divertido exercício de aprendizado profundo em um curso de aprendizado profundo que fiz no ano passado.

A essência do problema é que temos um “autocodificador variacional” (VAE) que pode produzir faces realistas a partir de um conjunto de 32 números normalmente distribuídos. Para a identificação de suspeitos, queremos usar o VAE para produzir um conjunto diversificado de rostos (teóricos) para uma testemunha escolher e, em seguida, restringir a busca produzindo mais rostos semelhantes aos que foram escolhidos. Para este exercício, foi sugerido randomizar o conjunto inicial de vetores, mas eu queria encontrar um estado inicial ideal.

Podemos formular o problema assim: Dado um espaço de 32 dimensões, encontre um conjunto de X vetores unitários que estão espalhados ao máximo. Em duas dimensões, isso é fácil de calcular exatamente. Mas para três dimensões (ou 32 dimensões!), não há uma resposta direta. No entanto, se pudermos definir uma função de perda adequada que esteja no mínimo quando atingirmos nosso estado alvo, talvez o gradiente descendente possa nos ajudar a chegar lá.

Dois gráficos. O gráfico da esquerda, Estado inicial para todos os experimentos, tem um ponto central conectado a outros pontos, quase todos formando um semicírculo ao seu redor; um ponto fica aproximadamente oposto ao semicírculo. O gráfico certo, Target State, é como uma roda, com os raios espalhados uniformemente.

Começaremos com um conjunto aleatório de 20 vetores, conforme mostrado acima, e experimentaremos três funções de perda diferentes, cada uma com complexidade crescente, para demonstrar os recursos do TensorFlow.

Vamos primeiro definir nosso loop de treinamento. Colocaremos toda a lógica do TensorFlow no método self.calc_loss() e, em seguida, podemos simplesmente substituir esse método para cada técnica, reciclando esse loop.

 # Define the framework for trying different loss functions # Base class implements loop, sub classes override self.calc_loss() class VectorSpreadAlgorithm: # ... def calc_loss(self, tensor2d): raise NotImplementedError("Define this in your derived class") def one_iter(self, i, learning_rate): # self.vecs is an 20x2 tensor, representing twenty 2D vectors tfvecs = tf.convert_to_tensor(self.vecs, dtype=tf.float32) with tf.GradientTape() as tape: tape.watch(tfvecs) loss = self.calc_loss(tfvecs) # Here's the magic again. Derivative of spread with respect to # input vectors gradients = tape.gradient(loss, tfvecs) self.vecs = self.vecs - learning_rate * gradients

A primeira técnica a tentar é a mais simples. Definimos uma métrica de dispersão que é o ângulo dos vetores que estão mais próximos. Queremos maximizar o spread, mas é convencional torná-lo um problema de minimização. Então, simplesmente pegamos o negativo da métrica de spread:

 class VectorSpread_Maximize_Min_Angle(VectorSpreadAlgorithm): def calc_loss(self, tensor2d): angle_pairs = tf.acos(tensor2d @ tf.transpose(tensor2d)) disable_diag = tf.eye(tensor2d.numpy().shape[0]) * 2 * np.pi spread_metric = tf.reduce_min(angle_pairs + disable_diag) # Convention is to return a quantity to be minimized, but we want # to maximize spread. So return negative spread return -spread_metric

Alguma mágica do Matplotlib produzirá uma visualização.

Uma animação indo do estado inicial para o estado de destino. O ponto solitário permanece fixo, e o resto dos raios no semicírculo se revezam para frente e para trás, espalhando-se lentamente e não alcançando a equidistância mesmo após 1.200 iterações.

Isso é desajeitado (literalmente!), mas funciona. Apenas dois dos 20 vetores são atualizados por vez, aumentando o espaço entre eles até que eles não sejam mais os mais próximos, então alternando para aumentar o ângulo entre os novos dois vetores mais próximos. A coisa importante a notar é que ele funciona . Vemos que o TensorFlow conseguiu passar gradientes pelos tf.reduce_min() e tf.acos() para fazer a coisa certa.

Vamos tentar algo um pouco mais elaborado. Sabemos que na solução ótima, todos os vetores devem ter o mesmo ângulo em relação aos seus vizinhos mais próximos. Então, vamos adicionar “variância de ângulos mínimos” à função de perda.

 class VectorSpread_MaxMinAngle_w_Variance(VectorSpreadAlgorithm): def spread_metric(self, tensor2d): """ Assumes all rows already normalized """ angle_pairs = tf.acos(tensor2d @ tf.transpose(tensor2d)) disable_diag = tf.eye(tensor2d.numpy().shape[0]) * 2 * np.pi all_mins = tf.reduce_min(angle_pairs + disable_diag, axis=1) # Same calculation as before: find the min-min angle min_min = tf.reduce_min(all_mins) # But now also calculate the variance of the min angles vector avg_min = tf.reduce_mean(all_mins) var_min = tf.reduce_sum(tf.square(all_mins - avg_min)) # Our spread metric now includes a term to minimize variance spread_metric = min_min - 0.4 * var_min # As before, want negative spread to keep it a minimization problem return -spread_metric 

Uma animação indo do estado inicial para o estado de destino. O raio solitário não fica fixo, movendo-se rapidamente em direção ao resto dos raios no semicírculo; em vez de fechar duas lacunas de cada lado do raio solitário, o tremor agora fecha uma grande lacuna ao longo do tempo. A equidistância aqui também não é alcançada após 1.200 iterações.

Esse vetor solitário para o norte agora rapidamente se junta a seus pares, porque o ângulo para seu vizinho mais próximo é enorme e aumenta o termo de variação que agora está sendo minimizado. Mas, em última análise, ainda é impulsionado pelo ângulo mínimo global que permanece lento para aumentar. Idéias que tenho para melhorar isso geralmente funcionam neste caso 2D, mas não em dimensões superiores.

Mas focar demais na qualidade dessa tentativa matemática está perdendo o foco. Veja quantas operações de tensor estão envolvidas nos cálculos de média e variância e como o TensorFlow rastreia e diferencia com sucesso cada cálculo para cada componente na matriz de entrada. E não tivemos que fazer nenhum cálculo manual. Acabamos de jogar algumas contas simples e o TensorFlow fez o cálculo para nós.

Finalmente, vamos tentar mais uma coisa: uma solução baseada em força. Imagine que cada vetor é um pequeno planeta preso a um ponto central. Cada planeta emite uma força que o repele dos outros planetas. Se fôssemos executar uma simulação física desse modelo, deveríamos chegar à solução desejada.

Minha hipótese é que a descida do gradiente também deve funcionar. Na solução ótima, a força tangente em todos os planetas de todos os outros planetas deve se cancelar para uma força líquida zero (se não fosse zero, os planetas estariam se movendo). Então, vamos calcular a magnitude da força em cada vetor e usar o gradiente descendente para empurrá-lo para zero.

Primeiro, precisamos definir o método que calcula a força usando os métodos tf.* :

 class VectorSpread_Force(VectorSpreadAlgorithm): def force_a_onto_b(self, vec_a, vec_b): # Calc force assuming vec_b is constrained to the unit sphere diff = vec_b - vec_a norm = tf.sqrt(tf.reduce_sum(diff**2)) unit_force_dir = diff / norm force_magnitude = 1 / norm**2 force_vec = unit_force_dir * force_magnitude # Project force onto this vec, calculate how much is radial b_dot_f = tf.tensordot(vec_b, force_vec, axes=1) b_dot_b = tf.tensordot(vec_b, vec_b, axes=1) radial_component = (b_dot_f / b_dot_b) * vec_b # Subtract radial component and return result return force_vec - radial_component

Então, definimos nossa função de perda usando a função de força acima. Acumulamos a força resultante em cada vetor e calculamos sua magnitude. Em nossa solução ótima, todas as forças devem se anular e devemos ter força zero.

 def calc_loss(self, tensor2d): n_vec = tensor2d.numpy().shape[0] all_force_list = [] for this_idx in range(n_vec): # Accumulate force of all other vecs onto this one this_force_list = [] for other_idx in range(n_vec): if this_idx == other_idx: continue this_vec = tensor2d[this_idx, :] other_vec = tensor2d[other_idx, :] tangent_force_vec = self.force_a_onto_b(other_vec, this_vec) this_force_list.append(tangent_force_vec) # Use list of all N-dimensional force vecs. Stack and sum. sum_tangent_forces = tf.reduce_sum(tf.stack(this_force_list)) this_force_mag = tf.sqrt(tf.reduce_sum(sum_tangent_forces**2)) # Accumulate all magnitudes, should all be zero at optimal solution all_force_list.append(this_force_mag) # We want to minimize total force sum, so simply stack, sum, return return tf.reduce_sum(tf.stack(all_force_list)) 

Uma animação indo do estado inicial para o estado de destino. Os primeiros quadros mostram movimento rápido em todos os raios e, após apenas 200 iterações, a imagem geral já está bastante próxima do alvo. Apenas 700 iterações são mostradas no total; após o 300º, os ângulos mudam apenas minuciosamente a cada quadro.

A solução não apenas funciona lindamente (além de algum caos nos primeiros quadros), mas o crédito real vai para o TensorFlow. Essa solução envolveu vários loops for , uma instrução if e uma enorme rede de cálculos, e o TensorFlow rastreou com sucesso os gradientes por tudo isso para nós.

Exemplo 3: Gerando Entradas Adversarial AI

Exemplo 3 Caderno

Neste ponto, os leitores podem estar pensando: "Ei! Este post não deveria ser sobre aprendizado profundo!" Mas, tecnicamente, a introdução se refere a ir além de " treinar modelos de aprendizado profundo". Nesse caso, não estamos treinando , mas explorando algumas propriedades matemáticas de uma rede neural profunda pré-treinada para enganá-la e nos dar os resultados errados. Isso acabou sendo muito mais fácil e eficaz do que se imaginava. E bastou outro pequeno blob de código do TensorFlow 2.0.

Começamos encontrando um classificador de imagem para atacar. Usaremos uma das principais soluções para a Competição Kaggle Dogs vs. Cats; especificamente, a solução apresentada por Kaggler “uysimty”. Todo o crédito a eles por fornecer um modelo eficaz de gato contra cachorro e fornecer uma ótima documentação. Este é um modelo poderoso que consiste em 13 milhões de parâmetros em 18 camadas de rede neural. (Os leitores podem ler mais sobre isso no caderno correspondente.)

Observe que o objetivo aqui não é destacar nenhuma deficiência nessa rede específica, mas mostrar como qualquer rede neural padrão com um grande número de entradas é vulnerável.

Relacionado: Modelos Sound Logic e Monotonic AI

Com alguns ajustes, consegui descobrir como carregar o modelo e pré-processar as imagens para serem classificadas por ele.

Cinco imagens de amostra, cada uma de um cão ou de um gato, com uma classificação e nível de confiança correspondentes. Os níveis de confiança mostrados variam de 95% a 100%.

Isso parece um classificador realmente sólido! Todas as classificações da amostra estão corretas e acima de 95% de confiança. Vamos atacá-lo!

Queremos produzir uma imagem que seja obviamente um gato, mas que o classificador decida que é um cachorro com alta confiança. Como podemos fazer isso?

Vamos começar com uma imagem de gato que ela classifica corretamente e, em seguida, descobrir como pequenas modificações em cada canal de cor (valores 0-255) de um determinado pixel de entrada afetam a saída final do classificador. Modificar um pixel provavelmente não fará muito, mas talvez os ajustes cumulativos de todos os valores de 128x128x3 = 49.152 pixels atinjam nosso objetivo.

Como sabemos para que lado empurrar cada pixel? Durante o treinamento normal da rede neural, tentamos minimizar a perda entre o rótulo alvo e o rótulo previsto, usando gradiente descendente no TensorFlow para atualizar simultaneamente todos os 13 milhões de parâmetros livres. Nesse caso, deixaremos os 13 milhões de parâmetros fixos e ajustaremos os valores de pixel da própria entrada.

Qual é a nossa função de perda? Bem, é o quanto a imagem se parece com um gato! Se calcularmos a derivada do valor do gato em relação a cada pixel de entrada, saberemos qual o caminho para empurrar cada um para minimizar a probabilidade de classificação do gato.

 def adversarial_modify(victim_img, to_dog=False, to_cat=False): # We only need four gradient descent steps for i in range(4): tf_victim_img = tf.convert_to_tensor(victim_img, dtype='float32') with tf.GradientTape() as tape: tape.watch(tf_victim_img) # Run the image through the model model_output = model(tf_victim_img) # Minimize cat confidence and maximize dog confidence loss = (model_output[0] - model_output[1]) dloss_dimg = tape.gradient(loss, tf_victim_img) # Ignore gradient magnitudes, only care about sign, +1/255 or -1/255 pixels_w_pos_grad = tf.cast(dloss_dimg > 0.0, 'float32') / 255. pixels_w_neg_grad = tf.cast(dloss_dimg < 0.0, 'float32') / 255. victim_img = victim_img - pixels_w_pos_grad + pixels_w_neg_grad

A magia do Matplotlib novamente ajuda a visualizar os resultados.

Uma amostra de imagem de gato original, juntamente com 4 iterações, com classificações, "Gato 99,0%", "Gato 67,3%", "Cão 71,7%", "Cão 94,3%" e "Cão 99,4%", respectivamente.

Uau! Para o olho humano, cada uma dessas imagens é idêntica. No entanto, após quatro iterações, convencemos o classificador de que este é um cachorro, com 99,4% de confiança!

Vamos ter certeza de que isso não é um acaso e funciona na outra direção também.

Uma imagem de amostra original do cão, juntamente com 4 iterações, com classificações, "Cão 98,4%", "Cão 83,9%", "Cão 54,6%", "Gato 90,4%" e "Gato 99,8%", respectivamente. Como antes, as diferenças são invisíveis a olho nu.

Sucesso! O classificador originalmente previu isso corretamente como um cachorro com 98,4% de confiança e agora acredita que é um gato com 99,8% de confiança.

Finalmente, vamos dar uma olhada em uma amostra de patch de imagem e ver como ela mudou.

Três grades de linhas e colunas de pixels, mostrando valores numéricos para o canal vermelho de cada pixel. O patch da imagem à esquerda mostra principalmente quadrados azulados, destacando valores de 218 ou abaixo, com alguns quadrados vermelhos (219 e acima) agrupados no canto inferior direito. A página de imagem do meio, "vitimada", mostra um layout numerado e colorido de maneira muito semelhante. O patch da imagem à direita mostra a diferença numérica entre os outros dois, com diferenças variando apenas de -4 a +4 e incluindo vários zeros.

Como esperado, o patch final é muito semelhante ao original, com cada pixel mudando apenas -4 a +4 no valor de intensidade do canal vermelho. Essa mudança não é suficiente para um humano distinguir a diferença, mas altera completamente a saída do classificador.

Considerações Finais: Otimização de Descida de Gradiente

Ao longo deste artigo, analisamos a aplicação manual de gradientes aos nossos parâmetros treináveis ​​por uma questão de simplicidade e transparência. No entanto, no mundo real, os cientistas de dados devem pular direto para o uso de otimizadores , porque eles tendem a ser muito mais eficazes, sem adicionar nenhum excesso de código.

Existem muitos otimizadores populares, incluindo RMSprop, Adagrad e Adadelta, mas o mais comum é provavelmente o Adam . Às vezes, eles são chamados de “métodos de taxa de aprendizado adaptável” porque mantêm dinamicamente uma taxa de aprendizado diferente para cada parâmetro. Muitos deles usam termos de momento e aproximam derivadas de ordem superior, com o objetivo de escapar de mínimos locais e alcançar uma convergência mais rápida.

Em uma animação emprestada de Sebastian Ruder, podemos ver o caminho de vários otimizadores descendo uma superfície de perda. As técnicas manuais que demonstramos são mais comparáveis ​​ao “SGD”. O otimizador de melhor desempenho não será o mesmo para cada superfície de perda; no entanto, otimizadores mais avançados normalmente têm um desempenho melhor do que os mais simples.

Um mapa de contorno animado, mostrando o caminho percorrido por seis métodos diferentes para convergir em um ponto alvo. O SGD é de longe o mais lento, fazendo uma curva constante desde o seu ponto de partida. Momentum inicialmente se afasta do alvo, então cruza seu próprio caminho duas vezes antes de ir em direção a ele não totalmente diretamente, e parece ultrapassá-lo e depois voltar atrás. O NAG é semelhante, mas não se afasta tanto do alvo e se cruza apenas uma vez, geralmente atingindo o alvo mais rápido e ultrapassando-o menos. Adagrad começa em uma linha reta que é a mais fora do curso, mas muito rapidamente faz uma curva fechada em direção à colina em que o alvo está, e curva em direção a ela mais rápido do que as três primeiras. Adadelta tem um caminho semelhante, mas com uma curva mais suave; ele ultrapassa Adagrad e fica à frente após o primeiro segundo ou mais. Finalmente, Rmsprop segue um caminho muito semelhante ao Adadelta, mas se inclina um pouco mais perto do alvo desde o início; notavelmente, seu curso é muito mais estável, ficando atrás de Adagrad e Adadelta na maior parte da animação; ao contrário dos outros cinco, parece ter dois saltos repentinos e rápidos em duas direções diferentes perto do final da animação antes de cessar o movimento, enquanto os outros, no último momento, continuam a se arrastar lentamente pelo alvo.

No entanto, raramente é útil ser um especialista em otimizadores, mesmo para aqueles que desejam fornecer serviços de desenvolvimento de inteligência artificial. É melhor usar o tempo dos desenvolvedores para se familiarizar com alguns, apenas para entender como eles melhoram a descida do gradiente no TensorFlow. Depois disso, eles podem usar o Adam por padrão e tentar diferentes apenas se seus modelos não estiverem convergindo.

Para os leitores que estão realmente interessados ​​em como e por que esses otimizadores funcionam, a visão geral de Ruder – na qual a animação aparece – é um dos melhores e mais completos recursos sobre o assunto.

Vamos atualizar nossa solução de regressão linear da primeira seção para usar otimizadores. A seguir está o código de descida de gradiente original usando gradientes manuais.

 # Manual gradient descent operations def run_gradient_descent(heights, weights, init_slope, init_icept, learning_rate): tf_slope = tf.Variable(init_slope, dtype='float32') tf_icept = tf.Variable(init_icept, dtype='float32') for i in range(25): with tf.GradientTape() as tape: tape.watch((tf_slope, tf_icept)) predictions = tf_slope * heights + tf_icept errors = predictions - weights loss = tf.reduce_mean(errors**2) gradients = tape.gradient(loss, [tf_slope, tf_icept]) tf_slope = tf_slope - learning_rate * gradients[0] tf_icept = tf_icept - learning_rate * gradients[1]

Agora, aqui está o mesmo código usando um otimizador. Você verá que dificilmente é um código extra (as linhas alteradas são destacadas em azul):

 # Gradient descent with Optimizer (RMSprop) def run_gradient_descent (heights, weights, init_slope, init_icept, learning_rate) : tf_slope = tf.Variable(init_slope, dtype= 'float32' ) tf_icept = tf.Variable(init_icept, dtype= 'float32' ) # Group trainable parameters into a list trainable_params = [tf_slope, tf_icept] # Define your optimizer (RMSprop) outside of the training loop optimizer = keras.optimizers.RMSprop(learning_rate) for i in range( 25 ): # GradientTape loop is the same with tf.GradientTape() as tape: tape.watch( trainable_params ) predictions = tf_slope * heights + tf_icept errors = predictions - weights loss = tf.reduce_mean(errors** 2 ) # We can use the trainable parameters list directly in gradient calcs gradients = tape.gradient(loss, trainable_params ) # Optimizers always aim to *minimize* the loss function optimizer.apply_gradients(zip(gradients, trainable_params))

É isso! Definimos um otimizador RMSprop fora do loop de descida de gradiente e, em seguida, usamos o método optimizer.apply_gradients() após cada cálculo de gradiente para atualizar os parâmetros treináveis. O otimizador é definido fora do loop porque acompanhará os gradientes históricos para calcular termos extras, como momento e derivadas de ordem superior.

Vamos ver como fica com o otimizador RMSprop .

Semelhante aos pares sincronizados anteriores de animações; a linha ajustada começa acima de seu local de descanso. O gráfico de perda mostra quase convergindo após meras cinco iterações.

Parece ótimo! Agora vamos tentar com o otimizador Adam .

Outro gráfico de dispersão sincronizado e animação de gráfico de perda correspondente. O gráfico de perdas se destaca dos demais por não continuar estritamente se aproximando do mínimo; em vez disso, assemelha-se ao caminho de uma bola quicando. A linha ajustada correspondente no gráfico de dispersão começa acima dos pontos de amostra, oscila em direção ao fundo deles, depois volta, mas não tão alto, e assim por diante, com cada mudança de direção mais próxima de uma posição central.

Uau, o que aconteceu aqui? Parece que a mecânica do momento em Adam faz com que ele ultrapasse a solução ideal e reverta o curso várias vezes. Normalmente, essa mecânica de momento ajuda com superfícies de perda complexas, mas nos prejudica neste caso simples. Isso enfatiza o conselho de fazer da escolha do otimizador um dos hiperparâmetros a serem ajustados ao treinar seu modelo.

Qualquer pessoa que queira explorar o aprendizado profundo deve se familiarizar com esse padrão, pois ele é amplamente usado em arquiteturas personalizadas do TensorFlow, nas quais há a necessidade de ter mecanismos de perda complexos que não são facilmente envolvidos no fluxo de trabalho padrão. Neste exemplo simples de descida de gradiente do TensorFlow, havia apenas dois parâmetros treináveis, mas é necessário otimizar ao trabalhar com arquiteturas contendo centenas de milhões de parâmetros.

Descida de gradiente no TensorFlow: de encontrar mínimos a atacar sistemas de IA

Todos os trechos de código e imagens foram produzidos a partir dos notebooks no repositório GitHub correspondente. Ele também contém um resumo de todas as seções, com links para os cadernos individuais, para os leitores que desejam ver o código completo. Para simplificar a mensagem, muitos detalhes foram deixados de fora que podem ser encontrados na extensa documentação em linha.

Espero que este artigo tenha sido perspicaz e tenha feito você pensar em maneiras de usar o gradiente descendente no TensorFlow. Mesmo que você não o use, esperamos que fique mais claro como todas as arquiteturas de rede neural modernas funcionam – crie um modelo, defina uma função de perda e use gradiente descendente para ajustar o modelo ao seu conjunto de dados.


Selo do Google Cloud Partner.

Como Google Cloud Partner, os especialistas certificados pelo Google da Toptal estão disponíveis para empresas sob demanda para seus projetos mais importantes.