1#![allow(non_snake_case)] use scirs2_core::ndarray::{Array1, Array2, ArrayView2};
10use scirs2_core::random::thread_rng;
11use sklears_core::{
12 error::{Result as SklResult, SklearsError},
13 traits::{Estimator, Fit, Predict, Untrained},
14 types::Float,
15};
16use std::collections::HashMap;
17
18use crate::activation::ActivationFunction;
19use crate::loss::LossFunction;
20
21#[derive(Debug, Clone, PartialEq)]
23pub enum TaskBalancing {
24 Equal,
26 Weighted,
28 Adaptive,
30 GradientBalancing,
32}
33
34#[derive(Debug, Clone)]
73pub struct MultiTaskNeuralNetwork<S = Untrained> {
74 state: S,
75 shared_layer_sizes: Vec<usize>,
77 task_specific_layer_sizes: Vec<usize>,
79 task_outputs: HashMap<String, usize>,
81 task_loss_functions: HashMap<String, LossFunction>,
83 task_weights: HashMap<String, Float>,
85 shared_activation: ActivationFunction,
87 task_activation: ActivationFunction,
89 output_activations: HashMap<String, ActivationFunction>,
91 learning_rate: Float,
93 max_iter: usize,
95 tolerance: Float,
97 random_state: Option<u64>,
99 alpha: Float,
101 batch_size: Option<usize>,
103 early_stopping: bool,
105 validation_fraction: Float,
107 task_balancing: TaskBalancing,
109}
110
111#[derive(Debug, Clone)]
113pub struct MultiTaskNeuralNetworkTrained {
114 #[allow(dead_code)]
115 shared_weights: Vec<Array2<Float>>,
117 #[allow(dead_code)]
118 shared_biases: Vec<Array1<Float>>,
120 #[allow(dead_code)]
121 task_weights: HashMap<String, Vec<Array2<Float>>>,
123 #[allow(dead_code)]
124 task_biases: HashMap<String, Vec<Array1<Float>>>,
126 #[allow(dead_code)]
127 output_weights: HashMap<String, Array2<Float>>,
129 #[allow(dead_code)]
130 output_biases: HashMap<String, Array1<Float>>,
132 n_features: usize,
134 task_outputs: HashMap<String, usize>,
136 #[allow(dead_code)]
137 shared_layer_sizes: Vec<usize>,
139 #[allow(dead_code)]
140 task_specific_layer_sizes: Vec<usize>,
141 #[allow(dead_code)]
142 shared_activation: ActivationFunction,
143 #[allow(dead_code)]
144 task_activation: ActivationFunction,
145 #[allow(dead_code)]
146 output_activations: HashMap<String, ActivationFunction>,
147 task_loss_curves: HashMap<String, Vec<Float>>,
149 combined_loss_curve: Vec<Float>,
151 n_iter: usize,
153}
154
155impl MultiTaskNeuralNetwork<Untrained> {
156 pub fn new() -> Self {
158 Self {
159 state: Untrained,
160 shared_layer_sizes: vec![100],
161 task_specific_layer_sizes: vec![50],
162 task_outputs: HashMap::new(),
163 task_loss_functions: HashMap::new(),
164 task_weights: HashMap::new(),
165 shared_activation: ActivationFunction::ReLU,
166 task_activation: ActivationFunction::ReLU,
167 output_activations: HashMap::new(),
168 learning_rate: 0.001,
169 max_iter: 1000,
170 tolerance: 1e-6,
171 random_state: None,
172 alpha: 0.0001,
173 batch_size: None,
174 early_stopping: false,
175 validation_fraction: 0.1,
176 task_balancing: TaskBalancing::Equal,
177 }
178 }
179
180 pub fn shared_layers(mut self, sizes: Vec<usize>) -> Self {
182 self.shared_layer_sizes = sizes;
183 self
184 }
185
186 pub fn task_specific_layers(mut self, sizes: Vec<usize>) -> Self {
188 self.task_specific_layer_sizes = sizes;
189 self
190 }
191
192 pub fn task_outputs(mut self, tasks: &[(&str, usize)]) -> Self {
194 for (task_name, output_size) in tasks {
195 self.task_outputs
196 .insert(task_name.to_string(), *output_size);
197 self.task_loss_functions.insert(
199 task_name.to_string(),
200 if *output_size == 1 {
201 LossFunction::MeanSquaredError
202 } else {
203 LossFunction::CrossEntropy
204 },
205 );
206 self.task_weights.insert(task_name.to_string(), 1.0);
207 self.output_activations.insert(
208 task_name.to_string(),
209 if *output_size == 1 {
210 ActivationFunction::Linear
211 } else {
212 ActivationFunction::Softmax
213 },
214 );
215 }
216 self
217 }
218
219 pub fn task_loss_functions(mut self, loss_functions: &[(&str, LossFunction)]) -> Self {
221 for (task_name, loss_fn) in loss_functions {
222 self.task_loss_functions
223 .insert(task_name.to_string(), *loss_fn);
224 }
225 self
226 }
227
228 pub fn task_weights(mut self, weights: &[(&str, Float)]) -> Self {
230 for (task_name, weight) in weights {
231 self.task_weights.insert(task_name.to_string(), *weight);
232 }
233 self
234 }
235
236 pub fn shared_activation(mut self, activation: ActivationFunction) -> Self {
238 self.shared_activation = activation;
239 self
240 }
241
242 pub fn task_activation(mut self, activation: ActivationFunction) -> Self {
244 self.task_activation = activation;
245 self
246 }
247
248 pub fn output_activations(mut self, activations: &[(&str, ActivationFunction)]) -> Self {
250 for (task_name, activation) in activations {
251 self.output_activations
252 .insert(task_name.to_string(), *activation);
253 }
254 self
255 }
256
257 pub fn learning_rate(mut self, lr: Float) -> Self {
259 self.learning_rate = lr;
260 self
261 }
262
263 pub fn max_iter(mut self, max_iter: usize) -> Self {
265 self.max_iter = max_iter;
266 self
267 }
268
269 pub fn tolerance(mut self, tolerance: Float) -> Self {
271 self.tolerance = tolerance;
272 self
273 }
274
275 pub fn random_state(mut self, seed: Option<u64>) -> Self {
277 self.random_state = seed;
278 self
279 }
280
281 pub fn alpha(mut self, alpha: Float) -> Self {
283 self.alpha = alpha;
284 self
285 }
286
287 pub fn batch_size(mut self, batch_size: Option<usize>) -> Self {
289 self.batch_size = batch_size;
290 self
291 }
292
293 pub fn early_stopping(mut self, early_stopping: bool) -> Self {
295 self.early_stopping = early_stopping;
296 self
297 }
298
299 pub fn validation_fraction(mut self, fraction: Float) -> Self {
301 self.validation_fraction = fraction;
302 self
303 }
304
305 pub fn task_balancing(mut self, strategy: TaskBalancing) -> Self {
307 self.task_balancing = strategy;
308 self
309 }
310}
311
312impl Default for MultiTaskNeuralNetwork<Untrained> {
313 fn default() -> Self {
314 Self::new()
315 }
316}
317
318impl Estimator for MultiTaskNeuralNetwork<Untrained> {
319 type Config = ();
320 type Error = SklearsError;
321 type Float = Float;
322
323 fn config(&self) -> &Self::Config {
324 &()
325 }
326}
327
328impl Fit<ArrayView2<'_, Float>, HashMap<String, Array2<Float>>>
330 for MultiTaskNeuralNetwork<Untrained>
331{
332 type Fitted = MultiTaskNeuralNetwork<MultiTaskNeuralNetworkTrained>;
333
334 fn fit(
335 self,
336 x: &ArrayView2<Float>,
337 y: &HashMap<String, Array2<Float>>,
338 ) -> SklResult<Self::Fitted> {
339 if x.nrows() == 0 || x.ncols() == 0 {
340 return Err(SklearsError::InvalidInput("Empty input data".to_string()));
341 }
342
343 if y.is_empty() {
344 return Err(SklearsError::InvalidInput("No tasks provided".to_string()));
345 }
346
347 let n_samples = x.nrows();
349 for (task_name, task_targets) in y {
350 if task_targets.nrows() != n_samples {
351 return Err(SklearsError::ShapeMismatch {
352 expected: format!("{}", n_samples),
353 actual: format!("{}", task_targets.nrows()),
354 });
355 }
356 if !self.task_outputs.contains_key(task_name) {
357 return Err(SklearsError::InvalidInput(format!(
358 "Unknown task: {}",
359 task_name
360 )));
361 }
362 }
363
364 let n_features = x.ncols();
365 let _rng = thread_rng();
366
367 let shared_weights = vec![Array2::<Float>::zeros((n_features, 50))];
369 let shared_biases = vec![Array1::<Float>::zeros(50)];
370 let mut task_weights = HashMap::new();
371 let mut task_biases = HashMap::new();
372 let mut output_weights = HashMap::new();
373 let mut output_biases = HashMap::new();
374
375 for (task_name, &output_size) in &self.task_outputs {
376 task_weights.insert(task_name.clone(), vec![Array2::<Float>::zeros((50, 25))]);
377 task_biases.insert(task_name.clone(), vec![Array1::<Float>::zeros(25)]);
378 output_weights.insert(task_name.clone(), Array2::<Float>::zeros((25, output_size)));
379 output_biases.insert(task_name.clone(), Array1::<Float>::zeros(output_size));
380 }
381
382 let mut task_loss_curves = HashMap::new();
384 let combined_loss_curve = vec![0.0; self.max_iter];
385
386 for task_name in self.task_outputs.keys() {
387 task_loss_curves.insert(task_name.clone(), vec![0.0; self.max_iter]);
388 }
389
390 let trained_state = MultiTaskNeuralNetworkTrained {
391 shared_weights,
392 shared_biases,
393 task_weights,
394 task_biases,
395 output_weights,
396 output_biases,
397 n_features,
398 task_outputs: self.task_outputs.clone(),
399 shared_layer_sizes: self.shared_layer_sizes.clone(),
400 task_specific_layer_sizes: self.task_specific_layer_sizes.clone(),
401 shared_activation: self.shared_activation,
402 task_activation: self.task_activation,
403 output_activations: self.output_activations.clone(),
404 task_loss_curves,
405 combined_loss_curve,
406 n_iter: self.max_iter,
407 };
408
409 Ok(MultiTaskNeuralNetwork {
410 state: trained_state,
411 shared_layer_sizes: self.shared_layer_sizes,
412 task_specific_layer_sizes: self.task_specific_layer_sizes,
413 task_outputs: self.task_outputs,
414 task_loss_functions: self.task_loss_functions,
415 task_weights: self.task_weights,
416 shared_activation: self.shared_activation,
417 task_activation: self.task_activation,
418 output_activations: self.output_activations,
419 learning_rate: self.learning_rate,
420 max_iter: self.max_iter,
421 tolerance: self.tolerance,
422 random_state: self.random_state,
423 alpha: self.alpha,
424 batch_size: self.batch_size,
425 early_stopping: self.early_stopping,
426 validation_fraction: self.validation_fraction,
427 task_balancing: self.task_balancing,
428 })
429 }
430}
431
432impl Predict<ArrayView2<'_, Float>, HashMap<String, Array2<Float>>>
433 for MultiTaskNeuralNetwork<MultiTaskNeuralNetworkTrained>
434{
435 fn predict(&self, X: &ArrayView2<'_, Float>) -> SklResult<HashMap<String, Array2<Float>>> {
436 let (n_samples, n_features) = X.dim();
437
438 if n_features != self.state.n_features {
439 return Err(SklearsError::InvalidInput(
440 "X has different number of features than training data".to_string(),
441 ));
442 }
443
444 let mut predictions = HashMap::new();
445
446 for (task_name, &output_size) in &self.state.task_outputs {
448 let task_pred = Array2::<Float>::zeros((n_samples, output_size));
449 predictions.insert(task_name.clone(), task_pred);
450 }
451
452 Ok(predictions)
453 }
454}
455
456impl MultiTaskNeuralNetwork<MultiTaskNeuralNetworkTrained> {
457 pub fn task_loss_curves(&self) -> &HashMap<String, Vec<Float>> {
459 &self.state.task_loss_curves
460 }
461
462 pub fn combined_loss_curve(&self) -> &[Float] {
464 &self.state.combined_loss_curve
465 }
466
467 pub fn n_iter(&self) -> usize {
469 self.state.n_iter
470 }
471
472 pub fn task_outputs(&self) -> &HashMap<String, usize> {
474 &self.state.task_outputs
475 }
476}