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
| Aspect | PyTorch | aprender |
|---|---|---|
| Initialization | Random | Deterministic |
| Training | Non-deterministic | Bit-exact reproducible |
| GPU state | Hidden | Explicit |
| Memory | Manual management | Ownership-based |
| Numerical precision | Varies | Guaranteed |
| Debugging | Difficult | Transparent |
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
- Deterministic Training: Same data produces same model every time
- Type-Safe Models: Compiler enforces correct dimensions
- Transparent Gradients: Every computation inspectable
- EU AI Act Compliant: Reproducibility built into design
- 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