WEB3DEV

Cover image for Rede Neural Profunda do Zero em Rust 🦀 - Parte 2 - Carregando Dados e Inicializando um Modelo de Rede Neural
Paulo Gio
Paulo Gio

Posted on • Atualizado em

Rede Neural Profunda do Zero em Rust 🦀 - Parte 2 - Carregando Dados e Inicializando um Modelo de Rede Neural

Série sobre Rede Neural Profunda do Zero em Rust

https://miro.medium.com/v2/resize:fit:1100/format:webp/1*tZUeB1ZFdstIP9AjxX8QGA.png

Espero que você tenha lido a parte 1 para entender a premissa de como uma rede neural funciona de maneira ampla e por que estamos usando o Rust para construir sua Rede Neural. Caso não tenha lido, eu recomendo fortemente que você navegue por este artigo: Rede Neural Profunda do Zero em Rust 🦀 - Parte 1 - Fundamentos da Rede Neural. Feito isso, vamos ver o que vamos aprender nesta parte da série.

  1. Iniciando um projeto Rust
  2. Adicionando e instalando dependências
  3. Carregando os dados para treinamento
  4. Criando uma estrutura de rede neural em Rust
  5. Implementando a inicialização aleatória dos parâmetros da rede neural

Iniciando um Projeto Rust

Se você não tiver o Rust instalado em seu computador, poderá começar facilmente visitando o site do Rust e seguindo as etapas de instalação. Além disso, você pode instalar a extensão rust-analyzer para o VS Code para melhorar sua experiência de programação em Rust. Uma vez instalado o Rust, vamos criar um novo projeto. Neste caso, iniciaremos o projeto Rust como uma biblioteca, já que nosso objetivo é criar um crate de rede neural que possa ser utilizado em várias aplicações de aprendizado de máquina. Para começar, abra seu terminal no diretório desejado para o projeto e execute o seguinte comando:

cargo new rust_dnn --lib
Enter fullscreen mode Exit fullscreen mode

Aqui, rust_dnn é o nome de nossa biblioteca, e o sinalizador --lib indica que este projeto será usado como uma biblioteca em vez de um binário. Abra o diretório do projeto e inicie o VS Code nesta pasta.

Adicionando e Instalando Dependências

Você pode copiar o arquivo Cargo.toml seguindo o link do repositório Git fornecido abaixo ou executar os seguintes comandos da CLI do cargo no terminal a partir do diretório do projeto:

cargo add polars -F lazy
Enter fullscreen mode Exit fullscreen mode
cargo add ndarray -F serde
Enter fullscreen mode Exit fullscreen mode
cargo add rand
Enter fullscreen mode Exit fullscreen mode

Estas são todas as dependências de que precisamos para esta parte. Podemos instalar essas dependências executando este comando:

cargo install
Enter fullscreen mode Exit fullscreen mode

Carregando os Dados

Para nossa biblioteca de rede neural, vamos construir um classificador que possa identificar imagens de gatos e não gatos. Para simplificar o processo, converti essas imagens em matrizes de 3 canais de tamanho 64 x 64, que representam a resolução da imagem. Se achatarmos esta matriz, obtemos um vetor de tamanho (3 x 64 x 64), que é igual a 12288. Esse é o número de recursos de entrada que serão alimentados na rede. Embora possamos fazer a rede se adaptar a diferentes tamanhos de recursos de entrada, vamos projetá-la especificamente para este conjunto de dados por enquanto. Podemos generalizá-la ainda mais nas partes subsequentes.

Você pode baixar os conjuntos de dados de treinamento e teste como arquivos CSV daqui. Vamos criar uma função no arquivo lib.rs para carregar o arquivo CSV como um quadro de dados Polars. Isso nos permitirá pré-processar os dados, se necessário. No nosso caso, a única etapa de pré-processamento é separar os rótulos dos dados de entrada no arquivo CSV. A seguinte função realiza essa tarefa e retorna dois quadros de dados: um para os dados de entrada e um para os rótulos.

// lib.rs
use ndarray::prelude::*;
use polars::prelude::*;
use rand::distributions::Uniform;
use rand::prelude::*;
use std::collections::HashMap;
use std::path::PathBuf;

pub fn dataframe_from_csv(file_path: PathBuf) -> PolarsResult<(DataFrame, DataFrame)> {

 let data = CsvReader::from_path(file_path)?.has_header(true).finish()?;

 let training_dataset = data.drop("y")?;
 let training_labels = data.select(["y"])?;

 return Ok((training_dataset, training_labels));

}
Enter fullscreen mode Exit fullscreen mode

Depois de carregar os dados como um quadro de dados Polars e separar os dados de entrada dos rótulos, precisamos converter os quadros de dados em ndarray, que pode ser usado como entrada para a rede neural. Esta função pode fazer isso para nós.

