WEB3DEV

Cover image for Rede Neural Profunda do Zero em Rust 🦀 - Parte 4 - Função de Perda e Propagação para Trás
Paulo Gio
Paulo Gio

Posted on

Rede Neural Profunda do Zero em Rust 🦀 - Parte 4 - Função de Perda e Propagação para Trás

Série sobre Rede Neural Profunda do Zero em Rust

https://miro.medium.com/v2/resize:fit:1100/format:webp/0*IGmGBHKJq3y8d1Kl.png

Após nosso último artigo, precisamos definir uma função de perda para calcular o quão errado nosso modelo está neste momento. Para um problema simples de classificação binária, a função de perda é dada conforme abaixo:

https://miro.medium.com/v2/resize:fit:1100/format:webp/0*_aObq3lyEts_5AGA.png

onde,

m ⇾ Número de exemplos de treinamento

Y ⇾ Rótulos de Treinamento Verdadeiros

A[L]⇾ Rótulos previstos da propagação para frente

O objetivo da função de perda é medir a discrepância entre os rótulos previstos e os rótulos verdadeiros. Ao minimizar essa perda, buscamos aproximar as previsões do nosso modelo o máximo possível da verdade absoluta.

Para treinar o modelo e minimizar a perda, empregamos uma técnica chamada propagação para trás (retropropagação). Esta técnica calcula os gradientes da função de custo em relação aos pesos e vieses, indicando a direção e magnitude dos ajustes necessários para cada parâmetro.

Depois de calcular os gradientes, podemos ajustar os pesos e vieses para minimizar a perda. As seguintes equações são usadas para atualizar os parâmetros usando uma taxa de aprendizado alpha:

https://miro.medium.com/v2/resize:fit:640/format:webp/0*g2Txu4AUUYmlt1mR.png

Derivações dessas equações podem ser encontradas aqui.

Essas equações atualizam os pesos e vieses de cada camada com base em seus respectivos gradientes. Ao realizar iterativamente as passagens para frente e para trás, e atualizando os parâmetros usando os gradientes, permitimos que o modelo aprenda e melhore seu desempenho ao longo do tempo.

https://miro.medium.com/v2/resize:fit:1100/format:webp/0*uLhP7GHlR8ufNfpz.png

O repositório git para todo o código até esta parte é fornecido no link abaixo. Por favor, consulte-o caso esteja com alguma dificuldade.

Função de Custo

Para calcular a função de custo com base na equação de custo acima, precisamos primeiro fornecer um trait de logaritmo para o Array2<f32>, já que não podemos obter o logaritmo de um array em rust diretamente. Faremos isso escrevendo o seguinte código no início de lib.rs.

trait Log {
   fn log(&self) -> Array2<f32>;
}

impl Log for Array2<f32> {
   fn log(&self) -> Array2<f32> {
       self.mapv(|x| x.log(std::f32::consts::E))
   }
}
Enter fullscreen mode Exit fullscreen mode

Em seguida, no nosso impl DeepNeuralNetwork, adicionaremos uma função para calcular o custo.

pub fn cost(&self, al: &Array2<f32>, y: &Array2<f32>) -> f32 {
       let m = y.shape()[1] as f32;
       let cost = -(1.0 / m)
           * (y.dot(&al.clone().reversed_axes().log())
               + (1.0 - y).dot(&(1.0 - al).reversed_axes().log()));

       return cost.sum();
   }
Enter fullscreen mode Exit fullscreen mode

Aqui passamos as ativações da última camada al e os rótulos verdadeiros y para calcular o custo.

Ativações para Trás

pub fn sigmoid_prime(z: &f32) -> f32 {
   sigmoid(z) * (1.0 - sigmoid(z))
}

pub fn relu_prime(z: &f32) -> f32 {
   match *z > 0.0 {
       true => 1.0,
       false => 0.0,
   }
}

pub fn sigmoid_backward(da: &Array2<f32>, activation_cache: ActivationCache) -> Array2<f32> {
   da * activation_cache.z.mapv(|x| sigmoid_prime(&x))
}

pub fn relu_backward(da: &Array2<f32>, activation_cache: ActivationCache) -> Array2<f32> {
   da * activation_cache.z.mapv(|x| relu_prime(&x))
}
Enter fullscreen mode Exit fullscreen mode

