Skip to content

Commit 7acca74

Browse files
authored
Merge pull request #106 from ml-stat-Sustech/development
Development
2 parents d628711 + e99138d commit 7acca74

19 files changed

Lines changed: 1014 additions & 97 deletions

File tree

README.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,9 @@ build the basic framework of TorchCP based on [`AdverTorch`](https://github.com/
6565
codebase is still under construction and maintained by [`Hongxin Wei`](https://hongxin001.github.io/)'s research group
6666
at SUSTech. Comments, issues, contributions, and collaborations are all welcomed!
6767

68-
## Updates of New Version (1.1.0)
68+
## Updates of New Version (1.2.0)
6969

70-
This release features a comprehensive refactoring of predictor modules, along with the addition of RC3P, EntmaxScore, and the SCPO trainer.
71-
This release features a comprehensive refactoring of predictor modules, along with the addition of RC3P, EntmaxScore, and the SCPO trainer.
70+
This release enhances functionality by introducing p-value computation, conformal predictive distributions, and expanding the NORABS score function with additional difficulty estimation methods.
7271
Detailed changelog can be found in the [Documentation](https://torchcp.readthedocs.io/en/latest/CHANGELOG.html).
7372

7473
# Overview
@@ -110,7 +109,7 @@ TorchCP has implemented the following methods:
110109
| 2019 | [**Adaptive, Distribution-Free Prediction Intervals for Deep Networks**](https://proceedings.mlr.press/v108/kivaranovic20a.html) | AISTATS'19 | [Link](https://github.com/yromano/cqr) | regression.score.cqrfm | |
111110
| 2019 | [**Conformalized Quantile Regression**](https://proceedings.neurips.cc/paper_files/paper/2019/file/5103c3584b063c431bd1268e9b5e76fb-Paper.pdf) | NeurIPS'19 | [Link](https://github.com/yromano/cqr) | regression.score.cqr | |
112111
| 2017 | [**Distribution-Free Predictive Inference For Regression**](https://arxiv.org/abs/1604.04173) | JASA | [Link](https://github.com/ryantibs/conformal) | regression.predictor.split | |
113-
| 2005 | [**Inductive Confidence Machines for Regression**](https://link.springer.com/chapter/10.1007/3-540-36755-1_29) | Springer | | regression.score.abs regression.score.norabs | |
112+
| 2005 | [**Inductive Confidence Machines for Regression**](https://link.springer.com/chapter/10.1007/3-540-36755-1_29) <br>[**Guaranteed Coverage Prediction Intervals with Gaussian Process Regression**](https://arxiv.org/abs/2310.15641)<br>[**Reliable Prediction Intervals with Regression Neural Networks**](https://arxiv.org/abs/2312.09606) | | | regression.score.abs regression.score.norabs | |
114113

115114
## Graph
116115

docs/source/CHANGELOG.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
Changelog
22
=========
33

4+
1.2.0 (2025-08-26)
5+
------------------
6+
* Added Conformal Predictive Distribution in regression task.
7+
* Added p-value computation in classification predictors and regression predictors.
8+
* Added more tyeps of difficulty estimation in the NORABS score function.
9+
410
1.1.0 (2025-07-15)
511
------------------
612
* Refactored `__init__` of predictors to include `alpha` and `device` parameters for greater flexibility.

docs/source/torchcp.regression.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ score function
1515
CQRM
1616
CQRR
1717
R2CCP
18+
Sign
1819

1920
.. autoclass:: ABS
2021
:members:
@@ -34,6 +35,9 @@ score function
3435
.. autoclass:: R2CCP
3536
:members:
3637

38+
.. autoclass:: Sign
39+
:members:
40+
3741

3842
.. automodule:: torchcp.regression.predictor
3943

@@ -46,6 +50,7 @@ predictor
4650
SplitPredictor
4751
EnsemblePredictor
4852
ACIPredictor
53+
ConformalPredictiveDistribution
4954

5055
.. autoclass:: SplitPredictor
5156
:members:
@@ -56,6 +61,9 @@ predictor
5661
.. autoclass:: ACIPredictor
5762
:members:
5863

64+
.. autoclass:: ConformalPredictiveDistribution
65+
:members:
66+
5967
.. automodule:: torchcp.regression.loss
6068

6169
loss

examples/classification_splitcp_cifar100.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,13 @@
6262
result_dict = predictor.evaluate(test_dataloader)
6363
print(f"Coverage Rate: {result_dict['coverage_rate']:.4f}")
6464
print(f"Average Set Size: {result_dict['average_size']:.4f}")
65+
66+
#########################################
67+
# Predict the p-value
68+
#########################################
69+
70+
p_values = predictor.predict_p(test_instances.unsqueeze(0))
71+
print("p_values: ", p_values)
72+
73+
74+
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import torch
2+
from torch.utils.data import DataLoader
3+
from tqdm import tqdm
4+
5+
from examples.regression_cqr_synthetic import prepare_dataset
6+
from torchcp.regression.predictor import SplitPredictor
7+
from torchcp.regression.utils import build_regression_model
8+
9+
# Ensure these classes are correctly imported depending on your project setup:
10+
# Option 1: If my_norabs.py is located in the same directory:
11+
# from my_norabs import DifficultyEstimator, NorABS
12+
# Option 2: If integrated into the torchcp library:
13+
from torchcp.regression.score import NorABS, DifficultyEstimator
14+
15+
16+
def extract_variance_as_function(x_batch, predicts_batch):
17+
"""
18+
Custom difficulty function that replicates the behavior of the 'variance' mode.
19+
20+
Args:
21+
x_batch (Tensor): Input features (unused here but kept for API consistency).
22+
predicts_batch (Tensor): Predictions with shape (batch_size, 2),
23+
where the second column is interpreted as variance.
24+
25+
Returns:
26+
Tensor: A vector representing the difficulty score based on variance.
27+
"""
28+
if predicts_batch is None or predicts_batch.ndim != 2 or predicts_batch.shape[-1] != 2:
29+
raise ValueError("This function requires `predicts_batch` to have shape (batch_size, 2).")
30+
return predicts_batch[:, 1]
31+
32+
33+
def run_experiment(mode='variance'):
34+
"""
35+
Executes a conformal prediction experiment using a lazily calibrated DifficultyEstimator.
36+
37+
Args:
38+
mode (str): Difficulty estimation strategy.
39+
Supported values: 'variance', 'knn_residual', 'knn_label', 'knn_distance', 'function'.
40+
"""
41+
print(f"\n--- Running Experiment in '{mode}' mode ---")
42+
43+
# 1. Prepare data loaders for training, calibration, and testing
44+
train_loader, cal_loader, test_loader = prepare_dataset(train_ratio=0.4, cal_ratio=0.2, batch_size=128)
45+
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
46+
47+
# Extract feature dimensionality from a sample batch to build the model
48+
sample_x, _ = next(iter(train_loader))
49+
feature_dim = sample_x.shape[1]
50+
51+
# 2. Initialize the regression model (untrained at this stage)
52+
model = build_regression_model("GaussianRegressionModel")(feature_dim, 64, 0.5)
53+
54+
# 3. Define the NorABS score function for difficulty-aware conformity estimation
55+
score_function = NorABS(data_loader=train_loader,
56+
estimate_type=mode,
57+
k=20,
58+
scalar=True,
59+
beta=0.01,
60+
device=device,
61+
custom_function=extract_variance_as_function)
62+
63+
# 4. Wrap the score function and model inside a SplitPredictor instance
64+
predictor = SplitPredictor(score_function=score_function, model=model, alpha=0.1, device=device)
65+
66+
# 5. Train the base regression model using the predictor’s utility function
67+
print("Training the base regression model...")
68+
predictor.train(train_loader, epochs=100, lr=0.01, device=device, verbose=True)
69+
# After training, the model is stored within `predictor.model`
70+
71+
# 6. Calibrate the predictor on the calibration set.
72+
# This step computes nonconformity scores and determines the quantile q_hat.
73+
print("Calibrating the predictor (computing q_hat)...")
74+
predictor.calibrate(cal_loader)
75+
76+
# 7. Online evaluation on the test set (transductive protocol).
77+
print("Evaluating on the test set...")
78+
cover_count, total, set_size_sum = 0, 0, 0
79+
80+
# Maintain a mutable calibration set that is updated sequentially
81+
online_cal_dataset = list(cal_loader.dataset)
82+
test_dataset = test_loader.dataset
83+
84+
for i in tqdm(range(len(test_dataset)), desc="Online Evaluation"):
85+
x_test, y_test = test_dataset[i]
86+
x_test_device = x_test.to(device).unsqueeze(0) # Predictor expects a batch dimension
87+
88+
# Generate prediction intervals for the test sample
89+
prediction_intervals = predictor.predict(x_test_device)[0][0]
90+
91+
# Evaluate coverage and interval size
92+
covered = (prediction_intervals[0] <= y_test <= prediction_intervals[1])
93+
cover_count += int(covered)
94+
set_size_sum += (prediction_intervals[1] - prediction_intervals[0]).item()
95+
total += 1
96+
97+
# Update the calibration set with the new (x, y) pair
98+
online_cal_dataset.append((x_test, y_test))
99+
online_cal_loader = DataLoader(online_cal_dataset, batch_size=128, shuffle=True)
100+
101+
# Recalibrate the predictor with the expanded calibration set
102+
predictor.calibrate(online_cal_loader)
103+
104+
# Compute final evaluation metrics
105+
coverage_rate = cover_count / total
106+
average_set_size = set_size_sum / total
107+
alpha = predictor.alpha
108+
109+
print(f"\nResults for '{mode}' mode:")
110+
print(f"Online CP Coverage Rate: {coverage_rate:.4f} (Target: {1-alpha:.2f})")
111+
print(f"Online CP Average Set Size: {average_set_size:.4f}")
112+
113+
114+
if __name__ == "__main__":
115+
# Run the experiment under different difficulty estimation modes
116+
run_experiment(mode='variance')
117+
run_experiment(mode='knn_residual')
118+
run_experiment(mode='knn_label')
119+
run_experiment(mode='knn_distance')
120+
run_experiment(mode='function')
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# Copyright (c) 2023-present, SUSTech-ML.
2+
# All rights reserved.
3+
#
4+
# This source code is licensed under the license found in the
5+
# LICENSE file in the root directory of this source tree.
6+
#
7+
8+
import numpy as np
9+
import torch
10+
from sklearn.preprocessing import StandardScaler
11+
from torch.utils.data import TensorDataset
12+
from tqdm import tqdm
13+
import torch.nn as nn
14+
import torch.optim as optim
15+
16+
from examples.regression_cqr_synthetic import prepare_dataset
17+
from torchcp.regression.predictor import ConformalPredictiveDistribution
18+
from torchcp.regression.score import Sign
19+
from torchcp.regression.utils import build_regression_model
20+
21+
22+
if __name__ == "__main__":
23+
# get dataloader
24+
train_loader, cal_loader, test_loader = prepare_dataset(train_ratio=0.4, cal_ratio=0.2, batch_size=128)
25+
# build regression model
26+
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
27+
model = build_regression_model("NonLinearNet")(next(iter(train_loader))[0].shape[1], 1, 64, 0.5).to(device)
28+
29+
30+
31+
32+
# train model
33+
epochs = 100
34+
criterion = nn.MSELoss()
35+
lr = 0.01
36+
optimizer = optim.Adam(model.parameters(), lr=lr)
37+
38+
for tmp_x, tmp_y in train_loader:
39+
outputs = model(tmp_x.to(device))
40+
loss = criterion(outputs, tmp_y.reshape(-1, 1).to(device))
41+
optimizer.zero_grad()
42+
loss.backward()
43+
optimizer.step()
44+
45+
# CPD
46+
predictor = ConformalPredictiveDistribution(score_function=Sign(), model=model)
47+
predictor.calibrate(cal_loader)
48+
49+
x = next(iter(test_loader))[0].to(device)
50+
prediction_intervals = predictor.predict(x)
51+
52+
print(prediction_intervals.shape)
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# Copyright (c) 2023-present, SUSTech-ML.
2+
# All rights reserved.
3+
#
4+
# This source code is licensed under the license found in the
5+
# LICENSE file in the root directory of this source tree.
6+
#
7+
8+
import torch
9+
import pytest
10+
11+
from torchcp.regression.predictor import ConformalPredictiveDistribution
12+
from torchcp.regression.score import ABS
13+
from torchcp.regression.utils import build_regression_model
14+
from torch.utils.data import DataLoader, TensorDataset
15+
16+
@pytest.fixture
17+
def mock_model():
18+
return build_regression_model("NonLinearNet")(5, 1, 64, 0.5)
19+
20+
21+
@pytest.fixture
22+
def cpds_predictor(mock_model):
23+
return ConformalPredictiveDistribution(model=mock_model)
24+
25+
26+
def test_invalid_initialization():
27+
with pytest.raises(ValueError):
28+
ConformalPredictiveDistribution(score_function=ABS())
29+
30+
@pytest.fixture
31+
def mock_data():
32+
"""
33+
提供用于回归预测器测试的假数据。
34+
返回: (train_loader, cal_loader, test_loader)
35+
- 特征维度为 5,目标为连续值
36+
"""
37+
num_samples = 3000
38+
num_features = 5
39+
40+
# 生成特征
41+
X = torch.rand((num_samples, num_features), dtype=torch.float32)
42+
43+
# 生成一个平滑的连续目标(非线性 + 噪声)
44+
y_wo_noise = (
45+
10 * torch.sin(X[:, 0] * X[:, 1] * torch.pi)
46+
+ 20 * (X[:, 2] - 0.5) ** 2
47+
+ 10 * X[:, 3]
48+
+ 5 * X[:, 4]
49+
)
50+
noise = 0.5 * torch.randn(num_samples, dtype=torch.float32)
51+
y = (y_wo_noise + noise).to(torch.float32)
52+
53+
# 划分 train/cal/test(40%/20%/40%)
54+
indices = torch.randperm(num_samples)
55+
split_index1 = int(num_samples * 0.4)
56+
split_index2 = int(num_samples * 0.6)
57+
part1 = indices[:split_index1]
58+
part2 = indices[split_index1:split_index2]
59+
part3 = indices[split_index2:]
60+
61+
train_dataset = TensorDataset(X[part1], y[part1])
62+
cal_dataset = TensorDataset(X[part2], y[part2])
63+
test_dataset = TensorDataset(X[part3], y[part3])
64+
65+
train_loader = DataLoader(train_dataset, batch_size=128, shuffle=True)
66+
cal_loader = DataLoader(cal_dataset, batch_size=128, shuffle=False)
67+
test_loader = DataLoader(test_dataset, batch_size=128, shuffle=False)
68+
69+
return train_loader, cal_loader, test_loader
70+
71+
def test_workflow(mock_data, cpds_predictor):
72+
# Extract mock data
73+
_, cal_dataloader, test_dataloader = mock_data
74+
75+
cpds_predictor.calibrate(cal_dataloader)
76+
assert hasattr(cpds_predictor, "scores"), "SplitPredictor should have scores after calibration."
77+
78+
for x_batch, _ in test_dataloader:
79+
cpds_predictor.predict(x_batch)
80+
81+
with pytest.raises(ValueError):
82+
tmp_cpds_predictor = ConformalPredictiveDistribution()
83+
for x_batch, _ in test_dataloader:
84+
tmp_cpds_predictor.predict(x_batch)

tests/classification/predictor/test_split.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,3 +166,23 @@ def test_evaluate(predictor, mock_score_function, mock_model, mock_dataset, q_ha
166166
assert len(results) == 2
167167
assert results['coverage_rate'] == metrics('coverage_rate')(excepted_sets, mock_dataset.labels)
168168
assert results['average_size'] == metrics('average_size')(excepted_sets)
169+
170+
171+
def test_predict_p(predictor, mock_score_function, mock_model, mock_dataset):
172+
with pytest.raises(ValueError):
173+
tmp_predictor = SplitPredictor(mock_score_function)
174+
tmp_predictor.predict_p(mock_dataset.x)
175+
176+
logits = mock_model(mock_dataset.x)
177+
logits = predictor._logits_transformation(logits)
178+
179+
predictor.calculate_threshold(logits, mock_dataset.labels)
180+
p_values = predictor.predict_p(mock_dataset.x)
181+
182+
assert p_values.shape == (mock_dataset.x.shape[0], logits.shape[1])
183+
184+
p_values = predictor.predict_p(mock_dataset.x, mock_dataset.labels)
185+
assert p_values.shape == (mock_dataset.x.shape[0], )
186+
187+
p_values = predictor.predict_p(mock_dataset.x, mock_dataset.labels, True)
188+
assert p_values.shape == (mock_dataset.x.shape[0], )

0 commit comments

Comments
 (0)