Implementando e comparando otimizadores de modelo em aprendizado profundo



Implementamos e comparamos 4 otimizadores de treinamento de rede neural populares: otimizador de impulso, propagação de rms, descida gradiente de minilote e estimativa de torque adaptativo. O repositório, muito código Python e sua saída, visualizações e fórmulas estão todos sob controle.






Introdução



Um modelo é o resultado de um algoritmo de aprendizado de máquina executado em alguns dados. O modelo representa o que foi aprendido pelo algoritmo. Essa é a "coisa" que persiste após a execução do algoritmo nos dados de treinamento e representa as regras, números e quaisquer outras estruturas de dados específicas do algoritmo e necessárias para a previsão.



O que é um otimizador?



Antes de prosseguirmos, precisamos saber o que é uma função de perda. A função de perda é uma medida de quão bem o seu modelo de previsão prevê o resultado esperado (ou valor). A função de perda também é chamada de função de custo (mais informações aqui ).



Durante o treinamento, tentamos minimizar a perda de função e atualizar os parâmetros para melhorar a precisão. Os parâmetros da rede neural são geralmente os pesos do link. Neste caso, os parâmetros são estudados na fase de treinamento. Portanto, o próprio algoritmo (e os dados de entrada) ajusta esses parâmetros. Mais informações podem ser encontradas aqui .



Assim, o otimizador é um método para obter melhores resultados, ajudando a acelerar o aprendizado. Em outras palavras, é um algoritmo usado para fazer pequenos ajustes em parâmetros como pesos e taxas de aprendizado para manter o modelo funcionando de forma correta e rápida. Aqui está uma visão geral básica dos vários otimizadores usados ​​no aprendizado profundo e um modelo simples para entender a implementação desse modelo. Eu recomendo fortemente clonar este repositório e fazer alterações observando os padrões de comportamento.



Alguns termos comumente usados:



  • Propagação de volta


Os objetivos da retropropagação são simples: ajustar cada peso na rede de acordo com o quanto ele contribui para o erro geral. Se você reduzir iterativamente o erro de cada peso, acabará com uma série de pesos que fornecem boas previsões. Encontramos a inclinação de cada parâmetro para a função de perda e atualizamos os parâmetros subtraindo a inclinação (mais informações aqui ).







  • Gradiente descendente


A descida gradiente é um algoritmo de otimização usado para minimizar uma função movendo-se iterativamente em direção à descida mais íngreme, definida por um valor de gradiente negativo. No aprendizado profundo, usamos gradiente descendente para atualizar os parâmetros do modelo (mais informações aqui ).



  • Hiperparâmetros


Um hiperparâmetro de modelo é uma configuração externa ao modelo, cujo valor não pode ser estimado a partir dos dados. Por exemplo, o número de neurônios ocultos, a taxa de aprendizado, etc. Não podemos estimar a taxa de aprendizado a partir dos dados (mais informações aqui ).



  • Taxa de Aprendizagem


A taxa de aprendizagem (α) é um parâmetro de ajuste no algoritmo de otimização que determina o tamanho do passo em cada iteração enquanto se move em direção ao mínimo da função de perda (mais informações aqui).



Otimizadores populares





Abaixo estão alguns dos SEOs mais populares:



  1. Descida em gradiente estocástico (SGD).
  2. Otimizador de impulso.
  3. Propagação quadrada média (RMSProp).
  4. Estimativa de torque adaptativo (Adam).


Vamos considerar cada um deles em detalhes.



1. Descida gradiente estocástica (especialmente minilote)



Usamos um exemplo por vez ao treinar o modelo (em SGD puro) e atualizar o parâmetro. Mas temos que usar outro para o loop. Isso levará muito tempo. Portanto, usamos mini-lote SGD.