pub fn array_from_dataframe(df: &DataFrame) -> Array2<f32> {
   df.to_ndarray::<Float32Type>().unwrap().reversed_axes()
}
Enter fullscreen mode Exit fullscreen mode

Aqui você pode ver que invertemos os eixos. Isso ocorre porque precisamos dos dados de entrada no seguinte formato:

https://miro.medium.com/v2/resize:fit:720/format:webp/0*eMxp2HbGf55GmeBx.png

Representação em Matriz de n Recursos de Entrada e m Exemplos de Entrada

Onde m é o número de exemplos e n é o número de recursos para cada exemplo. Em nosso quadro de dados, tivemos a transposição disso. Então, invertemos os eixos para obter a forma correta de (n x m).

Criando uma Classe de Modelo de Rede Neural Profunda

A seguir, vamos criar uma estrutura que conterá os parâmetros de nossa Rede Neural, como o número de camadas, o número de unidades ocultas em cada camada e a taxa de aprendizado. No futuro, podemos adicionar mais parâmetros a esta estrutura.

struct DeepNeuralNetwork{
   pub layers: Vec<usize>,
   pub learning_rate: f32,
}
Enter fullscreen mode Exit fullscreen mode

Inicializando Parâmetros Aleatórios

Precisamos inicializar os parâmetros da rede neural. Vejamos como os parâmetros podem ser representados para uma camada l de nossa rede.

Digamos, por exemplo, que temos uma rede de 3 camadas ocultas com número de unidades ocultas representadas por [100, 4,3,6], ou seja, 100 unidades de entrada, 4 unidades ocultas na primeira camada oculta, 3 na segunda e 6 na terceira. Para isso, a forma da matriz de peso (weight) para a primeira camada será (4 x 100), para a segunda camada (3x4) e para a terceira camada (6x4). E a forma da matriz de viés (bias) será (4x1) para a primeira camada, (3x1) para a segunda camada e (6x1) para a terceira camada. Em geral, esta é a representação matemática da matriz de peso e viés de qualquer camada l da rede.

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

Matriz de Peso e Matriz de Viés

https://miro.medium.com/v2/resize:fit:1100/format:webp/1*SCYdy8iJxGtOq4UxOdEMdw.png

Aqui está uma implementação initialize_parameter para a estrutura DeepNeuralNetwork que cria essas matrizes e inicializa os pesos e vieses aleatoriamente:

impl DeepNeuralNetwork {
   /// Inicializa os parâmetros da rede neural.
   ///
   /// Retorna um dicionário mapa de hash de pesos e vieses inicializados 
   /// aleatoriamente.
   pub fn initialize_parameters(&self) -> HashMap<String, Array2<f32>> {
       let between = Uniform::from(-1.0..1.0); // número aleatório entre -1 e 1
       let mut rng = rand::thread_rng(); // gerador de números aleatórios

       let number_of_layers = self.layers.len();

       let mut parameters: HashMap<String, Array2<f32>> = HashMap::new();

       // Inicie o loop da primeira camada oculta para a camada de saída.
       // Não estamos começando do 0 porque a camada zero é a camada de entrada.
       for l in 1..number_of_layers {
           let weight_array: Vec<f32> = (0..self.layers[l]*self.layers[l-1])
               .map(|_| between.sample(&mut rng))
               .collect(); //crie uma matriz de pesos achatados de valores (N * M)

           let bias_array: Vec<f32> = (0..self.layers[l]).map(|_| 0.0).collect();

           let weight_matrix = Array::from_shape_vec((self.layers[l], self.layers[l - 1]), weight_array).unwrap();
           let bias_matrix = Array::from_shape_vec((self.layers[l], 1), bias_array).unwrap();

           let weight_string = ["W", &l.to_string()].join("").to_string();
           let biases_string = ["b", &l.to_string()].join("").to_string();

           parameters.insert(weight_string, weight_matrix);
           parameters.insert(biases_string, bias_matrix);
       }
       parameters
   }
Enter fullscreen mode Exit fullscreen mode

Nesta implementação, usamos o crate rand para gerar números aleatórios. Criamos uma distribuição uniforme entre -1 e 1 e usamos isso para amostrar números aleatórios para a inicialização de peso. Inicializamos os vieses para 0.0. Os pesos e vieses são armazenados em um HashMap (mapa de hash, tabela de dispersão), onde as chaves são strings representando o número da camada seguido por "W" para weights (pesos) ou "b" para bias (viés).

E é isso para esta parte! Cobrimos o início de um projeto Rust, adicionando dependências, carregando dados e inicializando parâmetros de rede neural. Na próxima parte, continuaremos construindo nossa rede neural implementando a propagação para frente. Fique ligado!

Siga-me no Medium para ficar atualizado sobre o próximo lançamento.

Quer se conectar?

Meu website
LinkedIn
Twitter

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

Latest comments (0)