Skip to main content

sklears_multioutput/regularization/
task_relationship.rs

1//! Task Relationship Learning for Multi-Task Learning
2//!
3//! This method learns explicit relationships between tasks and uses this information
4//! to regularize the learning process. Tasks that are determined to be related
5//! are encouraged to have similar parameters.
6#![allow(non_snake_case)] // Standard ML notation: X for feature matrices, K for kernels
7
8// Use SciRS2-Core for arrays and random number generation (SciRS2 Policy)
9use scirs2_core::ndarray::{Array1, Array2, ArrayView2, Axis};
10use scirs2_core::random::thread_rng;
11use scirs2_core::random::RandNormal;
12use sklears_core::{
13    error::{Result as SklResult, SklearsError},
14    traits::{Estimator, Fit, Predict, Untrained},
15    types::Float,
16};
17use std::collections::HashMap;
18
19/// Methods for computing task similarity
20#[derive(Debug, Clone, PartialEq)]
21pub enum TaskSimilarityMethod {
22    /// Correlation-based similarity
23    Correlation,
24    /// Cosine similarity of task parameters
25    Cosine,
26    /// Euclidean distance-based similarity
27    Euclidean,
28    /// Mutual information-based similarity
29    MutualInformation,
30}
31
32/// Task Relationship Learning for Multi-Task Learning
33///
34/// This method learns explicit relationships between tasks and uses this information
35/// to regularize the learning process. Tasks that are determined to be related
36/// are encouraged to have similar parameters.
37///
38/// # Examples
39///
40/// ```
41/// use sklears_multioutput::regularization::TaskRelationshipLearning;
42/// use sklears_core::traits::{Predict, Fit};
43/// // Use SciRS2-Core for arrays and random number generation (SciRS2 Policy)
44/// use scirs2_core::ndarray::array;
45/// use std::collections::HashMap;
46///
47/// let X = array![[1.0, 2.0], [2.0, 3.0], [3.0, 1.0], [4.0, 4.0]];
48/// let mut y_tasks = HashMap::new();
49/// y_tasks.insert("task1".to_string(), array![[1.0], [2.0], [1.5], [2.5]]);
50/// y_tasks.insert("task2".to_string(), array![[0.5], [1.0], [0.8], [1.2]]);
51///
52/// let task_relationship = TaskRelationshipLearning::new()
53///     .relationship_strength(0.1)
54///     .similarity_threshold(0.5)
55///     .max_iter(1000);
56/// ```
57#[derive(Debug, Clone)]
58pub struct TaskRelationshipLearning<S = Untrained> {
59    pub(crate) state: S,
60    /// Strength of relationship regularization
61    pub(crate) relationship_strength: Float,
62    /// Threshold for task similarity to be considered related
63    pub(crate) similarity_threshold: Float,
64    /// Base regularization strength
65    pub(crate) base_alpha: Float,
66    /// Maximum iterations
67    pub(crate) max_iter: usize,
68    /// Convergence tolerance
69    pub(crate) tolerance: Float,
70    /// Learning rate
71    pub(crate) learning_rate: Float,
72    /// Task configurations
73    pub(crate) task_outputs: HashMap<String, usize>,
74    /// Include intercept term
75    pub(crate) fit_intercept: bool,
76    /// Method for computing task similarity
77    pub(crate) similarity_method: TaskSimilarityMethod,
78}
79
80/// Trained state for TaskRelationshipLearning
81#[derive(Debug, Clone)]
82pub struct TaskRelationshipLearningTrained {
83    /// Coefficients for each task
84    pub(crate) coefficients: HashMap<String, Array2<Float>>,
85    /// Intercepts for each task
86    pub(crate) intercepts: HashMap<String, Array1<Float>>,
87    /// Task relationship matrix (similarity scores)
88    pub(crate) relationship_matrix: Array2<Float>,
89    /// Task names in order
90    pub(crate) task_names: Vec<String>,
91    /// Number of input features
92    pub(crate) n_features: usize,
93    #[allow(dead_code)]
94    /// Task configurations
95    pub(crate) task_outputs: HashMap<String, usize>,
96    #[allow(dead_code)]
97    /// Training parameters
98    pub(crate) relationship_strength: Float,
99    pub(crate) similarity_threshold: Float,
100    #[allow(dead_code)]
101    pub(crate) similarity_method: TaskSimilarityMethod,
102    /// Training iterations performed
103    pub(crate) n_iter: usize,
104}
105
106impl TaskRelationshipLearning<Untrained> {
107    /// Create a new TaskRelationshipLearning instance
108    pub fn new() -> Self {
109        Self {
110            state: Untrained,
111            relationship_strength: 1.0,
112            similarity_threshold: 0.5,
113            base_alpha: 1.0,
114            max_iter: 1000,
115            tolerance: 1e-4,
116            learning_rate: 0.01,
117            task_outputs: HashMap::new(),
118            fit_intercept: true,
119            similarity_method: TaskSimilarityMethod::Correlation,
120        }
121    }
122
123    /// Set relationship regularization strength
124    pub fn relationship_strength(mut self, strength: Float) -> Self {
125        self.relationship_strength = strength;
126        self
127    }
128
129    /// Set similarity threshold for relationships
130    pub fn similarity_threshold(mut self, threshold: Float) -> Self {
131        self.similarity_threshold = threshold;
132        self
133    }
134
135    /// Set base regularization strength
136    pub fn base_alpha(mut self, alpha: Float) -> Self {
137        self.base_alpha = alpha;
138        self
139    }
140
141    /// Set task similarity method
142    pub fn similarity_method(mut self, method: TaskSimilarityMethod) -> Self {
143        self.similarity_method = method;
144        self
145    }
146
147    /// Set maximum iterations
148    pub fn max_iter(mut self, max_iter: usize) -> Self {
149        self.max_iter = max_iter;
150        self
151    }
152
153    /// Set tolerance
154    pub fn tolerance(mut self, tolerance: Float) -> Self {
155        self.tolerance = tolerance;
156        self
157    }
158
159    /// Set learning rate
160    pub fn learning_rate(mut self, lr: Float) -> Self {
161        self.learning_rate = lr;
162        self
163    }
164
165    /// Set task outputs
166    pub fn task_outputs(mut self, outputs: &[(&str, usize)]) -> Self {
167        self.task_outputs = outputs
168            .iter()
169            .map(|(name, size)| (name.to_string(), *size))
170            .collect();
171        self
172    }
173}
174
175impl Default for TaskRelationshipLearning<Untrained> {
176    fn default() -> Self {
177        Self::new()
178    }
179}
180
181impl Estimator for TaskRelationshipLearning<Untrained> {
182    type Config = ();
183    type Error = SklearsError;
184    type Float = Float;
185
186    fn config(&self) -> &Self::Config {
187        &()
188    }
189}
190
191impl Fit<ArrayView2<'_, Float>, HashMap<String, Array2<Float>>>
192    for TaskRelationshipLearning<Untrained>
193{
194    type Fitted = TaskRelationshipLearning<TaskRelationshipLearningTrained>;
195
196    fn fit(
197        self,
198        X: &ArrayView2<'_, Float>,
199        y: &HashMap<String, Array2<Float>>,
200    ) -> SklResult<Self::Fitted> {
201        let x = X.to_owned();
202        let (n_samples, n_features) = x.dim();
203
204        if n_samples == 0 || n_features == 0 {
205            return Err(SklearsError::InvalidInput("Empty input data".to_string()));
206        }
207
208        let task_names: Vec<String> = y.keys().cloned().collect();
209        let n_tasks = task_names.len();
210
211        // Initialize task coefficients
212        let mut task_coefficients: HashMap<String, Array2<Float>> = HashMap::new();
213        let mut task_intercepts: HashMap<String, Array1<Float>> = HashMap::new();
214
215        let mut rng_gen = thread_rng();
216
217        for (task_name, y_task) in y {
218            let n_outputs = y_task.ncols();
219            let mut coef = Array2::<Float>::zeros((n_features, n_outputs));
220            let normal_dist = RandNormal::new(0.0, 0.1).expect("operation should succeed");
221            for i in 0..n_features {
222                for j in 0..n_outputs {
223                    coef[[i, j]] = rng_gen.sample(normal_dist);
224                }
225            }
226            let intercept = Array1::<Float>::zeros(n_outputs);
227            task_coefficients.insert(task_name.clone(), coef);
228            task_intercepts.insert(task_name.clone(), intercept);
229        }
230
231        // Compute task similarity matrix
232        let mut relationship_matrix = Array2::<Float>::zeros((n_tasks, n_tasks));
233
234        for (i, task_i) in task_names.iter().enumerate() {
235            for (j, task_j) in task_names.iter().enumerate() {
236                if i != j {
237                    let similarity = self.compute_task_similarity(
238                        &y[task_i],
239                        &y[task_j],
240                        &self.similarity_method,
241                    );
242                    relationship_matrix[[i, j]] = similarity;
243                } else {
244                    relationship_matrix[[i, j]] = 1.0;
245                }
246            }
247        }
248
249        // Training loop
250        let mut prev_loss = Float::INFINITY;
251        let mut n_iter = 0;
252
253        for iteration in 0..self.max_iter {
254            let mut total_loss = 0.0;
255
256            // Update coefficients for each task
257            for (task_name, y_task) in y {
258                let current_coef = &task_coefficients[task_name];
259                let current_intercept = &task_intercepts[task_name];
260
261                // Compute predictions
262                let predictions = x.dot(current_coef);
263                let predictions_with_intercept = &predictions + current_intercept;
264
265                // Compute residuals
266                let residuals = &predictions_with_intercept - y_task;
267
268                // Compute gradients
269                let grad_coef = x.t().dot(&residuals) / (n_samples as Float);
270                let grad_intercept = residuals.sum_axis(Axis(0)) / (n_samples as Float);
271
272                // Add relationship regularization
273                let mut reg_grad_coef = grad_coef.clone();
274
275                // Find current task index
276                let task_idx = task_names
277                    .iter()
278                    .position(|t| t == task_name)
279                    .expect("operation should succeed");
280
281                // Add relationship penalties
282                for (other_idx, other_task) in task_names.iter().enumerate() {
283                    if other_task != task_name {
284                        let similarity = relationship_matrix[[task_idx, other_idx]];
285                        if similarity > self.similarity_threshold {
286                            let relationship_penalty = &(current_coef
287                                - &task_coefficients[other_task])
288                                * self.relationship_strength
289                                * similarity;
290                            reg_grad_coef = reg_grad_coef + relationship_penalty;
291                        }
292                    }
293                }
294
295                // Update parameters
296                let new_coef = current_coef - &(&reg_grad_coef * self.learning_rate);
297                let new_intercept = current_intercept - &(&grad_intercept * self.learning_rate);
298
299                task_coefficients.insert(task_name.clone(), new_coef);
300                task_intercepts.insert(task_name.clone(), new_intercept);
301
302                // Add to loss
303                total_loss += residuals.mapv(|x| x * x).sum();
304            }
305
306            // Check convergence
307            if (prev_loss - total_loss).abs() < self.tolerance {
308                n_iter = iteration + 1;
309                break;
310            }
311            prev_loss = total_loss;
312            n_iter = iteration + 1;
313        }
314
315        Ok(TaskRelationshipLearning {
316            state: TaskRelationshipLearningTrained {
317                coefficients: task_coefficients,
318                intercepts: task_intercepts,
319                relationship_matrix,
320                task_names,
321                n_features,
322                task_outputs: self.task_outputs.clone(),
323                relationship_strength: self.relationship_strength,
324                similarity_threshold: self.similarity_threshold,
325                similarity_method: self.similarity_method.clone(),
326                n_iter,
327            },
328            relationship_strength: self.relationship_strength,
329            similarity_threshold: self.similarity_threshold,
330            base_alpha: self.base_alpha,
331            max_iter: self.max_iter,
332            tolerance: self.tolerance,
333            learning_rate: self.learning_rate,
334            task_outputs: self.task_outputs,
335            fit_intercept: self.fit_intercept,
336            similarity_method: self.similarity_method,
337        })
338    }
339}
340
341impl TaskRelationshipLearning<Untrained> {
342    fn compute_task_similarity(
343        &self,
344        y1: &Array2<Float>,
345        y2: &Array2<Float>,
346        method: &TaskSimilarityMethod,
347    ) -> Float {
348        match method {
349            TaskSimilarityMethod::Correlation => {
350                // Compute correlation between task outputs
351                let y1_flat: Vec<Float> = y1.iter().copied().collect();
352                let y2_flat: Vec<Float> = y2.iter().copied().collect();
353
354                if y1_flat.len() != y2_flat.len() {
355                    return 0.0;
356                }
357
358                let mean1: Float = y1_flat.iter().sum::<Float>() / y1_flat.len() as Float;
359                let mean2: Float = y2_flat.iter().sum::<Float>() / y2_flat.len() as Float;
360
361                let mut num = 0.0;
362                let mut den1 = 0.0;
363                let mut den2 = 0.0;
364
365                for (v1, v2) in y1_flat.iter().zip(y2_flat.iter()) {
366                    let d1 = v1 - mean1;
367                    let d2 = v2 - mean2;
368                    num += d1 * d2;
369                    den1 += d1 * d1;
370                    den2 += d2 * d2;
371                }
372
373                if den1 > 0.0 && den2 > 0.0 {
374                    (num / (den1.sqrt() * den2.sqrt())).abs()
375                } else {
376                    0.0
377                }
378            }
379            TaskSimilarityMethod::Cosine => {
380                // Compute cosine similarity
381                let y1_flat: Vec<Float> = y1.iter().copied().collect();
382                let y2_flat: Vec<Float> = y2.iter().copied().collect();
383
384                let dot_product: Float =
385                    y1_flat.iter().zip(y2_flat.iter()).map(|(a, b)| a * b).sum();
386                let norm1: Float = y1_flat.iter().map(|x| x * x).sum::<Float>().sqrt();
387                let norm2: Float = y2_flat.iter().map(|x| x * x).sum::<Float>().sqrt();
388
389                if norm1 > 0.0 && norm2 > 0.0 {
390                    (dot_product / (norm1 * norm2)).abs()
391                } else {
392                    0.0
393                }
394            }
395            TaskSimilarityMethod::Euclidean => {
396                // Compute inverse euclidean distance as similarity
397                let y1_flat: Vec<Float> = y1.iter().copied().collect();
398                let y2_flat: Vec<Float> = y2.iter().copied().collect();
399
400                let distance: Float = y1_flat
401                    .iter()
402                    .zip(y2_flat.iter())
403                    .map(|(a, b)| (a - b) * (a - b))
404                    .sum::<Float>()
405                    .sqrt();
406
407                1.0 / (1.0 + distance)
408            }
409            TaskSimilarityMethod::MutualInformation => {
410                // Simple approximation of mutual information using correlation
411                self.compute_task_similarity(y1, y2, &TaskSimilarityMethod::Correlation)
412            }
413        }
414    }
415}
416
417impl Predict<ArrayView2<'_, Float>, HashMap<String, Array2<Float>>>
418    for TaskRelationshipLearning<TaskRelationshipLearningTrained>
419{
420    fn predict(&self, X: &ArrayView2<'_, Float>) -> SklResult<HashMap<String, Array2<Float>>> {
421        let x = X.to_owned();
422        let (_n_samples, n_features) = x.dim();
423
424        if n_features != self.state.n_features {
425            return Err(SklearsError::InvalidInput(
426                "Number of features doesn't match training data".to_string(),
427            ));
428        }
429
430        let mut predictions = HashMap::new();
431
432        for (task_name, coef) in &self.state.coefficients {
433            let task_predictions = x.dot(coef);
434            let intercept = &self.state.intercepts[task_name];
435            let final_predictions = &task_predictions + intercept;
436            predictions.insert(task_name.clone(), final_predictions);
437        }
438
439        Ok(predictions)
440    }
441}
442
443impl TaskRelationshipLearningTrained {
444    /// Get coefficients for a specific task
445    pub fn task_coefficients(&self, task_name: &str) -> Option<&Array2<Float>> {
446        self.coefficients.get(task_name)
447    }
448
449    /// Get intercepts for a specific task
450    pub fn task_intercepts(&self, task_name: &str) -> Option<&Array1<Float>> {
451        self.intercepts.get(task_name)
452    }
453
454    /// Get the relationship matrix (task similarity scores)
455    pub fn relationship_matrix(&self) -> &Array2<Float> {
456        &self.relationship_matrix
457    }
458
459    /// Get task names in order
460    pub fn task_names(&self) -> &Vec<String> {
461        &self.task_names
462    }
463
464    /// Get similarity score between two tasks
465    pub fn task_similarity(&self, task1: &str, task2: &str) -> Option<Float> {
466        let idx1 = self.task_names.iter().position(|t| t == task1)?;
467        let idx2 = self.task_names.iter().position(|t| t == task2)?;
468        Some(self.relationship_matrix[[idx1, idx2]])
469    }
470
471    /// Get related tasks for a given task (similarity above threshold)
472    pub fn related_tasks(&self, task_name: &str) -> Vec<(&String, Float)> {
473        if let Some(task_idx) = self.task_names.iter().position(|t| t == task_name) {
474            self.task_names
475                .iter()
476                .enumerate()
477                .filter_map(|(other_idx, other_task)| {
478                    if other_idx != task_idx {
479                        let similarity = self.relationship_matrix[[task_idx, other_idx]];
480                        if similarity > self.similarity_threshold {
481                            Some((other_task, similarity))
482                        } else {
483                            None
484                        }
485                    } else {
486                        None
487                    }
488                })
489                .collect()
490        } else {
491            Vec::new()
492        }
493    }
494
495    /// Get number of iterations performed
496    pub fn n_iter(&self) -> usize {
497        self.n_iter
498    }
499}