Rust

Rust e Machine Learning — Inferência, ONNX e Aceleração de Modelos Já leu

19 min de leitura

Rust e Machine Learning — Inferência, ONNX e Aceleração de Modelos
Rust — Artigo #46 Rust e Machine Learning — Inferência, ONNX e Aceleração de Modelos Por Prof. Dr. Marcelo Fontes | Série: Dominando Rust em 1 Ano O pipeline tí

Rust — Artigo #46

Rust e Machine Learning — Inferência, ONNX e Aceleração de Modelos

Por Prof. Dr. Marcelo Fontes | Série: Dominando Rust em 1 Ano


O pipeline típico de machine learning divide-se em duas fases com exigências opostas. Treinamento precisa de flexibilidade, iteração rápida e ecossistema rico — Python com PyTorch ou TensorFlow domina aqui. Inferência precisa de latência baixa, throughput alto, memória mínima e deployment confiável — Rust brilha aqui.

Esta divisão está se tornando a arquitetura padrão da indústria: treinar em Python, exportar para ONNX ou TorchScript, servir em Rust. Uma API de recomendação que responde em 2ms, um modelo de detecção de fraude que processa milhões de transações por segundo, um pipeline de NLP embarcado que roda em um dispositivo com 256MB de RAM — estes são os casos onde Rust entrega valor único.


Dependências

[package]
name = "ml_rust"
version = "0.1.0"
edition = "2021"

[dependencies]
# Tensores e álgebra linear
ndarray = { version = "0.15", features = ["rayon"] }
ndarray-rand = "0.14"
ndarray-stats = "0.5"
nalgebra = "0.32"

# Inferência ONNX
ort = { version = "1.16", features = ["load-dynamic"] }  # ONNX Runtime

# Candle — framework de deep learning da Hugging Face
candle-core = { version = "0.4", features = ["cuda"] }
candle-nn = "0.4"
candle-transformers = "0.4"

# Utilitários
rand = "0.8"
rand_distr = "0.4"
rayon = "1"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
anyhow = "1"
tokenizers = "0.15"  # tokenização para NLP

Álgebra linear fundamental para ML

use ndarray::{Array1, Array2, Axis, s};
use ndarray_rand::RandomExt;
use ndarray_rand::rand_distr::{Normal, Uniform};
use rand::SeedableRng;

// Função de ativação — o coração das redes neurais
fn relu(x: f32) -> f32 {
    x.max(0.0)
}

fn sigmoid(x: f32) -> f32 {
    1.0 / (1.0 + (-x).exp())
}

fn tanh(x: f32) -> f32 {
    x.tanh()
}

// Softmax — converte logits em probabilidades
fn softmax(x: &Array1<f32>) -> Array1<f32> {
    let max = x.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
    let exp: Array1<f32> = x.mapv(|v| (v - max).exp());
    let soma = exp.sum();
    exp / soma
}

// Camada densa (linear): y = XW + b
fn camada_densa(
    entrada: &Array2<f32>,     // [batch, input_dim]
    pesos: &Array2<f32>,       // [input_dim, output_dim]
    vies: &Array1<f32>,        // [output_dim]
) -> Array2<f32> {
    let mut saida = entrada.dot(pesos);

    // Broadcasting do viés
    for mut linha in saida.rows_mut() {
        linha += vies;
    }
    saida
}

// Normalização de batch — estabiliza treinamento
fn batch_norm(
    x: &Array2<f32>,
    gamma: &Array1<f32>,
    beta: &Array1<f32>,
    epsilon: f32,
) -> Array2<f32> {
    let media = x.mean_axis(Axis(0)).unwrap();
    let variancia = x.var_axis(Axis(0), 0.0);

    let mut normalizado = Array2::zeros(x.raw_dim());
    for (mut linha, linha_orig) in normalizado.rows_mut().into_iter()
        .zip(x.rows())
    {
        for i in 0..linha.len() {
            linha[i] = (linha_orig[i] - media[i])
                / (variancia[i] + epsilon).sqrt()
                * gamma[i] + beta[i];
        }
    }
    normalizado
}

// Dropout — regularização por zeragem aleatória
fn dropout(
    x: &Array2<f32>,
    taxa: f32,
    treinando: bool,
    seed: u64,
) -> Array2<f32> {
    if !treinando || taxa == 0.0 {
        return x.clone();
    }

    let mut rng = rand::rngs::SmallRng::seed_from_u64(seed);
    let mascara = Array2::random_using(
        x.raw_dim(),
        Uniform::new(0.0f32, 1.0),
        &mut rng,
    );

    x * &mascara.mapv(|v| if v > taxa { 1.0 / (1.0 - taxa) } else { 0.0 })
}