A função sigmoid_prime calcula a derivada da função de ativação sigmoide. Ela recebe a entrada z e retorna o valor da derivada, que é computado como o sigmoide de z multiplicado por 1.0 menos o sigmoide de z.

A função relu_prime calcula a derivada da função de ativação ReLU. Ela recebe a entrada z e retorna 1.0 se z for maior que 0, e 0.0 caso contrário.

A função sigmoid_backward calcula a propagação para trás para a função de ativação sigmoide. Ela pega a derivada da função de custo em relação à ativação da e o cache de ativação activation_cache. Realiza uma multiplicação elemento a elemento entre da e a derivada da função sigmoide, aplicada aos valores no cache de ativação activation_cache.z.

A função relu_backward calcula a propagação para trás para a função de ativação ReLU. Ela recebe a derivada da função de custo com relação à ativação da e o cache de ativação activation_cache. Ela realiza uma multiplicação elemento a elemento entre da e a derivada da função ReLU, aplicada aos valores no cache de ativação activation_cache.z.

Propagação Linear para Trás

pub fn linear_backward(
   dz: &Array2<f32>,
   linear_cache: LinearCache,
) -> (Array2<f32>, Array2<f32>, Array2<f32>) {
   let (a_prev, w, _b) = (linear_cache.a, linear_cache.w, linear_cache.b);
   let m = a_prev.shape()[1] as f32;
   let dw = (1.0 / m) * (dz.dot(&a_prev.reversed_axes()));
   let db_vec = ((1.0 / m) * dz.sum_axis(Axis(1))).to_vec();
   let db = Array2::from_shape_vec((db_vec.len(), 1), db_vec).unwrap();
   let da_prev = w.reversed_axes().dot(dz);

   (da_prev, dw, db)
}
Enter fullscreen mode Exit fullscreen mode

A função linear_backward calcula a propagação para trás para o componente linear de uma camada. Ela recebe o gradiente da função de custo em relação à saída linear dz e o cache linear linear_cache. Ela retorna os gradientes em relação à ativação da camada anterior da_prev, os pesos dw e os vieses db.

A função primeiro extrai a ativação da camada anterior a_prev, a matriz de pesos w e a matriz de vieses _b do cache linear. Ela calcula o número de exemplos de treinamento m acessando a forma de a_prev e dividindo o número de exemplos por m.

A função então calcula o gradiente dos pesos dw usando o produto escalar entre dz e a a_prev transposta, escalado por 1/m. Calcula o gradiente dos vieses db somando os elementos de dz ao longo do Axis(1) e escalando o resultado por 1/m. Finalmente, calcula o gradiente da ativação da camada anterior da_prev, fazendo o produto escalar entre o w transposto e o dz.

A função retorna da_prev, dw e db.

Propagação para Trás