A descida gradiente do minibote busca equilibrar a robustez da descida gradiente estocástica e a eficiência da descida gradiente do lote. Esta é a implementação mais comum de gradiente descendente usada no aprendizado profundo. No minilote SGD, ao treinar o modelo, tomamos um grupo de exemplos (por exemplo, 32, 64 exemplos, etc.). Essa abordagem funciona melhor porque leva apenas um único loop para os minilotes, não todos os exemplos. Mini-pacotes são selecionados aleatoriamente para cada iteração, mas por quê? Quando os mini-pacotes são escolhidos aleatoriamente, quando presos nos mínimos locais, alguns passos ruidosos podem levar à saída desses mínimos. Por que precisamos deste otimizador?



  • A taxa de atualização do parâmetro é maior do que na descida gradiente de lote simples, o que permite uma convergência mais confiável evitando mínimos locais.
  • As atualizações em lote fornecem um processo computacionalmente mais eficiente do que a descida gradiente estocástica.
  • Se você tiver pouca RAM, os mini-pacotes são a melhor opção. O batching é eficiente devido à falta de todos os dados de treinamento na memória e nas implementações de algoritmo.


Como gerar mini-pacotes aleatórios?



def RandomMiniBatches(X, Y, MiniBatchSize):

    m = X.shape[0]  
    miniBatches = [] 
   
    permutation = list(np.random.permutation(m))
    shuffled_X = X[permutation, :]
    shuffled_Y = Y[permutation, :].reshape((m,1))   #sure for uptpur shape

    num_minibatches = m // MiniBatchSize 
    for k in range(0, num_minibatches):
        miniBatch_X = shuffled_X[k * MiniBatchSize:(k + 1) * MiniBatchSize,:]
        miniBatch_Y = shuffled_Y[k * MiniBatchSize:(k + 1) * MiniBatchSize,:]
        miniBatch = (miniBatch_X, miniBatch_Y)
        miniBatches.append(miniBatch)
    
    #handeling last batch
    if m % MiniBatchSize != 0:
        # end = m - MiniBatchSize * m // MiniBatchSize
        miniBatch_X = shuffled_X[num_minibatches * MiniBatchSize:, :]
        miniBatch_Y = shuffled_Y[num_minibatches * MiniBatchSize:, :]

        miniBatch = (miniBatch_X, miniBatch_Y)
        miniBatches.append(miniBatch)
    
    return miniBatches 


Qual será o formato do modelo?



Estou apresentando uma visão geral do modelo, caso você seja novo no aprendizado profundo. É mais ou menos assim:



def model(X,Y,learning_rate,num_iter,hidden_size,keep_prob,optimizer):
    L = len(hidden_size)
    params = initilization(X.shape[1], hidden_size)
    for i in range(1,num_iter):
        MiniBatches = RandomMiniBatches(X, Y, 64)   # GET RAMDOMLY MINIBATCHES
        p , q = MiniBatches[2]
        for MiniBatch in MiniBatches:               #LOOP FOR MINIBATCHES

            (MiniBatch_X, MiniBatch_Y) = MiniBatch

            cache, A = model_forward(MiniBatch_X, params, L,keep_prob)             #FORWARD PROPOGATIONS
            cost = cost_f(A, MiniBatch_Y)                                          #COST FUNCTION
            grad = backward(MiniBatch_X, MiniBatch_Y, params, cache, L,keep_prob)  #BACKWARD PROPAGATION 
            params = update_params(params, grad, beta=0.9,learning_rate=learning_rate)
    return params


Na figura a seguir, você pode ver que há grandes oscilações no SGD. O movimento vertical não é necessário: queremos apenas o movimento horizontal. Se você diminuir o movimento vertical e aumentar o movimento horizontal, o modelo aprenderá mais rápido, não concorda?







Como minimizar vibrações indesejadas? Os otimizadores a seguir os minimizam e ajudam a acelerar o aprendizado.



2. Otimizador de impulso



Há muita hesitação no SGD ou na descida gradiente. Você precisa seguir em frente, não para cima e para baixo. Precisamos aumentar a taxa de aprendizado do modelo na direção certa e faremos isso com o otimizador de momentum.







Como você pode ver na imagem acima, a Linha Verde do Pulse Optimizer é mais rápida do que as outras. A importância de aprender rapidamente pode ser vista quando você tem grandes conjuntos de dados e muitas iterações. Como implementar este otimizador?





O valor normal para β é cerca de 0,9.Você



pode ver que criamos dois parâmetros - vdW e vdb - a partir dos parâmetros de retropropagação . Considere o valor β = 0,9, então a equação assume a forma:



vdw= 0.9 * vdw + 0.1 * dw
vdb = 0.9 * vdb + 0.1 * db


