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/candleburnframework — DL flexível e portável — https://burn.devlinfacrate — ML clássico (SVM, k-means, etc.) — https://docs.rs/linfa- ONNX Runtime Rust —
ortcrate — https://docs.rs/ort tch-rscrate — bindings PyTorch para Rust — https://docs.rs/tchtractcrate — 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