(&self, context: Context, arguments_types: Vec<Type>)
| 54 | #[typetag::serde] |
| 55 | impl CustomOperationBody for AucScore { |
| 56 | fn instantiate(&self, context: Context, arguments_types: Vec<Type>) -> Result<Graph> { |
| 57 | if arguments_types.len() != 2 { |
| 58 | return Err(runtime_error!("Invalid number of arguments for AucMetric")); |
| 59 | } |
| 60 | let t = arguments_types[0].clone(); |
| 61 | if !t.is_array() { |
| 62 | return Err(runtime_error!( |
| 63 | "`y_true` in AucMetric must be an array, got {t:?}" |
| 64 | )); |
| 65 | } |
| 66 | if t.get_dimensions().len() != 1 { |
| 67 | return Err(runtime_error!( |
| 68 | "`y_true` in AucMetric must be 1-dimensional, got {t:?}" |
| 69 | )); |
| 70 | } |
| 71 | let n = t.get_dimensions()[0]; |
| 72 | if n >= (1 << MAX_LOG_ARRAY_SIZE) { |
| 73 | return Err(runtime_error!( |
| 74 | "`y_true` in AucMetric must have less than 2^{} elements, got {n:?}", |
| 75 | MAX_LOG_ARRAY_SIZE |
| 76 | )); |
| 77 | } |
| 78 | let sc = t.get_scalar_type(); |
| 79 | if sc != INT64 { |
| 80 | return Err(runtime_error!( |
| 81 | "`y_true` in AucMetric must consist of INT64's, got {sc:?}" |
| 82 | )); |
| 83 | } |
| 84 | if arguments_types[1] != t { |
| 85 | return Err(runtime_error!( |
| 86 | "`y_pred` in AucMetric must be of the same type as `y_true`, got {:?} vs {:?}", |
| 87 | t, |
| 88 | arguments_types[1] |
| 89 | )); |
| 90 | } |
| 91 | |
| 92 | let g = context.create_graph()?; |
| 93 | let y_true = g.input(t.clone())?; |
| 94 | let y_pred = g.input(t)?; |
| 95 | // Intuitively, AUC is the probability that a randomly chosen positive example is ranked higher than a randomly chosen negative example. |
| 96 | // SKLearn definition expands it with the following: if two examples have the same prediction, this pair is accounted as 0.5 rather than 1. |
| 97 | // E.g., if we have y_true = [0, 0, 1, 1], and y_pred = [10, 10, 10, 10], SKLearn will return 0.5. |
| 98 | // This complicates implementation in CipherCore. Luckily, we can utilize the following trick. |
| 99 | // Let's assume that we compute the AUC by sorting the data over predictions, and ignoring the fact that some of the predictions are equal. |
| 100 | // Our sorting is stable, so if we repeat the process on the reversed data, pairs of equal predictions will be counted as 1 in one case, and |
| 101 | // 0 in the other one, while pairs of predictions which are not equal will be counted the same way in both cases. |
| 102 | // This means that we can compute AUC in a naive way on the data and reversed data, and just average the results. |
| 103 | let auc1 = compute_naive_auc(y_true.clone(), y_pred.clone(), &self.fp)?; |
| 104 | let y_true = y_true.get_slice(vec![SliceElement::SubArray(None, None, Some(-1))])?; |
| 105 | let y_pred = y_pred.get_slice(vec![SliceElement::SubArray(None, None, Some(-1))])?; |
| 106 | let auc2 = compute_naive_auc(y_true, y_pred, &self.fp)?; |
| 107 | let auc = auc1.add(auc2)?.truncate(2)?; |
| 108 | auc.set_as_output()?; |
| 109 | g.finalize()?; |
| 110 | Ok(g) |
| 111 | } |
| 112 | |
| 113 | fn get_name(&self) -> String { |
nothing calls this directly
no test coverage detected