fn demonstrar_operacoes_nn() {
    println!("══ Operações Fundamentais de Redes Neurais ══
");

    // Batch de 4 amostras, 3 features cada
    let entrada: Array2<f32> = ndarray::arr2(&[
        [1.0, 2.0, 3.0],
        [4.0, 5.0, 6.0],
        [7.0, 8.0, 9.0],
        [0.5, 1.5, 2.5],
    ]);

    // Pesos inicializados com Xavier/Glorot
    let n_entrada = 3usize;
    let n_saida = 4usize;
    let limite = (6.0 / (n_entrada + n_saida) as f32).sqrt();

    let mut rng = rand::rngs::SmallRng::seed_from_u64(42);
    let pesos = Array2::random_using(
        (n_entrada, n_saida),
        Uniform::new(-limite, limite),
        &mut rng,
    );
    let vies = Array1::zeros(n_saida);

    // Forward pass
    let linear = camada_densa(&entrada, &pesos, &vies);
    println!("Saída linear (4×4):
{linear:.3}
");

    let com_relu = linear.mapv(relu);
    println!("Após ReLU:
{com_relu:.3}
");

    // Softmax na primeira amostra
    let logits: Array1<f32> = com_relu.row(0).to_owned();
    let probs = softmax(&logits);
    println!("Softmax da amostra 0: {probs:.4}");
    println!("Soma das probs: {:.6}
", probs.sum()); // deve ser 1.0

    // Funções de ativação comparadas
    println!("── Comparação de ativações ──");
    let x_vals: Vec<f32> = (-4..=4).map(|x| x as f32).collect();
    println!("{:>6} {:>10} {:>10} {:>10}", "x", "relu", "sigmoid", "tanh");
    for x in &x_vals {
        println!("{:>6.1} {:>10.4} {:>10.4} {:>10.4}",
            x, relu(*x), sigmoid(*x), tanh(*x));
    }
}

Regressão logística do zero

use ndarray::{Array1, Array2};

struct RegressaoLogistica {
    pesos: Array1<f32>,
    vies: f32,
    taxa_aprendizado: f32,
    historico_loss: Vec<f32>,
}

impl RegressaoLogistica {
    fn novo(n_features: usize, taxa: f32) -> Self {
        let mut rng = rand::rngs::SmallRng::seed_from_u64(42);
        RegressaoLogistica {
            pesos: Array1::random_using(
                n_features,
                ndarray_rand::rand_distr::Normal::new(0.0f32, 0.01).unwrap(),
                &mut rng,
            ),
            vies: 0.0,
            taxa_aprendizado: taxa,
            historico_loss: Vec::new(),
        }
    }

    fn prever(&self, x: &Array2<f32>) -> Array1<f32> {
        let logits = x.dot(&self.pesos) + self.vies;
        logits.mapv(sigmoid)
    }

    fn prever_classe(&self, x: &Array2<f32>) -> Array1<u8> {
        self.prever(x).mapv(|p| if p >= 0.5 { 1 } else { 0 })
    }

    fn treinar(
        &mut self,
        x: &Array2<f32>,
        y: &Array1<f32>,
        epocas: usize,
        verbose: bool,
    ) {
        let n = x.nrows() as f32;

        for epoca in 0..epocas {
            // Forward pass
            let predicoes = self.prever(x);

            // Binary cross-entropy loss
            let loss = -( y * predicoes.mapv(|p| (p + 1e-15).ln())
                + (1.0 - y) * predicoes.mapv(|p| (1.0 - p + 1e-15).ln())
            ).mean().unwrap();

            self.historico_loss.push(loss);

            // Backpropagation — gradiente do BCE com sigmoid
            let erro = &predicoes - y;

            // Gradientes
            let grad_pesos = x.t().dot(&erro) / n;
            let grad_vies = erro.mean().unwrap();

            // Atualização gradient descent
            self.pesos -= &(grad_pesos * self.taxa_aprendizado);
            self.vies -= grad_vies * self.taxa_aprendizado;

            if verbose && (epoca + 1) % 100 == 0 {
                let acuracia = self.acuracia(x, y);
                println!("  Época {:4} | Loss: {:.4} | Acurácia: {:.2}%",
                    epoca + 1, loss, acuracia * 100.0);
            }
        }
    }

    fn acuracia(&self, x: &Array2<f32>, y: &Array1<f32>) -> f32 {
        let predicoes = self.prever_classe(x);
        let corretos = predicoes.iter()
            .zip(y.iter())
            .filter(|(&pred, &real)| pred as f32 == real)
            .count();
        corretos as f32 / y.len() as f32
    }

    fn matriz_confusao(&self, x: &Array2<f32>, y: &Array1<f32>) -> [[u32; 2]; 2] {
        let predicoes = self.prever_classe(x);
        let mut matriz = [[0u32; 2]; 2];
        for (&pred, &real) in predicoes.iter().zip(y.iter()) {
            matriz[real as usize][pred as usize] += 1;
        }
        matriz
    }
}

fn gerar_dataset_classificacao(
    n: usize,
    seed: u64,
) -> (Array2<f32>, Array1<f32>) {
    let mut rng = rand::rngs::SmallRng::seed_from_u64(seed);
    let normal = ndarray_rand::rand_distr::Normal::new(0.0f32, 1.0).unwrap();

    let mut x_data = Vec::with_capacity(n * 2);
    let mut y_data = Vec::with_capacity(n);

    for i in 0..n {
        let classe = (i % 2) as f32;
        let centro_x = if classe == 0.0 { -1.5f32 } else { 1.5f32 };
        let centro_y = if classe == 0.0 { -1.5f32 } else { 1.5f32 };

        let xi: f32 = ndarray_rand::rand_distr::Normal::new(
            centro_x, 0.8
        ).unwrap().sample(&mut rng);

        let yi: f32 = ndarray_rand::rand_distr::Normal::new(
            centro_y, 0.8
        ).unwrap().sample(&mut rng);

        x_data.push(xi);
        x_data.push(yi);
        y_data.push(classe);
    }

    let x = Array2::from_shape_vec((n, 2), x_data).unwrap();
    let y = Array1::from(y_data);
    (x, y)
}

fn demo_regressao_logistica() {
    println!("══ Regressão Logística ══
");

    let (x_treino, y_treino) = gerar_dataset_classificacao(200, 42);
    let (x_teste, y_teste)   = gerar_dataset_classificacao(50, 99);

    let mut modelo = RegressaoLogistica::novo(2, 0.1);

    println!("Treinando por 500 épocas...");
    modelo.treinar(&x_treino, &y_treino, 500, true);

    let acuracia_treino = modelo.acuracia(&x_treino, &y_treino);
    let acuracia_teste  = modelo.acuracia(&x_teste,  &y_teste);

    println!("
Acurácia treino : {:.2}%", acuracia_treino * 100.0);
    println!("Acurácia teste  : {:.2}%", acuracia_teste  * 100.0);

    let mc = modelo.matriz_confusao(&x_teste, &y_teste);
    println!("
Matriz de Confusão:");
    println!("           Previsto 0  Previsto 1");
    println!("  Real 0      {:>6}      {:>6}", mc[0][0], mc[0][1]);
    println!("  Real 1      {:>6}      {:>6}", mc[1][0], mc[1][1]);

    let vp = mc[1][1] as f32;
    let fp = mc[0][1] as f32;
    let vn = mc[0][0] as f32;
    let fn_ = mc[1][0] as f32;

    println!("
Métricas:");
    println!("  Precisão  : {:.4}", vp / (vp + fp));
    println!("  Recall    : {:.4}", vp / (vp + fn_));
    println!("  F1-Score  : {:.4}",
        2.0 * vp / (2.0 * vp + fp + fn_));
}

Rede neural MLP do zero

use ndarray::{Array1, Array2};
use rand::SeedableRng;
use ndarray_rand::RandomExt;
use ndarray_rand::rand_distr::Uniform;

#[derive(Debug, Clone)]
struct CamadaDensa {
    pesos: Array2<f32>,
    vies: Array1<f32>,
    grad_pesos: Array2<f32>,
    grad_vies: Array1<f32>,
    entrada_cache: Array2<f32>,
}

impl CamadaDensa {
    fn nova(n_entrada: usize, n_saida: usize, seed: u64) -> Self {
        let mut rng = rand::rngs::SmallRng::seed_from_u64(seed);
        // Inicialização He — adequada para ReLU
        let desvio = (2.0 / n_entrada as f32).sqrt();

        CamadaDensa {
            pesos: Array2::random_using(
                (n_entrada, n_saida),
                ndarray_rand::rand_distr::Normal::new(0.0f32, desvio).unwrap(),
                &mut rng,
            ),
            vies: Array1::zeros(n_saida),
            grad_pesos: Array2::zeros((n_entrada, n_saida)),
            grad_vies: Array1::zeros(n_saida),
            entrada_cache: Array2::zeros((1, n_entrada)),
        }
    }

    fn forward(&mut self, entrada: &Array2<f32>) -> Array2<f32> {
        self.entrada_cache = entrada.clone();
        let mut saida = entrada.dot(&self.pesos);
        for mut linha in saida.rows_mut() {
            linha += &self.vies;
        }
        saida
    }

    fn backward(&mut self, grad_saida: &Array2<f32>) -> Array2<f32> {
        let n = self.entrada_cache.nrows() as f32;

        self.grad_pesos = self.entrada_cache.t().dot(grad_saida) / n;
        self.grad_vies  = grad_saida.mean_axis(ndarray::Axis(0)).unwrap();

        grad_saida.dot(&self.pesos.t())
    }

    fn atualizar(&mut self, taxa: f32) {
        self.pesos -= &(self.grad_pesos.clone() * taxa);
        self.vies  -= &(self.grad_vies.clone()  * taxa);
    }
}

struct MLP {
    camadas: Vec<CamadaDensa>,
    taxa_aprendizado: f32,
}

impl MLP {
    fn novo(
        arquitetura: &[usize],
        taxa: f32,
    ) -> Self {
        let camadas = arquitetura.windows(2)
            .enumerate()
            .map(|(i, par)| CamadaDensa::nova(par[0], par[1], i as u64))
            .collect();

        MLP { camadas, taxa_aprendizado: taxa }
    }

    fn forward(&mut self, x: &Array2<f32>) -> Array2<f32> {
        let mut ativacao = x.clone();

        for (i, camada) in self.camadas.iter_mut().enumerate() {
            let linear = camada.forward(&ativacao);

            // ReLU em todas as camadas exceto a última
            ativacao = if i < self.camadas.len() - 1 {
                linear.mapv(|v| v.max(0.0))
            } else {
                linear // última camada: saída bruta (logits)
            };
        }

        ativacao
    }

    fn backward_e_atualizar(&mut self, grad: Array2<f32>) {
        let mut grad_atual = grad;

        for camada in self.camadas.iter_mut().rev() {
            // Gradiente através de ReLU (exceto última camada — simplificação)
            grad_atual = camada.backward(&grad_atual);
            camada.atualizar(self.taxa_aprendizado);
        }
    }

    fn mse_loss(predicoes: &Array2<f32>, targets: &Array2<f32>) -> f32 {
        let diff = predicoes - targets;
        (&diff * &diff).mean().unwrap()
    }

    fn treinar_step(
        &mut self,
        x: &Array2<f32>,
        y: &Array2<f32>,
    ) -> f32 {
        // Forward
        let predicoes = self.forward(x);

        // Loss MSE
        let loss = Self::mse_loss(&predicoes, y);

        // Gradiente da loss: dL/dŷ = 2(ŷ - y)/n
        let grad = (&predicoes - y) * 2.0
            / predicoes.nrows() as f32;

        // Backward
        self.backward_e_atualizar(grad);

        loss
    }
}

fn demo_mlp() {
    println!("══ MLP — Rede Neural Multi-Camada ══
");

    // Problema XOR — não linearmente separável
    let x: Array2<f32> = ndarray::arr2(&[
        [0.0, 0.0],
        [0.0, 1.0],
        [1.0, 0.0],
        [1.0, 1.0],
    ]);

    let y: Array2<f32> = ndarray::arr2(&[
        [0.0],
        [1.0],
        [1.0],
        [0.0],
    ]);

    // Arquitetura: 2 → 4 → 4 → 1
    let mut mlp = MLP::novo(&[2, 4, 4, 1], 0.1);

    println!("Treinando XOR por 2000 épocas...
");

    for epoca in 0..2000 {
        let loss = mlp.treinar_step(&x, &y);

        if (epoca + 1) % 400 == 0 {
            println!("  Época {:4} | Loss: {:.6}", epoca + 1, loss);
        }
    }

    println!("
Previsões após treinamento:");
    let preds = mlp.forward(&x);
    println!("{:>8} {:>8} │ {:>10} {:>10}",
        "x1", "x2", "Esperado", "Previsto");
    println!("{}", "─".repeat(42));
    for i in 0..4 {
        println!("{:>8.0} {:>8.0} │ {:>10.0} {:>10.4}",
            x[[i, 0]], x[[i, 1]],
            y[[i, 0]],
            preds[[i, 0]]);
    }

    // Verifica se aprendeu XOR
    let correto = preds.iter().zip(y.iter()).all(|(&p, &t)| {
        (p >= 0.5 && t == 1.0) || (p < 0.5 && t == 0.0)
    });
    println!("
XOR aprendido: {}", if correto { "✓" } else { "✗" });
}

Inferência com ONNX Runtime

O fluxo de produção real: treinar em Python, exportar para ONNX, servir em Rust:

# Em Python — treina e exporta (executado fora do Rust)
import torch
import torch.nn as nn
import torch.onnx

class ModeloSimples(nn.Module):
    def __init__(self):
        super().__init__()
        self.rede = nn.Sequential(
            nn.Linear(4, 16),
            nn.ReLU(),
            nn.Linear(16, 8),
            nn.ReLU(),
            nn.Linear(8, 3),
        )

    def forward(self, x):
        return self.rede(x)

modelo = ModeloSimples()
# ... treinar ...

# Exporta para ONNX
entrada_exemplo = torch.randn(1, 4)
torch.onnx.export(
    modelo,
    entrada_exemplo,
    "modelo.onnx",
    input_names=["entrada"],
    output_names=["saida"],
    dynamic_axes={"entrada": {0: "batch_size"}, "saida": {0: "batch_size"}},
    opset_version=17,
)
print("Exportado para modelo.onnx")
// Rust — carrega e executa inferência com ONNX Runtime
use ort::{Environment, Session, SessionBuilder, Value};
use ndarray::{Array2, CowArray};
use anyhow::Result;

struct ServicorInferencia {
    sessao: Session,
    nome_entrada: String,
    nome_saida: String,
}

impl ServicorInferencia {
    fn carregar(caminho_modelo: &str) -> Result<Self> {
        let ambiente = Environment::builder()
            .with_name("servico_ml")
            .build()?
            .into_arc();

        let sessao = SessionBuilder::new(&ambiente)?
            .with_intra_threads(4)?
            .with_model_from_file(caminho_modelo)?;

        let nome_entrada = sessao.inputs[0].name.clone();
        let nome_saida = sessao.outputs[0].name.clone();

        println!("Modelo carregado: {caminho_modelo}");
        println!("  Entrada: {} {:?}",
            nome_entrada, sessao.inputs[0].input_type);
        println!("  Saída:   {} {:?}",
            nome_saida, sessao.outputs[0].input_type);

        Ok(ServicorInferencia { sessao, nome_entrada, nome_saida })
    }

    fn inferir(&self, dados: Array2<f32>) -> Result<Array2<f32>> {
        let shape: Vec<i64> = dados.shape().iter()
            .map(|&d| d as i64)
            .collect();

        let array = CowArray::from(dados.into_dyn());

        let entrada = vec![(
            self.nome_entrada.as_str(),
            Value::from_array(self.sessao.allocator(), &array)?,
        )];

        let saidas = self.sessao.run(entrada)?;

        let resultado = saidas[0].try_extract::<f32>()?;
        let view = resultado.view().clone().into_dimensionality::<ndarray::Ix2>()?;

        Ok(view.to_owned())
    }

    fn inferir_batch_otimizado(
        &self,
        dados: Vec<Vec<f32>>,
        tamanho_batch: usize,
    ) -> Result<Vec<Vec<f32>>> {
        let n_features = dados[0].len();
        let mut resultados = Vec::new();

        for chunk in dados.chunks(tamanho_batch) {
            let batch_flat: Vec<f32> = chunk.iter().flatten().cloned().collect();
            let batch = Array2::from_shape_vec(
                (chunk.len(), n_features),
                batch_flat,
            )?;

            let saida = self.inferir(batch)?;
            for linha in saida.rows() {
                resultados.push(linha.to_vec());
            }
        }

        Ok(resultados)
    }
}

fn softmax_vec(logits: &[f32]) -> Vec<f32> {
    let max = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
    let exps: Vec<f32> = logits.iter().map(|&x| (x - max).exp()).collect();
    let soma: f32 = exps.iter().sum();
    exps.iter().map(|&e| e / soma).collect()
}

async fn demo_onnx() -> Result<()> {
    println!("══ Inferência ONNX em Rust ══
");

    // Simula o que seria carregado do arquivo
    // Na prática: ServicorInferencia::carregar("modelo.onnx")?
    println!("(Demonstração — requer modelo.onnx exportado do Python)
");

    // Simula inferência com dados do dataset Iris
    let amostras_iris = vec![
        // [comprimento_sep, largura_sep, comprimento_pet, largura_pet]
        vec![5.1f32, 3.5, 1.4, 0.2],  // setosa
        vec![7.0,   3.2, 4.7, 1.4],   // versicolor
        vec![6.3,   3.3, 6.0, 2.5],   // virginica
        vec![4.9,   3.0, 1.4, 0.2],   // setosa
        vec![5.7,   2.8, 4.5, 1.3],   // versicolor
    ];

    let classes = ["Setosa", "Versicolor", "Virginica"];

    // Simula saída de um modelo treinado (logits)
    let logits_simulados: Vec<Vec<f32>> = vec![
        vec![ 3.2, -1.1, -2.3],
        vec![-0.5,  2.8,  0.3],
        vec![-1.8,  0.2,  3.5],
        vec![ 3.0, -0.9, -2.1],
        vec![-0.3,  2.5,  0.1],
    ];

    println!("Resultados de classificação (Iris):");
    println!("{:>6} {:>10} {:>12} {:>12} {:>12}  {:>12}",
        "ID", "Setosa%", "Versic%", "Virg%", "Classe", "Confiança");
    println!("{}", "─".repeat(75));

    for (i, logits) in logits_simulados.iter().enumerate() {
        let probs = softmax_vec(logits);
        let (classe_idx, &confianca) = probs.iter().enumerate()
            .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap())
            .unwrap();

        println!("{:>6} {:>10.2}% {:>11.2}% {:>11.2}%  {:>12}  {:>11.2}%",
            i + 1,
            probs[0] * 100.0,
            probs[1] * 100.0,
            probs[2] * 100.0,
            classes[classe_idx],
            confianca * 100.0,
        );
    }

    Ok(())
}

Candle — deep learning nativo em Rust

use candle_core::{Tensor, Device, DType};
use candle_nn::{linear, Linear, Module, VarBuilder, VarMap};
use anyhow::Result;

// Rede neural com Candle
struct RedeCandle {
    l1: Linear,
    l2: Linear,
    l3: Linear,
}

impl RedeCandle {
    fn novo(vm: &VarMap, dev: &Device) -> Result<Self> {
        let vb = VarBuilder::from_varmap(vm, DType::F32, dev);
        Ok(RedeCandle {
            l1: linear(4, 16, vb.pp("l1"))?,
            l2: linear(16, 8, vb.pp("l2"))?,
            l3: linear(8, 3, vb.pp("l3"))?,
        })
    }

    fn forward(&self, x: &Tensor) -> Result<Tensor> {
        let x = self.l1.forward(x)?;
        let x = x.relu()?;
        let x = self.l2.forward(&x)?;
        let x = x.relu()?;
        let x = self.l3.forward(&x)?;
        Ok(x)
    }
}

fn demo_candle() -> Result<()> {
    println!("══ Candle — Deep Learning Nativo em Rust ══
");

    let dev = Device::Cpu;

    // Tensores básicos
    let a = Tensor::new(&[[1.0f32, 2.0], [3.0, 4.0]], &dev)?;
    let b = Tensor::new(&[[5.0f32, 6.0], [7.0, 8.0]], &dev)?;

    let soma = (&a + &b)?;
    let produto = a.matmul(&b)?;

    println!("A + B = {:?}", soma.to_vec2::<f32>()?);
    println!("A × B = {:?}", produto.to_vec2::<f32>()?);

    // Cria modelo
    let varmap = VarMap::new();
    let modelo = RedeCandle::novo(&varmap, &dev)?;

    // Dados sintéticos do Iris (4 features, 3 classes)
    let dados = Tensor::new(
        &[[5.1f32, 3.5, 1.4, 0.2],
          [7.0,   3.2, 4.7, 1.4],
          [6.3,   3.3, 6.0, 2.5]],
        &dev,
    )?;

    // Forward pass
    let logits = modelo.forward(&dados)?;
    println!("
Logits (3 amostras × 3 classes):");
    println!("{:?}", logits.to_vec2::<f32>()?);

    // Softmax manual
    let exp_logits = logits.exp()?;
    let soma_exp = exp_logits.sum_keepdim(1)?;
    let probs = exp_logits.broadcast_div(&soma_exp)?;
    println!("
Probabilidades:");
    println!("{:?}", probs.to_vec2::<f32>()?);

    // Argmax — classe predita
    let classes = probs.argmax(1)?;
    let nomes = ["Setosa", "Versicolor", "Virginica"];
    let preds: Vec<u32> = classes.to_vec1()?;
    for (i, &pred) in preds.iter().enumerate() {
        println!("Amostra {}: {} (classe {})", i + 1, nomes[pred as usize], pred);
    }

    Ok(())
}

Pipeline de NLP — tokenização e embeddings

use std::collections::HashMap;

// Tokenizador simples — versão educacional
struct TokenizadorSimples {
    vocabulario: HashMap<String, u32>,
    id_para_token: Vec<String>,
    token_unk: u32,
    token_pad: u32,
}

impl TokenizadorSimples {
    fn novo(textos: &[&str]) -> Self {
        let mut vocab: HashMap<String, u32> = HashMap::new();
        vocab.insert("<PAD>".to_string(), 0);
        vocab.insert("<UNK>".to_string(), 1);
        vocab.insert("<CLS>".to_string(), 2);
        vocab.insert("<SEP>".to_string(), 3);

        let mut id_para_token = vec![
            "<PAD>".to_string(),
            "<UNK>".to_string(),
            "<CLS>".to_string(),
            "<SEP>".to_string(),
        ];

        for texto in textos {
            for palavra in texto.split_whitespace() {
                let palavra = palavra.to_lowercase()
                    .trim_matches(|c: char| !c.is_alphabetic())
                    .to_string();

                if !palavra.is_empty() && !vocab.contains_key(&palavra) {
                    let id = vocab.len() as u32;
                    vocab.insert(palavra.clone(), id);
                    id_para_token.push(palavra);
                }
            }
        }

        TokenizadorSimples {
            token_unk: *vocab.get("<UNK>").unwrap(),
            token_pad: *vocab.get("<PAD>").unwrap(),
            vocabulario: vocab,
            id_para_token,
        }
    }

    fn tokenizar(&self, texto: &str, max_len: usize) -> Vec<u32> {
        let mut ids = vec![2u32]; // <CLS>

        for palavra in texto.split_whitespace() {
            let palavra = palavra.to_lowercase()
                .trim_matches(|c: char| !c.is_alphabetic())
                .to_string();

            if !palavra.is_empty() {
                let id = *self.vocabulario.get(&palavra)
                    .unwrap_or(&self.token_unk);
                ids.push(id);
            }

            if ids.len() >= max_len - 1 {
                break;
            }
        }

        ids.push(3); // <SEP>

        // Padding
        while ids.len() < max_len {
            ids.push(self.token_pad);
        }

        ids
    }

    fn decodificar(&self, ids: &[u32]) -> String {
        ids.iter()
            .filter(|&&id| id != self.token_pad && id != 2 && id != 3)
            .map(|&id| self.id_para_token.get(id as usize)
                .map(String::as_str)
                .unwrap_or("<UNK>"))
            .collect::<Vec<_>>()
            .join(" ")
    }

    fn tamanho_vocab(&self) -> usize {
        self.vocabulario.len()
    }
}

// Análise de sentimento baseada em léxico
struct AnalisadorSentimento {
    palavras_positivas: HashMap<String, f32>,
    palavras_negativas: HashMap<String, f32>,
    intensificadores: HashMap<String, f32>,
}

impl AnalisadorSentimento {
    fn novo() -> Self {
        let mut pos = HashMap::new();
        let mut neg = HashMap::new();
        let mut intens = HashMap::new();

        // Léxico positivo
        for (p, s) in [("ótimo", 0.9), ("bom", 0.7), ("excelente", 0.95),
                       ("perfeito", 0.9), ("maravilhoso", 0.85), ("incrível", 0.88),
                       ("satisfeito", 0.7), ("recomendo", 0.8), ("adorei", 0.9)] {
            pos.insert(p.to_string(), s);
        }

        // Léxico negativo
        for (p, s) in [("ruim", 0.8), ("péssimo", 0.95), ("horrível", 0.9),
                       ("terrível", 0.88), ("decepcionante", 0.85), ("horrendo", 0.92),
                       ("insatisfeito", 0.75), ("problema", 0.6), ("defeito", 0.7)] {
            neg.insert(p.to_string(), s);
        }

        // Intensificadores
        for (p, m) in [("muito", 1.5), ("extremamente", 2.0),
                       ("super", 1.4), ("bastante", 1.3)] {
            intens.insert(p.to_string(), m);
        }

        AnalisadorSentimento {
            palavras_positivas: pos,
            palavras_negativas: neg,
            intensificadores: intens,
        }
    }

    fn analisar(&self, texto: &str) -> (f32, &'static str) {
        let palavras: Vec<&str> = texto.split_whitespace().collect();
        let mut score = 0.0f32;
        let mut intensidade = 1.0f32;

        for (i, palavra) in palavras.iter().enumerate() {
            let p = palavra.to_lowercase();
            let p = p.trim_matches(|c: char| !c.is_alphabetic());

            if let Some(&mult) = self.intensificadores.get(p) {
                intensidade = mult;
                continue;
            }

            if let Some(&s) = self.palavras_positivas.get(p) {
                score += s * intensidade;
            } else if let Some(&s) = self.palavras_negativas.get(p) {
                score -= s * intensidade;
            }

            intensidade = 1.0; // reset após uso
        }

        let sentimento = if score > 0.3 {
            "Positivo"
        } else if score < -0.3 {
            "Negativo"
        } else {
            "Neutro"
        };

        (score, sentimento)
    }
}

fn demo_nlp() {
    println!("══ Pipeline de NLP ══
");

    let corpus = [
        "produto muito bom e de ótima qualidade",
        "péssimo atendimento terrível experiência",
        "chegou rápido e funcionando perfeitamente",
        "super satisfeito com a compra recomendo",
        "produto com defeito extremamente decepcionante",
    ];

    // Tokenização
    let tokenizador = TokenizadorSimples::novo(&corpus);
    println!("Vocabulário: {} tokens
", tokenizador.tamanho_vocab());

    println!("── Tokenização ──");
    for texto in &corpus[..2] {
        let ids = tokenizador.tokenizar(texto, 12);
        let decodificado = tokenizador.decodificar(&ids);
        println!("  Original : {texto}");
        println!("  IDs      : {:?}", &ids[..ids.iter().rposition(|&x| x != 0).map(|i| i + 1).unwrap_or(0)]);
        println!("  Decode   : {decodificado}
");
    }

    // Análise de sentimento
    let analisador = AnalisadorSentimento::novo();
    println!("── Análise de Sentimento ──");
    println!("{:>55}  {:>8}  {:>10}", "Texto", "Score", "Sentimento");
    println!("{}", "─".repeat(80));

    for texto in &corpus {
        let (score, sentimento) = analisador.analisar(texto);
        let icone = match sentimento {
            "Positivo" => "🟢",
            "Negativo" => "🔴",
            _          => "🟡",
        };
        println!("{:>55}  {:>8.3}  {} {}",
            &texto[..texto.len().min(55)],
            score,
            icone,
            sentimento,
        );
    }
}

#[tokio::main]
async fn main() -> Result<()> {
    demonstrar_operacoes_nn();
    println!("
{}
", "═".repeat(55));

    demo_regressao_logistica();
    println!("
{}
", "═".repeat(55));

    demo_mlp();
    println!("
{}
", "═".repeat(55));

    demo_onnx().await?;
    println!("
{}
", "═".repeat(55));

    demo_candle()?;
    println!("
{}
", "═".repeat(55));

    demo_nlp();

    Ok(())
}

Fontes e leituras recomendadas

  • candle — Hugging Face — framework de DL nativo em Rust — https://github.com/huggingface/candle
  • burn framework — DL flexível e portável — https://burn.dev
  • linfa crate — ML clássico (SVM, k-means, etc.) — https://docs.rs/linfa
  • ONNX Runtime Rustort crate — https://docs.rs/ort
  • tch-rs crate — bindings PyTorch para Rust — https://docs.rs/tch
  • tract crate — inferência ONNX/TF sem C++ — https://github.com/sonos/tract
  • "Deep Learning" — Goodfellow, Bengio, Courville — fundamentos teóricos — https://www.deeplearningbook.org
  • "The Illustrated Transformer" — visualização excelente — https://jalammar.github.io/illustrated-transformer/

Artigo #46 de 52 | Série: Dominando Rust em 1 Ano Próximo → Artigo #47: Segurança e Auditoria — Fuzzing, análise estática e hardening de código Rust


Comentários

Mais em Rust

Controle de Fluxo — if, loop, while e for como você nunca viu antes
Controle de Fluxo — if, loop, while e for como você nunca viu antes

No artigo anterior, aprendemos que Rust distingue statements de expressions,...

Compiladores e Interpretadores — Construindo uma Linguagem com Rust
Compiladores e Interpretadores — Construindo uma Linguagem com Rust

Rust — Artigo #44 Compiladores e Interpretadores — Construindo uma Linguagem...

Funções, Expressões e Como Rust Pensa Diferente sobre Retorno de Valores
Funções, Expressões e Como Rust Pensa Diferente sobre Retorno de Valores

Se voc&ecirc; vem de Python, JavaScript ou Java, j&aacute; sabe o que &eacute...