From 23512565ac7e4092474ffa5444075d6e22c910ba Mon Sep 17 00:00:00 2001 From: 33798 <3379854453@qq.com> Date: Sun, 31 Aug 2025 14:04:55 +0800 Subject: [PATCH 1/8] conformal selection --- examples/regression_selection_synthetic.py | 47 +++++++++++ torchcp/regression/predictor/__init__.py | 3 +- torchcp/regression/predictor/selection.py | 94 ++++++++++++++++++++++ torchcp/regression/score/__init__.py | 3 +- torchcp/regression/score/res.py | 20 +++++ torchcp/regression/utils/metrics.py | 14 ++++ 6 files changed, 179 insertions(+), 2 deletions(-) create mode 100644 examples/regression_selection_synthetic.py create mode 100644 torchcp/regression/predictor/selection.py create mode 100644 torchcp/regression/score/res.py diff --git a/examples/regression_selection_synthetic.py b/examples/regression_selection_synthetic.py new file mode 100644 index 0000000..6e853fc --- /dev/null +++ b/examples/regression_selection_synthetic.py @@ -0,0 +1,47 @@ +# Copyright (c) 2023-present, SUSTech-ML. +# All rights reserved. +# +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. +# + + +import numpy as np +import torch +from sklearn.preprocessing import StandardScaler +from torch.utils.data import TensorDataset +from tqdm import tqdm +import torch.nn as nn +import torch.optim as optim + +from examples.regression_cqr_synthetic import prepare_dataset +from torchcp.regression.predictor import Selector +from torchcp.regression.score import RES +from torchcp.regression.utils import build_regression_model + +if __name__ == "__main__": + # get dataloader + train_loader, cal_loader, test_loader = prepare_dataset(train_ratio=0.4, cal_ratio=0.2, batch_size=128) + # build regression model + device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") + model = build_regression_model("NonLinearNet")(next(iter(train_loader))[0].shape[1], 1, 64, 0.5).to(device) + + # train model + epochs = 100 + criterion = nn.MSELoss() + lr = 0.01 + optimizer = optim.Adam(model.parameters(), lr=lr) + + for tmp_x, tmp_y in train_loader: + outputs = model(tmp_x.to(device)) + loss = criterion(outputs, tmp_y.reshape(-1, 1).to(device)) + optimizer.zero_grad() + loss.backward() + optimizer.step() + + # Conformal Selection + thresholds = torch.ones(len(test_loader.dataset)) * 5 + + selector = Selector(score_function=RES(), model=model) + selector.calibrate(cal_loader) + print(selector.evaluate(test_loader, thresholds)) diff --git a/torchcp/regression/predictor/__init__.py b/torchcp/regression/predictor/__init__.py index 7eb405f..714d8ed 100644 --- a/torchcp/regression/predictor/__init__.py +++ b/torchcp/regression/predictor/__init__.py @@ -9,4 +9,5 @@ from .ensemble import EnsemblePredictor from .split import SplitPredictor from .agaci import AgACIPredictor -from .cpd import ConformalPredictiveDistribution \ No newline at end of file +from .cpd import ConformalPredictiveDistribution +from .selection import Selector \ No newline at end of file diff --git a/torchcp/regression/predictor/selection.py b/torchcp/regression/predictor/selection.py new file mode 100644 index 0000000..262947b --- /dev/null +++ b/torchcp/regression/predictor/selection.py @@ -0,0 +1,94 @@ +# Copyright (c) 2023-present, SUSTech-ML. +# All rights reserved. +# +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. +# + + +import torch + +from torchcp.regression.predictor.split import SplitPredictor +from torchcp.regression.utils.metrics import Metrics +from torchcp.utils.common import get_device + + +class Selector(SplitPredictor): + """ + Conformal Selection. + + a screening procedure that aims to select candidates whose unobserved outcomes exceed user-specified value. + + Args: + score_function (torchcp.regression.scores): A class that implements the score function. + model (torch.nn.Module): A PyTorch model capable of outputting quantile values. + The model should be an initialization model that has not been trained. + alpha (float, optional): The significance level. Default is 0.1. + device (torch.device, optional): The device on which the model is located. Default is None. + + Reference: + Paper: Selection by Prediction with Conformal p-values (Jin et al., 2023) + Link: https://arxiv.org/pdf/2210.01408 + Github: https://github.com/ying531/conformal-selection + """ + + def __init__(self, score_function, model, alpha=0.1, device=None): + super().__init__(score_function, model, alpha, device) + self._metric = Metrics() + + def calibrate(self, cal_dataloader): + self._model.eval() + predicts_list, y_truth_list = [], [] + with torch.no_grad(): + for tmp_x, tmp_labels in cal_dataloader: + tmp_x, tmp_labels = tmp_x.to(self._device), tmp_labels.to(self._device) + tmp_predicts = self._model(tmp_x).detach() + predicts_list.append(tmp_predicts) + y_truth_list.append(tmp_labels) + + predicts = torch.cat(predicts_list).float().to(self._device) + y_truth = torch.cat(y_truth_list).to(self._device) + self.cal_scores = self.score_function(predicts, y_truth) + + + def evaluate(self, data_loader, thresholds): + self._model.eval() + y_truth_list = [] + predicts_list = [] + with torch.no_grad(): + for examples in data_loader: + tmp_x, tmp_labels = examples[0].to(self._device), examples[1].to(self._device) + tmp_predicts = self._model(tmp_x).detach() + predicts_list.append(tmp_predicts) + y_truth_list.append(tmp_labels) + predicts = torch.cat(predicts_list).float().to(self._device) + y_truth = torch.cat(y_truth_list).to(self._device) + scores = self.score_function(predicts, thresholds) + + n_cal, n_test = self.cal_scores.shape[0], scores.shape[0] + + # Compute p-values with tie-breaking + u = torch.rand(n_test) + count_less = (self.cal_scores.view(1, n_cal) < scores.view(n_test, 1)).sum(dim=1) + count_tie = (self.cal_scores.view(1, n_cal) == scores.view(n_test, 1)).sum(dim=1) + 1 + p_values = (count_less + count_tie * u) / (n_cal + 1) + p_values= torch.sort(p_values)[0] + + # Conduct BH procedure + k_range = torch.arange(1, n_test + 1, device=p_values.device) + thresholds = k_range * self.alpha / n_test + mask = p_values <= thresholds + k_star = torch.max(torch.where(mask, k_range, torch.zeros_like(k_range))) if mask.any() else 0 + threshold = (k_star * self.alpha / n_test) if k_star > 0 else 0 + + # Get indices where p_values <= threshold + indices = torch.nonzero(p_values <= threshold, as_tuple=False).squeeze() + + if indices.dim() == 0: + indices = indices.unsqueeze(0) + + # Evaluation + res_dict = {"false_discovery_proportion": self._metric("false_discovery_proportion")(y_truth, thresholds, + indices), + "power": self._metric("power")(y_truth, thresholds, indices)} + return res_dict diff --git a/torchcp/regression/score/__init__.py b/torchcp/regression/score/__init__.py index a68921b..7964058 100644 --- a/torchcp/regression/score/__init__.py +++ b/torchcp/regression/score/__init__.py @@ -12,4 +12,5 @@ from .cqrm import CQRM from .cqrr import CQRR from .r2ccp import R2CCP -from .sign import Sign \ No newline at end of file +from .sign import Sign +from .res import RES \ No newline at end of file diff --git a/torchcp/regression/score/res.py b/torchcp/regression/score/res.py new file mode 100644 index 0000000..cd1667b --- /dev/null +++ b/torchcp/regression/score/res.py @@ -0,0 +1,20 @@ +# Copyright (c) 2023-present, SUSTech-ML. +# All rights reserved. +# +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. +# + + +from torchcp.regression.score.base import BaseScore + + +class RES(BaseScore): + """ + RES score (Jin et al., 2023) + paper: https://arxiv.org/pdf/2210.01408 + """ + def __call__(self, predicts, y_truth): + if len(predicts.shape) == 2: + predicts = predicts.squeeze().view(-1) + return y_truth - predicts diff --git a/torchcp/regression/utils/metrics.py b/torchcp/regression/utils/metrics.py index 0303ea9..0e75665 100644 --- a/torchcp/regression/utils/metrics.py +++ b/torchcp/regression/utils/metrics.py @@ -71,6 +71,20 @@ def average_size(prediction_intervals): return average_size +@METRICS_REGISTRY_REGRESSION.register() +def false_discovery_proportion(y_truth, thresholds, indices): + false_positives = torch.sum(y_truth[indices] <= thresholds[indices]) + fdp = false_positives / indices.shape[-1] if indices.shape[-1] > 0 else torch.tensor(0.) + return fdp.item() + + +@METRICS_REGISTRY_REGRESSION.register() +def power(y_truth, thresholds, indices): + true_positives = torch.sum(y_truth[indices] > thresholds[indices]) + power = true_positives / torch.sum(y_truth > thresholds) + return power.item() + + class Metrics: def __call__(self, metric) -> Any: if metric not in METRICS_REGISTRY_REGRESSION.registered_names(): From 8451dd3cef565df9f1bc831f07bf5ec2de9fb27e Mon Sep 17 00:00:00 2001 From: 33798 <3379854453@qq.com> Date: Sun, 31 Aug 2025 14:04:55 +0800 Subject: [PATCH 2/8] conformal selection --- examples/regression_selection_synthetic.py | 56 +++++++++++----------- torchcp/regression/predictor/selection.py | 23 +++++++-- torchcp/regression/utils/metrics.py | 29 +++++++++++ 3 files changed, 76 insertions(+), 32 deletions(-) diff --git a/examples/regression_selection_synthetic.py b/examples/regression_selection_synthetic.py index 6e853fc..c00580f 100644 --- a/examples/regression_selection_synthetic.py +++ b/examples/regression_selection_synthetic.py @@ -15,33 +15,33 @@ import torch.optim as optim from examples.regression_cqr_synthetic import prepare_dataset -from torchcp.regression.predictor import Selector -from torchcp.regression.score import RES +from torchcp.regression.predictor import ConformalSelector +from torchcp.regression.score import Sign from torchcp.regression.utils import build_regression_model -if __name__ == "__main__": - # get dataloader - train_loader, cal_loader, test_loader = prepare_dataset(train_ratio=0.4, cal_ratio=0.2, batch_size=128) - # build regression model - device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") - model = build_regression_model("NonLinearNet")(next(iter(train_loader))[0].shape[1], 1, 64, 0.5).to(device) - - # train model - epochs = 100 - criterion = nn.MSELoss() - lr = 0.01 - optimizer = optim.Adam(model.parameters(), lr=lr) - - for tmp_x, tmp_y in train_loader: - outputs = model(tmp_x.to(device)) - loss = criterion(outputs, tmp_y.reshape(-1, 1).to(device)) - optimizer.zero_grad() - loss.backward() - optimizer.step() - - # Conformal Selection - thresholds = torch.ones(len(test_loader.dataset)) * 5 - - selector = Selector(score_function=RES(), model=model) - selector.calibrate(cal_loader) - print(selector.evaluate(test_loader, thresholds)) + +# get dataloader +train_loader, cal_loader, test_loader = prepare_dataset(train_ratio=0.4, cal_ratio=0.2, batch_size=128) +# build regression model +device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") +model = build_regression_model("NonLinearNet")(next(iter(train_loader))[0].shape[1], 1, 64, 0.5).to(device) + +# train model +epochs = 100 +criterion = nn.MSELoss() +lr = 0.01 +optimizer = optim.Adam(model.parameters(), lr=lr) + +for tmp_x, tmp_y in train_loader: + outputs = model(tmp_x.to(device)) + loss = criterion(outputs, tmp_y.reshape(-1, 1).to(device)) + optimizer.zero_grad() + loss.backward() + optimizer.step() + +# Conformal Selection +thresholds = torch.ones(len(test_loader.dataset)) * 5 + +selector = ConformalSelector(score_function=Sign(), model=model) +selector.calibrate(cal_loader) +print(selector.evaluate(test_loader, thresholds)) diff --git a/torchcp/regression/predictor/selection.py b/torchcp/regression/predictor/selection.py index 262947b..98bb3fa 100644 --- a/torchcp/regression/predictor/selection.py +++ b/torchcp/regression/predictor/selection.py @@ -13,7 +13,7 @@ from torchcp.utils.common import get_device -class Selector(SplitPredictor): +class ConformalSelector(SplitPredictor): """ Conformal Selection. @@ -52,6 +52,24 @@ def calibrate(self, cal_dataloader): def evaluate(self, data_loader, thresholds): + """ + Evaluate the performance of conformal selection on a test dataset by calculating false discovery proportion + (FDP) and power of the selection set. + + Args: + data_loader (DataLoader): The DataLoader providing the test data batches. + thresholds (torch.Tensor): A tensor of user-defined thresholds. + + Returns: + dict: A dictionary containing: + - "False discovery proportion": The FDP of the selection set. + - "Power": The power of the selection set. + + Example:: + + >>> eval_results = selector.evaluate(test_loader, thresholds) + >>> print(eval_results) + """ self._model.eval() y_truth_list = [] predicts_list = [] @@ -84,9 +102,6 @@ def evaluate(self, data_loader, thresholds): # Get indices where p_values <= threshold indices = torch.nonzero(p_values <= threshold, as_tuple=False).squeeze() - if indices.dim() == 0: - indices = indices.unsqueeze(0) - # Evaluation res_dict = {"false_discovery_proportion": self._metric("false_discovery_proportion")(y_truth, thresholds, indices), diff --git a/torchcp/regression/utils/metrics.py b/torchcp/regression/utils/metrics.py index 0e75665..cdfae02 100644 --- a/torchcp/regression/utils/metrics.py +++ b/torchcp/regression/utils/metrics.py @@ -73,6 +73,21 @@ def average_size(prediction_intervals): @METRICS_REGISTRY_REGRESSION.register() def false_discovery_proportion(y_truth, thresholds, indices): + """ + Conpute the false discovery proportion (the proportion of false discovery among all selected points) of the + selection set. + + Args: + y_truth (torch.Tensor): A tensor of ground truth values. + thresholds (torch.Tensor): Tensor of user-defined thresholds. + indices (torch.Tensor): A tensor containing the indices of selected points. + + Returns: + torch.Tensor: The false discovery proportion of the selection set. + """ + if indices.dim() == 0: + indices = indices.unsqueeze(0) + false_positives = torch.sum(y_truth[indices] <= thresholds[indices]) fdp = false_positives / indices.shape[-1] if indices.shape[-1] > 0 else torch.tensor(0.) return fdp.item() @@ -80,6 +95,20 @@ def false_discovery_proportion(y_truth, thresholds, indices): @METRICS_REGISTRY_REGRESSION.register() def power(y_truth, thresholds, indices): + """ + Conpute the power (the proportion of desirable points that are correctly selected) of the selection set. + + Args: + y_truth (torch.Tensor): A tensor of ground truth values. + thresholds (torch.Tensor): Tensor of user-defined thresholds. + indices (torch.Tensor): A tensor containing the indices of selected points. + + Returns: + torch.Tensor: The power of the selection set. + """ + if indices.dim() == 0: + indices = indices.unsqueeze(0) + true_positives = torch.sum(y_truth[indices] > thresholds[indices]) power = true_positives / torch.sum(y_truth > thresholds) return power.item() From d19b5c9fbab9a6f4c10b583ab5b12eb002a7330c Mon Sep 17 00:00:00 2001 From: 33798 <3379854453@qq.com> Date: Sun, 31 Aug 2025 15:28:22 +0800 Subject: [PATCH 3/8] conformal selection --- torchcp/regression/predictor/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/torchcp/regression/predictor/__init__.py b/torchcp/regression/predictor/__init__.py index 714d8ed..0852cb7 100644 --- a/torchcp/regression/predictor/__init__.py +++ b/torchcp/regression/predictor/__init__.py @@ -10,4 +10,4 @@ from .split import SplitPredictor from .agaci import AgACIPredictor from .cpd import ConformalPredictiveDistribution -from .selection import Selector \ No newline at end of file +from .selection import ConformalSelector \ No newline at end of file From 18b88471f25e1369fec7c4b43b4333403399de50 Mon Sep 17 00:00:00 2001 From: 33798 <3379854453@qq.com> Date: Sat, 18 Oct 2025 15:50:23 +0800 Subject: [PATCH 4/8] conformal selection --- examples/regression_selection_synthetic.py | 13 ++-- torchcp/classification/loss/__init__.py | 1 - torchcp/classification/trainer/__init__.py | 1 - torchcp/regression/predictor/__init__.py | 1 - torchcp/regression/score/__init__.py | 1 - torchcp/selection/__init__.py | 0 torchcp/selection/score/__init__.py | 10 +++ torchcp/selection/score/clip.py | 21 ++++++ .../{regression => selection}/score/res.py | 0 torchcp/selection/selector/__init__.py | 8 +++ .../selector}/selection.py | 23 ++----- .../selection/testing_correction/__init__.py | 10 +++ torchcp/selection/testing_correction/base.py | 27 ++++++++ .../testing_correction/bh_procedure.py | 49 ++++++++++++++ torchcp/selection/utils/__init__.py | 0 torchcp/selection/utils/metric.py | 64 +++++++++++++++++++ torchcp/utils/metrics.py | 0 17 files changed, 201 insertions(+), 28 deletions(-) create mode 100644 torchcp/selection/__init__.py create mode 100644 torchcp/selection/score/__init__.py create mode 100644 torchcp/selection/score/clip.py rename torchcp/{regression => selection}/score/res.py (100%) create mode 100644 torchcp/selection/selector/__init__.py rename torchcp/{regression/predictor => selection/selector}/selection.py (81%) create mode 100644 torchcp/selection/testing_correction/__init__.py create mode 100644 torchcp/selection/testing_correction/base.py create mode 100644 torchcp/selection/testing_correction/bh_procedure.py create mode 100644 torchcp/selection/utils/__init__.py create mode 100644 torchcp/selection/utils/metric.py create mode 100644 torchcp/utils/metrics.py diff --git a/examples/regression_selection_synthetic.py b/examples/regression_selection_synthetic.py index c00580f..252305a 100644 --- a/examples/regression_selection_synthetic.py +++ b/examples/regression_selection_synthetic.py @@ -6,18 +6,15 @@ # -import numpy as np import torch -from sklearn.preprocessing import StandardScaler -from torch.utils.data import TensorDataset -from tqdm import tqdm import torch.nn as nn import torch.optim as optim from examples.regression_cqr_synthetic import prepare_dataset -from torchcp.regression.predictor import ConformalSelector -from torchcp.regression.score import Sign from torchcp.regression.utils import build_regression_model +from torchcp.selection.score import RES +from torchcp.selection.selector import ConformalSelector +from torchcp.selection.testing_correction import BH_procedure # get dataloader @@ -42,6 +39,6 @@ # Conformal Selection thresholds = torch.ones(len(test_loader.dataset)) * 5 -selector = ConformalSelector(score_function=Sign(), model=model) +selector = ConformalSelector(score_function=RES(), testing_correction=BH_procedure(), model=model) selector.calibrate(cal_loader) -print(selector.evaluate(test_loader, thresholds)) +print(selector.select(test_loader, thresholds)) diff --git a/torchcp/classification/loss/__init__.py b/torchcp/classification/loss/__init__.py index c163bbc..83cb900 100644 --- a/torchcp/classification/loss/__init__.py +++ b/torchcp/classification/loss/__init__.py @@ -8,5 +8,4 @@ from .cd import CDLoss from .conftr import ConfTrLoss from .confts import ConfTSLoss -from .uncertainty_aware import UncertaintyAwareLoss from .scpo import SCPOLoss \ No newline at end of file diff --git a/torchcp/classification/trainer/__init__.py b/torchcp/classification/trainer/__init__.py index 52161e4..a3538ef 100644 --- a/torchcp/classification/trainer/__init__.py +++ b/torchcp/classification/trainer/__init__.py @@ -10,6 +10,5 @@ from .confts_trainer import ConfTSTrainer from .model_zoo import TemperatureScalingModel from .ts_trainer import TSTrainer -from .ua_trainer import UncertaintyAwareTrainer from .ordinal_trainer import OrdinalTrainer from .scpo_trainer import SCPOTrainer \ No newline at end of file diff --git a/torchcp/regression/predictor/__init__.py b/torchcp/regression/predictor/__init__.py index 0852cb7..77bcbc3 100644 --- a/torchcp/regression/predictor/__init__.py +++ b/torchcp/regression/predictor/__init__.py @@ -10,4 +10,3 @@ from .split import SplitPredictor from .agaci import AgACIPredictor from .cpd import ConformalPredictiveDistribution -from .selection import ConformalSelector \ No newline at end of file diff --git a/torchcp/regression/score/__init__.py b/torchcp/regression/score/__init__.py index 7964058..1519448 100644 --- a/torchcp/regression/score/__init__.py +++ b/torchcp/regression/score/__init__.py @@ -13,4 +13,3 @@ from .cqrr import CQRR from .r2ccp import R2CCP from .sign import Sign -from .res import RES \ No newline at end of file diff --git a/torchcp/selection/__init__.py b/torchcp/selection/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/torchcp/selection/score/__init__.py b/torchcp/selection/score/__init__.py new file mode 100644 index 0000000..bb9d168 --- /dev/null +++ b/torchcp/selection/score/__init__.py @@ -0,0 +1,10 @@ +# Copyright (c) 2023-present, SUSTech-ML. +# All rights reserved. +# +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. +# + + +from .clip import CLIP +from .res import RES \ No newline at end of file diff --git a/torchcp/selection/score/clip.py b/torchcp/selection/score/clip.py new file mode 100644 index 0000000..889843a --- /dev/null +++ b/torchcp/selection/score/clip.py @@ -0,0 +1,21 @@ +# Copyright (c) 2023-present, SUSTech-ML. +# All rights reserved. +# +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. +# + + +import torch +from torchcp.regression.score.base import BaseScore + + +class CLIP(BaseScore): + """ + CLIP score (Jin et al., 2023), only apply to binary classification. + paper: https://arxiv.org/pdf/2210.01408 + """ + def __call__(self, predicts, y_truth, M=100): + if len(predicts.shape) == 2: + predicts = predicts.squeeze().view(-1) + return M * torch.max(predicts, 0) - predicts diff --git a/torchcp/regression/score/res.py b/torchcp/selection/score/res.py similarity index 100% rename from torchcp/regression/score/res.py rename to torchcp/selection/score/res.py diff --git a/torchcp/selection/selector/__init__.py b/torchcp/selection/selector/__init__.py new file mode 100644 index 0000000..5e15819 --- /dev/null +++ b/torchcp/selection/selector/__init__.py @@ -0,0 +1,8 @@ +# Copyright (c) 2023-present, SUSTech-ML. +# All rights reserved. +# +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. +# + +from .selection import ConformalSelector \ No newline at end of file diff --git a/torchcp/regression/predictor/selection.py b/torchcp/selection/selector/selection.py similarity index 81% rename from torchcp/regression/predictor/selection.py rename to torchcp/selection/selector/selection.py index 98bb3fa..537b86a 100644 --- a/torchcp/regression/predictor/selection.py +++ b/torchcp/selection/selector/selection.py @@ -10,14 +10,12 @@ from torchcp.regression.predictor.split import SplitPredictor from torchcp.regression.utils.metrics import Metrics -from torchcp.utils.common import get_device class ConformalSelector(SplitPredictor): """ - Conformal Selection. - - a screening procedure that aims to select candidates whose unobserved outcomes exceed user-specified value. + Conformal Selection: + a screening procedure that aims to select candidates whose unobserved outcomes exceed user-specified value. Args: score_function (torchcp.regression.scores): A class that implements the score function. @@ -32,10 +30,12 @@ class ConformalSelector(SplitPredictor): Github: https://github.com/ying531/conformal-selection """ - def __init__(self, score_function, model, alpha=0.1, device=None): + def __init__(self, score_function, testing_correction, model, alpha=0.1, device=None): super().__init__(score_function, model, alpha, device) + self.testing_correction = testing_correction self._metric = Metrics() + def calibrate(self, cal_dataloader): self._model.eval() predicts_list, y_truth_list = [], [] @@ -51,7 +51,7 @@ def calibrate(self, cal_dataloader): self.cal_scores = self.score_function(predicts, y_truth) - def evaluate(self, data_loader, thresholds): + def select(self, data_loader, thresholds): """ Evaluate the performance of conformal selection on a test dataset by calculating false discovery proportion (FDP) and power of the selection set. @@ -90,17 +90,8 @@ def evaluate(self, data_loader, thresholds): count_less = (self.cal_scores.view(1, n_cal) < scores.view(n_test, 1)).sum(dim=1) count_tie = (self.cal_scores.view(1, n_cal) == scores.view(n_test, 1)).sum(dim=1) + 1 p_values = (count_less + count_tie * u) / (n_cal + 1) - p_values= torch.sort(p_values)[0] - - # Conduct BH procedure - k_range = torch.arange(1, n_test + 1, device=p_values.device) - thresholds = k_range * self.alpha / n_test - mask = p_values <= thresholds - k_star = torch.max(torch.where(mask, k_range, torch.zeros_like(k_range))) if mask.any() else 0 - threshold = (k_star * self.alpha / n_test) if k_star > 0 else 0 - # Get indices where p_values <= threshold - indices = torch.nonzero(p_values <= threshold, as_tuple=False).squeeze() + indices = self.testing_correction(p_values, self.alpha) # Evaluation res_dict = {"false_discovery_proportion": self._metric("false_discovery_proportion")(y_truth, thresholds, diff --git a/torchcp/selection/testing_correction/__init__.py b/torchcp/selection/testing_correction/__init__.py new file mode 100644 index 0000000..a0f6b8e --- /dev/null +++ b/torchcp/selection/testing_correction/__init__.py @@ -0,0 +1,10 @@ +# Copyright (c) 2023-present, SUSTech-ML. +# All rights reserved. +# +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. +# + + +from .base import Base +from .bh_procedure import BH_procedure \ No newline at end of file diff --git a/torchcp/selection/testing_correction/base.py b/torchcp/selection/testing_correction/base.py new file mode 100644 index 0000000..6f18b93 --- /dev/null +++ b/torchcp/selection/testing_correction/base.py @@ -0,0 +1,27 @@ +# Copyright (c) 2023-present, SUSTech-ML. +# All rights reserved. +# +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. +# + + +from abc import ABCMeta, abstractmethod +import torch +from tqdm import tqdm + +from torchcp.utils.common import get_device + + +class Base(object): + """ + Abstract base class for all multiple testing correction algorithms. + """ + __metaclass__ = ABCMeta + + def __init__(self) -> None: + pass + + @abstractmethod + def __call__(self, p_values, alpha): + raise NotImplementedError diff --git a/torchcp/selection/testing_correction/bh_procedure.py b/torchcp/selection/testing_correction/bh_procedure.py new file mode 100644 index 0000000..66e1705 --- /dev/null +++ b/torchcp/selection/testing_correction/bh_procedure.py @@ -0,0 +1,49 @@ +# Copyright (c) 2023-present, SUSTech-ML. +# All rights reserved. +# +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. +# + + +import torch + +from torchcp.regression.score.base import BaseScore + + +class BH_procedure(BaseScore): + """ + Benjamini-Hochberg (BH) procedure: + finds a p-value threshold from a list of p-values to determine which null hypotheses to reject, given a target + FDR level 'alpha'. + + References: + Paper: Controlling the False Discovery Rate: A Practical and Powerful Approach to Multiple Testing + (Benjamini and Hochberg, 1995) + Link: https://www.jstor.org/stable/2346101 + """ + def __init__(self): + super().__init__() + + def __call__(self, p_values, alpha): + """ + Apply the Benjamini-Hochberg procedure. + + Args: + p_values (torch.Tensor): A 1D tensor of p-values. + alpha (float): The desired False Discovery Rate (FDR) level (e.g., 0.1). + + Returns: + torch.Tensor: A 1D tensor of indices corresponding to the p-values (hypotheses) that are rejected. + """ + p_values_sorted, _ = torch.sort(p_values) + n_test = p_values_sorted.shape[0] + + k_range = torch.arange(1, n_test + 1, device=p_values_sorted.device) + thresholds = k_range * alpha / n_test + mask = p_values_sorted <= thresholds + k_star = torch.max(torch.where(mask, k_range, torch.zeros_like(k_range))) if mask.any() else 0 + threshold = (k_star * alpha / n_test) if k_star > 0 else 0 + indices = torch.nonzero(p_values <= threshold, as_tuple=False).squeeze() + + return indices diff --git a/torchcp/selection/utils/__init__.py b/torchcp/selection/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/torchcp/selection/utils/metric.py b/torchcp/selection/utils/metric.py new file mode 100644 index 0000000..5f71b09 --- /dev/null +++ b/torchcp/selection/utils/metric.py @@ -0,0 +1,64 @@ +# Copyright (c) 2023-present, SUSTech-ML. +# All rights reserved. +# +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. +# + +from typing import Any + +import torch + +from torchcp.utils.registry import Registry + +METRICS_REGISTRY_REGRESSION = Registry("METRICS") + + +@METRICS_REGISTRY_REGRESSION.register() +def false_discovery_proportion(y_truth, thresholds, indices): + """ + Conpute the false discovery proportion (the proportion of false discovery among all selected points) of the + selection set. + + Args: + y_truth (torch.Tensor): A tensor of ground truth values. + thresholds (torch.Tensor): Tensor of user-defined thresholds. + indices (torch.Tensor): A tensor containing the indices of selected points. + + Returns: + torch.Tensor: The false discovery proportion of the selection set. + """ + if indices.dim() == 0: + indices = indices.unsqueeze(0) + + false_positives = torch.sum(y_truth[indices] <= thresholds[indices]) + fdp = false_positives / indices.shape[-1] if indices.shape[-1] > 0 else torch.tensor(0.) + return fdp.item() + + +@METRICS_REGISTRY_REGRESSION.register() +def power(y_truth, thresholds, indices): + """ + Conpute the power (the proportion of desirable points that are correctly selected) of the selection set. + + Args: + y_truth (torch.Tensor): A tensor of ground truth values. + thresholds (torch.Tensor): Tensor of user-defined thresholds. + indices (torch.Tensor): A tensor containing the indices of selected points. + + Returns: + torch.Tensor: The power of the selection set. + """ + if indices.dim() == 0: + indices = indices.unsqueeze(0) + + true_positives = torch.sum(y_truth[indices] > thresholds[indices]) + power = true_positives / torch.sum(y_truth > thresholds) + return power.item() + + +class Metrics: + def __call__(self, metric) -> Any: + if metric not in METRICS_REGISTRY_REGRESSION.registered_names(): + raise NameError(f"The metric: {metric} is not defined in TorchCP.") + return METRICS_REGISTRY_REGRESSION.get(metric) \ No newline at end of file diff --git a/torchcp/utils/metrics.py b/torchcp/utils/metrics.py new file mode 100644 index 0000000..e69de29 From 63a0a2d144de8d0676cc4ad712abfb72415b9747 Mon Sep 17 00:00:00 2001 From: 33798 <3379854453@qq.com> Date: Sat, 18 Oct 2025 15:50:23 +0800 Subject: [PATCH 5/8] conformal selection --- examples/regression_selection_synthetic.py | 13 ++-- torchcp/classification/loss/__init__.py | 1 - torchcp/classification/trainer/__init__.py | 1 - torchcp/regression/predictor/__init__.py | 1 - torchcp/regression/score/__init__.py | 1 - torchcp/selection/__init__.py | 0 torchcp/selection/score/__init__.py | 10 +++ torchcp/selection/score/clip.py | 21 ++++++ .../{regression => selection}/score/res.py | 0 torchcp/selection/selector/__init__.py | 8 +++ .../selector}/selection.py | 23 ++----- .../selection/testing_correction/__init__.py | 10 +++ torchcp/selection/testing_correction/base.py | 27 ++++++++ .../testing_correction/bh_procedure.py | 49 ++++++++++++++ torchcp/selection/utils/__init__.py | 0 torchcp/selection/utils/metric.py | 64 +++++++++++++++++++ torchcp/utils/metrics.py | 0 17 files changed, 201 insertions(+), 28 deletions(-) create mode 100644 torchcp/selection/__init__.py create mode 100644 torchcp/selection/score/__init__.py create mode 100644 torchcp/selection/score/clip.py rename torchcp/{regression => selection}/score/res.py (100%) create mode 100644 torchcp/selection/selector/__init__.py rename torchcp/{regression/predictor => selection/selector}/selection.py (81%) create mode 100644 torchcp/selection/testing_correction/__init__.py create mode 100644 torchcp/selection/testing_correction/base.py create mode 100644 torchcp/selection/testing_correction/bh_procedure.py create mode 100644 torchcp/selection/utils/__init__.py create mode 100644 torchcp/selection/utils/metric.py create mode 100644 torchcp/utils/metrics.py diff --git a/examples/regression_selection_synthetic.py b/examples/regression_selection_synthetic.py index c00580f..252305a 100644 --- a/examples/regression_selection_synthetic.py +++ b/examples/regression_selection_synthetic.py @@ -6,18 +6,15 @@ # -import numpy as np import torch -from sklearn.preprocessing import StandardScaler -from torch.utils.data import TensorDataset -from tqdm import tqdm import torch.nn as nn import torch.optim as optim from examples.regression_cqr_synthetic import prepare_dataset -from torchcp.regression.predictor import ConformalSelector -from torchcp.regression.score import Sign from torchcp.regression.utils import build_regression_model +from torchcp.selection.score import RES +from torchcp.selection.selector import ConformalSelector +from torchcp.selection.testing_correction import BH_procedure # get dataloader @@ -42,6 +39,6 @@ # Conformal Selection thresholds = torch.ones(len(test_loader.dataset)) * 5 -selector = ConformalSelector(score_function=Sign(), model=model) +selector = ConformalSelector(score_function=RES(), testing_correction=BH_procedure(), model=model) selector.calibrate(cal_loader) -print(selector.evaluate(test_loader, thresholds)) +print(selector.select(test_loader, thresholds)) diff --git a/torchcp/classification/loss/__init__.py b/torchcp/classification/loss/__init__.py index c163bbc..83cb900 100644 --- a/torchcp/classification/loss/__init__.py +++ b/torchcp/classification/loss/__init__.py @@ -8,5 +8,4 @@ from .cd import CDLoss from .conftr import ConfTrLoss from .confts import ConfTSLoss -from .uncertainty_aware import UncertaintyAwareLoss from .scpo import SCPOLoss \ No newline at end of file diff --git a/torchcp/classification/trainer/__init__.py b/torchcp/classification/trainer/__init__.py index 52161e4..a3538ef 100644 --- a/torchcp/classification/trainer/__init__.py +++ b/torchcp/classification/trainer/__init__.py @@ -10,6 +10,5 @@ from .confts_trainer import ConfTSTrainer from .model_zoo import TemperatureScalingModel from .ts_trainer import TSTrainer -from .ua_trainer import UncertaintyAwareTrainer from .ordinal_trainer import OrdinalTrainer from .scpo_trainer import SCPOTrainer \ No newline at end of file diff --git a/torchcp/regression/predictor/__init__.py b/torchcp/regression/predictor/__init__.py index 0852cb7..77bcbc3 100644 --- a/torchcp/regression/predictor/__init__.py +++ b/torchcp/regression/predictor/__init__.py @@ -10,4 +10,3 @@ from .split import SplitPredictor from .agaci import AgACIPredictor from .cpd import ConformalPredictiveDistribution -from .selection import ConformalSelector \ No newline at end of file diff --git a/torchcp/regression/score/__init__.py b/torchcp/regression/score/__init__.py index 7964058..1519448 100644 --- a/torchcp/regression/score/__init__.py +++ b/torchcp/regression/score/__init__.py @@ -13,4 +13,3 @@ from .cqrr import CQRR from .r2ccp import R2CCP from .sign import Sign -from .res import RES \ No newline at end of file diff --git a/torchcp/selection/__init__.py b/torchcp/selection/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/torchcp/selection/score/__init__.py b/torchcp/selection/score/__init__.py new file mode 100644 index 0000000..bb9d168 --- /dev/null +++ b/torchcp/selection/score/__init__.py @@ -0,0 +1,10 @@ +# Copyright (c) 2023-present, SUSTech-ML. +# All rights reserved. +# +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. +# + + +from .clip import CLIP +from .res import RES \ No newline at end of file diff --git a/torchcp/selection/score/clip.py b/torchcp/selection/score/clip.py new file mode 100644 index 0000000..889843a --- /dev/null +++ b/torchcp/selection/score/clip.py @@ -0,0 +1,21 @@ +# Copyright (c) 2023-present, SUSTech-ML. +# All rights reserved. +# +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. +# + + +import torch +from torchcp.regression.score.base import BaseScore + + +class CLIP(BaseScore): + """ + CLIP score (Jin et al., 2023), only apply to binary classification. + paper: https://arxiv.org/pdf/2210.01408 + """ + def __call__(self, predicts, y_truth, M=100): + if len(predicts.shape) == 2: + predicts = predicts.squeeze().view(-1) + return M * torch.max(predicts, 0) - predicts diff --git a/torchcp/regression/score/res.py b/torchcp/selection/score/res.py similarity index 100% rename from torchcp/regression/score/res.py rename to torchcp/selection/score/res.py diff --git a/torchcp/selection/selector/__init__.py b/torchcp/selection/selector/__init__.py new file mode 100644 index 0000000..5e15819 --- /dev/null +++ b/torchcp/selection/selector/__init__.py @@ -0,0 +1,8 @@ +# Copyright (c) 2023-present, SUSTech-ML. +# All rights reserved. +# +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. +# + +from .selection import ConformalSelector \ No newline at end of file diff --git a/torchcp/regression/predictor/selection.py b/torchcp/selection/selector/selection.py similarity index 81% rename from torchcp/regression/predictor/selection.py rename to torchcp/selection/selector/selection.py index 98bb3fa..537b86a 100644 --- a/torchcp/regression/predictor/selection.py +++ b/torchcp/selection/selector/selection.py @@ -10,14 +10,12 @@ from torchcp.regression.predictor.split import SplitPredictor from torchcp.regression.utils.metrics import Metrics -from torchcp.utils.common import get_device class ConformalSelector(SplitPredictor): """ - Conformal Selection. - - a screening procedure that aims to select candidates whose unobserved outcomes exceed user-specified value. + Conformal Selection: + a screening procedure that aims to select candidates whose unobserved outcomes exceed user-specified value. Args: score_function (torchcp.regression.scores): A class that implements the score function. @@ -32,10 +30,12 @@ class ConformalSelector(SplitPredictor): Github: https://github.com/ying531/conformal-selection """ - def __init__(self, score_function, model, alpha=0.1, device=None): + def __init__(self, score_function, testing_correction, model, alpha=0.1, device=None): super().__init__(score_function, model, alpha, device) + self.testing_correction = testing_correction self._metric = Metrics() + def calibrate(self, cal_dataloader): self._model.eval() predicts_list, y_truth_list = [], [] @@ -51,7 +51,7 @@ def calibrate(self, cal_dataloader): self.cal_scores = self.score_function(predicts, y_truth) - def evaluate(self, data_loader, thresholds): + def select(self, data_loader, thresholds): """ Evaluate the performance of conformal selection on a test dataset by calculating false discovery proportion (FDP) and power of the selection set. @@ -90,17 +90,8 @@ def evaluate(self, data_loader, thresholds): count_less = (self.cal_scores.view(1, n_cal) < scores.view(n_test, 1)).sum(dim=1) count_tie = (self.cal_scores.view(1, n_cal) == scores.view(n_test, 1)).sum(dim=1) + 1 p_values = (count_less + count_tie * u) / (n_cal + 1) - p_values= torch.sort(p_values)[0] - - # Conduct BH procedure - k_range = torch.arange(1, n_test + 1, device=p_values.device) - thresholds = k_range * self.alpha / n_test - mask = p_values <= thresholds - k_star = torch.max(torch.where(mask, k_range, torch.zeros_like(k_range))) if mask.any() else 0 - threshold = (k_star * self.alpha / n_test) if k_star > 0 else 0 - # Get indices where p_values <= threshold - indices = torch.nonzero(p_values <= threshold, as_tuple=False).squeeze() + indices = self.testing_correction(p_values, self.alpha) # Evaluation res_dict = {"false_discovery_proportion": self._metric("false_discovery_proportion")(y_truth, thresholds, diff --git a/torchcp/selection/testing_correction/__init__.py b/torchcp/selection/testing_correction/__init__.py new file mode 100644 index 0000000..a0f6b8e --- /dev/null +++ b/torchcp/selection/testing_correction/__init__.py @@ -0,0 +1,10 @@ +# Copyright (c) 2023-present, SUSTech-ML. +# All rights reserved. +# +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. +# + + +from .base import Base +from .bh_procedure import BH_procedure \ No newline at end of file diff --git a/torchcp/selection/testing_correction/base.py b/torchcp/selection/testing_correction/base.py new file mode 100644 index 0000000..6f18b93 --- /dev/null +++ b/torchcp/selection/testing_correction/base.py @@ -0,0 +1,27 @@ +# Copyright (c) 2023-present, SUSTech-ML. +# All rights reserved. +# +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. +# + + +from abc import ABCMeta, abstractmethod +import torch +from tqdm import tqdm + +from torchcp.utils.common import get_device + + +class Base(object): + """ + Abstract base class for all multiple testing correction algorithms. + """ + __metaclass__ = ABCMeta + + def __init__(self) -> None: + pass + + @abstractmethod + def __call__(self, p_values, alpha): + raise NotImplementedError diff --git a/torchcp/selection/testing_correction/bh_procedure.py b/torchcp/selection/testing_correction/bh_procedure.py new file mode 100644 index 0000000..66e1705 --- /dev/null +++ b/torchcp/selection/testing_correction/bh_procedure.py @@ -0,0 +1,49 @@ +# Copyright (c) 2023-present, SUSTech-ML. +# All rights reserved. +# +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. +# + + +import torch + +from torchcp.regression.score.base import BaseScore + + +class BH_procedure(BaseScore): + """ + Benjamini-Hochberg (BH) procedure: + finds a p-value threshold from a list of p-values to determine which null hypotheses to reject, given a target + FDR level 'alpha'. + + References: + Paper: Controlling the False Discovery Rate: A Practical and Powerful Approach to Multiple Testing + (Benjamini and Hochberg, 1995) + Link: https://www.jstor.org/stable/2346101 + """ + def __init__(self): + super().__init__() + + def __call__(self, p_values, alpha): + """ + Apply the Benjamini-Hochberg procedure. + + Args: + p_values (torch.Tensor): A 1D tensor of p-values. + alpha (float): The desired False Discovery Rate (FDR) level (e.g., 0.1). + + Returns: + torch.Tensor: A 1D tensor of indices corresponding to the p-values (hypotheses) that are rejected. + """ + p_values_sorted, _ = torch.sort(p_values) + n_test = p_values_sorted.shape[0] + + k_range = torch.arange(1, n_test + 1, device=p_values_sorted.device) + thresholds = k_range * alpha / n_test + mask = p_values_sorted <= thresholds + k_star = torch.max(torch.where(mask, k_range, torch.zeros_like(k_range))) if mask.any() else 0 + threshold = (k_star * alpha / n_test) if k_star > 0 else 0 + indices = torch.nonzero(p_values <= threshold, as_tuple=False).squeeze() + + return indices diff --git a/torchcp/selection/utils/__init__.py b/torchcp/selection/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/torchcp/selection/utils/metric.py b/torchcp/selection/utils/metric.py new file mode 100644 index 0000000..5f71b09 --- /dev/null +++ b/torchcp/selection/utils/metric.py @@ -0,0 +1,64 @@ +# Copyright (c) 2023-present, SUSTech-ML. +# All rights reserved. +# +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. +# + +from typing import Any + +import torch + +from torchcp.utils.registry import Registry + +METRICS_REGISTRY_REGRESSION = Registry("METRICS") + + +@METRICS_REGISTRY_REGRESSION.register() +def false_discovery_proportion(y_truth, thresholds, indices): + """ + Conpute the false discovery proportion (the proportion of false discovery among all selected points) of the + selection set. + + Args: + y_truth (torch.Tensor): A tensor of ground truth values. + thresholds (torch.Tensor): Tensor of user-defined thresholds. + indices (torch.Tensor): A tensor containing the indices of selected points. + + Returns: + torch.Tensor: The false discovery proportion of the selection set. + """ + if indices.dim() == 0: + indices = indices.unsqueeze(0) + + false_positives = torch.sum(y_truth[indices] <= thresholds[indices]) + fdp = false_positives / indices.shape[-1] if indices.shape[-1] > 0 else torch.tensor(0.) + return fdp.item() + + +@METRICS_REGISTRY_REGRESSION.register() +def power(y_truth, thresholds, indices): + """ + Conpute the power (the proportion of desirable points that are correctly selected) of the selection set. + + Args: + y_truth (torch.Tensor): A tensor of ground truth values. + thresholds (torch.Tensor): Tensor of user-defined thresholds. + indices (torch.Tensor): A tensor containing the indices of selected points. + + Returns: + torch.Tensor: The power of the selection set. + """ + if indices.dim() == 0: + indices = indices.unsqueeze(0) + + true_positives = torch.sum(y_truth[indices] > thresholds[indices]) + power = true_positives / torch.sum(y_truth > thresholds) + return power.item() + + +class Metrics: + def __call__(self, metric) -> Any: + if metric not in METRICS_REGISTRY_REGRESSION.registered_names(): + raise NameError(f"The metric: {metric} is not defined in TorchCP.") + return METRICS_REGISTRY_REGRESSION.get(metric) \ No newline at end of file diff --git a/torchcp/utils/metrics.py b/torchcp/utils/metrics.py new file mode 100644 index 0000000..e69de29 From 9241334350a43c07ea81738cb4383e896381213e Mon Sep 17 00:00:00 2001 From: 33798 <3379854453@qq.com> Date: Sat, 18 Oct 2025 16:08:52 +0800 Subject: [PATCH 6/8] delete FDR and power from regression\utils\metrics.py --- torchcp/regression/utils/metrics.py | 43 ----------------------------- 1 file changed, 43 deletions(-) diff --git a/torchcp/regression/utils/metrics.py b/torchcp/regression/utils/metrics.py index cdfae02..0303ea9 100644 --- a/torchcp/regression/utils/metrics.py +++ b/torchcp/regression/utils/metrics.py @@ -71,49 +71,6 @@ def average_size(prediction_intervals): return average_size -@METRICS_REGISTRY_REGRESSION.register() -def false_discovery_proportion(y_truth, thresholds, indices): - """ - Conpute the false discovery proportion (the proportion of false discovery among all selected points) of the - selection set. - - Args: - y_truth (torch.Tensor): A tensor of ground truth values. - thresholds (torch.Tensor): Tensor of user-defined thresholds. - indices (torch.Tensor): A tensor containing the indices of selected points. - - Returns: - torch.Tensor: The false discovery proportion of the selection set. - """ - if indices.dim() == 0: - indices = indices.unsqueeze(0) - - false_positives = torch.sum(y_truth[indices] <= thresholds[indices]) - fdp = false_positives / indices.shape[-1] if indices.shape[-1] > 0 else torch.tensor(0.) - return fdp.item() - - -@METRICS_REGISTRY_REGRESSION.register() -def power(y_truth, thresholds, indices): - """ - Conpute the power (the proportion of desirable points that are correctly selected) of the selection set. - - Args: - y_truth (torch.Tensor): A tensor of ground truth values. - thresholds (torch.Tensor): Tensor of user-defined thresholds. - indices (torch.Tensor): A tensor containing the indices of selected points. - - Returns: - torch.Tensor: The power of the selection set. - """ - if indices.dim() == 0: - indices = indices.unsqueeze(0) - - true_positives = torch.sum(y_truth[indices] > thresholds[indices]) - power = true_positives / torch.sum(y_truth > thresholds) - return power.item() - - class Metrics: def __call__(self, metric) -> Any: if metric not in METRICS_REGISTRY_REGRESSION.registered_names(): From 10cb8ab82fe21cb08f454dccdcc41250b439e6f2 Mon Sep 17 00:00:00 2001 From: 33798 <3379854453@qq.com> Date: Sat, 18 Oct 2025 16:13:27 +0800 Subject: [PATCH 7/8] style: rename selection\utils\metrics.py --- torchcp/selection/utils/{metric.py => metrics.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename torchcp/selection/utils/{metric.py => metrics.py} (100%) diff --git a/torchcp/selection/utils/metric.py b/torchcp/selection/utils/metrics.py similarity index 100% rename from torchcp/selection/utils/metric.py rename to torchcp/selection/utils/metrics.py From 6ee10927bb4689edbc637e62d41f6a60a633d9a4 Mon Sep 17 00:00:00 2001 From: 33798 <3379854453@qq.com> Date: Sat, 18 Oct 2025 16:17:22 +0800 Subject: [PATCH 8/8] style: rename selection\selector\conformal_selector.py --- torchcp/selection/selector/__init__.py | 2 +- .../selection/selector/{selection.py => conformal_selector.py} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename torchcp/selection/selector/{selection.py => conformal_selector.py} (98%) diff --git a/torchcp/selection/selector/__init__.py b/torchcp/selection/selector/__init__.py index 5e15819..556c204 100644 --- a/torchcp/selection/selector/__init__.py +++ b/torchcp/selection/selector/__init__.py @@ -5,4 +5,4 @@ # LICENSE file in the root directory of this source tree. # -from .selection import ConformalSelector \ No newline at end of file +from .conformal_selector import ConformalSelector \ No newline at end of file diff --git a/torchcp/selection/selector/selection.py b/torchcp/selection/selector/conformal_selector.py similarity index 98% rename from torchcp/selection/selector/selection.py rename to torchcp/selection/selector/conformal_selector.py index 537b86a..81efaae 100644 --- a/torchcp/selection/selector/selection.py +++ b/torchcp/selection/selector/conformal_selector.py @@ -9,7 +9,7 @@ import torch from torchcp.regression.predictor.split import SplitPredictor -from torchcp.regression.utils.metrics import Metrics +from torchcp.selection.utils.metrics import Metrics class ConformalSelector(SplitPredictor):