Skip to content

Rede Neural Profunda do Zero em Rust 🦀 - Parte 5 - Treinamento e Inferência

Rede Neural Profunda do Zero em Rust 🦀 - Parte 5 - Treinamento e Inferência

Série sobre Rede Neural Profunda do Zero em Rust

/images/f0797fcd0b64.png

Ótimo!! Você chegou à parte final da série. Nesta parte, vamos treinar nosso modelo e testá-lo construindo uma função de previsão. Felizmente, não há matemática envolvida nesta parte 😃. Então, vamos ao código.

Treinamento

Primeiro, vamos construir nosso loop de treinamento, que recebe os dados de treinamento: x_train_data, os rótulos de treinamento: y_train_data, o dicionário de parâmetros: parameters, o número de iterações do loop de treinamento: iterations e a taxa de aprendizado: learning_rate. A função retornará os novos parâmetros após uma iteração de treinamento. Em impl DeepNeuralNetwork, adicione a seguinte função.

pub fn train_model(
        &self,
        x_train_data: &Array2<f32>,
        y_train_data: &Array2<f32>,
        mut parameters: HashMap<String, Array2<f32>>,
        iterations: usize,
        learning_rate: f32,
    ) -> HashMap<String, Array2<f32>> {
        let mut costs: Vec<f32> = vec![];

        for i in 0..iterations {
            let (al, caches) = self.forward(&x_train_data, &parameters);
            let cost = self.cost(&al, &y_train_data);
            let grads = self.backward(&al, &y_train_data, caches);
            parameters = self.update_parameters(&parameters, grads.clone(), learning_rate);

            if i % 100 == 0 {
                costs.append(&mut vec![cost]);
                println!("Época : {}/{}    Custo: {:?}", i, iterations, cost);
            }
        }
        parameters
    }

Essa função realiza os seguintes passos:

  1. Inicializa um vetor vazio chamado costs para armazenar os valores de custo para cada iteração.
  2. Itera sobre o número especificado de iterações (iterations).
  3. Em cada iteração:
    • Realiza a propagação para frente usando o método forward para obter a ativação final al e os caches.
    • Calcula o custo usando o método cost.
    • Realiza a propagação para trás usando o método backward para calcular os gradientes.
    • Atualiza os parâmetros usando o método update_parameters com os gradientes calculados e a taxa de aprendizado.
    • Se a iteração atual é um múltiplo de 100, anexa o valor do custo ao vetor costs e imprime o número atual da época e o valor do custo.
  4. Após o loop, retorna os parâmetros atualizados.

Previsão

Após o loop de treinamento ser concluído, podemos fazer uma função de previsão que recebe os dados de teste: x_test_data e os parâmetros otimizados: parameters.

pub fn predict(
        &self,
        x_test_data: &Array2<f32>,
        parameters: HashMap<String, Array2<f32>>,
    ) -> Array2<f32> {
        let (al, _) = self.forward(&x_test_data, &parameters);

        let y_hat = al.map(|x| (x > &0.5) as i32 as f32);
        y_hat
    }

    pub fn score(&self, y_hat: &Array2<f32>, y_test_data: &Array2<f32>) -> f32 {
        let error =
            (y_hat - y_test_data).map(|x| x.abs()).sum() / y_test_data.shape()[1] as f32 * 100.0;
        100.0 - error
    }

A função predict realiza os seguintes passos:

  1. Chama o método forward com os dados de teste e os parâmetros otimizados para obter a ativação final al.
  2. Aplica um limite de 0.5 aos elementos de al usando o método map, convertendo valores maiores que 0.5 para 1.0 e valores menores ou iguais a 0.5 para 0.0.
  3. Retorna os rótulos previstos como y_hat.

A função score calcula a pontuação de acurácia dos rótulos previstos em comparação com os rótulos de teste reais. Ela realiza os seguintes passos:

  1. Calcula a diferença absoluta elemento a elemento entre os rótulos previstos y_hat e os rótulos de teste reais y_test_data.
  2. Soma as diferenças absolutas usando o método sum.
  3. Divide a soma pelo número de exemplos (y_test_data.shape()[1]) e multiplica por 100.0 para obter a porcentagem de erro.
  4. Subtrai a porcentagem de erro de 100.0 para obter a pontuação de acurácia e retorna-a.

Gravação de Parâmetros em um Arquivo JSON

//lib.rs

use std::fs::OpenOptions;

pub fn write_parameters_to_json_file(
   parameters: &HashMap<String, Array2<f32>>,
   file_path: PathBuf,
) {
   let file = OpenOptions::new()
       .create(true)
       .write(true)
       .truncate(true)
       .open(file_path)
       .unwrap();

   _ = serde_json::to_writer(file, parameters);
}

Podemos definir uma função auxiliar para gravar os parâmetros do modelo treinado em um arquivo JSON. Isso nos permite salvar os parâmetros para uso posterior sem a necessidade de retrabalho.

Primeiro, no Cargo.toml, adicione esta linha.

serde_json = "1.0.91"

Depois, no lib.rs, importe OpenOptions e crie a função.

