Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

aprender: ML Training Framework

Toyota Way Principle (Genchi Genbutsu): Go and see for yourself. Every training run must be reproducible and inspectable.

Status: Complete

The Problem: Non-Deterministic Training

Traditional ML frameworks suffer from:

# PyTorch - Non-deterministic by default
model = nn.Linear(10, 1)
loss1 = train(model, data)  # Random initialization

model2 = nn.Linear(10, 1)
loss2 = train(model2, data)  # Different result!

assert loss1 == loss2  # FAILS!

aprender Solution: Deterministic Training

┌─────────────────────────────────────────────────────────┐
│                  aprender Pipeline                       │
├─────────────────────────────────────────────────────────┤
│                                                         │
│  Data → Preprocessing → Training → Validation → Export │
│    │          │            │           │          │    │
│    ↓          ↓            ↓           ↓          ↓    │
│  Typed    Deterministic  Reproducible  Logged   Safe   │
│  Inputs   Transforms     Gradients     Metrics  Format │
│                                                         │
└─────────────────────────────────────────────────────────┘

Validation

Run all chapter examples:

make run-ch12      # Run ML training example
make test-ch12     # Run all tests

Linear Regression: The Foundation

Type-Safe Model Definition

#![allow(unused)]
fn main() {
#[derive(Debug, Clone)]
struct LinearRegression {
    weights: Vec<f64>,
    bias: f64,
    learning_rate: f64,
}

impl LinearRegression {
    fn new(features: usize, learning_rate: f64) -> Self {
        Self {
            weights: vec![0.0; features],  // Deterministic init
            bias: 0.0,
            learning_rate,
        }
    }
}
}

Key improvements over PyTorch:

  • Zero initialization (deterministic)
  • Type-safe learning rate
  • No hidden global state

Forward Pass

#![allow(unused)]
fn main() {
fn predict(&self, x: &[f64]) -> f64 {
    let sum: f64 = self.weights.iter()
        .zip(x.iter())
        .map(|(w, xi)| w * xi)
        .sum();
    sum + self.bias
}
}

Gradient Descent

#![allow(unused)]
fn main() {
fn train_step(&mut self, x: &[Vec<f64>], y: &[f64]) {
    let n = x.len() as f64;
    let mut weight_grads = vec![0.0; self.weights.len()];
    let mut bias_grad = 0.0;

    for (xi, yi) in x.iter().zip(y.iter()) {
        let pred = self.predict(xi);
        let error = pred - yi;

        for (j, xij) in xi.iter().enumerate() {
            weight_grads[j] += error * xij;
        }
        bias_grad += error;
    }

    // Update weights
    for (w, grad) in self.weights.iter_mut().zip(weight_grads.iter()) {
        *w -= self.learning_rate * grad / n;
    }
    self.bias -= self.learning_rate * bias_grad / n;
}
}

Determinism Guarantee

#![allow(unused)]
fn main() {
#[test]
fn test_training_determinism() {
    let x = vec![vec![1.0], vec![2.0], vec![3.0]];
    let y = vec![2.0, 4.0, 6.0];

    let mut results = Vec::new();
    for _ in 0..5 {
        let mut model = LinearRegression::new(1, 0.1);
        model.fit(&x, &y, 50);
        results.push(model.weights[0]);
    }

    let first = results[0];
    assert!(results.iter().all(|&r| (r - first).abs() < 1e-10),
        "Training must be deterministic");
}
}

Result: All 5 runs produce identical weights to 10 decimal places.

Training Loop

#![allow(unused)]
fn main() {
fn fit(&mut self, x: &[Vec<f64>], y: &[f64], epochs: usize) -> Vec<f64> {
    let mut losses = Vec::with_capacity(epochs);
    for _ in 0..epochs {
        self.train_step(x, y);
        losses.push(self.mse(x, y));
    }
    losses
}
}

Convergence Visualization

 Epoch │          MSE
───────┼─────────────
     1 │     4.040000
     2 │     1.689856
     3 │     0.731432
     4 │     0.331714
   ... │          ...
    19 │     0.000024
    20 │     0.000015

Mean Squared Error

#![allow(unused)]
fn main() {
fn mse(&self, x: &[Vec<f64>], y: &[f64]) -> f64 {
    let n = x.len() as f64;
    let sum: f64 = x.iter()
        .zip(y.iter())
        .map(|(xi, yi)| {
            let pred = self.predict(xi);
            (pred - yi).powi(2)
        })
        .sum();
    sum / n
}
}

EU AI Act Compliance

Article 10: Data Governance

  • Training data fully local
  • No external API calls
  • Deterministic preprocessing
  • All data transformations logged

Article 13: Transparency

  • Model weights fully inspectable
  • Training history logged
  • Reproducible training runs
  • Gradient computation transparent

Article 15: Robustness

  • Numerical stability guaranteed
  • Type-safe operations
  • Memory-safe training loops
  • No undefined behavior

Comparison: aprender vs PyTorch

AspectPyTorchaprender
InitializationRandomDeterministic
TrainingNon-deterministicBit-exact reproducible
GPU stateHiddenExplicit
MemoryManual managementOwnership-based
Numerical precisionVariesGuaranteed
DebuggingDifficultTransparent

Testing

#![allow(unused)]
fn main() {
#[test]
fn test_linear_regression_creation() {
    let model = LinearRegression::new(3, 0.01);
    assert_eq!(model.weights.len(), 3);
    assert_eq!(model.bias, 0.0);
}

#[test]
fn test_prediction() {
    let mut model = LinearRegression::new(2, 0.01);
    model.weights = vec![2.0, 3.0];
    model.bias = 1.0;

    // y = 2*1 + 3*2 + 1 = 9
    let pred = model.predict(&[1.0, 2.0]);
    assert!((pred - 9.0).abs() < 1e-10);
}

#[test]
fn test_training_reduces_loss() {
    let x = vec![vec![1.0], vec![2.0], vec![3.0]];
    let y = vec![2.0, 4.0, 6.0];

    let mut model = LinearRegression::new(1, 0.1);
    let initial_loss = model.mse(&x, &y);
    model.fit(&x, &y, 100);
    let final_loss = model.mse(&x, &y);

    assert!(final_loss < initial_loss);
}
}

Key Takeaways

  1. Deterministic Training: Same data produces same model every time
  2. Type-Safe Models: Compiler enforces correct dimensions
  3. Transparent Gradients: Every computation inspectable
  4. EU AI Act Compliant: Reproducibility built into design
  5. Zero Hidden State: No global configuration affecting results

Next Steps

  • Chapter 13: realizar - Inference engine
  • Chapter 14: entrenar - Distributed training

Source Code

Full implementation: examples/ch12-aprender/

# Verify all claims
make test-ch12

# Run examples
make run-ch12