torsh_cli/commands/
real_training.rs1use anyhow::{anyhow, Result};
14
15use torsh::core::device::DeviceType;
16use torsh::nn::container::Sequential;
17use torsh::nn::layers::{Linear, ReLU};
18use torsh::nn::Module;
19use torsh::optim::sgd::SGD;
20use torsh::optim::Optimizer;
21use torsh::tensor::Tensor;
22
23#[derive(Debug, Clone, Copy)]
25pub struct MlpConfig {
26 pub input_dim: usize,
28 pub hidden_dim: usize,
30 pub output_dim: usize,
32}
33
34#[derive(Debug)]
39pub struct RegressionData {
40 pub inputs: Tensor,
42 pub targets: Tensor,
44 pub n: usize,
46 pub input_dim: usize,
48 pub output_dim: usize,
50}
51
52pub fn build_mlp(cfg: &MlpConfig) -> Result<Sequential> {
54 if cfg.input_dim == 0 || cfg.hidden_dim == 0 || cfg.output_dim == 0 {
55 return Err(anyhow!(
56 "MLP dimensions must all be non-zero (got input={}, hidden={}, output={})",
57 cfg.input_dim,
58 cfg.hidden_dim,
59 cfg.output_dim
60 ));
61 }
62 let model = Sequential::new()
63 .add(Linear::new(cfg.input_dim, cfg.hidden_dim, true))
64 .add(ReLU::new())
65 .add(Linear::new(cfg.hidden_dim, cfg.output_dim, true));
66 Ok(model)
67}
68
69pub fn synthetic_regression(
76 n: usize,
77 input_dim: usize,
78 output_dim: usize,
79 seed: u64,
80) -> Result<RegressionData> {
81 if n == 0 || input_dim == 0 || output_dim == 0 {
82 return Err(anyhow!("synthetic dataset dimensions must be non-zero"));
83 }
84
85 let mut state = seed.wrapping_mul(6364136223846793005).wrapping_add(1);
88 let mut next_unit = || -> f32 {
89 state = state
90 .wrapping_mul(6364136223846793005)
91 .wrapping_add(1442695040888963407);
92 ((state >> 40) as f32) / ((1u64 << 24) as f32)
94 };
95
96 let true_w: Vec<f32> = (0..input_dim * output_dim)
98 .map(|k| 0.5 - ((k % 7) as f32) * 0.1)
99 .collect();
100 let true_b: Vec<f32> = (0..output_dim).map(|j| 0.05 * (j as f32 + 1.0)).collect();
101
102 let mut inputs = Vec::with_capacity(n * input_dim);
103 let mut targets = Vec::with_capacity(n * output_dim);
104
105 for _ in 0..n {
106 let row: Vec<f32> = (0..input_dim).map(|_| next_unit() - 0.5).collect();
107 for j in 0..output_dim {
108 let mut acc = true_b[j];
109 for (i, &x) in row.iter().enumerate() {
110 acc += x * true_w[i * output_dim + j];
111 }
112 acc += (next_unit() - 0.5) * 0.01;
114 targets.push(acc);
115 }
116 inputs.extend_from_slice(&row);
117 }
118
119 let inputs = Tensor::from_data(inputs, vec![n, input_dim], DeviceType::Cpu)?;
120 let targets = Tensor::from_data(targets, vec![n, output_dim], DeviceType::Cpu)?;
121
122 Ok(RegressionData {
123 inputs,
124 targets,
125 n,
126 input_dim,
127 output_dim,
128 })
129}
130
131pub fn mse_loss(pred: &Tensor, target: &Tensor, count: usize) -> Result<Tensor> {
136 let diff = pred.sub(target)?;
137 let sq = diff.mul(&diff)?;
138 let sse = sq.sum()?;
139 Ok(sse.mul_scalar(1.0 / count as f32)?)
140}
141
142pub fn evaluate_loss(model: &Sequential, data: &RegressionData) -> Result<f64> {
144 let pred = model.forward(&data.inputs)?;
145 let loss = mse_loss(&pred, &data.targets, data.n * data.output_dim)?;
146 let value = loss
147 .to_vec()?
148 .first()
149 .copied()
150 .ok_or_else(|| anyhow!("loss tensor was empty"))?;
151 Ok(value as f64)
152}
153
154pub fn train_regression(
159 model: &Sequential,
160 data: &RegressionData,
161 learning_rate: f32,
162 steps: usize,
163) -> Result<Vec<f64>> {
164 if data.input_dim == 0 || data.output_dim == 0 {
165 return Err(anyhow!("dataset has zero-sized dimensions"));
166 }
167 let params: Vec<_> = model.parameters().values().map(|p| p.tensor()).collect();
168 if params.is_empty() {
169 return Err(anyhow!("model exposes no trainable parameters"));
170 }
171
172 let mut optimizer = SGD::new(params, learning_rate, Some(0.9), None, None, false);
173 let denom = data.n * data.output_dim;
174 let mut history = Vec::with_capacity(steps);
175
176 for _ in 0..steps {
177 optimizer.zero_grad();
178 let pred = model.forward(&data.inputs)?;
179 let loss = mse_loss(&pred, &data.targets, denom)?;
180 loss.backward()?;
181 optimizer
182 .step()
183 .map_err(|e| anyhow!("optimizer step failed: {e}"))?;
184
185 let value = loss
186 .to_vec()?
187 .first()
188 .copied()
189 .ok_or_else(|| anyhow!("loss tensor was empty"))? as f64;
190 history.push(value);
191 }
192
193 Ok(history)
194}