É uma rede neural dupla, sendo que a primeira parte é dedicada a capturar as características de cada imagem, enquanto a segunda, classifica essa característica de acordo com os pesos.
Neste exemplo veremos a implementação de uma rede CNN simplificada em Rust, para treinar e inferir algarismos desenhados manualmente, baseados no dataset MINST.
Para começar, baixe o dataset e descompacte para uma pasta data na raiz do projeto:
cd data
wget https://ossci-datasets.s3.amazonaws.com/mnist/train-images-idx3-ubyte.gz
wget https://ossci-datasets.s3.amazonaws.com/mnist/train-labels-idx1-ubyte.gz
wget https://ossci-datasets.s3.amazonaws.com/mnist/t10k-images-idx3-ubyte.gz
wget https://ossci-datasets.s3.amazonaws.com/mnist/t10k-labels-idx1-ubyte.gz
gunzip data/train-images-idx3-ubyte.gz
gunzip data/train-labels-idx1-ubyte.gz
gunzip data/t10k-images-idx3-ubyte.gz
gunzip data/t10k-labels-idx1-ubyte.gz
Este é um exemplo de código em Rust que adapta a ideia do código fornecido para uma rede neural convolucional (CNN) simples. Esse exemplo implementa (de forma “do-it-yourself” e não otimizada) as seguintes camadas:
O treinamento usa o gradiente descendente estocástico (SGD) e o erro é calculado com MSE.
Atenção: Este exemplo é didático e simplificado. Para aplicações reais (como reconhecimento de objetos em imagens reais), recomenda‑se o uso de bibliotecas otimizadas e uma implementação mais robusta do cálculo dos gradientes (backpropagation).
Convolução é o processo de adicionar cada elemento da imagem aos seus vizinhos locais, compensado por um núcleo ou Kernel. O kernel ou matriz de convolução, ou ainda “máscara”, é uma pequena matriz, que determina o que deve ser ser multiplicado e adicionado a cada pixel, gerando uma imagem menor.
É utilizada em operações de sharpening, blurring, embossing, detecção de bordas e outras operações em imagens.
(crédito)
O kernel é rotacionado sobre todos os pixes da imagem original, gerando uma matriz resumida, onde cada ponto é um resumo das operações de multiplicação de cada elemento do kernel.
Pooling é o processo de reduzir a dimensionalidade das características, reduzindo o tamanho da entrada. Além de tornar o processamento mais simples e rápido, evita o overfitting.
Utilizamos uma matriz de pooling para sumarizar os pontos da imagem convolucionada. Podemos sumarizar pegando o maior valor de um grupo de pontos (MAX) ou a sua média (AVERAGE).
(crédito)
Geralmente, temos um núcleo convolucional, formado por 3 camadas, que pode se repetir em uma CNN, antes de passar para a segunda parte, que é a classificação:
A retificação elimina os pontos negativos na imagem, substituindo-os por zero. Ela pode ser feita utilizando qualquer uma das funções de ativação mais comuns, como: ReLU, sigmoid ou tanh.
Geralmente, uma CNN é a união de duas Redes Neurais: uma Convolucional, e outra Classificatória, como na imagem:
(crédito)
O link para o projeto no GitHub está aqui!.
A seguir, uma explicação detalhada do código, dividida em seções.
Este código implementa uma rede neural convolucional (CNN) do zero, voltada para o dataset MNIST (imagens de dígitos manuscritos). A implementação inclui:
fn sigmoid(x: f32) -> f32 {
1.0 / (1.0 + (-x).exp())
}
fn sigmoid_derivada_da_saida(a: f32) -> f32 {
a * (1.0 - a)
}
fn produto_externo(a: &Array1<f32>, b: &Array1<f32>) -> Array2<f32> {
let n = a.len();
let m = b.len();
let mut result = Array2::<f32>::zeros((n, m));
for i in 0..n {
for j in 0..m {
result[[i, j]] = a[i] * b[j];
}
}
result
}
A struct CamadaConvolucional
representa uma camada convolucional básica:
struct CamadaConvolucional {
num_filters: usize,
filter_height: usize,
filter_width: usize,
in_channels: usize,
filters: Array4<f32>,
biases: Array1<f32>,
last_input: Option<Array3<f32>>,
last_output: Option<Array3<f32>>,
}
filters
: Um tensor 4D com os filtros (kernels) com formato (número de filtros, canais de entrada, altura do filtro, largura do filtro).biases
: Um vetor com um bias por filtro.new: Inicializa os filtros e os biases com valores aleatórios (distribuição uniforme entre -1 e 1).
d_out
) e na derivada da função sigmoide.A camada de max pooling reduz a dimensionalidade espacial extraindo o valor máximo em blocos não sobrepostos.
struct MaxPoolingLayer {
pool_size: usize,
last_input: Option<Array3<f32>>,
max_indices: Option<Vec<Array2<(usize, usize)>>>,
}
pool_size
e seleciona o valor máximo de cada bloco.A camada de flatten “achata” um tensor 3D (por exemplo, com dimensões: canais, altura e largura) em um vetor 1D, que será a entrada para a camada totalmente conectada.
struct FlattenLayer {
input_shape: Option<(usize, usize, usize)>,
}
Representa uma camada densa (fully connected):
struct DenseLayer {
weights: Array2<f32>, // (output_size, input_size)
biases: Array1<f32>, // (output_size)
last_input: Option<Array1<f32>>,
last_output: Option<Array1<f32>>,
}
A struct CNN
compõe as camadas implementadas:
struct CNN {
conv: CamadaConvolucional,
pool: MaxPoolingLayer,
flatten: FlattenLayer,
fc: DenseLayer,
}
A struct CNNWeights
(anotada com #[derive(Serialize, Deserialize)]
) é usada para armazenar os parâmetros que desejamos salvar:
#[derive(Serialize, Deserialize)]
struct CNNWeights {
conv_filters: Array4<f32>,
conv_biases: Array1<f32>,
fc_weights: Array2<f32>,
fc_biases: Array1<f32>,
}
As funções save_weights
e load_weights_from_file
utilizam a crate serde_json
para serializar e desserializar os pesos para um arquivo JSON. Essa estratégia permite que, se os pesos já estiverem salvos, o programa os carregue e evite treinar novamente a rede.
No main
:
Carregamento do MNIST:
A crate mnist
é usada para baixar e preparar o dataset. As imagens são normalizadas para o intervalo [0, 1]. Em seguida, os dados são convertidos para o formato esperado (imagens como Array3<f32>
e rótulos convertidos para vetores one-hot).
Divisão dos Dados:
Para acelerar o exemplo, é usado um subconjunto (por exemplo, 1000 amostras de treinamento e 200 de teste).
Configuração da CNN:
São definidos os parâmetros da rede (por exemplo, 1 canal de entrada, dimensões 28×28, 8 filtros na camada convolucional, etc.).
Verificação dos Pesos:
Se o arquivo de pesos existe, os parâmetros são carregados; caso contrário, a rede é treinada por um número de épocas, e após o treinamento os pesos são salvos para futuras execuções.
Predição Final:
O programa realiza uma predição em uma imagem de teste e exibe a saída prevista e o rótulo esperado.
Este código demonstra, de maneira didática, como implementar uma CNN do zero em Rust, desde a definição das camadas básicas e suas operações forward/backward até a persistência dos pesos usando serialização JSON. Cada parte foi construída de forma modular, permitindo que desenvolvedores intermediários possam entender e adaptar a implementação para experimentos e projetos mais complexos.
Essa abordagem proporciona uma visão aprofundada dos conceitos de redes neurais convolucionais e do fluxo de dados durante o treinamento e a inferência, servindo tanto para aprendizado quanto para futuras extensões e otimizações.
Ao executar cargo run
ele treinará o modelo e salvará os pesos no arquivo pesos.json
, depois, fará uma inferência de teste.
Baixando e carregando o MNIST...
Treinamento: 1000 amostras; Teste: 200 amostras
Pesos não encontrados. Iniciando treinamento...
Epoch 1: Test MSE = 0.093142
Epoch 2: Test MSE = 0.089582
Epoch 3: Test MSE = 0.086736
Epoch 4: Test MSE = 0.084437
Epoch 5: Test MSE = 0.083292
Epoch 6: Test MSE = 0.081915
Epoch 7: Test MSE = 0.080214
Epoch 8: Test MSE = 0.078670
Epoch 9: Test MSE = 0.077400
Epoch 10: Test MSE = 0.076310
Treinamento concluído. Salvando pesos...
Predição para a primeira imagem do teste:
Saída prevista: [9.21565e-5, 0.00017337395, 0.007715295, 0.015219106, 0.000297992, 2.2828715e-5, 0.0006794478, 0.9792802, 0.033919033, 0.0019736418], shape=[10], strides=[1], layout=CFcf (0xf), const ndim=1
Rótulo esperado: [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0], shape=[10], strides=[1], layout=CFcf (0xf), const ndim=1
Se quiser que ele repita o treinamento, é só deletar o arquivo pesos.json
.
Como podemos ver, ele acertou o dígito: 7.