sklears_multioutput/regularization/
task_relationship.rs1#![allow(non_snake_case)] use 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#[derive(Debug, Clone, PartialEq)]
21pub enum TaskSimilarityMethod {
22 Correlation,
24 Cosine,
26 Euclidean,
28 MutualInformation,
30}
31
32#[derive(Debug, Clone)]
58pub struct TaskRelationshipLearning<S = Untrained> {
59 pub(crate) state: S,
60 pub(crate) relationship_strength: Float,
62 pub(crate) similarity_threshold: Float,
64 pub(crate) base_alpha: Float,
66 pub(crate) max_iter: usize,
68 pub(crate) tolerance: Float,
70 pub(crate) learning_rate: Float,
72 pub(crate) task_outputs: HashMap<String, usize>,
74 pub(crate) fit_intercept: bool,
76 pub(crate) similarity_method: TaskSimilarityMethod,
78}
79
80#[derive(Debug, Clone)]
82pub struct TaskRelationshipLearningTrained {
83 pub(crate) coefficients: HashMap<String, Array2<Float>>,
85 pub(crate) intercepts: HashMap<String, Array1<Float>>,
87 pub(crate) relationship_matrix: Array2<Float>,
89 pub(crate) task_names: Vec<String>,
91 pub(crate) n_features: usize,
93 #[allow(dead_code)]
94 pub(crate) task_outputs: HashMap<String, usize>,
96 #[allow(dead_code)]
97 pub(crate) relationship_strength: Float,
99 pub(crate) similarity_threshold: Float,
100 #[allow(dead_code)]
101 pub(crate) similarity_method: TaskSimilarityMethod,
102 pub(crate) n_iter: usize,
104}
105
106impl TaskRelationshipLearning<Untrained> {
107 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 pub fn relationship_strength(mut self, strength: Float) -> Self {
125 self.relationship_strength = strength;
126 self
127 }
128
129 pub fn similarity_threshold(mut self, threshold: Float) -> Self {
131 self.similarity_threshold = threshold;
132 self
133 }
134
135 pub fn base_alpha(mut self, alpha: Float) -> Self {
137 self.base_alpha = alpha;
138 self
139 }
140
141 pub fn similarity_method(mut self, method: TaskSimilarityMethod) -> Self {
143 self.similarity_method = method;
144 self
145 }
146
147 pub fn max_iter(mut self, max_iter: usize) -> Self {
149 self.max_iter = max_iter;
150 self
151 }
152
153 pub fn tolerance(mut self, tolerance: Float) -> Self {
155 self.tolerance = tolerance;
156 self
157 }
158
159 pub fn learning_rate(mut self, lr: Float) -> Self {
161 self.learning_rate = lr;
162 self
163 }
164
165 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 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 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 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 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 let predictions = x.dot(current_coef);
263 let predictions_with_intercept = &predictions + current_intercept;
264
265 let residuals = &predictions_with_intercept - y_task;
267
268 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 let mut reg_grad_coef = grad_coef.clone();
274
275 let task_idx = task_names
277 .iter()
278 .position(|t| t == task_name)
279 .expect("operation should succeed");
280
281 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 let new_coef = current_coef - &(®_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 total_loss += residuals.mapv(|x| x * x).sum();
304 }
305
306 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 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 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 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 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 pub fn task_coefficients(&self, task_name: &str) -> Option<&Array2<Float>> {
446 self.coefficients.get(task_name)
447 }
448
449 pub fn task_intercepts(&self, task_name: &str) -> Option<&Array1<Float>> {
451 self.intercepts.get(task_name)
452 }
453
454 pub fn relationship_matrix(&self) -> &Array2<Float> {
456 &self.relationship_matrix
457 }
458
459 pub fn task_names(&self) -> &Vec<String> {
461 &self.task_names
462 }
463
464 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 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 pub fn n_iter(&self) -> usize {
497 self.n_iter
498 }
499}