//lib.rs

use std::fs::OpenOptions;

pub fn write_parameters_to_json_file(
    parameters: &HashMap<String, Array2<f32>>,
    file_path: PathBuf,
) {
    let file = OpenOptions::new()
        .create(true)
        .write(true)
        .truncate(true)
        .open(file_path)
        .unwrap();

    _ = serde_json::to_writer(file, parameters);
}

Esta função recebe os parâmetros e um caminho de arquivo onde o arquivo JSON será criado. Ela abre o arquivo no modo de gravação, truncando qualquer conteúdo existente. Em seguida, usa o crate serde_json para serializar os parâmetros e gravá-los no arquivo.

Exemplo de Aplicação

Para demonstrar o uso da biblioteca, podemos criar uma aplicação que carrega os dados, treina o modelo e o testa. Podemos criar um arquivo chamado rust_dnn.rs dentro da pasta src/bin. Aqui está um exemplo de implementação:

use dnn_rust_blog::*;
use std::env;
fn main() {
    env::set_var("RUST_BACKTRACE", "1");
    let neural_network_layers: Vec<usize> = vec![12288, 20, 7, 5, 1];
    let learning_rate = 0.0075;
    let iterations = 1000;

    let (training_data, training_labels) =
        dataframe_from_csv("datasets/training_set.csv".into()).unwrap();
    let (test_data, test_labels) = dataframe_from_csv("datasets/test_set.csv".into()).unwrap();

    let training_data_array = array_from_dataframe(&training_data)/255.0;
    let training_labels_array = array_from_dataframe(&training_labels);
    let test_data_array = array_from_dataframe(&test_data)/255.0;
    let test_labels_array = array_from_dataframe(&test_labels);

    let model = DeepNeuralNetwork {
        layers: neural_network_layers,
        learning_rate,
    };

    let parameters = model.initialize_parameters();

    let parameters = model.train_model(
        &training_data_array,
        &training_labels_array,
        parameters,
        iterations,
        model.learning_rate,
    );
    write_parameters_to_json_file(&parameters, "model.json".into());

    let training_predictions = model.predict(&training_data_array, &parameters);
    println!(
        "Acurácia do Conjunto de Treinamento: {}%",
        model.score(&training_predictions, &training_labels_array)
    );

    let test_predictions = model.predict(&test_data_array, &parameters);
    println!(
        "Acurácia do Conjunto de Teste: {}%",
        model.score(&test_predictions, &test_labels_array)
    );
}
  1. Nós definimos as camadas da rede neural, a taxa de aprendizado e o número de iterações.
  2. Carregamos os dados de treinamento e teste a partir de arquivos CSV usando a função dataframe_from_csv.
  3. Convertemos os dataframes em arrays e normalizamos os valores dos pixels para a faixa [0, 1].
  4. Criamos uma instância da estrutura DeepNeuralNetwork com as camadas especificadas e a taxa de aprendizado.
  5. Inicializamos os parâmetros usando o método initialize_parameters.
  6. Treinamos o modelo usando o método train_model, passando os dados de treinamento, os rótulos de treinamento, os parâmetros iniciais, as iterações e a taxa de aprendizado.
  7. Gravamos os parâmetros treinados em um arquivo JSON usando a função write_parameters_to_json_file.
  8. Prevemos os rótulos para os dados de treinamento e teste usando o método predict.
  9. Calculamos e imprimimos as pontuações de acurácia para as previsões de treinamento e teste usando o método score.

Agora, no terminal, execute o seguinte comando para instalar o binário e executá-lo:

cargo install --path && rust_dnn

Isso instalará as dependências e iniciará o treinamento.

/images/37a1e8d94baa.png

Conclusão

Embora o conjunto de dados usado em nosso exemplo seja pequeno e a arquitetura da rede não seja complexa, o objetivo desta série é fornecer um fluxo de trabalho geral e introduzir o funcionamento interno de uma rede neural. Com esta base, você pode agora expandir e aprimorar a biblioteca para lidar com conjuntos de dados maiores, arquiteturas de rede mais complexas e recursos adicionais.

Ao construir esta biblioteca de rede neural em Rust, nos beneficiamos das características de segurança, desempenho e simultaneidade da linguagem. O forte sistema de tipos e as garantias de segurança de memória do Rust ajudam a prevenir bugs comuns e garantir a exatidão do código. Além disso, o foco do Rust em eficiência e paralelismo nos permite aproveitar recursos de multiencadeamento e tirar vantagem das capacidades de hardware modernas.

Com esta biblioteca, você agora tem uma ferramenta poderosa para desenvolver modelos de redes neurais em Rust. Você pode explorar e experimentar ainda mais com diferentes arquiteturas de rede, funções de ativação, técnicas de otimização e métodos de regularização para melhorar o desempenho de seus modelos.

À medida que você continua sua jornada em aprendizado de máquina e aprendizado profundo, lembre-se de manter a curiosidade, continuar explorando novos conceitos e técnicas, e aproveitar o rico ecossistema Rust para aprimorar ainda mais sua biblioteca de redes neurais.

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.