Como você pode ver, vdw depende mais do valor anterior de vdw do que de dw. Quando a renderização é um gráfico, você pode ver que o Momentum Optimizer leva em consideração os gradientes anteriores para suavizar a atualização. É por isso que é possível minimizar as flutuações. Quando usamos SGD, o caminho percorrido pela descida gradiente do minilote oscilou em direção à convergência. O Momentum Optimizer ajuda a reduzir essas flutuações.



def update_params_with_momentum(params, grads, v, beta, learning_rate):
    
    # grads has the dw and db parameters from backprop
    # params  has the W and b parameters which we have to update 
    for l in range(len(params) // 2 ):

        # HERE WE COMPUTING THE VELOCITIES 
        v["dW" + str(l + 1)] = beta * v["dW" + str(l + 1)] + (1 - beta) * grads['dW' + str(l + 1)]
        v["db" + str(l + 1)] = beta * v["db" + str(l + 1)] + (1 - beta) * grads['db' + str(l + 1)]
        
        #updating parameters W and b
        params["W" + str(l + 1)] = params["W" + str(l + 1)] - learning_rate * v["dW" + str(l + 1)]
        params["b" + str(l + 1)] = params["b" + str(l + 1)] - learning_rate * v["db" + str(l + 1)]
    return params


O repositório está aqui



3. Distribuição quadrada média da raiz



Root root mean square (RMSprop) é uma média de decadência exponencial. A propriedade essencial do RMSprop é que você não está limitado apenas à soma dos gradientes anteriores, mas está mais limitado aos gradientes das últimas etapas de tempo. RMSprop contribui para a média exponencialmente decadente de "gradientes de lei quadrada" anteriores. No RMSProp estamos tentando reduzir o movimento vertical usando a média, porque eles somam cerca de 0, tomando a média. RMSprop fornece a média para a atualização.





Uma fonte







Dê uma olhada no código abaixo. Isso lhe dará uma compreensão básica de como implementar esse otimizador. Tudo é igual ao SGD, temos que mudar a função de atualização.



def initilization_RMS(params):
    s = {}
    for i in range(len(params)//2 ):
        s["dW" + str(i)] = np.zeros(params["W" + str(i)].shape)
        s["db" + str(i)] = np.zeros(params["b" + str(i)].shape)
    return s

def update_params_with_RMS(params, grads,s, beta, learning_rate):
    
    # grads has the dw and db parameters from backprop
    # params  has the W and b parameters which we have to update 
    for l in range(len(params) // 2 ):
        # HERE WE COMPUTING THE VELOCITIES 
        s["dW" + str(l)]= beta * s["dW" + str(l)] + (1 - beta) * np.square(grads['dW' + str(l)])
        s["db" + str(l)] = beta * s["db" + str(l)] + (1 - beta) * np.square(grads['db' + str(l)])
        
        #updating parameters W and b
        params["W" + str(l)] = params["W" + str(l)] - learning_rate * grads['dW' + str(l)] / (np.sqrt( s["dW" + str(l)] )+ pow(10,-4))
        params["b" + str(l)] = params["b" + str(l)] - learning_rate * grads['db' + str(l)] / (np.sqrt( s["db" + str(l)]) + pow(10,-4))

    return params


4. Adam Optimizer



Adam é um dos algoritmos de otimização mais eficientes no treinamento de redes neurais. Ele combina as idéias de RMSProp e Pulse Optimizer. Em vez de adaptar a taxa de aprendizado dos parâmetros com base na média do primeiro momento (média), como no RMSProp, Adam também usa a média dos segundos momentos dos gradientes. Especificamente, o algoritmo calcula a média móvel exponencial do gradiente e o gradiente quadrático, e os parâmetros beta1e beta2controla a taxa de decaimento dessas médias móveis. Como?



def initilization_Adam(params):
    s = {}
    v = {}
    for i in range(len(params)//2 ):

        v["dW" + str(i)] = np.zeros(params["W" + str(i)].shape)
        v["db" + str(i)] = np.zeros(params["b" + str(i)].shape)

        s["dW" + str(i)] = np.zeros(params["W" + str(i)].shape)
        s["db" + str(i)] = np.zeros(params["b" + str(i)].shape)
    return v, s
    
def update_params_with_Adam(params, grads,v, s, beta1,beta2, learning_rate,t):
    epsilon = pow(10,-8)
    v_corrected = {}                         
    s_corrected = {} 
    # grads has the dw and db parameters from backprop
    # params  has the W and b parameters which we have to update 
    for l in range(len(params) // 2 ):
        # HERE WE COMPUTING THE VELOCITIES 

        v["dW" + str(l)] = beta1 * v["dW" + str(l)] + (1 - beta1) * grads['dW' + str(l)]
        v["db" + str(l)] = beta1 * v["db" + str(l)] + (1 - beta1) * grads['db' + str(l)]

        v_corrected["dW" + str(l)] = v["dW" + str(l)] / (1 - np.power(beta1, t))
        v_corrected["db" + str(l)] = v["db" + str(l)] / (1 - np.power(beta1, t))


        s["dW" + str(l)] = beta2 * s["dW" + str(l)] + (1 - beta2) * np.power(grads['dW' + str(l)], 2)
        s["db" + str(l)] = beta2 * s["db" + str(l)] + (1 - beta2) * np.power(grads['db' + str(l)], 2)

        s_corrected["dW" + str(l)] = s["dW" + str(l)] / (1 - np.power(beta2, t))
        s_corrected["db" + str(l)] = s["db" + str(l)] / (1 - np.power(beta2, t))

        params["W" + str(l)] = params["W" + str(l)] - learning_rate * v_corrected["dW" + str(l)] / np.sqrt(s_corrected["dW" + str(l)] + epsilon)
        params["b" + str(l)] = params["b" + str(l)] - learning_rate * v_corrected["db" + str(l)] / np.sqrt(s_corrected["db" + str(l)] + epsilon)
    return params


Hiperparâmetros



  • β1 (beta1) valor quase 0,9
  • β2 (beta2) - quase 0,999
  • ε - evita a divisão por zero (10 ^ -8) (não afeta muito a aprendizagem)


Por que esse otimizador?



Suas vantagens:



  • Implementação simples.
  • Eficiência computacional.
  • Requisitos de memória baixos.
  • Escala invariante para diagonal de gradientes.
  • Bem adequado para grandes tarefas em termos de dados e parâmetros.
  • Adequado para fins não estacionários.
  • Adequado para tarefas com gradientes muito barulhentos ou esparsos.
  • Os hiperparâmetros são diretos e geralmente exigem poucos ajustes.


Vamos construir um modelo e ver como os hiperparâmetros aceleram o aprendizado



Vamos fazer uma demonstração prática de como acelerar o aprendizado. Neste artigo não iremos explicar as outras coisas (inicialização, rastreios, forward_prop, back_prop, gradiente descendente, e assim por diante. D.). As funções necessárias para o treinamento já estão incorporadas ao NumPy. Se você quiser dar uma olhada, aqui está o link !



Vamos começar!



Estou criando uma função de modelo genérico que funciona para todos os otimizadores discutidos aqui.



1. Inicialização:



inicializamos os parâmetros usando uma função de inicialização que recebe entradas como features_size (em nosso caso 12288) e uma matriz oculta de tamanhos (usamos [100,1]) e esta saída como parâmetros de inicialização. Existe outro método de inicialização. Eu encorajo você a ler este artigo.



def initilization(input_size,layer_size):
    params = {}
    np.random.seed(0) 
    params['W' + str(0)] = np.random.randn(layer_size[0], input_size) * np.sqrt(2 / input_size)
    params['b' + str(0)] = np.zeros((layer_size[0], 1))
    for l in range(1,len(layer_size)):
        params['W' + str(l)] = np.random.randn(layer_size[l],layer_size[l-1]) * np.sqrt(2/layer_size[l])
        params['b' + str(l)] = np.zeros((layer_size[l],1))
    return params


2. Propagação direta:



Nesta função, a entrada é X, assim como os parâmetros, a extensão das camadas ocultas e o dropout, que são usados ​​na técnica de dropout.



Defino o valor em 1 para que nenhum efeito seja visto no exercício. Se o seu modelo for super ajustado, você poderá definir um valor diferente. Eu apenas aplico o dropout nas camadas pares .



Calculamos o valor de ativação para cada camada usando uma função forward_activation.



#activations-----------------------------------------------
def forward_activation(A_prev, w, b, activation):
    z = np.dot(A_prev, w.T) + b.T
    if activation == 'relu':
        A = np.maximum(0, z)
    elif activation == 'sigmoid':
        A = 1/(1+np.exp(-z))
    else:
        A = np.tanh(z)
    return A


#________model forward ____________________________________________________________________________________________________________
def model_forward(X,params, L,keep_prob):
    cache = {}
    A =X

    for l in range(L-1):
        w = params['W' + str(l)]
        b = params['b' + str(l)]
        A = forward_activation(A, w, b, 'relu')
        if l%2 == 0:
            cache['D' + str(l)] = np.random.randn(A.shape[0],A.shape[1]) < keep_prob
            A = A * cache['D' + str(l)] / keep_prob
        cache['A' + str(l)] = A
    w = params['W' + str(L-1)]
    b = params['b' + str(L-1)]
    A = forward_activation(A, w, b, 'sigmoid')
    cache['A' + str(L-1)] = A
    return cache, A


3. Retropropagação:



aqui escrevemos a função de retropropagação. Ele retornará grad ( inclinação ). Usamos gradao atualizar os parâmetros (se você não souber sobre isso). Eu recomendo a leitura deste artigo.



def backward(X, Y, params, cach,L,keep_prob):
    grad ={}
    m = Y.shape[0]

    cach['A' + str(-1)] = X
    grad['dz' + str(L-1)] = cach['A' + str(L-1)] - Y
    cach['D' + str(- 1)] = 0
    for l in reversed(range(L)):
        grad['dW' + str(l)] = (1 / m) * np.dot(grad['dz' + str(l)].T, cach['A' + str(l-1)])
        grad['db' + str(l)] = 1 / m * np.sum(grad['dz' + str(l)].T, axis=1, keepdims=True)
        if l%2 != 0:
            grad['dz' + str(l-1)] = ((np.dot(grad['dz' + str(l)], params['W' + str(l)]) * cach['D' + str(l-1)] / keep_prob) *
                                 np.int64(cach['A' + str(l-1)] > 0))
        else :
            grad['dz' + str(l - 1)] = (np.dot(grad['dz' + str(l)], params['W' + str(l)]) *
                                       np.int64(cach['A' + str(l - 1)] > 0))

    return grad


Já vimos o recurso de atualização do otimizador, então vamos usá-lo aqui. Vamos fazer algumas pequenas alterações na função de modelo da discussão SGD.



def model(X,Y,learning_rate,num_iter,hidden_size,keep_prob,optimizer):
    L = len(hidden_size)
    params = initilization(X.shape[1], hidden_size)
    costs = []
    itr  = []

    if optimizer == 'momentum':
        v = initilization_moment(params)

    elif optimizer == 'rmsprop':
        s = initilization_RMS(params)

    elif optimizer == 'adam' :
        v,s = initilization_Adam(params)

    for i in range(1,num_iter):
        MiniBatches = RandomMiniBatches(X, Y, 32)   # GET RAMDOMLY MINIBATCHES
        p , q = MiniBatches[2]
        for MiniBatch in MiniBatches:               #LOOP FOR MINIBATCHES

            (MiniBatch_X, MiniBatch_Y) = MiniBatch

            cache, A = model_forward(MiniBatch_X, params, L,keep_prob)     #FORWARD PROPOGATIONS
            cost = cost_f(A, MiniBatch_Y)                                  #COST FUNCTION
            grad = backward(MiniBatch_X, MiniBatch_Y, params, cache, L,keep_prob) #BACKWARD PROPAGATION 

            if optimizer == 'momentum':
                params = update_params_with_momentum(params, grad, v, beta=0.9,learning_rate=learning_rate)

            elif optimizer == 'rmsprop':
               params = update_params_with_RMS(params, grad, s, beta=0.9,learning_rate=learning_rate)

            elif optimizer == 'adam' :
                params = update_params_with_Adam(params, grad,v, s, beta1=0.9,beta2=0.999,  learning_rate=learning_rate,t=i)                                         #UPDATE PARAMETERS
            elif optimizer == "minibatch":
                params = update_params(params, grad,learning_rate=learning_rate) 

           
        
        if i%5 == 0:
            costs.append(cost)
            itr.append(i)
            if i % 100 == 0 :
                print('cost of iteration______{}______{}'.format(i,cost))
    return params,costs,itr


Treino com mini packs



params, cost_sgd,itr = model(X_train, Y_train, learning_rate = 0.01,
               num_iter=500, hidden_size=[100, 1],keep_prob=1,optimizer='minibatch')
Y_train_pre = predict(X_train, params, 2)
print('train_accuracy------------', accuracy_score(Y_train_pre, Y_train))


Conclusão ao abordar com mini-pacotes:



cost of iteration______100______0.35302967575683797 
cost of iteration______200______0.472914548745098 
cost of iteration______300______0.4884728238471557 
cost of iteration______400______0.21551100063345618
 
train_accuracy------------ 0.8494208494208494


Treinamento do Pulse Optimizer



params,cost_momentum, itr = model(X_train, Y_train, learning_rate = 0.01,
               num_iter=500, hidden_size=[100, 1],keep_prob=1,optimizer='momentum')
Y_train_pre = predict(X_train, params, 2)
print('train_accuracy------------', accuracy_score(Y_train_pre, Y_train))


Saída do otimizador de pulso:



cost of iteration______100______0.36278494129038086 
cost of iteration______200______0.4681552335189021 
cost of iteration______300______0.382226159384529 
cost of iteration______400______0.18219310793752702 train_accuracy------------ 0.8725868725868726


Treinamento com RMSprop



params,cost_rms,itr = model(X_train, Y_train, learning_rate = 0.01,
               num_iter=500, hidden_size=[100, 1],keep_prob=1,optimizer='rmsprop')
Y_train_pre = predict(X_train, params, 2)
print('train_accuracy------------', accuracy_score(Y_train_pre, Y_train))


Saída RMSprop:



cost of iteration______100______0.2983858963793841 
cost of iteration______200______0.004245700579927428 
cost of iteration______300______0.2629426607580565 
cost of iteration______400______0.31944824707807556 train_accuracy------------ 0.9613899613899614


Treinando com Adam



params,cost_adam, itr = model(X_train, Y_train, learning_rate = 0.01,
               num_iter=500, hidden_size=[100, 1],keep_prob=1,optimizer='adam')
Y_train_pre = predict(X_train, params, 2)
print('train_accuracy------------', accuracy_score(Y_train_pre, Y_train))


Conclusão de Adam:



cost of iteration______100______0.3266223660473619 
cost of iteration______200______0.08214547683157716 
cost of iteration______300______0.0025645257286439583 
cost of iteration______400______0.058015188756586206 train_accuracy------------ 0.9845559845559846


Você viu a diferença de precisão entre os dois? Usamos os mesmos parâmetros de inicialização, a mesma taxa de aprendizado e o mesmo número de iterações; apenas o otimizador é diferente, mas olhe o resultado!



Mini-batch accuracy : 0.8494208494208494
momemtum accuracy   : 0.8725868725868726
Rms accuracy        : 0.9613899613899614
adam accuracy       : 0.9845559845559846


Visualização gráfica do modelo





Você pode verificar o repositório se tiver dúvidas sobre o código.



Resumo





origem

Como vimos, o otimizador Adam oferece boa precisão em comparação com outros otimizadores. A imagem acima mostra como o modelo aprende por meio de iterações. Momentum fornece a velocidade SGD e RMSProp fornece a média exponencial dos pesos para os parâmetros atualizados. Usamos menos dados no modelo acima, mas veremos mais benefícios dos otimizadores ao trabalhar com grandes conjuntos de dados e muitas iterações. Discutimos a ideia básica dos otimizadores e espero que isso lhe dê alguma motivação para aprender mais sobre otimizadores e usá-los!



Recursos




As perspectivas para redes neurais e aprendizado de máquina profundo são enormes e, pelas estimativas mais conservadoras, seu impacto no mundo será quase o mesmo que o impacto da eletricidade na indústria no século XIX. Os especialistas que avaliarão essas perspectivas antes de qualquer outra pessoa terão todas as chances de se tornar o chefe do progresso. Para essas pessoas, fizemos um código promocional HABR , que dá um adicional de 10% ao desconto de treinamento indicado no banner.



imagem






Mais cursos




Artigos recomendados






All Articles