|
| 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') |
0 commit comments