impl DeepNeuralNetwork {
   pub fn initialize_parameters(&self) -> HashMap<String, Array2<f32>> {
// o mesmo da última parte
   }
   pub fn forward(
       &self,
       x: &Array2<f32>,
       parameters: &HashMap<String, Array2<f32>>,
   ) -> (Array2<f32>, HashMap<String, (LinearCache, ActivationCache)>) {
   // o mesmo da última parte
   }

pub fn backward(
       &self,
       al: &Array2<f32>,
       y: &Array2<f32>,
       caches: HashMap<String, (LinearCache, ActivationCache)>,
   ) -> HashMap<String, Array2<f32>> {
       let mut grads = HashMap::new();
       let num_of_layers = self.layers.len() - 1;

       let dal = -(y / al - (1.0 - y) / (1.0 - al));

       let current_cache = caches[&num_of_layers.to_string()].clone();
       let (mut da_prev, mut dw, mut db) =
           linear_backward_activation(&dal, current_cache, "sigmoid");

       let weight_string = ["dW", &num_of_layers.to_string()].join("").to_string();
       let bias_string = ["db", &num_of_layers.to_string()].join("").to_string();
       let activation_string = ["dA", &num_of_layers.to_string()].join("").to_string();

       grads.insert(weight_string, dw);
       grads.insert(bias_string, db);
       grads.insert(activation_string, da_prev.clone());

       for l in (1..num_of_layers).rev() {
           let current_cache = caches[&l.to_string()].clone();
           (da_prev, dw, db) =
               linear_backward_activation(&da_prev, current_cache, "relu");

           let weight_string = ["dW", &l.to_string()].join("").to_string();
           let bias_string = ["db", &l.to_string()].join("").to_string();
           let activation_string = ["dA", &l.to_string()].join("").to_string();

           grads.insert(weight_string, dw);
           grads.insert(bias_string, db);
           grads.insert(activation_string, da_prev.clone());
       }

       grads
   }
Enter fullscreen mode Exit fullscreen mode

O método backward na struct DeepNeuralNetwork executa o algoritmo de propagação para trás para calcular os gradientes da função de custo em relação aos parâmetros (pesos e vieses) de cada camada.

O método recebe a ativação final al obtida da propagação para frente, os rótulos verdadeiros y e os caches contendo os valores lineares e de ativação para cada camada.

Primeiro, o método inicializa um HashMap vazio chamado grads para armazenar os gradientes. Calcula a derivada inicial da função de custo em relação à al usando a fórmula fornecida.

Depois, começando da última camada (camada de saída), recupera o cache para a camada atual e chama a função linear_backward_activation para calcular os gradientes da função de custo em relação aos parâmetros dessa camada. A função de ativação usada é “sigmoide” para a última camada. Os gradientes computados para os pesos, vieses e ativação são armazenados no hashmap grads.

Em seguida, o método itera sobre as camadas restantes em ordem reversa. Para cada camada, ele recupera o cache, chama a função linear_backward_activation para calcular os gradientes e os armazena no hashmap grads.

Finalmente, o método retorna o hashmap grads contendo os gradientes da função de custo em relação a cada parâmetro da rede neural.

Isso completa a etapa de propagação para trás, onde os gradientes da função de custo são computados em relação aos pesos, vieses e ativações de cada camada. Esses gradientes serão usados na etapa de otimização para atualizar os parâmetros e minimizar o custo.

Atualizar Parâmetros

pub fn update_parameters(
       &self,
       params: &HashMap<String, Array2<f32>>,
       grads: HashMap<String, Array2<f32>>,
       m: f32,
       learning_rate: f32,

   ) -> HashMap<String, Array2<f32>> {
       let mut parameters = params.clone();
       let num_of_layers = self.layer_dims.len() - 1;
       for l in 1..num_of_layers + 1 {
           let weight_string_grad = ["dW", &l.to_string()].join("").to_string();
           let bias_string_grad = ["db", &l.to_string()].join("").to_string();
           let weight_string = ["W", &l.to_string()].join("").to_string();
           let bias_string = ["b", &l.to_string()].join("").to_string();

           *parameters.get_mut(&weight_string).unwrap() = parameters[&weight_string].clone()
               - (learning_rate * (grads[&weight_string_grad].clone() + (self.lambda/m) *parameters[&weight_string].clone()) );
           *parameters.get_mut(&bias_string).unwrap() = parameters[&bias_string].clone()
               - (learning_rate * grads[&bias_string_grad].clone());
       }
       parameters
   }
Enter fullscreen mode Exit fullscreen mode

Agora vamos atualizar os parâmetros usando os gradientes que calculamos.

Neste código, passamos por cada camada e atualizamos os parâmetros no HashMap para cada camada usando o HashMap de gradientes naquela camada. Isso nos retornará os parâmetros atualizados.

Isso é tudo para esta parte. Eu sei que isso foi um pouco complicado, mas esta parte é o coração de uma rede neural profunda. Aqui estão alguns recursos que podem ajudá-lo a entender o algoritmo de forma mais visual.

Uma Visão Geral do Algoritmo de Propagação para Trás: https://www.youtube.com/watch?v=Ilg3gGewQ5U&t=203s

Cálculo Por Trás do Algoritmo de Propagação para Trás: https://www.youtube.com/watch?v=tIeHLnjs5U8

Na próxima e última parte desta série, vamos executar nosso loop de treinamento e testar nosso modelo em algumas imagens de gatos 🐈.

Repositório GitHub: https://github.com/akshayballal95/dnn_rust_blog.git

Quer se conectar?

Meu website
LinkedIn
Twitter

Artigo original publicado por Akshay Ballal. Traduzido por Paulinho Giovannini.

Top comments (0)