1use crate::clustering::{Clusterings, WorkingClustering};
2use crate::log2cache::Log2Cache;
3use crate::loss::*;
4use crate::*;
5use dahl_partition::*;
6use ndarray::{Array2, Array3, Axis};
7use rand::distr::{Distribution, Uniform};
8use rand::seq::SliceRandom;
9use rand::{Rng, SeedableRng};
10use rand_pcg::Pcg64Mcg;
11use std::sync::mpsc;
12use std::time::Instant;
13
14pub trait CMLossComputer {
19 #[allow(unused_variables)]
20 fn initialize(&mut self, state: &WorkingClustering, cms: &Array3<CountType>) {}
21
22 fn compute_loss(&self, state: &WorkingClustering, cms: &Array3<CountType>) -> f64;
23
24 fn change_in_loss(
25 &self,
26 item_index: usize,
27 to_label: LabelType,
28 from_label_option: Option<LabelType>,
29 state: &WorkingClustering,
30 cms: &Array3<CountType>,
31 draws: &Clusterings,
32 ) -> f64;
33
34 #[allow(unused_variables)]
35 fn decision_callback(
36 &mut self,
37 item_index: usize,
38 to_label_option: Option<LabelType>,
39 from_label_option: Option<LabelType>,
40 state: &WorkingClustering,
41 cms: &Array3<CountType>,
42 draws: &Clusterings,
43 ) {
44 }
45}
46
47pub struct BinderCMLossComputer {
50 a: f64,
51}
52
53impl BinderCMLossComputer {
54 pub fn new(a: f64) -> Self {
55 Self { a }
56 }
57
58 pub fn n_squared(x: CountType) -> f64 {
59 let x = x as f64;
60 x * x
61 }
62}
63
64impl CMLossComputer for BinderCMLossComputer {
65 fn compute_loss(&self, state: &WorkingClustering, cms: &Array3<CountType>) -> f64 {
66 let n_draws = cms.len_of(Axis(2));
67 let sum2 = state
68 .occupied_clusters()
69 .iter()
70 .map(|i| BinderCMLossComputer::n_squared(state.size_of(*i)))
71 .sum::<f64>()
72 * n_draws as f64;
73 let mut sum1 = 0.0;
74 let mut sum3 = 0.0;
75 for draw_index in 0..n_draws {
76 for other_index in 0..cms.len_of(Axis(1)) {
77 let n = cms[(0, other_index, draw_index)];
78 if n > 0 {
79 sum1 += BinderCMLossComputer::n_squared(cms[(0, other_index, draw_index)]);
80 for main_label in state.occupied_clusters().iter() {
81 sum3 += BinderCMLossComputer::n_squared(
82 cms[(*main_label as usize + 1, other_index, draw_index)],
83 );
84 }
85 }
86 }
87 }
88 (self.a * sum1 + (2.0 - self.a) * sum2 - 2.0 * sum3)
89 / (n_draws as f64 * BinderCMLossComputer::n_squared(state.n_items()))
90 }
91
92 fn change_in_loss(
93 &self,
94 item_index: usize,
95 to_label: LabelType,
96 from_label_option: Option<LabelType>,
97 state: &WorkingClustering,
98 cms: &Array3<CountType>,
99 draws: &Clusterings,
100 ) -> f64 {
101 let offset = if from_label_option.is_some() && to_label == from_label_option.unwrap() {
102 1
103 } else {
104 0
105 };
106 let n_draws = cms.len_of(Axis(2));
107 let sum2 = (n_draws as f64) * ((state.size_of(to_label) - offset) as f64);
108 let to_index = to_label as usize + 1;
109 let mut sum3 = 0.0;
110 for draw_index in 0..n_draws {
111 let other_index = draws.label(draw_index, item_index) as usize;
112 sum3 += (cms[(to_index, other_index, draw_index)] - offset) as f64;
113 }
114 (2.0 - self.a) * sum2 - 2.0 * sum3
115 }
116}
117
118pub struct OMARICMLossComputer {
121 n: CountType,
122 sum: f64,
123 sums: Array2<f64>,
124}
125
126impl OMARICMLossComputer {
127 pub fn new(n_draws: usize) -> Self {
128 Self {
129 n: 0,
130 sum: 0.0,
131 sums: Array2::<f64>::zeros((n_draws, 2)),
132 }
133 }
134
135 pub fn n_choose_2_times_2(x: CountType) -> f64 {
136 let x = x as f64;
137 x * (x - 1.0)
138 }
139}
140
141impl CMLossComputer for OMARICMLossComputer {
142 fn initialize(&mut self, state: &WorkingClustering, cms: &Array3<CountType>) {
143 self.n = state.n_items();
144 self.sum = state
145 .occupied_clusters()
146 .iter()
147 .map(|i| OMARICMLossComputer::n_choose_2_times_2(state.size_of(*i)))
148 .sum();
149 let n_draws = cms.len_of(Axis(2));
150 for draw_index in 0..n_draws {
151 for other_index in 0..cms.len_of(Axis(1)) {
152 let n = cms[(0, other_index, draw_index)];
153 if n > 0 {
154 self.sums[(draw_index, 0)] += OMARICMLossComputer::n_choose_2_times_2(n);
155 for main_label in state.occupied_clusters().iter() {
156 self.sums[(draw_index, 1)] += OMARICMLossComputer::n_choose_2_times_2(
157 cms[(*main_label as usize + 1, other_index, draw_index)],
158 );
159 }
160 }
161 }
162 }
163 }
164
165 fn compute_loss(&self, _state: &WorkingClustering, _cms: &Array3<CountType>) -> f64 {
166 let mut sum = 0.0;
167 let sum2 = self.sum;
168 let sum2_div_denom = sum2 / OMARICMLossComputer::n_choose_2_times_2(self.n);
169 let n_draws = self.sums.len_of(Axis(0));
170 for draw_index in 0..n_draws {
171 let sum1 = self.sums[(draw_index, 0)];
172 let offset = sum1 * sum2_div_denom;
173 let denominator = 0.5 * (sum1 + sum2) - offset;
174 if denominator > 0.0 {
175 let numerator = self.sums[(draw_index, 1)] - offset;
176 sum += numerator / denominator;
177 }
178 }
179 1.0 - sum / (n_draws as f64)
180 }
181
182 fn change_in_loss(
183 &self,
184 item_index: usize,
185 to_label: LabelType,
186 from_label_option: Option<LabelType>,
187 state: &WorkingClustering,
188 cms: &Array3<CountType>,
189 draws: &Clusterings,
190 ) -> f64 {
191 let offset = if from_label_option.is_some() && to_label == from_label_option.unwrap() {
192 1
193 } else {
194 0
195 };
196 let mut sum2 = if offset == 0 {
197 self.sum + 2.0 * state.size_of(to_label) as f64
198 } else {
199 self.sum
200 };
201 let to_index = to_label as usize + 1;
202 let mut n = self.n;
203 let from_index = match from_label_option {
204 Some(from_label) => {
205 if offset == 0 {
206 sum2 -= 2.0 * (state.size_of(from_label) - 1) as f64;
207 }
208 from_label as usize + 1
209 }
210 None => {
211 n += 1;
212 0
213 }
214 };
215 let sum2_div_denom = sum2 / OMARICMLossComputer::n_choose_2_times_2(n);
216 let mut sum = 0.0;
217 let n_draws = self.sums.len_of(Axis(0));
218 for draw_index in 0..n_draws {
219 let other_index = draws.label(draw_index, item_index) as usize;
220 let sum1 = self.sums[(draw_index, 0)]
221 + if from_label_option.is_none() {
222 2.0 * n as f64
223 } else {
224 0.0
225 };
226 let chance_adjustment = sum1 * sum2_div_denom;
227 let denominator = 0.5 * (sum1 + sum2) - chance_adjustment;
228 if denominator > 0.0 {
229 let mut unadjusted_numerator = self.sums[(draw_index, 1)];
230 if offset == 0 {
231 unadjusted_numerator += 2.0 * cms[(to_index, other_index, draw_index)] as f64;
232 if from_label_option.is_some() {
233 unadjusted_numerator -=
234 2.0 * (cms[(from_index, other_index, draw_index)] - 1) as f64;
235 }
236 }
237 let numerator = unadjusted_numerator - chance_adjustment;
238 sum += numerator / denominator;
239 }
240 }
241 -sum
242 }
243
244 fn decision_callback(
245 &mut self,
246 item_index: usize,
247 to_label_option: Option<LabelType>,
248 from_label_option: Option<LabelType>,
249 state: &WorkingClustering,
250 cms: &Array3<CountType>,
251 draws: &Clusterings,
252 ) {
253 let to_index = match to_label_option {
254 Some(to_label) => {
255 self.sum += 2.0 * state.size_of(to_label) as f64;
256 to_label as usize + 1
257 }
258 None => {
259 self.n -= 1;
260 0
261 }
262 };
263 let from_index = match from_label_option {
264 Some(from_label) => {
265 self.sum -= 2.0 * (state.size_of(from_label) - 1) as f64;
266 from_label as usize + 1
267 }
268 None => {
269 self.n += 1;
270 0
271 }
272 };
273 let n_draws = draws.n_clusterings();
274 for draw_index in 0..n_draws {
275 let other_index = draws.label(draw_index, item_index) as usize;
276 if from_label_option.is_some() {
277 self.sums[(draw_index, 1)] -=
278 2.0 * (cms[(from_index, other_index, draw_index)] - 1) as f64;
279 } else {
280 self.sums[(draw_index, 0)] += 2.0 * cms[(0, other_index, draw_index)] as f64;
281 }
282 if to_label_option.is_some() {
283 self.sums[(draw_index, 1)] += 2.0 * cms[(to_index, other_index, draw_index)] as f64;
284 } else {
285 self.sums[(draw_index, 0)] -= 2.0 * (cms[(0, other_index, draw_index)] - 1) as f64;
286 }
287 }
288 }
289}
290
291pub struct VICMLossComputer<'a> {
294 a: f64,
295 cache: &'a Log2Cache,
296}
297
298impl<'a> VICMLossComputer<'a> {
299 pub fn new(a: f64, cache: &'a Log2Cache) -> Self {
300 Self { a, cache }
301 }
302}
303
304impl CMLossComputer for VICMLossComputer<'_> {
305 fn compute_loss(&self, state: &WorkingClustering, cms: &Array3<CountType>) -> f64 {
306 let sum2: f64 = state
307 .occupied_clusters()
308 .iter()
309 .map(|i| self.cache.nlog2n(state.size_of(*i)))
310 .sum();
311 let n_draws = cms.len_of(Axis(2));
312 let mut sum = 0.0;
313 for draw_index in 0..n_draws {
314 let mut sum1 = 0.0;
315 let mut sum3 = 0.0;
316 for other_index in 0..cms.len_of(Axis(1)) {
317 let n = cms[(0, other_index, draw_index)];
318 if n > 0 {
319 sum1 += self.cache.nlog2n(cms[(0, other_index, draw_index)]);
320 for main_label in state.occupied_clusters().iter() {
321 sum3 += self
322 .cache
323 .nlog2n(cms[(*main_label as usize + 1, other_index, draw_index)]);
324 }
325 }
326 }
327 sum += self.a * sum1 - 2.0 * sum3;
328 }
329 ((2.0 - self.a) * sum2 + (sum / (n_draws as f64))) / (state.n_items() as f64)
330 }
331
332 fn change_in_loss(
333 &self,
334 item_index: usize,
335 to_label: LabelType,
336 from_label_option: Option<LabelType>,
337 state: &WorkingClustering,
338 cms: &Array3<CountType>,
339 draws: &Clusterings,
340 ) -> f64 {
341 let offset = if from_label_option.is_some() && to_label == from_label_option.unwrap() {
342 1
343 } else {
344 0
345 };
346 let n_draws = cms.len_of(Axis(2));
347 assert_eq!(n_draws, draws.n_clusterings(), "cms and draws disagree");
348 let sum2 = (n_draws as f64)
349 * self
350 .cache
351 .nlog2n_difference(state.size_of(to_label) - offset);
352 let to_index = to_label as usize + 1;
353 let mut sum3 = 0.0;
354 for (draw_index, other_index) in (0..n_draws).zip(draws.labels_of_item(item_index)) {
356 sum3 += self
357 .cache
358 .nlog2n_difference(cms[(to_index, *other_index as usize, draw_index)] - offset);
359 }
360 (2.0 - self.a) * sum2 - 2.0 * sum3
361 }
362}
363
364pub struct GeneralInformationBasedCMLossComputer<'a, T>
367where
368 T: InformationBasedLoss,
369{
370 cache: &'a Log2Cache,
371 n: CountType,
372 sum: f64,
373 sums: Array2<f64>,
374 ibloss: T,
375}
376
377impl<'a, T> GeneralInformationBasedCMLossComputer<'a, T>
378where
379 T: InformationBasedLoss,
380{
381 pub fn new(n_draws: usize, cache: &'a Log2Cache, ibloss: T) -> Self {
382 Self {
383 cache,
384 n: 0,
385 sum: 0.0,
386 sums: Array2::<f64>::zeros((n_draws, 2)),
387 ibloss,
388 }
389 }
390}
391
392impl<T> CMLossComputer for GeneralInformationBasedCMLossComputer<'_, T>
393where
394 T: InformationBasedLoss,
395{
396 fn initialize(&mut self, state: &WorkingClustering, cms: &Array3<CountType>) {
397 self.n = state.n_items();
398 self.sum = state
399 .occupied_clusters()
400 .iter()
401 .map(|i| self.cache.nlog2n(state.size_of(*i)))
402 .sum();
403 let n_draws = cms.len_of(Axis(2));
404 for draw_index in 0..n_draws {
405 for other_index in 0..cms.len_of(Axis(1)) {
406 let n = cms[(0, other_index, draw_index)];
407 if n > 0 {
408 self.sums[(draw_index, 0)] +=
409 self.cache.nlog2n(cms[(0, other_index, draw_index)]);
410 for main_label in state.occupied_clusters().iter() {
411 self.sums[(draw_index, 1)] += self
412 .cache
413 .nlog2n(cms[(*main_label as usize + 1, other_index, draw_index)]);
414 }
415 }
416 }
417 }
418 }
419
420 fn compute_loss(&self, _state: &WorkingClustering, _cms: &Array3<CountType>) -> f64 {
421 let ni = self.n as f64;
422 let nlog2n = ni * ni.log2();
423 let u = self.sum;
424 let mut sum = 0.0;
425 let n_draws = self.sums.len_of(Axis(0));
426 for draw_index in 0..n_draws {
427 let v = self.sums[(draw_index, 0)];
428 let uv = self.sums[(draw_index, 1)];
429 sum += self.ibloss.compute_kernel(u, v, uv, nlog2n);
430 }
431 self.ibloss.finalize(sum, n_draws as f64, ni)
432 }
433
434 fn change_in_loss(
435 &self,
436 item_index: usize,
437 to_label: LabelType,
438 from_label_option: Option<LabelType>,
439 state: &WorkingClustering,
440 cms: &Array3<CountType>,
441 draws: &Clusterings,
442 ) -> f64 {
443 let offset = if from_label_option.is_some() && to_label == from_label_option.unwrap() {
444 1
445 } else {
446 0
447 };
448 let mut v = if offset == 0 {
449 self.sum + self.cache.nlog2n_difference(state.size_of(to_label))
450 } else {
451 self.sum
452 };
453 let to_index = to_label as usize + 1;
454 let (n, from_index) = match from_label_option {
455 Some(from_label) => {
456 if offset == 0 {
457 v -= self.cache.nlog2n_difference(state.size_of(from_label) - 1);
458 }
459 (self.n, from_label as usize + 1)
460 }
461 None => (self.n + 1, 0),
462 };
463 let ni = n as f64;
464 let nlog2n = ni * ni.log2();
465 let mut sum = 0.0;
466 let n_draws = self.sums.len_of(Axis(0));
467 for draw_index in 0..n_draws {
468 let other_index = draws.label(draw_index, item_index) as usize;
469 let u = self.sums[(draw_index, 0)]
470 + if from_label_option.is_none() {
471 self.cache
472 .nlog2n_difference(cms[(0, other_index, draw_index)])
473 } else {
474 0.0
475 };
476 let mut uv = self.sums[(draw_index, 1)];
477 if offset == 0 {
478 uv += self
479 .cache
480 .nlog2n_difference(cms[(to_index, other_index, draw_index)]);
481 if from_label_option.is_some() {
482 uv -= self
483 .cache
484 .nlog2n_difference(cms[(from_index, other_index, draw_index)] - 1)
485 }
486 }
487 sum += self.ibloss.compute_kernel(u, v, uv, nlog2n);
488 }
489 self.ibloss.score(sum)
490 }
491
492 fn decision_callback(
493 &mut self,
494 item_index: usize,
495 to_label_option: Option<LabelType>,
496 from_label_option: Option<LabelType>,
497 state: &WorkingClustering,
498 cms: &Array3<CountType>,
499 draws: &Clusterings,
500 ) {
501 let to_index = match to_label_option {
502 Some(to_label) => {
503 self.sum += self.cache.nlog2n_difference(state.size_of(to_label));
504 to_label as usize + 1
505 }
506 None => {
507 self.n -= 1;
508 0
509 }
510 };
511 let from_index = match from_label_option {
512 Some(from_label) => {
513 self.sum -= self.cache.nlog2n_difference(state.size_of(from_label) - 1);
514 from_label as usize + 1
515 }
516 None => {
517 self.n += 1;
518 0
519 }
520 };
521 let n_draws = draws.n_clusterings();
522 for draw_index in 0..n_draws {
523 let other_index = draws.label(draw_index, item_index) as usize;
524 if from_label_option.is_some() {
525 self.sums[(draw_index, 1)] -= self
526 .cache
527 .nlog2n_difference(cms[(from_index, other_index, draw_index)] - 1);
528 } else {
529 self.sums[(draw_index, 0)] += self
530 .cache
531 .nlog2n_difference(cms[(0, other_index, draw_index)]);
532 }
533 if to_label_option.is_some() {
534 self.sums[(draw_index, 1)] += self
535 .cache
536 .nlog2n_difference(cms[(to_index, other_index, draw_index)]);
537 } else {
538 self.sums[(draw_index, 0)] -= self
539 .cache
540 .nlog2n_difference(cms[(0, other_index, draw_index)] - 1);
541 }
542 }
543 }
544}
545
546pub trait InformationBasedLoss {
547 fn compute_kernel(&self, u: f64, v: f64, uv: f64, nlog2n: f64) -> f64;
548 fn finalize(&self, sum: f64, n_draws: f64, n_items: f64) -> f64;
549 fn score(&self, sum: f64) -> f64;
550}
551
552pub struct NVIInformationBasedLoss;
555
556impl InformationBasedLoss for NVIInformationBasedLoss {
557 fn compute_kernel(&self, u: f64, v: f64, uv: f64, nlog2n: f64) -> f64 {
558 if u == uv && v == uv {
560 return 0.0;
561 }
562 (u + v - 2.0 * uv) / (nlog2n - uv)
563 }
564 fn finalize(&self, sum: f64, n_draws: f64, _n_items: f64) -> f64 {
565 sum / n_draws
566 }
567 fn score(&self, sum: f64) -> f64 {
568 sum
569 }
570}
571
572pub struct IDInformationBasedLoss;
575
576impl InformationBasedLoss for IDInformationBasedLoss {
577 fn compute_kernel(&self, u: f64, v: f64, uv: f64, _nlog2n: f64) -> f64 {
578 u + v - uv - u.min(v)
579 }
580 fn finalize(&self, sum: f64, n_draws: f64, n_items: f64) -> f64 {
581 sum / (n_draws * n_items)
582 }
583 fn score(&self, sum: f64) -> f64 {
584 sum
585 }
586}
587
588pub struct NIDInformationBasedLoss;
591
592impl InformationBasedLoss for NIDInformationBasedLoss {
593 fn compute_kernel(&self, u: f64, v: f64, uv: f64, nlog2n: f64) -> f64 {
594 if u == uv && v == uv {
597 return 1.0;
598 }
599 (nlog2n + uv - u - v) / (nlog2n - u.min(v))
600 }
601 fn finalize(&self, sum: f64, n_draws: f64, _n_items: f64) -> f64 {
602 1.0 - sum / n_draws
603 }
604 fn score(&self, sum: f64) -> f64 {
605 -sum
606 }
607}
608
609fn find_label_of_minimum<I: Iterator<Item = (LabelType, f64)>>(pairs: I) -> LabelType {
612 let mut s0 = f64::INFINITY;
613 let mut l0 = 0;
614 for pair in pairs {
615 if pair.1 < s0 {
616 s0 = pair.1;
617 l0 = pair.0;
618 }
619 }
620 l0
621}
622
623fn allocation_scan<T: CMLossComputer>(
624 sweetening_scan: bool,
625 singletons_initialization: bool,
626 state: &mut WorkingClustering,
627 cms: &mut Array3<CountType>,
628 permutation: &[usize],
629 loss_computer: &mut T,
630 draws: &Clusterings,
631) -> bool {
632 let mut state_changed = false;
633 let mut chosen_label_option = None;
634 for item_index in permutation {
635 let item_index = *item_index;
636 let label_of_empty_cluster = state.label_of_empty_cluster();
637 let from_label_option = match sweetening_scan {
638 true => Some(state.get(item_index)),
639 false => None,
640 };
641 let to_label = match label_of_empty_cluster {
642 Some(loec) if !sweetening_scan && singletons_initialization => loec,
643 Some(_) | None => {
644 let iter = state
645 .occupied_clusters()
646 .iter()
647 .chain(label_of_empty_cluster.iter())
648 .map(|to_label| {
649 (
650 *to_label,
651 loss_computer.change_in_loss(
652 item_index,
653 *to_label,
654 from_label_option,
655 state,
656 cms,
657 draws,
658 ),
659 )
660 });
661 find_label_of_minimum(iter)
662 }
663 };
664 if !sweetening_scan {
665 state.assign(item_index, to_label, loss_computer, cms, draws);
666 if chosen_label_option.is_none() {
667 chosen_label_option = Some(to_label);
668 state_changed = state.size_of(to_label) != 1;
669 }
670 if to_label != chosen_label_option.unwrap() {
671 state_changed = true;
672 }
673 } else if to_label != from_label_option.unwrap() {
674 state.reassign(item_index, to_label, loss_computer, cms, draws);
675 state_changed = true;
676 }
677 }
678 state_changed
679}
680
681#[allow(clippy::too_many_arguments)]
682fn sweetening_scans<T: CMLossComputer, U: Rng>(
683 state: &mut WorkingClustering,
684 cms: &mut Array3<CountType>,
685 permutation: &mut [usize],
686 loss_computer: &mut T,
687 scan_counter: &mut u32,
688 draws: &Clusterings,
689 p: &SALSOParameters,
690 rng: &mut U,
691) -> bool {
692 let mut global_state_changed = false;
693 let mut state_changed = true;
694 while state_changed && *scan_counter < p.max_scans {
695 *scan_counter += 1;
696 permutation.shuffle(rng);
697 state_changed = allocation_scan(true, false, state, cms, permutation, loss_computer, draws);
698 global_state_changed = global_state_changed || state_changed;
699 }
700 global_state_changed
701}
702
703pub fn minimize_once_by_salso_v2<'a, T: CMLossComputer, U: Rng>(
704 loss_computer_factory: Box<dyn Fn() -> T + 'a>,
705 draws: &Clusterings,
706 p: &SALSOParameters,
707 seconds_target: f64,
708 rng: &mut U,
709) -> SALSOResults {
710 let n_items = draws.n_items();
711 let max_size = match p.max_size {
712 0 => draws.max_clusters(),
713 _ => p.max_size,
714 };
715 let mut permutation: Vec<usize> = (0..p.n_items).collect();
716 let mut best = SALSOResults::dummy(max_size);
717 let start_time = Instant::now();
718 let uniform_0_1 = Uniform::new(0.0, 1.0).unwrap();
719 for run_counter in 1..=p.n_runs {
720 let mut loss_computer = loss_computer_factory();
721 let singletons_initialization = uniform_0_1.sample(rng) < p.prob_singletons_initialization;
722 let (mut state, mut cms, initialization_method) =
723 if uniform_0_1.sample(rng) < p.prob_sequential_allocation {
724 let mut state = WorkingClustering::empty(n_items, max_size);
725 let mut cms = Array3::<CountType>::zeros((
726 max_size as usize + 1,
727 draws.max_clusters() as usize,
728 draws.n_clusterings(),
729 ));
730 permutation.shuffle(rng);
732 allocation_scan(
733 false,
734 singletons_initialization,
735 &mut state,
736 &mut cms,
737 &permutation,
738 &mut loss_computer,
739 draws,
740 );
741 let initialization_method = if singletons_initialization {
742 InitializationMethod::SequentialFromSingletons
743 } else {
744 InitializationMethod::SequentialFromEmpty
745 };
746 (state, cms, initialization_method)
747 } else {
748 let state = if p.max_size_as_rf {
749 WorkingClustering::random_as_rf(n_items, max_size, draws.max_clusters(), rng)
750 } else {
751 WorkingClustering::random(n_items, max_size, rng)
752 };
753 let cms = draws.make_confusion_matrices(&state);
754 loss_computer.initialize(&state, &cms);
755 (
756 state,
757 cms,
758 InitializationMethod::SampleOne2MaxWithReplacement,
759 )
760 };
761 let mut scan_counter = 0;
762 sweetening_scans(
763 &mut state,
764 &mut cms,
765 &mut permutation,
766 &mut loss_computer,
767 &mut scan_counter,
768 draws,
769 p,
770 rng,
771 );
772 let mut expected_loss = loss_computer.compute_loss(&state, &cms);
773 let (mut n_zealous_accepts, mut n_zealous_attempts) = (0, 0);
774 if n_zealous_attempts < p.max_zealous_updates {
775 let labels = {
776 let mut x = state.occupied_clusters().clone();
777 x.shuffle(rng);
778 x
779 };
780 for label in labels {
781 let s = state.size_of(label) as usize;
782 if s <= 1 {
783 continue;
785 }
786 n_zealous_attempts += 1;
787 if n_zealous_attempts > p.max_zealous_updates {
788 break;
789 }
790 let mut active_items = Vec::with_capacity(s);
791 for item_index in 0..(state.n_items() as usize) {
792 if state.get(item_index) == label {
793 state.remove(item_index, &mut loss_computer, &mut cms, draws);
794 active_items.push(item_index)
795 }
796 if active_items.len() == s {
797 break;
798 }
799 }
800 active_items.shuffle(rng);
801 let state_changed = allocation_scan(
802 false,
803 singletons_initialization,
804 &mut state,
805 &mut cms,
806 &active_items,
807 &mut loss_computer,
808 draws,
809 );
810 if state_changed {
811 let labels_for_undo = state.clone_labels();
812 let state_changed = sweetening_scans(
813 &mut state,
814 &mut cms,
815 &mut permutation,
816 &mut loss_computer,
817 &mut scan_counter,
818 draws,
819 p,
820 rng,
821 );
822 let expected_loss_of_candidate = loss_computer.compute_loss(&state, &cms);
823 if expected_loss_of_candidate < expected_loss {
824 expected_loss = expected_loss_of_candidate;
826 n_zealous_accepts += 1;
827 } else {
828 if state_changed {
830 for (item_index, new_label) in labels_for_undo.iter().enumerate() {
831 state.reassign(
832 item_index,
833 *new_label,
834 &mut loss_computer,
835 &mut cms,
836 draws,
837 )
838 }
839 }
840 for item_index in active_items {
841 state.reassign(item_index, label, &mut loss_computer, &mut cms, draws)
842 }
843 }
844 }
845 }
846 }
847 if expected_loss < best.expected_loss {
849 let clustering = state.standardize().iter().map(|x| *x as usize).collect();
850 best = SALSOResults {
851 clustering,
852 expected_loss,
853 n_scans: scan_counter,
854 n_zealous_accepts,
855 n_zealous_attempts,
856 initialization_method,
857 max_size: state.max_clusters(),
858 ..best
859 }
860 }
861 if seconds_target.is_finite() {
862 let seconds = start_time.elapsed().as_secs_f64();
863 let rc = run_counter as f64;
864 let seconds_per_run = seconds / rc;
865 let projected_seconds_if_another = (rc + 1.0) * seconds_per_run;
866 if projected_seconds_if_another >= seconds_target {
867 let prob_stop = (projected_seconds_if_another - seconds_target) / seconds_per_run;
868 if uniform_0_1.sample(rng) < prob_stop {
869 return SALSOResults {
870 n_runs: run_counter,
871 seconds,
872 ..best
873 };
874 }
875 }
876 }
877 }
878 SALSOResults {
879 n_runs: p.n_runs,
880 seconds: start_time.elapsed().as_secs_f64(),
881 ..best
882 }
883}
884
885pub trait GeneralLossComputer {
890 fn expected_loss_kernel(&self) -> f64;
891 fn speculative_add(&mut self, partition: &Partition, i: usize, subset_index: LabelType) -> f64;
892 fn new_subset(&mut self, partition: &mut Partition);
893 fn add_with_index(&mut self, partition: &mut Partition, i: usize, subset_index: LabelType);
894 fn remove(&mut self, partition: &mut Partition, i: usize) -> LabelType;
895}
896
897#[derive(Debug)]
900struct BinderSubsetCalculations {
901 committed_loss: f64,
902 speculative_loss: f64,
903}
904
905pub struct BinderGLossComputer<'a> {
906 subsets: Vec<BinderSubsetCalculations>,
907 psm: &'a SquareMatrixBorrower<'a>,
908}
909
910impl<'a> BinderGLossComputer<'a> {
911 pub fn new(psm: &'a SquareMatrixBorrower<'a>) -> Self {
912 Self {
913 subsets: Vec::new(),
914 psm,
915 }
916 }
917 fn expected_loss_from_kernel(psm: &'a SquareMatrixBorrower<'a>, kernel: f64) -> f64 {
918 let nif = psm.n_items() as f64;
919 (2.0 * kernel + psm.sum_of_triangle()) * 2.0 / (nif * nif)
920 }
921}
922
923impl GeneralLossComputer for BinderGLossComputer<'_> {
924 fn expected_loss_kernel(&self) -> f64 {
925 self.subsets
926 .iter()
927 .fold(0.0, |s, subset| s + subset.committed_loss)
928 }
929
930 fn speculative_add(&mut self, partition: &Partition, i: usize, subset_index: LabelType) -> f64 {
931 self.subsets[subset_index as usize].speculative_loss = partition.subsets()
932 [subset_index as usize]
933 .items()
934 .iter()
935 .fold(0.0, |s, j| {
936 s + 0.5 - unsafe { *self.psm.get_unchecked((i, *j)) }
937 });
938 self.subsets[subset_index as usize].speculative_loss
939 }
940
941 fn new_subset(&mut self, partition: &mut Partition) {
942 partition.new_subset();
943 self.subsets.push(BinderSubsetCalculations {
944 committed_loss: 0.0,
945 speculative_loss: 0.0,
946 });
947 }
948
949 fn add_with_index(&mut self, partition: &mut Partition, i: usize, subset_index: LabelType) {
950 self.subsets[subset_index as usize].committed_loss +=
951 self.subsets[subset_index as usize].speculative_loss;
952 partition.add_with_index(i, subset_index as usize);
953 }
954
955 fn remove(&mut self, partition: &mut Partition, i: usize) -> LabelType {
956 let subset_index = partition.label_of(i).unwrap();
957 self.subsets[subset_index].committed_loss -= partition.subsets()[subset_index]
958 .items()
959 .iter()
960 .fold(0.0, |s, j| {
961 let jj = *j;
962 s + if jj != i {
963 0.5 - unsafe { *self.psm.get_unchecked((i, jj)) }
964 } else {
965 0.0
966 }
967 });
968 partition.remove_clean_and_relabel(i, |killed_subset_index, moved_subset_index| {
969 self.subsets.swap_remove(killed_subset_index);
970 assert_eq!(moved_subset_index, self.subsets.len());
971 });
972 subset_index as LabelType
973 }
974}
975
976#[derive(Debug)]
979struct OMARIApproxSubsetCalculations {
980 committed_ip: f64,
981 committed_i: f64,
982 speculative_ip: f64,
983 speculative_i: f64,
984}
985
986pub struct OMARIApproxGLossComputer<'a> {
987 committed_n_items: usize,
988 committed_sum_psm: f64,
989 speculative_sum_psm: f64,
990 subsets: Vec<OMARIApproxSubsetCalculations>,
991 psm: &'a SquareMatrixBorrower<'a>,
992}
993
994impl<'a> OMARIApproxGLossComputer<'a> {
995 pub fn new(psm: &'a SquareMatrixBorrower<'a>) -> Self {
996 Self {
997 committed_n_items: 0,
998 committed_sum_psm: 0.0,
999 speculative_sum_psm: f64::NEG_INFINITY,
1000 subsets: Vec::new(),
1001 psm,
1002 }
1003 }
1004}
1005
1006impl OMARIApproxGLossComputer<'_> {
1007 fn engine(
1008 &self,
1009 speculative_n_items: usize,
1010 speculative_ip: f64,
1011 speculative_i: f64,
1012 speculative_sum_psm: f64,
1013 ) -> f64 {
1014 let n_items = speculative_n_items + self.committed_n_items;
1015 if n_items <= 1 {
1016 return f64::INFINITY;
1017 }
1018 let n_choose_2 = (n_items * (n_items - 1) / 2) as f64;
1019 let all_ip = speculative_ip + self.subsets.iter().fold(0.0, |s, c| s + c.committed_ip);
1020 let all_i = speculative_i + self.subsets.iter().fold(0.0, |s, c| s + c.committed_i);
1021 let all_p = speculative_sum_psm + self.committed_sum_psm;
1022 let subtractor = all_i * all_p / n_choose_2;
1023 let numerator = all_ip - subtractor;
1024 let denominator = 0.5 * (all_i + all_p) - subtractor;
1025 1.0 - numerator / denominator
1026 }
1027}
1028
1029impl GeneralLossComputer for OMARIApproxGLossComputer<'_> {
1030 fn expected_loss_kernel(&self) -> f64 {
1031 self.engine(0, 0.0, 0.0, 0.0)
1032 }
1033
1034 fn speculative_add(&mut self, partition: &Partition, i: usize, subset_index: LabelType) -> f64 {
1035 let s = &partition.subsets()[subset_index as usize];
1036 self.subsets[subset_index as usize].speculative_ip = s
1037 .items()
1038 .iter()
1039 .fold(0.0, |s, j| s + unsafe { *self.psm.get_unchecked((i, *j)) });
1040 self.subsets[subset_index as usize].speculative_i = s.n_items() as f64;
1041 if self.speculative_sum_psm == f64::NEG_INFINITY {
1042 self.speculative_sum_psm = partition.subsets().iter().fold(0.0, |s, subset| {
1043 s + subset.items().iter().fold(0.0, |ss, j| {
1045 ss + unsafe { *self.psm.get_unchecked((i, *j)) }
1046 })
1047 })
1048 }
1049 self.engine(
1050 1,
1051 self.subsets[subset_index as usize].speculative_ip,
1052 self.subsets[subset_index as usize].speculative_i,
1053 self.speculative_sum_psm,
1054 )
1055 }
1056
1057 fn new_subset(&mut self, partition: &mut Partition) {
1058 partition.new_subset();
1059 self.subsets.push(OMARIApproxSubsetCalculations {
1060 committed_ip: 0.0,
1061 committed_i: 0.0,
1062 speculative_ip: 0.0,
1063 speculative_i: 0.0,
1064 });
1065 }
1066
1067 fn add_with_index(&mut self, partition: &mut Partition, i: usize, subset_index: LabelType) {
1068 let sc = &mut self.subsets[subset_index as usize];
1069 sc.committed_ip += sc.speculative_ip;
1070 sc.committed_i += sc.speculative_i;
1071 self.committed_n_items += 1;
1072 self.committed_sum_psm += self.speculative_sum_psm;
1073 self.speculative_sum_psm = f64::NEG_INFINITY;
1074 partition.add_with_index(i, subset_index as usize);
1075 }
1076
1077 fn remove(&mut self, partition: &mut Partition, i: usize) -> LabelType {
1078 let subset_index = partition.label_of(i).unwrap();
1079 self.subsets[subset_index].committed_ip -= partition.subsets()[subset_index]
1080 .items()
1081 .iter()
1082 .fold(0.0, |s, j| {
1083 let jj = *j;
1084 s + if jj != i {
1085 unsafe { *self.psm.get_unchecked((i, jj)) }
1086 } else {
1087 0.0
1088 }
1089 });
1090 self.subsets[subset_index].committed_i -=
1091 (partition.subsets()[subset_index].n_items() - 1) as f64;
1092 self.committed_n_items -= 1;
1093 self.committed_sum_psm -= partition.subsets().iter().fold(0.0, |s, subset| {
1094 s + subset.items().iter().fold(0.0, |ss, j| {
1096 let jj = *j;
1097 ss + if jj != i {
1098 unsafe { *self.psm.get_unchecked((i, jj)) }
1099 } else {
1100 0.0
1101 }
1102 })
1103 });
1104 partition.remove_clean_and_relabel(i, |killed_subset_index, moved_subset_index| {
1105 self.subsets.swap_remove(killed_subset_index);
1106 assert_eq!(moved_subset_index, self.subsets.len());
1107 });
1108 subset_index as LabelType
1109 }
1110}
1111
1112#[derive(Debug)]
1115struct VILBCacheUnit {
1116 item: usize,
1117 committed_sum: f64,
1118 committed_contribution: f64,
1119 speculative_sum: f64,
1120 speculative_contribution: f64,
1121}
1122
1123#[derive(Debug)]
1124struct VILBSubsetCalculations {
1125 cached_units: Vec<VILBCacheUnit>,
1126 committed_loss: f64,
1127 speculative_loss: f64,
1128}
1129
1130pub struct VILBGLossComputer<'a> {
1131 subsets: Vec<VILBSubsetCalculations>,
1132 psm: &'a SquareMatrixBorrower<'a>,
1133}
1134
1135impl<'a> VILBGLossComputer<'a> {
1136 pub fn new(psm: &'a SquareMatrixBorrower<'a>) -> Self {
1137 Self {
1138 subsets: Vec::new(),
1139 psm,
1140 }
1141 }
1142 pub fn expected_loss_from_kernel(psm: &'a SquareMatrixBorrower<'a>, kernel: f64) -> f64 {
1143 let nif = psm.n_items() as f64;
1144 (kernel + vilb_expected_loss_constant(psm)) / nif
1145 }
1146}
1147
1148impl GeneralLossComputer for VILBGLossComputer<'_> {
1149 fn expected_loss_kernel(&self) -> f64 {
1150 self.subsets
1151 .iter()
1152 .fold(0.0, |s, subset| s + subset.committed_loss)
1153 }
1154
1155 fn speculative_add(&mut self, partition: &Partition, i: usize, subset_index: LabelType) -> f64 {
1156 let subset_of_partition = &partition.subsets()[subset_index as usize];
1157 if subset_of_partition.n_items() == 0 {
1158 self.subsets[subset_index as usize]
1159 .cached_units
1160 .push(VILBCacheUnit {
1161 item: i,
1162 committed_sum: 0.0,
1163 committed_contribution: 0.0,
1164 speculative_sum: 1.0,
1165 speculative_contribution: 0.0,
1166 });
1167 return 0.0;
1168 }
1169 for cu in self.subsets[subset_index as usize].cached_units.iter_mut() {
1170 cu.speculative_sum =
1171 cu.committed_sum + unsafe { *self.psm.get_unchecked((cu.item, i)) };
1172 cu.speculative_contribution = cu.speculative_sum.log2();
1173 }
1174 let sum = subset_of_partition
1175 .items()
1176 .iter()
1177 .fold(0.0, |s, j| s + unsafe { *self.psm.get_unchecked((i, *j)) })
1178 + 1.0; self.subsets[subset_index as usize]
1180 .cached_units
1181 .push(VILBCacheUnit {
1182 item: i,
1183 committed_sum: 0.0,
1184 committed_contribution: 0.0,
1185 speculative_sum: sum,
1186 speculative_contribution: sum.log2(),
1187 });
1188 let nif = subset_of_partition.n_items() as f64;
1189 let s1 = (nif + 1.0) * (nif + 1.0).log2();
1190 let s2 = self.subsets[subset_index as usize]
1191 .cached_units
1192 .iter()
1193 .fold(0.0, |s, cu| s + cu.speculative_contribution);
1194 self.subsets[subset_index as usize].speculative_loss = s1 - 2.0 * s2;
1195 self.subsets[subset_index as usize].speculative_loss
1196 - self.subsets[subset_index as usize].committed_loss
1197 }
1198
1199 fn new_subset(&mut self, partition: &mut Partition) {
1200 partition.new_subset();
1201 self.subsets.push(VILBSubsetCalculations {
1202 cached_units: Vec::new(),
1203 committed_loss: 0.0,
1204 speculative_loss: 0.0,
1205 })
1206 }
1207
1208 fn add_with_index(&mut self, partition: &mut Partition, i: usize, subset_index: LabelType) {
1209 for (index, subset) in self.subsets.iter_mut().enumerate() {
1210 if index == (subset_index as usize) {
1211 for cu in subset.cached_units.iter_mut() {
1212 cu.committed_sum = cu.speculative_sum;
1213 cu.committed_contribution = cu.speculative_contribution;
1214 }
1215 } else {
1216 subset.cached_units.pop();
1217 }
1218 }
1219 self.subsets[subset_index as usize].committed_loss =
1220 self.subsets[subset_index as usize].speculative_loss;
1221 partition.add_with_index(i, subset_index as usize);
1222 }
1223
1224 fn remove(&mut self, partition: &mut Partition, i: usize) -> LabelType {
1225 let subset_index = partition.label_of(i).unwrap();
1226 for cu in self.subsets[subset_index].cached_units.iter_mut() {
1227 cu.committed_sum -= unsafe { *self.psm.get_unchecked((cu.item, i)) };
1228 cu.committed_contribution = cu.committed_sum.log2();
1229 }
1230 let pos = self.subsets[subset_index]
1231 .cached_units
1232 .iter()
1233 .enumerate()
1234 .find(|cu| cu.1.item == i)
1235 .unwrap()
1236 .0;
1237 self.subsets[subset_index].cached_units.swap_remove(pos);
1238 self.subsets[subset_index].committed_loss =
1239 match partition.subsets()[subset_index].n_items() {
1240 0 => 0.0,
1241 ni => {
1242 let nif = ni as f64;
1243 nif * nif.log2()
1244 - 2.0
1245 * self.subsets[subset_index]
1246 .cached_units
1247 .iter()
1248 .fold(0.0, |s, cu| s + cu.committed_contribution)
1249 }
1250 };
1251 partition.remove_clean_and_relabel(i, |killed_subset_index, moved_subset_index| {
1252 self.subsets.swap_remove(killed_subset_index);
1253 assert_eq!(moved_subset_index, self.subsets.len());
1254 });
1255 subset_index as LabelType
1256 }
1257}
1258
1259fn label_of_empty_cluster<U: GeneralLossComputer>(
1262 partition: &mut Partition,
1263 computer: &mut U,
1264 max_label: LabelType,
1265) -> Option<LabelType> {
1266 match partition.subsets().last() {
1267 None => {
1268 computer.new_subset(partition);
1269 Some(0)
1270 }
1271 Some(last) => {
1272 if last.is_empty() {
1273 Some(partition.n_subsets() as LabelType - 1)
1274 } else if partition.n_subsets() <= max_label as usize {
1275 computer.new_subset(partition);
1276 Some(partition.n_subsets() as LabelType - 1)
1277 } else {
1278 None
1279 }
1280 }
1281 }
1282}
1283
1284fn micro_optimized_allocation<U: GeneralLossComputer>(
1285 partition: &mut Partition,
1286 computer: &mut U,
1287 i: usize,
1288 label_to_take: Option<LabelType>,
1289) -> LabelType {
1290 let max_label = partition.n_subsets() as LabelType - 1;
1291 let iter = (0..=max_label).map(|subset_index| {
1292 let value = computer.speculative_add(partition, i, subset_index as LabelType);
1293 match label_to_take {
1294 Some(label_to_take) => {
1295 if subset_index == label_to_take {
1296 (subset_index, f64::NEG_INFINITY)
1297 } else {
1298 (subset_index, f64::INFINITY)
1299 }
1300 }
1301 None => (subset_index, value),
1302 }
1303 });
1304 let subset_index = find_label_of_minimum(iter);
1305 computer.add_with_index(partition, i, subset_index);
1306 subset_index as LabelType
1307}
1308
1309pub fn minimize_once_by_salso<'a, T: Rng, U: GeneralLossComputer>(
1310 computer_factory: Box<dyn Fn() -> U + 'a>,
1311 p: &SALSOParameters,
1312 seconds_target: f64,
1313 rng: &mut T,
1314) -> SALSOResults {
1315 let start_time = Instant::now();
1316 let max_label = if p.max_size == 0 {
1317 MAX_LABEL
1318 } else {
1319 p.max_size.max(1) - 1
1320 };
1321 let mut best = SALSOResults::dummy(max_label + 1);
1322 let mut permutation: Vec<usize> = (0..p.n_items).collect();
1323 let uniform_0_1 = Uniform::new(0.0, 1.0).unwrap();
1324 let mut run_counter = 0;
1325 while run_counter < p.n_runs {
1326 let mut computer = computer_factory();
1327 let (mut partition, initialization_method) = if uniform_0_1.sample(rng)
1328 < p.prob_sequential_allocation
1329 {
1330 let mut partition = Partition::new(p.n_items);
1331 permutation.shuffle(rng);
1332 let singletons_initialization =
1333 uniform_0_1.sample(rng) < p.prob_singletons_initialization;
1334 for i in 0..p.n_items {
1336 let ii = unsafe { *permutation.get_unchecked(i) };
1337 let empty_label = label_of_empty_cluster(&mut partition, &mut computer, max_label);
1338 if singletons_initialization {
1339 micro_optimized_allocation(&mut partition, &mut computer, ii, empty_label);
1340 } else {
1341 micro_optimized_allocation(&mut partition, &mut computer, ii, None);
1342 }
1343 }
1344 let initialization_method = if singletons_initialization {
1345 InitializationMethod::SequentialFromSingletons
1346 } else {
1347 InitializationMethod::SequentialFromEmpty
1348 };
1349 (partition, initialization_method)
1350 } else {
1351 panic!("Disabled because it's problematic.");
1355 };
1371 let mut n_scans = 0;
1373 let mut change = true;
1374 while change && n_scans < p.max_scans {
1375 change = false;
1376 n_scans += 1;
1377 permutation.shuffle(rng);
1378 for i in 0..p.n_items {
1379 label_of_empty_cluster(&mut partition, &mut computer, max_label);
1380 let ii = unsafe { *permutation.get_unchecked(i) };
1381 let previous_subset_index = computer.remove(&mut partition, ii);
1382 let subset_index =
1383 micro_optimized_allocation(&mut partition, &mut computer, ii, None);
1384 if subset_index != previous_subset_index {
1385 change = true;
1386 };
1387 }
1388 }
1389 let expected_loss = computer.expected_loss_kernel();
1390 if expected_loss < best.expected_loss {
1391 partition.canonicalize();
1392 best = SALSOResults {
1393 clustering: partition.labels_via_copying(),
1394 expected_loss,
1395 n_scans,
1396 initialization_method,
1397 ..best
1398 };
1399 }
1400 run_counter += 1;
1401 if seconds_target.is_finite() {
1402 let seconds = start_time.elapsed().as_secs_f64();
1403 let rc = run_counter as f64;
1404 let seconds_per_run = seconds / rc;
1405 let projected_seconds_if_another = (rc + 1.0) * seconds_per_run;
1406 if projected_seconds_if_another >= seconds_target {
1407 let prob_stop = (projected_seconds_if_another - seconds_target) / seconds_per_run;
1408 if uniform_0_1.sample(rng) < prob_stop {
1409 return SALSOResults {
1410 n_runs: run_counter,
1411 seconds,
1412 ..best
1413 };
1414 }
1415 }
1416 }
1417 }
1418 SALSOResults {
1419 n_runs: p.n_runs,
1420 seconds: start_time.elapsed().as_secs_f64(),
1421 ..best
1422 }
1423}
1424
1425#[derive(Debug, Clone)]
1428pub struct SALSOParameters {
1429 pub n_items: usize,
1430 pub max_size: LabelType,
1431 pub max_size_as_rf: bool,
1432 pub max_scans: u32,
1433 pub max_zealous_updates: u32,
1434 pub n_runs: u32,
1435 pub prob_sequential_allocation: f64,
1436 pub prob_singletons_initialization: f64,
1437}
1438
1439pub struct SALSOResults {
1440 pub clustering: Vec<usize>,
1441 pub expected_loss: f64,
1442 pub n_scans: u32,
1443 pub n_zealous_accepts: u32,
1444 pub n_zealous_attempts: u32,
1445 pub initialization_method: InitializationMethod,
1446 pub n_runs: u32,
1447 pub max_size: LabelType,
1448 pub seconds: f64,
1449}
1450
1451impl SALSOResults {
1452 #[allow(clippy::too_many_arguments)]
1453 pub fn new(
1454 clustering: Vec<usize>,
1455 expected_loss: f64,
1456 n_scans: u32,
1457 n_zealous_accepts: u32,
1458 n_zealous_attempts: u32,
1459 initialization_method: InitializationMethod,
1460 n_runs: u32,
1461 max_size: LabelType,
1462 seconds: f64,
1463 ) -> Self {
1464 Self {
1465 clustering,
1466 expected_loss,
1467 n_scans,
1468 n_zealous_accepts,
1469 n_zealous_attempts,
1470 initialization_method,
1471 n_runs,
1472 max_size,
1473 seconds,
1474 }
1475 }
1476
1477 pub fn dummy(max_size: LabelType) -> Self {
1478 SALSOResults::new(
1479 vec![0usize; 0],
1480 f64::INFINITY,
1481 0,
1482 0,
1483 0,
1484 InitializationMethod::SequentialFromEmpty,
1485 0,
1486 max_size,
1487 0.0,
1488 )
1489 }
1490}
1491
1492pub fn minimize_by_salso<T: Rng>(
1493 pdi: PartitionDistributionInformation,
1494 loss_function: LossFunction,
1495 p: &SALSOParameters,
1496 seconds: f64,
1497 n_cores: u32,
1498 mut rng: &mut T,
1499) -> SALSOResults {
1500 let start_time = Instant::now();
1501 let cache = Log2Cache::new(match loss_function {
1502 LossFunction::VI(_) | LossFunction::NVI | LossFunction::ID | LossFunction::NID => p.n_items,
1503 _ => 0,
1504 });
1505 let result = if n_cores == 1 {
1506 match loss_function {
1507 LossFunction::BinderDraws(a) => minimize_once_by_salso_v2(
1508 Box::new(|| BinderCMLossComputer::new(a)),
1509 pdi.draws(),
1510 p,
1511 seconds,
1512 rng,
1513 ),
1514 LossFunction::BinderPSM => minimize_once_by_salso(
1515 Box::new(|| BinderGLossComputer::new(pdi.psm())),
1516 p,
1517 seconds,
1518 rng,
1519 ),
1520 LossFunction::OneMinusARI => minimize_once_by_salso_v2(
1521 Box::new(|| OMARICMLossComputer::new(pdi.draws().n_clusterings())),
1522 pdi.draws(),
1523 p,
1524 seconds,
1525 rng,
1526 ),
1527 LossFunction::OneMinusARIapprox => minimize_once_by_salso(
1528 Box::new(|| OMARIApproxGLossComputer::new(pdi.psm())),
1529 p,
1530 seconds,
1531 rng,
1532 ),
1533 LossFunction::VI(a) => minimize_once_by_salso_v2(
1534 Box::new(|| VICMLossComputer::new(a, &cache)),
1535 pdi.draws(),
1536 p,
1537 seconds,
1538 rng,
1539 ),
1540 LossFunction::VIlb => minimize_once_by_salso(
1541 Box::new(|| VILBGLossComputer::new(pdi.psm())),
1542 p,
1543 seconds,
1544 rng,
1545 ),
1546 LossFunction::NVI => minimize_once_by_salso_v2(
1547 Box::new(|| {
1548 GeneralInformationBasedCMLossComputer::new(
1549 pdi.draws().n_clusterings(),
1550 &cache,
1551 NVIInformationBasedLoss {},
1552 )
1553 }),
1554 pdi.draws(),
1555 p,
1556 seconds,
1557 rng,
1558 ),
1559 LossFunction::ID => minimize_once_by_salso_v2(
1560 Box::new(|| {
1561 GeneralInformationBasedCMLossComputer::new(
1562 pdi.draws().n_clusterings(),
1563 &cache,
1564 IDInformationBasedLoss {},
1565 )
1566 }),
1567 pdi.draws(),
1568 p,
1569 seconds,
1570 rng,
1571 ),
1572 LossFunction::NID => minimize_once_by_salso_v2(
1573 Box::new(|| {
1574 GeneralInformationBasedCMLossComputer::new(
1575 pdi.draws().n_clusterings(),
1576 &cache,
1577 NIDInformationBasedLoss {},
1578 )
1579 }),
1580 pdi.draws(),
1581 p,
1582 seconds,
1583 rng,
1584 ),
1585 }
1586 } else {
1587 let (tx, rx) = mpsc::channel();
1588 let n_cores = if n_cores == 0 {
1589 std::thread::available_parallelism()
1590 .map(|x| x.get() as u32)
1591 .unwrap_or(1)
1592 } else {
1593 n_cores
1594 };
1595 let p = p.clone();
1596 let p = SALSOParameters {
1597 n_runs: p.n_runs.div_ceil(n_cores),
1598 ..p
1599 };
1600 let cache_ref = &cache;
1601 crossbeam::scope(|s| {
1602 for _ in 0..n_cores {
1603 let tx = mpsc::Sender::clone(&tx);
1604 let mut child_rng = Pcg64Mcg::from_rng(&mut rng);
1605 let p = p.clone();
1606 s.spawn(move |_| {
1607 let result = match loss_function {
1608 LossFunction::BinderDraws(a) => minimize_once_by_salso_v2(
1609 Box::new(|| BinderCMLossComputer::new(a)),
1610 pdi.draws(),
1611 &p,
1612 seconds,
1613 &mut child_rng,
1614 ),
1615 LossFunction::BinderPSM => minimize_once_by_salso(
1616 Box::new(|| BinderGLossComputer::new(pdi.psm())),
1617 &p,
1618 seconds,
1619 &mut child_rng,
1620 ),
1621 LossFunction::OneMinusARI => minimize_once_by_salso_v2(
1622 Box::new(|| OMARICMLossComputer::new(pdi.draws().n_clusterings())),
1623 pdi.draws(),
1624 &p,
1625 seconds,
1626 &mut child_rng,
1627 ),
1628 LossFunction::OneMinusARIapprox => minimize_once_by_salso(
1629 Box::new(|| OMARIApproxGLossComputer::new(pdi.psm())),
1630 &p,
1631 seconds,
1632 &mut child_rng,
1633 ),
1634 LossFunction::VI(a) => minimize_once_by_salso_v2(
1635 Box::new(|| VICMLossComputer::new(a, cache_ref)),
1636 pdi.draws(),
1637 &p,
1638 seconds,
1639 &mut child_rng,
1640 ),
1641 LossFunction::VIlb => minimize_once_by_salso(
1642 Box::new(|| VILBGLossComputer::new(pdi.psm())),
1643 &p,
1644 seconds,
1645 &mut child_rng,
1646 ),
1647 LossFunction::NVI => minimize_once_by_salso_v2(
1648 Box::new(|| {
1649 GeneralInformationBasedCMLossComputer::new(
1650 pdi.draws().n_clusterings(),
1651 cache_ref,
1652 NVIInformationBasedLoss {},
1653 )
1654 }),
1655 pdi.draws(),
1656 &p,
1657 seconds,
1658 &mut child_rng,
1659 ),
1660 LossFunction::ID => minimize_once_by_salso_v2(
1661 Box::new(|| {
1662 GeneralInformationBasedCMLossComputer::new(
1663 pdi.draws().n_clusterings(),
1664 cache_ref,
1665 IDInformationBasedLoss {},
1666 )
1667 }),
1668 pdi.draws(),
1669 &p,
1670 seconds,
1671 &mut child_rng,
1672 ),
1673 LossFunction::NID => minimize_once_by_salso_v2(
1674 Box::new(|| {
1675 GeneralInformationBasedCMLossComputer::new(
1676 pdi.draws().n_clusterings(),
1677 cache_ref,
1678 NIDInformationBasedLoss {},
1679 )
1680 }),
1681 pdi.draws(),
1682 &p,
1683 seconds,
1684 &mut child_rng,
1685 ),
1686 };
1687 tx.send(result).unwrap();
1688 });
1689 }
1690 })
1691 .unwrap();
1692 std::mem::drop(tx); let mut best = SALSOResults::dummy(p.max_size);
1694 let mut run_counter = 0;
1695 for candidate in rx {
1696 run_counter += candidate.n_runs;
1697 if candidate.expected_loss < best.expected_loss {
1698 best = candidate;
1699 }
1700 }
1701 best.n_runs = run_counter;
1702 best
1703 };
1704 SALSOResults {
1705 expected_loss: match loss_function {
1706 LossFunction::BinderPSM => {
1707 BinderGLossComputer::expected_loss_from_kernel(pdi.psm(), result.expected_loss)
1708 }
1709 LossFunction::VIlb => {
1710 VILBGLossComputer::expected_loss_from_kernel(pdi.psm(), result.expected_loss)
1711 }
1712 _ => result.expected_loss,
1713 },
1714 seconds: start_time.elapsed().as_secs_f64(),
1715 ..result
1716 }
1717}
1718
1719pub fn minimize_by_enumeration(
1720 f: fn(&[LabelType], &SquareMatrixBorrower) -> f64,
1721 psm: &SquareMatrixBorrower,
1722) -> Vec<usize> {
1723 let (tx, rx) = mpsc::channel();
1724 crossbeam::scope(|s| {
1725 let n_cores = std::thread::available_parallelism()
1726 .map(|x| x.get() as u32)
1727 .unwrap_or(1);
1728 for iter in Partition::iter_sharded(n_cores, psm.n_items()) {
1729 let tx = mpsc::Sender::clone(&tx);
1730 s.spawn(move |_| {
1731 let mut working_minimum = f64::INFINITY;
1732 let mut working_minimizer = vec![0usize; psm.n_items()];
1733 for partition in iter {
1734 let part: Vec<LabelType> = partition.iter().map(|x| *x as LabelType).collect();
1735 let value = f(&part[..], psm);
1736 if value < working_minimum {
1737 working_minimum = value;
1738 working_minimizer = partition;
1739 }
1740 }
1741 tx.send(working_minimizer).unwrap();
1742 });
1743 }
1744 })
1745 .unwrap();
1746 std::mem::drop(tx); let mut working_minimum = f64::INFINITY;
1748 let mut working_minimizer = vec![0usize; psm.n_items()];
1749 for partition in rx {
1750 let part: Vec<LabelType> = partition.iter().map(|x| *x as LabelType).collect();
1751 let value = f(&part[..], psm);
1752 if value < working_minimum {
1753 working_minimum = value;
1754 working_minimizer = partition;
1755 }
1756 }
1757 working_minimizer
1758}
1759
1760#[cfg(test)]
1761mod tests_optimize {
1762 use super::*;
1763 use rand::{RngExt, rng};
1764
1765 #[test]
1766 fn test_max_scan() {
1767 let n_items = 5;
1768 let mut psm = SquareMatrix::identity(n_items);
1769 let psm_view = &psm.view();
1770 let p = SALSOParameters {
1771 n_items,
1772 max_size: 2,
1773 max_size_as_rf: true,
1774 max_scans: 10,
1775 max_zealous_updates: 10,
1776 n_runs: 100,
1777 prob_sequential_allocation: 1.0,
1778 prob_singletons_initialization: 0.5,
1779 };
1780 minimize_by_salso(
1781 PartitionDistributionInformation::PairwiseSimilarityMatrix(psm_view),
1782 LossFunction::BinderPSM,
1783 &p,
1784 5.0,
1785 1,
1786 &mut rng(),
1787 );
1788 }
1789
1790 fn random_draws(n_items: usize, n_draws: usize) -> Clusterings {
1791 let mut rng = Pcg64Mcg::seed_from_u64(7);
1792 let labels: Vec<i32> = (0..n_items * n_draws)
1793 .map(|_| rng.random_range(0..4))
1794 .collect();
1795 Clusterings::from_i32_column_major_order(&labels, n_items)
1796 }
1797
1798 fn build<T: CMLossComputer>(
1800 mut loss_computer: T,
1801 draws: &Clusterings,
1802 assignments: &[(usize, LabelType)],
1803 ) -> (T, WorkingClustering, Array3<CountType>) {
1804 let max_size = 6;
1805 let mut state = WorkingClustering::empty(draws.n_items(), max_size);
1806 let mut cms = Array3::<CountType>::zeros((
1807 max_size as usize + 1,
1808 draws.max_clusters() as usize,
1809 draws.n_clusterings(),
1810 ));
1811 for &(item_index, label) in assignments {
1812 state.assign(item_index, label, &mut loss_computer, &mut cms, draws);
1813 }
1814 (loss_computer, state, cms)
1815 }
1816
1817 fn check_information_based_change_in_loss<T: InformationBasedLoss>(
1820 ibloss: impl Fn() -> T,
1821 to_loss: impl Fn(f64, f64, f64) -> f64,
1822 ) {
1823 let n_items = 12;
1824 let n_draws = 9;
1825 let draws = random_draws(n_items, n_draws);
1826 let cache = Log2Cache::new(n_items);
1828 let new = || GeneralInformationBasedCMLossComputer::new(n_draws, &cache, ibloss());
1829 let actual = |assignments: &[(usize, LabelType)]| {
1830 let (loss_computer, state, cms) = build(new(), &draws, assignments);
1831 loss_computer.compute_loss(&state, &cms)
1832 };
1833 for m in 0..n_items {
1835 let assignments: Vec<_> = (0..m).map(|i| (i, (i % 3) as LabelType)).collect();
1836 let (loss_computer, state, cms) = build(new(), &draws, &assignments);
1837 for to_label in 0..4 {
1838 let score = loss_computer.change_in_loss(m, to_label, None, &state, &cms, &draws);
1839 let mut moved = assignments.clone();
1840 moved.push((m, to_label));
1841 assert_relative_eq!(
1842 to_loss(score, n_draws as f64, (m + 1) as f64),
1843 actual(&moved),
1844 max_relative = 1e-12
1845 );
1846 }
1847 }
1848 let assignments: Vec<_> = (0..n_items).map(|i| (i, (i % 3) as LabelType)).collect();
1850 let (loss_computer, state, cms) = build(new(), &draws, &assignments);
1851 for m in 0..n_items {
1852 for to_label in 0..4 {
1853 let from_label = Some(state.get(m));
1854 let score =
1855 loss_computer.change_in_loss(m, to_label, from_label, &state, &cms, &draws);
1856 let mut moved = assignments.clone();
1857 moved[m].1 = to_label;
1858 assert_relative_eq!(
1859 to_loss(score, n_draws as f64, n_items as f64),
1860 actual(&moved),
1861 max_relative = 1e-12
1862 );
1863 }
1864 }
1865 }
1866
1867 #[test]
1868 fn nvi_change_in_loss_matches_compute_loss() {
1869 check_information_based_change_in_loss(|| NVIInformationBasedLoss, |s, d, _| s / d);
1870 }
1871
1872 #[test]
1873 fn id_change_in_loss_matches_compute_loss() {
1874 check_information_based_change_in_loss(|| IDInformationBasedLoss, |s, d, n| s / (d * n));
1875 }
1876
1877 #[test]
1878 fn nid_change_in_loss_matches_compute_loss() {
1879 check_information_based_change_in_loss(|| NIDInformationBasedLoss, |s, d, _| 1.0 + s / d);
1880 }
1881
1882 fn check_normalized_information_loss<T: InformationBasedLoss>(ibloss: impl Fn() -> T) {
1883 for n_items in [1, 2, 4, 31] {
1884 let cache = Log2Cache::new(n_items);
1885 for one_cluster in [false, true] {
1886 let labels: Vec<_> = (0..n_items)
1887 .map(|i| if one_cluster { 0 } else { i as i32 })
1888 .collect();
1889 let draws = Clusterings::from_i32_column_major_order(&labels, n_items);
1890 for same_partition in [false, true] {
1891 let assignments: Vec<_> = (0..n_items)
1892 .map(|i| {
1893 let label = if one_cluster == same_partition {
1894 0
1895 } else {
1896 i as LabelType
1897 };
1898 (i, label)
1899 })
1900 .collect();
1901 let new = || GeneralInformationBasedCMLossComputer::new(1, &cache, ibloss());
1902 let state = WorkingClustering::from_vector(
1903 assignments.iter().map(|x| x.1).collect(),
1904 n_items as LabelType,
1905 );
1906 let cms = draws.make_confusion_matrices(&state);
1907 let mut initialized = new();
1908 initialized.initialize(&state, &cms);
1909 let expected = if same_partition || n_items == 1 {
1910 0.0
1911 } else {
1912 1.0
1913 };
1914 assert_relative_eq!(
1915 initialized.compute_loss(&state, &cms),
1916 expected,
1917 epsilon = 1e-12
1918 );
1919
1920 let mut incremental = new();
1921 let mut state = WorkingClustering::empty(n_items, n_items as LabelType);
1922 let mut cms = Array3::zeros((n_items + 1, draws.max_clusters() as usize, 1));
1923 for (item, label) in assignments {
1924 state.assign(item, label, &mut incremental, &mut cms, &draws);
1925 }
1926 assert_relative_eq!(
1927 incremental.compute_loss(&state, &cms),
1928 expected,
1929 epsilon = 1e-12
1930 );
1931 if one_cluster {
1932 let mut rng = Pcg64Mcg::seed_from_u64(7);
1933 for _ in 0..n_items * 10 {
1934 let item = rng.random_range(0..n_items);
1935 let label = rng.random_range(0..n_items) as LabelType;
1936 state.reassign(item, label, &mut incremental, &mut cms, &draws);
1937 }
1938 for item in 0..n_items {
1939 state.reassign(item, 0, &mut incremental, &mut cms, &draws);
1940 }
1941 assert_relative_eq!(
1942 incremental.compute_loss(&state, &cms),
1943 0.0,
1944 epsilon = 1e-12
1945 );
1946 }
1947 }
1948 }
1949 }
1950 }
1951
1952 #[test]
1953 fn nvi_handles_one_cluster_and_singleton_partitions() {
1954 check_normalized_information_loss(|| NVIInformationBasedLoss);
1955 }
1956
1957 #[test]
1958 fn nid_handles_one_cluster_and_singleton_partitions() {
1959 check_normalized_information_loss(|| NIDInformationBasedLoss);
1960 }
1961
1962 #[test]
1963 fn salso_handles_zero_entropy_draws() {
1964 for n_items in [1, 4, 31] {
1965 let draws = Clusterings::from_i32_column_major_order(&vec![1; n_items * 3], n_items);
1966 for loss in [LossFunction::NVI, LossFunction::NID] {
1967 for prob_sequential_allocation in [0.0, 1.0] {
1968 for n_cores in [1, 2] {
1969 let p = SALSOParameters {
1970 n_items,
1971 max_size: 0,
1972 max_size_as_rf: false,
1973 max_scans: 10,
1974 max_zealous_updates: 4,
1975 n_runs: 4,
1976 prob_sequential_allocation,
1977 prob_singletons_initialization: 0.0,
1978 };
1979 let result = minimize_by_salso(
1980 PartitionDistributionInformation::Draws(&draws),
1981 loss,
1982 &p,
1983 f64::INFINITY,
1984 n_cores,
1985 &mut Pcg64Mcg::seed_from_u64(1),
1986 );
1987 assert_eq!(result.clustering, vec![0; n_items]);
1988 assert_relative_eq!(result.expected_loss, 0.0, epsilon = 1e-12);
1989 }
1990 }
1991 }
1992 }
1993 }
1994
1995 #[test]
1998 fn vi_change_in_loss_differences_match_compute_loss() {
1999 let n_items = 12;
2000 let n_draws = 9;
2001 let draws = random_draws(n_items, n_draws);
2002 let cache = Log2Cache::new(n_items);
2003 let new = || VICMLossComputer::new(1.0, &cache);
2004 let actual = |assignments: &[(usize, LabelType)]| {
2005 let (loss_computer, state, cms) = build(new(), &draws, assignments);
2006 loss_computer.compute_loss(&state, &cms)
2007 };
2008 let scale = (n_draws * n_items) as f64;
2009 for m in 1..n_items {
2010 let assignments: Vec<_> = (0..m).map(|i| (i, (i % 3) as LabelType)).collect();
2011 let (loss_computer, state, cms) = build(new(), &draws, &assignments);
2012 let change =
2013 |to_label| loss_computer.change_in_loss(m, to_label, None, &state, &cms, &draws);
2014 let mut moved = assignments.clone();
2015 moved.push((m, 0));
2016 let baseline = actual(&moved);
2017 for to_label in 1..4 {
2018 moved[m].1 = to_label;
2019 assert_relative_eq!(
2020 (change(to_label) - change(0)) / scale,
2021 actual(&moved) - baseline,
2022 epsilon = 1e-12
2023 );
2024 }
2025 }
2026 let assignments: Vec<_> = (0..n_items).map(|i| (i, (i % 3) as LabelType)).collect();
2027 let baseline = actual(&assignments);
2028 let (loss_computer, state, cms) = build(new(), &draws, &assignments);
2029 for m in 0..n_items {
2030 let from_label = state.get(m);
2031 let change = |to_label| {
2032 loss_computer.change_in_loss(m, to_label, Some(from_label), &state, &cms, &draws)
2033 };
2034 for to_label in 0..4 {
2035 let mut moved = assignments.clone();
2036 moved[m].1 = to_label;
2037 assert_relative_eq!(
2038 (change(to_label) - change(from_label)) / scale,
2039 actual(&moved) - baseline,
2040 epsilon = 1e-12
2041 );
2042 }
2043 }
2044 }
2045
2046 #[test]
2047 #[should_panic(expected = "cms and draws disagree")]
2048 fn vi_change_in_loss_rejects_mismatched_draws() {
2049 let draws = random_draws(4, 3);
2050 let cache = Log2Cache::new(4);
2051 let state = WorkingClustering::empty(4, 2);
2052 let cms = Array3::<CountType>::zeros((3, draws.max_clusters() as usize, 4));
2053 let _ = VICMLossComputer::new(1.0, &cache).change_in_loss(0, 0, None, &state, &cms, &draws);
2054 }
2055
2056 #[test]
2057 fn salso_runs_with_every_draws_based_loss() {
2058 let n_items = 30;
2059 let draws = random_draws(n_items, 20);
2060 for loss_function in [
2061 LossFunction::BinderDraws(1.0),
2062 LossFunction::OneMinusARI,
2063 LossFunction::VI(1.0),
2064 LossFunction::NVI,
2065 LossFunction::ID,
2066 LossFunction::NID,
2067 ] {
2068 for prob_sequential_allocation in [0.0, 1.0] {
2069 let p = SALSOParameters {
2070 n_items,
2071 max_size: 0,
2072 max_size_as_rf: true,
2073 max_scans: 10,
2074 max_zealous_updates: 10,
2075 n_runs: 4,
2076 prob_sequential_allocation,
2077 prob_singletons_initialization: 0.0,
2078 };
2079 let result = minimize_by_salso(
2080 PartitionDistributionInformation::Draws(&draws),
2081 loss_function,
2082 &p,
2083 5.0,
2084 1,
2085 &mut Pcg64Mcg::seed_from_u64(1),
2086 );
2087 assert_eq!(result.clustering.len(), n_items);
2088 assert!(result.expected_loss.is_finite());
2089 }
2090 }
2091 }
2092}