sklears_multioutput/regularization/
task_clustering.rs1#![allow(non_snake_case)] use scirs2_core::ndarray::{Array1, Array2, ArrayView2, Axis};
11use scirs2_core::random::thread_rng;
12use scirs2_core::random::RandNormal;
13use sklears_core::{
14 error::{Result as SklResult, SklearsError},
15 traits::{Estimator, Fit, Predict, Untrained},
16 types::Float,
17};
18use std::collections::HashMap;
19
20#[derive(Debug, Clone)]
49pub struct TaskClusteringRegularization<S = Untrained> {
50 pub(crate) state: S,
51 pub(crate) n_clusters: usize,
53 pub(crate) intra_cluster_alpha: Float,
55 pub(crate) inter_cluster_alpha: Float,
57 pub(crate) max_iter: usize,
59 pub(crate) tolerance: Float,
61 pub(crate) learning_rate: Float,
63 pub(crate) task_outputs: HashMap<String, usize>,
65 pub(crate) fit_intercept: bool,
67 pub(crate) random_state: Option<u64>,
69}
70
71#[derive(Debug, Clone)]
73pub struct TaskClusteringRegressionTrained {
74 pub(crate) coefficients: HashMap<String, Array2<Float>>,
76 pub(crate) intercepts: HashMap<String, Array1<Float>>,
78 pub(crate) task_clusters: HashMap<String, usize>,
80 pub(crate) cluster_centroids: Array2<Float>,
82 pub(crate) n_features: usize,
84 #[allow(dead_code)]
85 pub(crate) task_outputs: HashMap<String, usize>,
87 #[allow(dead_code)]
88 pub(crate) n_clusters: usize,
90 #[allow(dead_code)]
91 pub(crate) intra_cluster_alpha: Float,
92 #[allow(dead_code)]
93 pub(crate) inter_cluster_alpha: Float,
94 pub(crate) n_iter: usize,
96}
97
98impl TaskClusteringRegularization<Untrained> {
99 pub fn new() -> Self {
101 Self {
102 state: Untrained,
103 n_clusters: 2,
104 intra_cluster_alpha: 1.0,
105 inter_cluster_alpha: 0.1,
106 max_iter: 1000,
107 tolerance: 1e-4,
108 learning_rate: 0.01,
109 task_outputs: HashMap::new(),
110 fit_intercept: true,
111 random_state: None,
112 }
113 }
114
115 pub fn n_clusters(mut self, n_clusters: usize) -> Self {
117 self.n_clusters = n_clusters;
118 self
119 }
120
121 pub fn intra_cluster_alpha(mut self, alpha: Float) -> Self {
123 self.intra_cluster_alpha = alpha;
124 self
125 }
126
127 pub fn inter_cluster_alpha(mut self, alpha: Float) -> Self {
129 self.inter_cluster_alpha = alpha;
130 self
131 }
132
133 pub fn max_iter(mut self, max_iter: usize) -> Self {
135 self.max_iter = max_iter;
136 self
137 }
138
139 pub fn tolerance(mut self, tolerance: Float) -> Self {
141 self.tolerance = tolerance;
142 self
143 }
144
145 pub fn learning_rate(mut self, lr: Float) -> Self {
147 self.learning_rate = lr;
148 self
149 }
150
151 pub fn random_state(mut self, seed: u64) -> Self {
153 self.random_state = Some(seed);
154 self
155 }
156
157 pub fn task_outputs(mut self, outputs: &[(&str, usize)]) -> Self {
159 self.task_outputs = outputs
160 .iter()
161 .map(|(name, size)| (name.to_string(), *size))
162 .collect();
163 self
164 }
165}
166
167impl Default for TaskClusteringRegularization<Untrained> {
168 fn default() -> Self {
169 Self::new()
170 }
171}
172
173impl Estimator for TaskClusteringRegularization<Untrained> {
174 type Config = ();
175 type Error = SklearsError;
176 type Float = Float;
177
178 fn config(&self) -> &Self::Config {
179 &()
180 }
181}
182
183impl Fit<ArrayView2<'_, Float>, HashMap<String, Array2<Float>>>
184 for TaskClusteringRegularization<Untrained>
185{
186 type Fitted = TaskClusteringRegularization<TaskClusteringRegressionTrained>;
187
188 fn fit(
189 self,
190 X: &ArrayView2<'_, Float>,
191 y: &HashMap<String, Array2<Float>>,
192 ) -> SklResult<Self::Fitted> {
193 let x = X.to_owned();
194 let (n_samples, n_features) = x.dim();
195
196 if n_samples == 0 || n_features == 0 {
197 return Err(SklearsError::InvalidInput("Empty input data".to_string()));
198 }
199
200 if self.n_clusters == 0 {
201 return Err(SklearsError::InvalidInput(
202 "Number of clusters must be > 0".to_string(),
203 ));
204 }
205
206 let mut task_coefficients: HashMap<String, Array2<Float>> = HashMap::new();
208 let mut task_intercepts: HashMap<String, Array1<Float>> = HashMap::new();
209
210 let mut rng_gen = thread_rng();
211
212 for (task_name, y_task) in y {
213 let n_outputs = y_task.ncols();
214 let mut coef = Array2::<Float>::zeros((n_features, n_outputs));
215 let normal_dist = RandNormal::new(0.0, 0.1).expect("operation should succeed");
216 for i in 0..n_features {
217 for j in 0..n_outputs {
218 coef[[i, j]] = rng_gen.sample(normal_dist);
219 }
220 }
221 let intercept = Array1::<Float>::zeros(n_outputs);
222 task_coefficients.insert(task_name.clone(), coef);
223 task_intercepts.insert(task_name.clone(), intercept);
224 }
225
226 let task_names: Vec<String> = y.keys().cloned().collect();
228 let _n_tasks = task_names.len();
229
230 let mut task_vectors = Vec::new();
232 for task_name in &task_names {
233 let coef = &task_coefficients[task_name];
234 let flattened: Vec<Float> = coef.iter().copied().collect();
235 task_vectors.push(flattened);
236 }
237
238 let mut task_clusters: HashMap<String, usize> = HashMap::new();
240 let cluster_centroids =
241 Array2::<Float>::zeros((self.n_clusters, n_features * y[&task_names[0]].ncols()));
242
243 for (i, task_name) in task_names.iter().enumerate() {
245 task_clusters.insert(task_name.clone(), i % self.n_clusters);
246 }
247
248 let mut prev_loss = Float::INFINITY;
250 let mut n_iter = 0;
251
252 for iteration in 0..self.max_iter {
253 let mut total_loss = 0.0;
254
255 for (task_name, y_task) in y {
257 let task_cluster = task_clusters[task_name];
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 mut cluster_center: Array2<Float> = Array2::<Float>::zeros(current_coef.dim());
277 let mut cluster_count = 0;
278
279 for (other_task, other_cluster) in &task_clusters {
280 if *other_cluster == task_cluster && other_task != task_name {
281 cluster_center = &cluster_center + &task_coefficients[other_task];
282 cluster_count += 1;
283 }
284 }
285
286 if cluster_count > 0 {
287 cluster_center /= cluster_count as Float;
288 let intra_penalty =
289 &(current_coef - &cluster_center) * self.intra_cluster_alpha;
290 reg_grad_coef = reg_grad_coef + intra_penalty;
291 }
292
293 for (other_task, other_cluster) in &task_clusters {
295 if *other_cluster != task_cluster {
296 let inter_penalty = &(current_coef - &task_coefficients[other_task])
297 * self.inter_cluster_alpha
298 * 0.1;
299 reg_grad_coef = reg_grad_coef + inter_penalty;
300 }
301 }
302
303 let new_coef = current_coef - &(®_grad_coef * self.learning_rate);
305 let new_intercept = current_intercept - &(&grad_intercept * self.learning_rate);
306
307 task_coefficients.insert(task_name.clone(), new_coef);
308 task_intercepts.insert(task_name.clone(), new_intercept);
309
310 total_loss += residuals.mapv(|x| x * x).sum();
312 }
313
314 if (prev_loss - total_loss).abs() < self.tolerance {
316 n_iter = iteration + 1;
317 break;
318 }
319 prev_loss = total_loss;
320 n_iter = iteration + 1;
321 }
322
323 Ok(TaskClusteringRegularization {
324 state: TaskClusteringRegressionTrained {
325 coefficients: task_coefficients,
326 intercepts: task_intercepts,
327 task_clusters,
328 cluster_centroids,
329 n_features,
330 task_outputs: self.task_outputs.clone(),
331 n_clusters: self.n_clusters,
332 intra_cluster_alpha: self.intra_cluster_alpha,
333 inter_cluster_alpha: self.inter_cluster_alpha,
334 n_iter,
335 },
336 n_clusters: self.n_clusters,
337 intra_cluster_alpha: self.intra_cluster_alpha,
338 inter_cluster_alpha: self.inter_cluster_alpha,
339 max_iter: self.max_iter,
340 tolerance: self.tolerance,
341 learning_rate: self.learning_rate,
342 task_outputs: self.task_outputs,
343 fit_intercept: self.fit_intercept,
344 random_state: self.random_state,
345 })
346 }
347}
348
349impl Predict<ArrayView2<'_, Float>, HashMap<String, Array2<Float>>>
350 for TaskClusteringRegularization<TaskClusteringRegressionTrained>
351{
352 fn predict(&self, X: &ArrayView2<'_, Float>) -> SklResult<HashMap<String, Array2<Float>>> {
353 let x = X.to_owned();
354 let (_n_samples, n_features) = x.dim();
355
356 if n_features != self.state.n_features {
357 return Err(SklearsError::InvalidInput(
358 "Number of features doesn't match training data".to_string(),
359 ));
360 }
361
362 let mut predictions = HashMap::new();
363
364 for (task_name, coef) in &self.state.coefficients {
365 let task_predictions = x.dot(coef);
366 let intercept = &self.state.intercepts[task_name];
367 let final_predictions = &task_predictions + intercept;
368 predictions.insert(task_name.clone(), final_predictions);
369 }
370
371 Ok(predictions)
372 }
373}
374
375impl TaskClusteringRegressionTrained {
376 pub fn task_coefficients(&self, task_name: &str) -> Option<&Array2<Float>> {
378 self.coefficients.get(task_name)
379 }
380
381 pub fn task_intercepts(&self, task_name: &str) -> Option<&Array1<Float>> {
383 self.intercepts.get(task_name)
384 }
385
386 pub fn task_cluster(&self, task_name: &str) -> Option<usize> {
388 self.task_clusters.get(task_name).copied()
389 }
390
391 pub fn task_clusters(&self) -> &HashMap<String, usize> {
393 &self.task_clusters
394 }
395
396 pub fn cluster_centroids(&self) -> &Array2<Float> {
398 &self.cluster_centroids
399 }
400
401 pub fn n_iter(&self) -> usize {
403 self.n_iter
404 }
405
406 pub fn cluster_tasks(&self, cluster_id: usize) -> Vec<&String> {
408 self.task_clusters
409 .iter()
410 .filter_map(|(task_name, &cluster)| {
411 if cluster == cluster_id {
412 Some(task_name)
413 } else {
414 None
415 }
416 })
417 .collect()
418 }
419}