{"metadata":{"kernelspec":{"name":"python3","display_name":"Python 3","language":"python"},"language_info":{"name":"python","version":"3.11.11","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":1199870,"sourceType":"datasetVersion","datasetId":615374},{"sourceId":1412771,"sourceType":"datasetVersion","datasetId":824374}],"dockerImageVersionId":31041,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Explainable Learning to Reject trong Phân Tích Hình Ảnh CT COVID-19\n\nĐây là một hệ thống phân loại có chọn lọc (selective classification) sử dụng các phương pháp XAI để phân tích hình ảnh CT COVID-19. Hệ thống implement nhiều baseline khác nhau bao gồm ensemble learning, Monte Carlo Dropout, calibration methods, và OOD detection.\n\n## Tổng quan các Baseline:\n\n### Nhóm A: Cải thiện Ước lượng Độ tin cậy\n- **A.1**: Ensemble + Monte Carlo Dropout (MCDO)\n- **A.2.1**: Ensemble + Isotonic Regression  \n- **A.2.2**: Ensemble + Beta Calibration\n- **A.2.3**: Ensemble + Label Smoothing\n\n### Nhóm B: Phương pháp Phân biệt Nâng cao & OOD Detection\n- **B.1.1**: Ensemble + ODIN (Basic)\n- **B.1.2**: Ensemble + ODIN (Combined)\n- **B.2.1**: Ensemble + Energy Score (Basic)  \n- **B.2.2**: Ensemble + Energy Score (Combined)\n- **B.3**: Ensemble + Training Dynamics Insights\n","metadata":{}},{"cell_type":"code","source":"# Install specific versions of required packages\n!pip install numpy pandas matplotlib tqdm torch torchvision scikit-learn scipy pillow\n\n# Ensure tqdm is compatible with Jupyter/Kaggle notebooks\n!pip install tqdm --upgrade\n\n!pip install lime shap -qqq\n\n\n!git clone https://github.com/jacobgil/pytorch-grad-cam.git\n%cd pytorch-grad-cam\n!pip install .\n%cd ..","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-29T18:03:28.933131Z","iopub.execute_input":"2025-06-29T18:03:28.933418Z","iopub.status.idle":"2025-06-29T18:05:04.346528Z","shell.execute_reply.started":"2025-06-29T18:03:28.93339Z","shell.execute_reply":"2025-06-29T18:05:04.345834Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 1. Import Libraries và Dependencies\n\nImport tất cả các thư viện cần thiết cho project bao gồm:\n- Deep learning framework (PyTorch)\n- Data processing (NumPy, Pandas, PIL)\n- Machine learning utilities (Scikit-learn)\n- XAI libraries (pytorch-grad-cam)\n- Visualization (Matplotlib)\n","metadata":{"vscode":{"languageId":"raw"}}},{"cell_type":"code","source":"import os\nimport shutil\nimport random\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\nimport matplotlib.pyplot as plt\nfrom tqdm.notebook import tqdm \n\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nimport torch.nn.functional as F # Import F for functional operations\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms, models\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import accuracy_score, roc_curve, auc, precision_recall_curve, confusion_matrix, brier_score_loss, log_loss, f1_score, roc_auc_score  # ✅ FIXED: Added f1_score\nfrom scipy.special import softmax # For converting logits to probabilities\nfrom sklearn.metrics.pairwise import cosine_similarity # For similarity-based confidence adjustment\nfrom sklearn.isotonic import IsotonicRegression # For Isotonic Regression\nfrom scipy.optimize import minimize # For Beta Calibration\n\n# --- XAI Library Imports ---\nfrom pytorch_grad_cam import GradCAMPlusPlus # Using GradCAMPlusPlus as requested\nfrom pytorch_grad_cam.utils.model_targets import ClassifierOutputTarget\nfrom pytorch_grad_cam.utils.image import show_cam_on_image","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-29T18:05:04.348266Z","iopub.execute_input":"2025-06-29T18:05:04.348496Z","iopub.status.idle":"2025-06-29T18:05:15.775195Z","shell.execute_reply.started":"2025-06-29T18:05:04.348472Z","shell.execute_reply":"2025-06-29T18:05:15.774598Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 2. Configuration và Hyperparameters\n\nĐịnh nghĩa class Config chứa tất cả các hyperparameters và cài đặt của hệ thống:\n- Đường dẫn dataset và model\n- Tham số training (learning rate, epochs, batch size)\n- Cấu hình cho từng baseline (MCDO, Label Smoothing, ODIN, Energy Score)\n- Ngưỡng và trọng số cho optimization\n","metadata":{"vscode":{"languageId":"raw"}}},{"cell_type":"code","source":"# --- Configuration and Hyperparameters ---\nclass Config:\n    \"\"\"\n    Cấu hình và siêu tham số cho toàn bộ quy trình phân loại có chọn lọc.\n    Các tham số này có thể được điều chỉnh để phù hợp với các tập dữ liệu\n    và yêu cầu khác nhau.\n    \"\"\"\n    def __init__(self):\n        # Đường dẫn tập dữ liệu (giả sử môi trường Kaggle)\n        self.DATA_DIR = '/kaggle/input/preprocessed-ct-scans-for-covid19'\n        self.COVID_DIR = os.path.join(self.DATA_DIR, '/kaggle/input/preprocessed-ct-scans-for-covid19/Preprocessed CT scans/pCT')\n        self.NON_COVID_DIR = os.path.join(self.DATA_DIR, '/kaggle/input/preprocessed-ct-scans-for-covid19/Preprocessed CT scans/nCT')\n\n        # Mô hình và Huấn luyện\n        self.IMAGE_SIZE = (224, 224) # Kích thước chuẩn cho nhiều CNN tiền huấn luyện\n        self.BATCH_SIZE = 16 # Đã giảm batch size để tiết kiệm bộ nhớ GPU\n        self.NUM_EPOCHS_PER_MODEL = 5 # Tăng số epoch để huấn luyện tốt hơn\n        self.LEARNING_RATE = 1e-4\n        self.NUM_ENSEMBLE_MODELS = 3 # Số lượng mô hình trong ensemble\n\n        # Cấu hình Monte Carlo Dropout (MCDO)\n        self.MCDO_ENABLE = False       # Bật/tắt Monte Carlo Dropout (Baseline A.1)\n        self.MCDO_DROPOUT_RATE = 0.5   # Tỷ lệ dropout cho MCDO\n        self.MCDO_NUM_RUNS = 10        # Đã giảm số lần chạy forward pass cho MCDO để ước tính độ bất định\n\n        # Cấu hình Label Smoothing (Baseline A.2.3)\n        self.LABEL_SMOOTHING_ENABLE = False # Bật/tắt Label Smoothing\n        self.LABEL_SMOOTHING_EPSILON = 0.1 # Tham số epsilon cho Label Smoothing\n\n        # Cấu hình Training Dynamics (Baseline B.3)\n        self.ENABLE_TRAINING_DYNAMICS = False # Cờ bật/tắt điều chỉnh độ tin cậy bằng training dynamics\n        self.TRAINING_DYNAMICS_CONF_PENALTY = 0.1 # Mức độ các ví dụ \"khó học\" trong quá trình huấn luyện làm giảm độ tin cậy của các ví dụ test tương tự\n        \n        # ODIN/Energy Score Parameters (Baseline B.1.x, B.2.x)\n        self.ODIN_TEMP = 1000.0 # Nhiệt độ cho ODIN. Nhiệt độ cao thường hoạt động tốt.\n        self.ODIN_EPSILON = 0.001 # Độ lớn nhiễu loạn cho ODIN (có thể điều chỉnh dựa trên tập dữ liệu)\n        self.ENERGY_CLASSIFY_PERCENTILE_THRESHOLD = 20 # Các mẫu với điểm năng lượng trong X% thấp nhất được coi là OOD tiềm năng\n        \n        # Mục tiêu từ chối\n        self.TARGET_ACCEPTED_ACCURACY = 0.99 # 99.5% độ chính xác trên các trường hợp được chấp nhận\n        self.TARGET_REJECTION_RATE = 0.07      # Từ chối khoảng 10% các trường hợp\n\n        # Các tham số có thể điều chỉnh thủ công cho phân loại chọn lọc (thử nghiệm với chúng!)\n        self.DISAGREEMENT_PENALTY_FACTOR = 5.0 # Mức độ bất đồng của ensemble làm giảm độ tin cậy\n        \n        # Trọng số cho mục tiêu tối ưu hóa ngưỡng từ chối (mức độ quan trọng của mỗi yếu tố)\n        # Các trọng số này cho phép điều chỉnh sự đánh đổi giữa độ chính xác được chấp nhận, tỷ lệ từ chối và ECE\n        self.ACCURACY_DEVIATION_WEIGHT = 10.0 # Trọng số cao để mạnh mẽ thực thi TARGET_ACCEPTED_ACCURACY\n        self.REJECTION_RATE_DEVIATION_WEIGHT = 1.0 # Trọng số chuẩn cho độ lệch tỷ lệ từ chối\n        self.ECE_DEVIATION_WEIGHT = 5.0       # Trọng số cho ECE trong tối ưu hóa ngưỡng từ chối (điều chỉnh cái này, giá trị cao hơn có nghĩa là tập chấp nhận được hiệu chỉnh tốt hơn)\n\n        # Ngưỡng phát hiện OOD (có thể điều chỉnh thủ công trong categorize_rejected_cases)\n        # Đây là các ngưỡng được sử dụng để PHÂN LOẠI các trường hợp từ chối, KHÔNG phải để ra quyết định từ chối ban đầu\n        self.OOD_CONFIDENCE_THRESHOLD = 0.65 # Các mẫu dưới độ tin cậy này\n        self.OOD_VARIANCE_THRESHOLD = 0.08  # Và trên phương sai này là OOD tiềm năng (dùng cho các baseline cũ)\n        self.ODIN_CLASSIFY_THRESHOLD = 0.8 # Các mẫu với điểm ODIN < ngưỡng này được coi là OOD tiềm năng\n\n        # Các cài đặt khác\n        self.RANDOM_SEED = 42\n        self.DEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n        self.MODEL_SAVE_DIR = '/kaggle/working/models' # Thư mục để lưu các mô hình ensemble đã huấn luyện\n        os.makedirs(self.MODEL_SAVE_DIR, exist_ok=True)\n        self.XAI_SAVE_DIR = '/kaggle/working/xai_visualizations' # Thư mục để lưu các hình ảnh XAI\n        os.makedirs(self.XAI_SAVE_DIR, exist_ok=True) # Đảm bảo thư mục đầu ra XAI tồn tại\n\ncfg = Config()\n\n# Thiết lập seed ngẫu nhiên để tái sản xuất kết quả\ndef set_seed(seed):\n    \"\"\"Đặt seed ngẫu nhiên để tái sản xuất kết quả trên các thư viện khác nhau.\"\"\"\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed_all(seed)\n    np.random.seed(seed)\n    random.seed(seed)\n    torch.backends.cudnn.deterministic = True\n    torch.backends.cudnn.benchmark = False\n\nset_seed(cfg.RANDOM_SEED)\n\nprint(f\"Sử dụng thiết bị: {cfg.DEVICE}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-29T18:05:15.776038Z","iopub.execute_input":"2025-06-29T18:05:15.776423Z","iopub.status.idle":"2025-06-29T18:05:15.876009Z","shell.execute_reply.started":"2025-06-29T18:05:15.776399Z","shell.execute_reply":"2025-06-29T18:05:15.875393Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 3. Data Loading và Preprocessing\n\nTạo Dataset class và các hàm để:\n- Load và preprocess hình ảnh CT scan\n- Chia dataset thành train/validation/test sets\n- Apply data augmentation cho training\n- Tạo DataLoader cho từng split\n","metadata":{"vscode":{"languageId":"raw"}}},{"cell_type":"code","source":"# --- 1. Tải và tiền xử lý dữ liệu ---\nclass CTScanDataset(Dataset):\n    \"\"\"\n    Lớp Dataset tùy chỉnh để tải hình ảnh CT scan và nhãn của chúng.\n    Xử lý đường dẫn hình ảnh và áp dụng các phép biến đổi.\n    Trả về hình ảnh, nhãn, và chỉ mục toàn cục gốc của nó.\n    \"\"\"\n    def __init__(self, image_paths, labels, transform=None, global_indices=None):\n        self.image_paths = image_paths\n        self.labels = labels\n        self.transform = transform\n        # Đảm bảo global_indices là một mảng numpy\n        self.global_indices = np.array(global_indices) if global_indices is not None else np.arange(len(image_paths))\n        # Tạo ánh xạ từ global_idx đến local idx trong phân tách này để tra cứu thuận tiện\n        self.original_indices_map = {self.global_indices[i]: i for i in range(len(self.global_indices))}\n\n    def __len__(self):\n        return len(self.image_paths)\n\n    def __getitem__(self, idx):\n        img_path = self.image_paths[idx]\n        image = Image.open(img_path).convert('RGB') # Đảm bảo 3 kênh cho các mô hình tiền huấn luyện\n        label = self.labels[idx]\n        global_idx = self.global_indices[idx] # Trả về chỉ mục toàn cục gốc\n\n        if self.transform:\n            image = self.transform(image)\n\n        return image, label, global_idx # Trả về chỉ mục toàn cục gốc để theo dõi động lực huấn luyện\n\n\ndef prepare_datasets(cfg):\n    \"\"\"\n    Thu thập đường dẫn hình ảnh và nhãn, sau đó chia thành các tập huấn luyện, validation và test.\n    Gán một chỉ mục toàn cục duy nhất cho mỗi hình ảnh từ tập dữ liệu gốc.\n    \"\"\"\n    all_image_paths_raw = []\n    all_labels_raw = []\n\n    # Thu thập hình ảnh COVID\n    covid_paths = [os.path.join(cfg.COVID_DIR, f) for f in os.listdir(cfg.COVID_DIR) if f.endswith('.png') or f.endswith('.jpg')]\n    all_image_paths_raw.extend(covid_paths)\n    all_labels_raw.extend([1] * len(covid_paths)) # 1 cho COVID\n\n    # Thu thập hình ảnh Non-COVID\n    non_covid_paths = [os.path.join(cfg.NON_COVID_DIR, f) for f in os.listdir(cfg.NON_COVID_DIR) if f.endswith('.png') or f.endswith('.jpg')]\n    all_image_paths_raw.extend(non_covid_paths)\n    all_labels_raw.extend([0] * len(non_covid_paths)) # 0 cho non-COVID\n\n    # Gán chỉ mục toàn cục\n    all_global_indices = list(range(len(all_image_paths_raw)))\n\n    print(f\"Tổng số hình ảnh tìm thấy: {len(all_image_paths_raw)}\")\n    print(f\"Hình ảnh COVID: {len(covid_paths)}, Hình ảnh Non-COVID: {len(non_covid_paths)}\")\n\n    # Tạo các phân tách phân tầng cho tập huấn luyện+validation và tập test trước\n    # Mong muốn: Test = 15% tổng số.\n    train_val_paths, test_paths, train_val_labels, test_labels, \\\n    train_val_global_indices, test_global_indices = train_test_split(\n        all_image_paths_raw, all_labels_raw, all_global_indices,\n        test_size=0.15, random_state=cfg.RANDOM_SEED, stratify=all_labels_raw\n    )\n    \n    # Sau đó, chia tập train_val thành các tập huấn luyện và validation thực tế\n    # train_val là 85% tổng số. Chúng ta muốn Val = 15% tổng số.\n    # Vì vậy, val_size_relative_to_train_val = 0.15 / (1.0 - 0.15)\n    val_size_relative_to_train_val = 0.15 / (1.0 - 0.15)\n    train_paths, val_paths, train_labels, val_labels, \\\n    train_global_indices, val_global_indices = train_test_split(\n        train_val_paths, train_val_labels, train_val_global_indices,\n        test_size=val_size_relative_to_train_val, random_state=cfg.RANDOM_SEED, stratify=train_val_labels\n    )\n\n    print(f\"Kích thước tập huấn luyện: {len(train_paths)}\")\n    print(f\"Kích thước tập Validation: {len(val_paths)}\")\n    print(f\"Kích thước tập Test: {len(test_paths)}\") # test_paths này tham chiếu đến tập test cuối cùng, độc lập\n\n    # Định nghĩa các phép biến đổi\n    train_transform = transforms.Compose([\n        transforms.Resize(cfg.IMAGE_SIZE),\n        transforms.RandomHorizontalFlip(),\n        transforms.RandomRotation(10),\n        transforms.ColorJitter(brightness=0.2, contrast=0.2),\n        transforms.ToTensor(),\n        transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) # Chuẩn hóa ImageNet\n    ])\n\n    val_test_transform = transforms.Compose([\n        transforms.Resize(cfg.IMAGE_SIZE),\n        transforms.ToTensor(),\n        transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])\n    ])\n\n    # Truyền global_indices vào constructor của Dataset\n    train_dataset = CTScanDataset(train_paths, train_labels, train_transform, train_global_indices)\n    val_dataset = CTScanDataset(val_paths, val_labels, val_test_transform, val_global_indices)\n    test_dataset = CTScanDataset(test_paths, test_labels, val_test_transform, test_global_indices)\n\n    train_loader = DataLoader(train_dataset, batch_size=cfg.BATCH_SIZE, shuffle=True, num_workers=2)\n    val_loader = DataLoader(val_dataset, batch_size=cfg.BATCH_SIZE, shuffle=False, num_workers=2)\n    test_loader = DataLoader(test_dataset, batch_size=cfg.BATCH_SIZE, shuffle=False, num_workers=2)\n\n    return train_loader, val_loader, test_loader, train_dataset, val_dataset, test_dataset\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-29T18:05:15.877572Z","iopub.execute_input":"2025-06-29T18:05:15.877873Z","iopub.status.idle":"2025-06-29T18:05:15.890494Z","shell.execute_reply.started":"2025-06-29T18:05:15.877854Z","shell.execute_reply":"2025-06-29T18:05:15.889925Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 4. Model Definition\n\nĐịnh nghĩa các model và loss functions:\n- BaseClassifier: CNN dựa trên ResNet18 với dropout support cho MCDO\n- LabelSmoothingLoss: Custom loss function cho Label Smoothing\n- Target layer setup cho Grad-CAM visualization\n","metadata":{"vscode":{"languageId":"raw"}}},{"cell_type":"code","source":"# --- 2. Định nghĩa Base Classifier ---\nclass BaseClassifier(nn.Module):\n    \"\"\"\n    Một bộ phân loại CNN cơ bản sử dụng mô hình ResNet tiền huấn luyện.\n    Bao gồm một bộ trích xuất đặc trưng rõ ràng để dễ dàng kết nối cho Grad-CAM\n    và để lấy đặc trưng cho phân tích dựa trên sự tương đồng.\n    Tích hợp các lớp Dropout cho Monte Carlo Dropout (MCDO).\n    \"\"\"\n    def __init__(self, num_classes=2, dropout_rate=0.0):\n        super(BaseClassifier, self).__init__()\n        self.dropout_rate = dropout_rate\n        # Tải mô hình ResNet18 tiền huấn luyện\n        self.model = models.resnet18(weights=models.ResNet18_Weights.DEFAULT)\n        \n        # Xác định lớp mục tiêu cho Grad-CAM (lớp tích chập cuối cùng)\n        self.target_layer = self.model.layer4[-1] \n        \n        # Định nghĩa phần trích xuất đặc trưng (mọi thứ trước lớp FC cuối cùng)\n        feature_extractor_layers = list(self.model.children())[:-1]\n        \n        # Thêm lớp dropout nếu dropout_rate dương\n        if self.dropout_rate > 0:\n            # Tìm chỉ mục của lớp AdaptiveAvgPool2d để chèn dropout sau nó\n            try:\n                avgpool_idx = [i for i, layer in enumerate(feature_extractor_layers) if isinstance(layer, nn.AdaptiveAvgPool2d)][0]\n                feature_extractor_layers.insert(avgpool_idx + 1, nn.Dropout(p=self.dropout_rate))\n                print(f\"Đã thêm lớp Dropout với tỷ lệ {self.dropout_rate} vào bộ trích xuất đặc trưng.\")\n            except IndexError:\n                print(\"Cảnh báo: Không tìm thấy lớp AdaptiveAvgPool2d. Đang thêm Dropout sau tất cả các lớp tích chập.\")\n                feature_extractor_layers.append(nn.Dropout(p=self.dropout_rate))\n\n\n        self.feature_extractor = nn.Sequential(*feature_extractor_layers)\n        \n        # Thay thế lớp kết nối đầy đủ cuối cùng cho phân loại nhị phân\n        num_ftrs = self.model.fc.in_features\n        self.model.fc = nn.Linear(num_ftrs, num_classes)\n\n    def forward(self, x):\n        # Forward pass qua bộ trích xuất đặc trưng\n        features = self.feature_extractor(x)\n        features = torch.flatten(features, 1) # Làm phẳng các đặc trưng\n        # Forward pass qua lớp phân loại cuối cùng\n        output = self.model.fc(features)\n        return output\n\n    def get_features(self, x):\n        \"\"\"\n        Trích xuất đặc trưng từ lớp trước đầu phân loại cuối cùng.\n        \"\"\"\n        # Lưu ý: Các lớp Dropout trong feature_extractor sẽ tự động tắt nếu model.eval()\n        # hoặc hoạt động nếu model.train() và dropout_rate > 0\n        with torch.no_grad(): \n            features = self.feature_extractor(x)\n            features = torch.flatten(features, 1) \n        return features\n\n\n# --- Label Smoothing Loss (Baseline A.2.3) ---\nclass LabelSmoothingLoss(nn.Module):\n    def __init__(self, classes, epsilon=0.1, dim=-1):\n        super(LabelSmoothingLoss, self).__init__()\n        self.confidence = 1.0 - epsilon\n        self.epsilon = epsilon\n        self.cls = classes\n        self.dim = dim\n\n    def forward(self, pred, target):\n        pred = pred.log_softmax(dim=self.dim)\n        with torch.no_grad():\n            true_dist = torch.zeros_like(pred)\n            true_dist.fill_(self.epsilon / (self.cls - 1))\n            true_dist.scatter_(1, target.data.unsqueeze(1), self.confidence)\n        return torch.mean(torch.sum(-true_dist * pred, dim=self.dim))\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-29T18:05:15.891176Z","iopub.execute_input":"2025-06-29T18:05:15.89137Z","iopub.status.idle":"2025-06-29T18:05:15.904452Z","shell.execute_reply.started":"2025-06-29T18:05:15.891355Z","shell.execute_reply":"2025-06-29T18:05:15.903879Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 5. Training Functions\n\nCác hàm training cho ensemble models với:\n- Training dynamics tracking (để phân tích độ khó của samples)\n- Support cho Label Smoothing và MCDO\n- Model saving và validation\n- Learning metrics collection\n","metadata":{"vscode":{"languageId":"raw"}}},{"cell_type":"code","source":"# --- 3. Hàm huấn luyện cho các thành viên Ensemble với theo dõi động lực huấn luyện ---\ndef train_model(model, train_loader, val_loader, epochs, lr, device, model_idx, cfg):\n    \"\"\"\n    Huấn luyện một thể hiện duy nhất của bộ phân loại cơ bản và theo dõi động lực huấn luyện chi tiết\n    (nhãn dự đoán và độ tin cậy cho mỗi mẫu ở mỗi epoch).\n    \"\"\"\n    if cfg.LABEL_SMOOTHING_ENABLE:\n        criterion = LabelSmoothingLoss(classes=2, epsilon=cfg.LABEL_SMOOTHING_EPSILON).to(device)\n        print(f\"Đã bật Label Smoothing với epsilon: {cfg.LABEL_SMOOTHING_EPSILON}\")\n    else:\n        criterion = nn.CrossEntropyLoss().to(device)\n\n    optimizer = optim.Adam(model.parameters(), lr=lr)\n    scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=2, gamma=0.5)\n\n    model.to(device)\n    best_val_accuracy = 0.0\n    \n    # Từ điển để lưu trữ lịch sử dự đoán chi tiết cho mỗi mẫu huấn luyện qua các epoch\n    # Key: global_idx của mẫu, Value: Danh sách các dict, mỗi dict (epoch, is_correct, predicted_label, confidence)\n    training_prediction_details = {global_idx: [] for global_idx in train_loader.dataset.global_indices}\n\n\n    print(f\"\\n--- Huấn luyện Mô hình Ensemble {model_idx + 1} ---\")\n    for epoch in range(epochs):\n        model.train() # Đảm bảo mô hình ở chế độ train (dropout hoạt động nếu có)\n        running_loss = 0.0\n        correct_predictions = 0\n        total_samples = 0\n\n        for inputs, labels, global_indices_batch in tqdm(train_loader, desc=f\"Epoch {epoch+1}/{epochs} (Train)\"):\n            inputs, labels = inputs.to(device), labels.to(device)\n\n            optimizer.zero_grad()\n            outputs = model(inputs)\n            loss = criterion(outputs, labels)\n            loss.backward()\n            optimizer.step()\n\n            running_loss += loss.item() * inputs.size(0)\n            \n            # Tách các outputs trước khi chuyển đổi sang numpy\n            probs = softmax(outputs.detach().cpu().numpy(), axis=1) \n            predicted_labels = np.argmax(probs, axis=1)\n            confidences_batch = np.max(probs, axis=1)\n\n            total_samples += labels.size(0)\n            correct_in_batch = (predicted_labels == labels.cpu().numpy()).sum().item()\n            correct_predictions += correct_in_batch\n\n            # Cập nhật động lực huấn luyện: lưu nhãn dự đoán và độ tin cậy của nó cho mỗi mẫu ở epoch này\n            for i, global_idx_tensor in enumerate(global_indices_batch):\n                global_idx = global_idx_tensor.item()\n                is_correct_prediction = (predicted_labels[i] == labels[i].item())\n                training_prediction_details[global_idx].append({\n                    'epoch': epoch,\n                    'is_correct': is_correct_prediction,\n                    'predicted_label': predicted_labels[i],\n                    'confidence': confidences_batch[i]\n                })\n        \n        # Clear CUDA cache after each epoch to free up memory\n        if device.type == 'cuda':\n            torch.cuda.empty_cache()\n\n        epoch_loss = running_loss / total_samples\n        epoch_accuracy = correct_predictions / total_samples\n\n        # Giai đoạn Validation\n        model.eval() # Đặt mô hình về chế độ eval cho validation\n        val_correct_predictions = 0\n        val_total_samples = 0\n        val_loss = 0.0\n        with torch.no_grad():\n            for inputs, labels, _ in val_loader: # Không cần global_indices trong val_loader cho bước validation\n                inputs, labels = inputs.to(device), labels.to(device)\n                outputs = model(inputs)\n                val_loss += criterion(outputs, labels).item() * inputs.size(0)\n                _, predicted = torch.max(outputs.data, 1)\n                val_total_samples += labels.size(0)\n                val_correct_predictions += (predicted == labels).sum().item()\n\n        val_accuracy = val_correct_predictions / val_total_samples\n        val_loss /= val_total_samples\n\n        print(f\"Epoch {epoch+1}: Train Loss: {epoch_loss:.4f}, Train Acc: {epoch_accuracy:.4f}, \"\n              f\"Val Loss: {val_loss:.4f}, Val Acc: {val_accuracy:.4f}\")\n\n        scheduler.step()\n\n        # Lưu mô hình tốt nhất dựa trên độ chính xác validation\n        if val_accuracy > best_val_accuracy:\n            best_val_accuracy = val_accuracy\n            torch.save(model.state_dict(), os.path.join(cfg.MODEL_SAVE_DIR, f'best_model_ensemble_{model_idx}.pth'))\n            print(f\"Đã lưu mô hình tốt nhất {model_idx + 1} với Val Acc: {best_val_accuracy:.4f}\")\n\n    print(f\"Hoàn thành huấn luyện Mô hình {model_idx + 1}. Val Acc tốt nhất: {best_val_accuracy:.4f}\")\n    \n    # Sau tất cả các epoch cho mô hình này, tính toán 'learning metrics' cho mỗi mẫu\n    sample_learning_metrics = {}\n    for global_idx, history in training_prediction_details.items():\n        correct_epochs_history = [h for h in history if h['is_correct']]\n        \n        avg_correct_confidence = np.mean([h['confidence'] for h in correct_epochs_history]) if correct_epochs_history else 0.0\n        \n        # Sự chậm trễ trong học tập: epoch đầu tiên mà nó đúng và vẫn đúng cho tất cả các epoch tiếp theo\n        first_correct_epoch = epochs # Mặc định là 'chưa bao giờ học thực sự' (max epochs)\n        for k_idx in range(len(history)): \n            if history[k_idx]['is_correct']:\n                # Kiểm tra xem nó có giữ đúng cho đến cuối không\n                if all(h_sub['is_correct'] for h_sub in history[k_idx:]):\n                    first_correct_epoch = history[k_idx]['epoch']\n                    break\n        \n        # Tính nhất quán: tỷ lệ dự đoán đúng trên tất cả các epoch cho mô hình này\n        consistency = len(correct_epochs_history) / epochs if epochs > 0 else 0.0\n\n        sample_learning_metrics[global_idx] = {\n            'avg_correct_confidence': avg_correct_confidence,\n            'first_correct_epoch': first_correct_epoch,\n            'consistency': consistency\n        }\n    \n    return sample_learning_metrics\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-29T18:05:15.90528Z","iopub.execute_input":"2025-06-29T18:05:15.905549Z","iopub.status.idle":"2025-06-29T18:05:15.928423Z","shell.execute_reply.started":"2025-06-29T18:05:15.905526Z","shell.execute_reply":"2025-06-29T18:05:15.927854Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Complete function for training whole ensemble\n\ndef train_ensemble(cfg, train_loader, val_loader):\n    \"\"\"\n    Train ensemble của NUM_ENSEMBLE_MODELS models và trả về training dynamics.\n    \"\"\"\n    print(f\"🔥 Training ensemble of {cfg.NUM_ENSEMBLE_MODELS} models...\")\n    \n    # Khởi tạo dictionary để lưu trữ learning metrics từ tất cả models\n    overall_learning_metrics = {}\n    \n    for i in range(cfg.NUM_ENSEMBLE_MODELS):\n        print(f\"\\n🤖 Training model {i+1}/{cfg.NUM_ENSEMBLE_MODELS}\")\n        \n        # Set different seed for diversity\n        set_seed(cfg.RANDOM_SEED + i)\n        \n        # Tạo model mới cho mỗi ensemble member\n        model = BaseClassifier(\n            num_classes=2, \n            dropout_rate=(cfg.MCDO_DROPOUT_RATE if cfg.MCDO_ENABLE else 0.0)\n        )\n        \n        # Train model và thu thập learning metrics\n        individual_learning_metrics = train_model(\n            model, train_loader, val_loader, \n            cfg.NUM_EPOCHS_PER_MODEL, cfg.LEARNING_RATE, \n            cfg.DEVICE, i, cfg\n        )\n        \n        # Merge learning metrics từ model này vào overall metrics  \n        for global_idx, metrics in individual_learning_metrics.items():\n            if global_idx not in overall_learning_metrics:\n                overall_learning_metrics[global_idx] = {\n                    'avg_correct_confidence_list': [],\n                    'first_correct_epoch_list': [],\n                    'consistency_list': []\n                }\n            \n            overall_learning_metrics[global_idx]['avg_correct_confidence_list'].append(metrics['avg_correct_confidence'])\n            overall_learning_metrics[global_idx]['first_correct_epoch_list'].append(metrics['first_correct_epoch'])\n            overall_learning_metrics[global_idx]['consistency_list'].append(metrics['consistency'])\n        \n        # Clear CUDA cache after each model's training\n        if cfg.DEVICE.type == 'cuda':\n            torch.cuda.empty_cache()\n    \n    # Aggregate learning metrics across all ensemble members\n    final_overall_learning_metrics = {}\n    for global_idx, all_model_metrics in overall_learning_metrics.items():\n        # Tính average learning metrics across ensemble members với tên đúng\n        avg_conf_list = all_model_metrics['avg_correct_confidence_list']\n        epoch_list = all_model_metrics['first_correct_epoch_list']\n        consistency_list = all_model_metrics['consistency_list']\n        \n        final_overall_learning_metrics[global_idx] = {\n            'mean_avg_correct_confidence': np.mean(avg_conf_list) if avg_conf_list else 0.0,\n            'mean_first_correct_epoch': np.mean(epoch_list) if epoch_list else cfg.NUM_EPOCHS_PER_MODEL,\n            'mean_consistency': np.mean(consistency_list) if consistency_list else 0.0\n        }\n    \n    print(f\"✅ Ensemble training completed!\")\n    print(f\"📊 Training dynamics tracked for {len(final_overall_learning_metrics)} samples\")\n    \n    return final_overall_learning_metrics\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-29T18:05:15.929438Z","iopub.execute_input":"2025-06-29T18:05:15.929701Z","iopub.status.idle":"2025-06-29T18:05:15.942937Z","shell.execute_reply.started":"2025-06-29T18:05:15.92968Z","shell.execute_reply":"2025-06-29T18:05:15.942319Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 6. Ensemble Training Coordination\n\nĐiều phối việc training multiple models và tổng hợp training dynamics metrics từ tất cả các models trong ensemble.\n","metadata":{"vscode":{"languageId":"raw"}}},{"cell_type":"code","source":"# (Duplicate function removed - using the one in cell 11)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-29T18:05:15.943495Z","iopub.execute_input":"2025-06-29T18:05:15.943702Z","iopub.status.idle":"2025-06-29T18:05:15.957346Z","shell.execute_reply.started":"2025-06-29T18:05:15.943686Z","shell.execute_reply":"2025-06-29T18:05:15.956825Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 7. Confidence Estimation và Calibration Methods\n\nImplement các phương pháp calibration khác nhau để cải thiện ước lượng độ tin cậy:\n\n### A.2.1: Isotonic Regression\n### A.2.2: Beta Calibration  \n### A.2.3: Temperature Scaling\n\nCũng bao gồm các utility functions cho feature extraction và training dynamics adjustment.\n","metadata":{"vscode":{"languageId":"raw"}}},{"cell_type":"code","source":"# --- 5. Ước tính và hiệu chỉnh độ tin cậy nâng cao ---\nclass TemperatureScaler(nn.Module):\n    \"\"\"\n    Học một tham số nhiệt độ scalar duy nhất để hiệu chỉnh các xác suất.\n    Dựa trên Guo et al. \"On Calibration of Modern Neural Networks\" (ICML 2017).\n    \"\"\"\n    def __init__(self):\n        super().__init__()\n        self.temperature = nn.Parameter(torch.ones(1))\n\n    def forward(self, logits):\n        return logits / self.temperature\n\n    def calibrate(self, logits_to_calibrate, labels_for_calibration, device):\n        \"\"\"\n        Điều chỉnh tham số nhiệt độ bằng cách sử dụng các logits và nhãn đã tính toán trước.\n        \"\"\"\n        # Đảm bảo logits và nhãn nằm trên thiết bị chính xác\n        logits_all = logits_to_calibrate.to(device)\n        labels_all = labels_for_calibration.to(device)\n\n        nll_criterion = nn.CrossEntropyLoss().to(device)\n        optimizer = optim.LBFGS([self.temperature], lr=0.01, max_iter=50, line_search_fn='strong_wolfe')\n\n        def eval():\n            optimizer.zero_grad()\n            loss = nll_criterion(self.forward(logits_all), labels_all)\n            loss.backward()\n            return loss\n\n        optimizer.step(eval)\n        print(f\"Bộ hiệu chỉnh nhiệt độ đã được hiệu chỉnh. Tối ưu T: {self.temperature.item():.4f}\")\n\nclass IsotonicCalibrator:\n    \"\"\"\n    Hiệu chỉnh bằng Isotonic Regression.\n    \"\"\"\n    def __init__(self):\n        self.ir = IsotonicRegression(out_of_bounds=\"clip\")\n\n    def calibrate(self, confidences_to_calibrate, labels_for_calibration):\n        # confidences_to_calibrate should be 1D array of confidence scores\n        # labels_for_calibration should be 1D array of binary labels (0 or 1)\n        self.ir.fit(confidences_to_calibrate, labels_for_calibration)\n        print(\"Isotonic Regression đã được hiệu chỉnh.\")\n\n    def predict_proba(self, confidences_to_transform):\n        return self.ir.transform(confidences_to_transform)\n\n\nclass BetaCalibrator:\n    \"\"\"\n    Hiệu chỉnh bằng Beta Calibration.\n    Tham số alpha và beta của phân phối Beta được tối ưu hóa.\n    \"\"\"\n    def __init__(self):\n        self.alpha = None\n        self.beta = None\n    \n    def _objective_function(self, params, confidences, labels):\n        alpha, beta = params\n        # Clip confidences to avoid log(0) or log(1) issues\n        conf_clamped = np.clip(confidences, 1e-10, 1 - 1e-10)\n        \n        # Apply transformation: logit(p_calibrated) = sigmoid(alpha * logit(p_original) + beta)\n        logit_original = np.log(conf_clamped / (1 - conf_clamped))\n        calibrated_confidences = 1.0 / (1.0 + np.exp(- (alpha * logit_original + beta)))\n        \n        # Clamp again to avoid log(0) or log(1)\n        calibrated_confidences = np.clip(calibrated_confidences, 1e-10, 1 - 1e-10)\n        \n        # Negative Log-Likelihood as objective\n        nll = -np.mean(labels * np.log(calibrated_confidences) + (1 - labels) * np.log(1 - calibrated_confidences))\n        return nll\n\n    def calibrate(self, confidences_to_calibrate, labels_for_calibration):\n        # Initial guess for alpha and beta\n        initial_params = [1.0, 0.0] # alpha=1.0, beta=0.0 means no change (identity)\n        \n        # Perform optimization using L-BFGS-B (bounded to avoid extreme values)\n        result = minimize(self._objective_function, initial_params, \n                          args=(confidences_to_calibrate, labels_for_calibration), \n                          method='L-BFGS-B', \n                          bounds=[(0.01, None), (None, None)]) # alpha must be positive\n        \n        self.alpha, self.beta = result.x\n        print(f\"Beta Calibration đã được hiệu chỉnh. Alpha: {self.alpha:.4f}, Beta: {self.beta:.4f}\")\n\n    def predict_proba(self, confidences_to_transform):\n        if self.alpha is None or self.beta is None:\n            raise ValueError(\"BetaCalibrator chưa được hiệu chỉnh. Vui lòng gọi .calibrate() trước.\")\n        \n        conf_clamped = np.clip(confidences_to_transform, 1e-10, 1 - 1e-10)\n        logit_original = np.log(conf_clamped / (1 - conf_clamped))\n        calibrated_conf = 1.0 / (1.0 + np.exp(- (self.alpha * logit_original + self.beta)))\n        return np.clip(calibrated_conf, 0.0, 1.0)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-29T18:05:15.958098Z","iopub.execute_input":"2025-06-29T18:05:15.958328Z","iopub.status.idle":"2025-06-29T18:05:15.972529Z","shell.execute_reply.started":"2025-06-29T18:05:15.958313Z","shell.execute_reply":"2025-06-29T18:05:15.971812Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def extract_features(model, data_loader, device):\n    \"\"\"\n    Trích xuất đặc trưng từ feature_extractor của mô hình cho tất cả các mẫu trong data_loader.\n    Trả về các đặc trưng dưới dạng mảng numpy và các chỉ mục toàn cục gốc tương ứng.\n    \"\"\"\n    model.eval() # Đảm bảo model ở chế độ eval cho việc trích xuất đặc trưng thông thường\n    all_features = []\n    all_indices = []\n    with torch.no_grad():\n        for inputs, _, global_indices_batch in tqdm(data_loader, desc=\"Trích xuất Đặc trưng\"):\n            inputs = inputs.to(device)\n            features = model.get_features(inputs) # Sử dụng phương thức get_features mới\n            all_features.append(features.cpu().numpy())\n            all_indices.extend(global_indices_batch.cpu().numpy())\n    return np.vstack(all_features), np.array(all_indices)\n\n\ndef adjust_confidence_with_training_dynamics(cfg, test_features, current_scores,\n                                             train_features, train_global_indices, final_overall_learning_metrics):\n    \"\"\"\n    Điều chỉnh điểm tin cậy/từ chối của tập test dựa trên sự tương đồng với các mẫu huấn luyện và động lực học của chúng.\n    Điểm thấp hơn đối với các mẫu test tương tự các mẫu huấn luyện 'khó' (ví dụ: học muộn, không nhất quán).\n    \"\"\"\n    print(\"Điều chỉnh điểm của tập test với động lực huấn luyện...\")\n    adjusted_scores = np.copy(current_scores)\n\n    # Tạo ánh xạ từ global_idx đến từ điển learning metrics để tra cứu hiệu quả\n    train_global_idx_to_metrics = {idx: metrics for idx, metrics in final_overall_learning_metrics.items()}\n\n    # Tính toán độ tương đồng cosine giữa các đặc trưng test và huấn luyện\n    if len(test_features) == 0 or len(train_features) == 0:\n        print(\"Bỏ qua điều chỉnh động lực huấn luyện: Không có đặc trưng test hoặc huấn luyện.\")\n        return adjusted_scores\n\n    similarities = cosine_similarity(test_features, train_features)\n    \n    for i in tqdm(range(len(test_features)), desc=\"Áp dụng điều chỉnh động lực huấn luyện\"):\n        # Tìm mẫu huấn luyện tương đồng nhất (bằng chỉ mục của nó trong mảng `train_features`)\n        most_similar_train_idx_in_features_array = np.argmax(similarities[i])\n        # Lấy chỉ mục toàn cục gốc của mẫu huấn luyện tương đồng nhất đó\n        most_similar_train_global_idx = train_global_indices[most_similar_train_idx_in_features_array]\n        \n        # Kiểm tra xem các learning metrics cho chỉ mục toàn cục này có tồn tại không\n        if most_similar_train_global_idx in train_global_idx_to_metrics:\n            sample_metrics = train_global_idx_to_metrics[most_similar_train_global_idx]\n            \n            # Sử dụng 'mean_first_correct_epoch' làm proxy cho 'learning lateness' hoặc 'difficulty'.\n            # 'mean_first_epoch' cao hơn cho thấy một mẫu khó học hơn.\n            # Sử dụng .get() với giá trị mặc định trong trường hợp key bị thiếu bất ngờ\n            difficulty_value = sample_metrics.get('mean_first_correct_epoch', cfg.NUM_EPOCHS_PER_MODEL)\n            \n            # Chuẩn hóa độ khó nằm giữa 0 và 1 (0 = dễ, 1 = khó).\n            # Nếu một mẫu được học muộn (epoch cao hơn), nó khó hơn, vì vậy độ khó chuẩn hóa gần 1.\n            if cfg.NUM_EPOCHS_PER_MODEL > 0:\n                normalized_difficulty = difficulty_value / cfg.NUM_EPOCHS_PER_MODEL\n            else:\n                normalized_difficulty = 0.0 # Mặc định nếu không có epoch nào được định nghĩa (hoặc 0.5 cho trung tính)\n\n\n        else:\n            # Nếu không tìm thấy chỉ mục (ví dụ: một mẫu huấn luyện bằng cách nào đó bị bỏ lỡ trong quá trình theo dõi),\n            # mặc định không có hình phạt (độ khó trung tính).\n            normalized_difficulty = 0.0 \n\n        # Giảm điểm dựa trên độ khó và một yếu tố hình phạt có thể điều chỉnh (cfg.TRAINING_DYNAMICS_CONF_PENALTY)\n        # Độ khó cao hơn dẫn đến giảm điểm lớn hơn\n        adjustment_factor = 1.0 - (cfg.TRAINING_DYNAMICS_CONF_PENALTY * normalized_difficulty)\n        \n        adjusted_scores[i] *= adjustment_factor\n        adjusted_scores[i] = max(0.0, adjusted_scores[i]) # Đảm bảo điểm không âm\n\n    return adjusted_scores\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-29T18:05:15.975021Z","iopub.execute_input":"2025-06-29T18:05:15.975246Z","iopub.status.idle":"2025-06-29T18:05:15.988257Z","shell.execute_reply.started":"2025-06-29T18:05:15.975229Z","shell.execute_reply":"2025-06-29T18:05:15.987631Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 8. OOD Detection Methods (Baseline B.1.x và B.2.x)\n\nImplement các phương pháp phát hiện Out-of-Distribution samples:\n\n### B.1.x: ODIN (Out-of-Distribution Detector for Neural Networks)\n- Sử dụng temperature scaling và input perturbation\n- Tính toán ODIN score để identify OOD samples\n\n### B.2.x: Energy Score  \n- Tính toán Energy Score từ logits\n- Samples với energy thấp được coi là OOD\n","metadata":{"vscode":{"languageId":"raw"}}},{"cell_type":"code","source":"def calculate_single_model_odin_score(model, inputs, temp, epsilon, device):\n    \"\"\"\n    Tính toán điểm ODIN cho đầu vào batch trên một mô hình duy nhất.\n    Trả về điểm ODIN (numpy array), điểm cao hơn nghĩa là trong phân phối hơn.\n    \"\"\"\n    # Đảm bảo inputs có thể tính toán gradient\n    inputs.requires_grad_(True)\n    \n    # Đặt mô hình ở chế độ đánh giá\n    model.eval() \n    \n    # Forward pass để lấy logits\n    outputs = model(inputs)\n    \n    # Áp dụng nhiệt độ cho logits\n    temp_outputs = outputs / temp\n    \n    # Lấy lớp dự đoán cho việc nhiễu loạn\n    pred_class = temp_outputs.argmax(dim=1)\n    \n    # Tính toán loss (negative log-likelihood) cho lớp dự đoán.\n    # Mục tiêu là tối đa hóa xác suất của lớp dự đoán này bằng cách nhiễu loạn đầu vào.\n    loss = F.cross_entropy(temp_outputs, pred_class)\n    \n    # Tính toán gradient của loss đối với đầu vào\n    # create_graph=False để không xây dựng biểu đồ cho các lần backward tiếp theo\n    grad = torch.autograd.grad(loss, inputs, create_graph=False)[0] \n    \n    # Tạo đầu vào bị nhiễu loạn\n    perturbed_inputs = inputs - epsilon * torch.sign(grad)\n    \n    # Chuyển đầu vào bị nhiễu loạn qua mô hình một lần nữa\n    with torch.no_grad(): # Không cần gradient cho bước này\n        perturbed_outputs = model(perturbed_inputs)\n        \n    # Điểm ODIN là xác suất tối đa của đầu ra bị nhiễu loạn sau khi áp dụng nhiệt độ\n    odin_probs = F.softmax(perturbed_outputs / temp, dim=1)\n    odin_score = torch.max(odin_probs, dim=1)[0]\n    \n    inputs.requires_grad_(False) # Đặt lại yêu cầu gradient của đầu vào\n    \n    return odin_score.cpu().numpy()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-29T18:05:15.98893Z","iopub.execute_input":"2025-06-29T18:05:15.989209Z","iopub.status.idle":"2025-06-29T18:05:16.003138Z","shell.execute_reply.started":"2025-06-29T18:05:15.989193Z","shell.execute_reply":"2025-06-29T18:05:16.002678Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 9. Rejection Score Calculation - Main Function\n\nĐây là hàm chính để tính toán rejection scores và predictions cho ensemble. Hàm này:\n\n- Load ensemble models và thực hiện inference\n- Apply các phương pháp calibration khác nhau\n- Tính toán ODIN/Energy scores cho OOD detection\n- Combine ensemble predictions với disagreement và OOD signals\n- Apply training dynamics adjustment nếu enabled\n- Trả về final rejection scores và raw data cho analysis\n","metadata":{"vscode":{"languageId":"raw"}}},{"cell_type":"code","source":"def get_rejection_scores_and_predictions(cfg, data_loader, ensemble_models_dir, \n                                        final_overall_learning_metrics=None, train_dataset=None,\n                                        calibration_method='temperature_scaling', # e.g., 'temperature_scaling', 'isotonic_regression', 'beta_calibration'\n                                        ood_detection_method='none', # e.g., 'none', 'odin', 'energy'\n                                        combine_ood_with_disagreement=False): # Controls B.1.2, B.2.2 vs B.1.1, B.2.1\n    \"\"\"\n    Tính toán điểm từ chối và dự đoán của ensemble cho các mẫu.\n    Tùy chọn tích hợp Monte Carlo Dropout (MCDO), các phương pháp hiệu chỉnh,\n    và các phương pháp phát hiện OOD (ODIN, Energy Score).\n    Áp dụng điều chỉnh dựa trên động lực huấn luyện nếu `cfg.ENABLE_TRAINING_DYNAMICS` là True.\n\n    Trả về:\n    - all_predictions: Các dự đoán ensemble cuối cùng\n    - final_rejection_scores: Điểm từ chối cuối cùng (cao hơn là được chấp nhận)\n    - all_labels: Nhãn thực\n    - all_original_indices: Các chỉ mục toàn cục gốc của các mẫu\n    - all_ensemble_individual_probs_stacked: Mảng NumPy (num_samples, total_runs_or_models, num_classes) của các xác suất mô hình riêng lẻ/chạy MCDO.\n    - all_odin_scores_raw: Điểm ODIN thô cho mỗi mẫu (None nếu không áp dụng)\n    - all_energy_scores_raw: Điểm Energy thô cho mỗi mẫu (None nếu không áp dụng)\n    \"\"\"\n    all_predictions = []\n    all_labels = []\n    all_original_indices = []\n    \n    all_calibrated_confidences = [] # Điểm tin cậy sau hiệu chỉnh, trước bất đồng/OOD\n    \n    all_individual_run_probs_across_batches_if_mcdo_enabled = [] # Để tính toán bất đồng\n    all_odin_scores_across_batches = [] # Điểm ODIN trung bình cho mỗi mẫu\n    all_energy_scores_across_batches = [] # Điểm Energy cho mỗi mẫu\n\n    # Tải tất cả các mô hình ensemble\n    loaded_models = []\n    for i in range(cfg.NUM_ENSEMBLE_MODELS):\n        model = BaseClassifier(num_classes=2, dropout_rate=(cfg.MCDO_DROPOUT_RATE if cfg.MCDO_ENABLE else 0.0)).to(cfg.DEVICE)\n        model.load_state_dict(torch.load(os.path.join(ensemble_models_dir, f'best_model_ensemble_{i}.pth')))\n        model.eval() # Luôn ở chế độ eval cho inference khi không dùng MCDO, nhưng sẽ bật dropout nếu MCDO_ENABLE\n        loaded_models.append(model)\n    \n    # Clear CUDA cache before starting inference/calibration to ensure maximum free memory\n    if cfg.DEVICE.type == 'cuda':\n        torch.cuda.empty_cache()\n            # Khởi tạo bộ hiệu chỉnh\n    calibrator = None\n    if calibration_method == 'temperature_scaling':\n        calibrator = TemperatureScaler()\n        calibrator.to(cfg.DEVICE)\n        print(\"Sử dụng Temperature Scaling để hiệu chỉnh.\")\n    elif calibration_method == 'isotonic_regression':\n        calibrator = IsotonicCalibrator()\n        print(\"Sử dụng Isotonic Regression để hiệu chỉnh.\")\n    elif calibration_method == 'beta_calibration':\n        calibrator = BetaCalibrator()\n        print(\"Sử dụng Beta Calibration để hiệu chỉnh.\")\n    else:\n        print(\"Không sử dụng hiệu chỉnh post-hoc (hoặc phương pháp không hợp lệ).\")\n\n    print(f\"Hiệu chỉnh bộ hiệu chỉnh ({calibration_method}) trên các logits/xác suất trung bình của ensemble từ data_loader hiện tại...\")\n    # --- Thu thập tất cả các logits/confidences/labels từ data_loader hiện tại để hiệu chỉnh ---\n    data_loader_ensemble_raw_outputs = [] # Logits hoặc xác suất trung bình\n    data_loader_labels_for_calibration = []\n\n    with torch.no_grad():\n        for inputs_batch_cal, labels_batch_cal, _ in tqdm(data_loader, desc=\"Thu thập Dữ liệu để hiệu chỉnh\"):\n            inputs_batch_cal = inputs_batch_cal.to(cfg.DEVICE)\n            \n            ensemble_logits_batch_cal = []\n            if cfg.MCDO_ENABLE:\n                for model_idx, model in enumerate(loaded_models):\n                    model.train() # Enable dropout for MCDO during inference for avg_logits for calibration\n                    model_logits_runs = []\n                    for _ in range(cfg.MCDO_NUM_RUNS):\n                        model_logits_runs.append(model(inputs_batch_cal))\n                    ensemble_logits_batch_cal.append(torch.stack(model_logits_runs).mean(dim=0))\n            else:\n                for model_idx, model in enumerate(loaded_models):\n                    model.eval()\n                    ensemble_logits_batch_cal.append(model(inputs_batch_cal))\n            \n            avg_logits_batch_cal = torch.stack(ensemble_logits_batch_cal).mean(dim=0)\n            \n            data_loader_ensemble_raw_outputs.append(avg_logits_batch_cal.cpu()) # Move to CPU\n            data_loader_labels_for_calibration.append(labels_batch_cal.cpu())\n\n    # Clear CUDA cache after collecting data for calibration\n    if cfg.DEVICE.type == 'cuda':\n        torch.cuda.empty_cache()\n\n    if data_loader_ensemble_raw_outputs: \n        data_loader_ensemble_raw_outputs_all = torch.cat(data_loader_ensemble_raw_outputs)\n        data_loader_labels_for_calibration_all = torch.cat(data_loader_labels_for_calibration)\n\n        if calibration_method == 'temperature_scaling':\n            calibrator.calibrate(data_loader_ensemble_raw_outputs_all.to(cfg.DEVICE), # Move back to GPU for calibration\n                                 data_loader_labels_for_calibration_all.to(cfg.DEVICE), cfg.DEVICE)\n        elif calibration_method in ['isotonic_regression', 'beta_calibration']:\n            probs_for_calibration = softmax(data_loader_ensemble_raw_outputs_all.numpy(), axis=1)\n            confidences_for_calibration = np.max(probs_for_calibration, axis=1)\n            calibrator.calibrate(confidences_for_calibration, data_loader_labels_for_calibration_all.numpy())\n    else:\n        print(\"Cảnh báo: Không có dữ liệu trong data_loader để hiệu chỉnh. Bỏ qua hiệu chỉnh post-hoc.\")\n\n    # Clear CUDA cache after calibration\n    if cfg.DEVICE.type == 'cuda':\n        torch.cuda.empty_cache()\n    \n    # Trích xuất đặc trưng từ data_loader hiện tại (tập val/test) để điều chỉnh động lực huấn luyện\n    print(\"Trích xuất đặc trưng từ trình tải dữ liệu hiện tại để điều chỉnh độ tin cậy...\")\n    loaded_models[0].eval() # Đảm bảo mô hình ở chế độ eval khi trích xuất đặc trưng\n    all_extracted_features, extracted_original_indices = extract_features(\n        loaded_models[0], data_loader, cfg.DEVICE \n    )\n\n    # Clear CUDA cache after feature extraction\n    if cfg.DEVICE.type == 'cuda':\n        torch.cuda.empty_cache()\n\n    print(\"Tạo dự đoán và các điểm từ chối...\")\n    for inputs, labels, original_indices_batch in tqdm(data_loader, desc=\"Dự đoán và Tính điểm\"):\n        inputs, labels = inputs.to(cfg.DEVICE), labels.to(cfg.DEVICE)\n\n        ensemble_logits_per_model = [] \n        current_batch_ensemble_run_probs = [] # For all_ensemble_individual_probs_stacked\n\n        # Calculate ODIN/Energy for each sample/model\n        current_batch_odin_scores_individual_models = []\n        current_batch_energy_scores_individual_models = []\n\n        for model_idx, model in enumerate(loaded_models):\n            # Ensure model is in correct mode (train for MCDO, eval otherwise)\n            if cfg.MCDO_ENABLE:\n                model.train() # Enable dropout during inference for MCDO\n            else:\n                model.eval()\n\n            with torch.no_grad(): # Use no_grad for main inference\n                if cfg.MCDO_ENABLE:\n                    model_logits_runs = []\n                    for _ in range(cfg.MCDO_NUM_RUNS):\n                        model_logits_runs.append(model(inputs).cpu()) # Move logits to CPU immediately\n                    \n                    avg_logits_for_model = torch.stack(model_logits_runs).mean(dim=0).to(cfg.DEVICE)\n                    ensemble_logits_per_model.append(avg_logits_for_model)\n\n                    # Store all MCDO runs' probabilities for disagreement calculation\n                    probs_runs = softmax(torch.stack([l.to(cfg.DEVICE) for l in model_logits_runs]).detach().cpu().numpy(), axis=2)\n                    current_batch_ensemble_run_probs.append(np.transpose(probs_runs, (1, 0, 2)))\n                else: # MCDO is disabled, just one forward pass per model\n                    logits = model(inputs)\n                    ensemble_logits_per_model.append(logits)\n                    \n                    # Store single run probabilities for disagreement calculation\n                    probs_individual = softmax(logits.detach().cpu().numpy(), axis=1)\n                    current_batch_ensemble_run_probs.append(probs_individual[:, np.newaxis, :])\n\n            # --- Calculate ODIN Score for each model ---\n            if ood_detection_method == 'odin':\n                odin_score_batch = calculate_single_model_odin_score(model, inputs.clone().detach(), cfg.ODIN_TEMP, cfg.ODIN_EPSILON, cfg.DEVICE)\n                current_batch_odin_scores_individual_models.append(odin_score_batch)\n            \n            # --- Calculate Energy Score for each model's logits ---\n            if ood_detection_method == 'energy':\n                with torch.no_grad():\n                    if cfg.MCDO_ENABLE:\n                        energy_score_batch = -torch.logsumexp(avg_logits_for_model, dim=1).detach().cpu().numpy()\n                    else: # If MCDO is disabled, use the single pass logits\n                        energy_score_batch = -torch.logsumexp(logits, dim=1).detach().cpu().numpy()\n                current_batch_energy_scores_individual_models.append(energy_score_batch)\n\n        # Clear CUDA cache after processing each model within the batch loop\n        if cfg.DEVICE.type == 'cuda':\n            torch.cuda.empty_cache()\n\n        # Average ODIN/Energy scores across ensemble models for the current batch\n        if current_batch_odin_scores_individual_models:\n            all_odin_scores_across_batches.append(np.mean(np.stack(current_batch_odin_scores_individual_models, axis=1), axis=1))\n        if current_batch_energy_scores_individual_models:\n            all_energy_scores_across_batches.append(np.mean(np.stack(current_batch_energy_scores_individual_models, axis=1), axis=1))\n\n        # Concatenate individual run probs for current batch across models\n        if current_batch_ensemble_run_probs:\n            all_individual_run_probs_across_batches_if_mcdo_enabled.append(np.concatenate(current_batch_ensemble_run_probs, axis=1))\n        \n        # Average logits from ensemble for this batch\n        avg_ensemble_logits = torch.stack(ensemble_logits_per_model).mean(dim=0)\n\n        # --- Apply Calibrator ---\n        if calibrator:\n            if calibration_method == 'temperature_scaling':\n                calibrated_logits = calibrator.forward(avg_ensemble_logits)\n                calibrated_probs = softmax(calibrated_logits.detach().cpu().numpy(), axis=1)\n            elif calibration_method in ['isotonic_regression', 'beta_calibration']:\n                initial_probs = softmax(avg_ensemble_logits.detach().cpu().numpy(), axis=1)\n                initial_confidences = np.max(initial_probs, axis=1)\n                \n                calibrated_confidences_vals = calibrator.predict_proba(initial_confidences)\n                calibrated_probs = initial_probs.copy()\n                for k_idx in range(len(calibrated_probs)):\n                    predicted_class = np.argmax(calibrated_probs[k_idx])\n                    if calibrated_probs[k_idx][predicted_class] > 0:\n                        scaling_factor = calibrated_confidences_vals[k_idx] / calibrated_probs[k_idx][predicted_class]\n                        calibrated_probs[k_idx, :] *= scaling_factor\n                        calibrated_probs[k_idx, :] = np.maximum(0, calibrated_probs[k_idx, :])\n                        calibrated_probs[k_idx, :] /= (np.sum(calibrated_probs[k_idx, :]) + 1e-9)\n                    else: # Fallback for edge case\n                        calibrated_probs[k_idx, predicted_class] = calibrated_confidences_vals[k_idx]\n                        other_class_indices = [j for j in range(calibrated_probs.shape[1]) if j != predicted_class]\n                        if len(other_class_indices) > 0:\n                            total_other_prob = 1.0 - calibrated_confidences_vals[k_idx]\n                            if np.sum(calibrated_probs[k_idx, other_class_indices]) > 0:\n                                calibrated_probs[k_idx, other_class_indices] *= (total_other_prob / np.sum(calibrated_probs[k_idx, other_class_indices]))\n                            else:\n                                calibrated_probs[k_idx, other_class_indices] = total_other_prob / len(other_class_indices)\n        else: # No post-hoc calibration\n            calibrated_probs = softmax(avg_ensemble_logits.detach().cpu().numpy(), axis=1)\n\n        # This is the confidence after calibration, BEFORE disagreement/OOD penalty\n        current_calibrated_confidences = np.max(calibrated_probs, axis=1)\n        all_calibrated_confidences.extend(current_calibrated_confidences)\n\n        # Store predictions and true labels\n        predictions = np.argmax(calibrated_probs, axis=1)\n        all_predictions.extend(predictions)\n        all_labels.extend(labels.cpu().numpy())\n        all_original_indices.extend(original_indices_batch.cpu().numpy())\n    \n    # --- After iterating through all batches, stack probabilities for disagreement ---\n    if all_individual_run_probs_across_batches_if_mcdo_enabled:\n        all_ensemble_individual_probs_stacked = np.concatenate(all_individual_run_probs_across_batches_if_mcdo_enabled, axis=0)\n    else:\n        all_ensemble_individual_probs_stacked = np.array([])\n\n    # Consolidate ODIN and Energy scores\n    all_odin_scores_raw = np.concatenate(all_odin_scores_across_batches, axis=0) if all_odin_scores_across_batches else None\n    all_energy_scores_raw = np.concatenate(all_energy_scores_across_batches, axis=0) if all_energy_scores_across_batches else None\n\n    # Convert lists to numpy arrays\n    all_calibrated_confidences = np.array(all_calibrated_confidences)\n    all_predictions = np.array(all_predictions)\n    all_labels = np.array(all_labels)\n    all_original_indices = np.array(all_original_indices)\n\n    # --- Calculate Disagreement Penalty ---\n    disagreement_penalties = np.zeros_like(all_calibrated_confidences)\n    if all_ensemble_individual_probs_stacked.size > 0:\n        num_samples = all_ensemble_individual_probs_stacked.shape[0]\n        for i_idx in range(num_samples):\n            current_sample_individual_probs = all_ensemble_individual_probs_stacked[i_idx, :, :] \n            ensemble_predicted_class = all_predictions[i_idx] \n            \n            if (current_sample_individual_probs.size > 0 and \n                ensemble_predicted_class < current_sample_individual_probs.shape[1] and \n                ensemble_predicted_class >= 0):\n                variance_disagreement = np.var(current_sample_individual_probs[:, ensemble_predicted_class])\n            else:\n                variance_disagreement = 0.0 \n            \n            disagreement_penalties[i_idx] = variance_disagreement * cfg.DISAGREEMENT_PENALTY_FACTOR\n\n    # --- Calculate Final Rejection Score based on Method ---\n    final_rejection_scores = np.copy(all_calibrated_confidences)\n\n    if ood_detection_method == 'none':\n        final_rejection_scores = all_calibrated_confidences * (1.0 - disagreement_penalties)\n        final_rejection_scores = np.clip(final_rejection_scores, 0.0, 1.0)\n    elif ood_detection_method == 'odin':\n        if all_odin_scores_raw is None or all_odin_scores_raw.size == 0:\n            print(\"Cảnh báo: ODIN được yêu cầu nhưng không có điểm ODIN. Sử dụng điểm tin cậy thông thường.\")\n            final_rejection_scores = all_calibrated_confidences * (1.0 - disagreement_penalties)\n        else:\n            final_rejection_scores = all_odin_scores_raw\n            if combine_ood_with_disagreement:\n                final_rejection_scores = final_rejection_scores * (1.0 - disagreement_penalties)\n            final_rejection_scores = np.clip(final_rejection_scores, 0.0, 1.0)\n    elif ood_detection_method == 'energy':\n        if all_energy_scores_raw is None or all_energy_scores_raw.size == 0:\n            print(\"Cảnh báo: Energy Score được yêu cầu nhưng không có điểm Energy. Sử dụng điểm tin cậy thông thường.\")\n            final_rejection_scores = all_calibrated_confidences * (1.0 - disagreement_penalties)\n        else:\n            if all_energy_scores_raw.size > 0:\n                min_e, max_e = np.min(all_energy_scores_raw), np.max(all_energy_scores_raw)\n                if (max_e - min_e) > 0:\n                    normalized_energy_scores = (all_energy_scores_raw - min_e) / (max_e - min_e)\n                else: \n                    normalized_energy_scores = np.full_like(all_energy_scores_raw, 0.5) \n            else:\n                normalized_energy_scores = np.array([])\n\n            final_rejection_scores = normalized_energy_scores\n            if combine_ood_with_disagreement:\n                final_rejection_scores = final_rejection_scores * (1.0 - disagreement_penalties)\n            final_rejection_scores = np.clip(final_rejection_scores, 0.0, 1.0)\n\n    # --- Áp dụng Điều chỉnh Động lực Huấn luyện nếu cờ ENABLE_TRAINING_DYNAMICS là True ---\n    if cfg.ENABLE_TRAINING_DYNAMICS and final_overall_learning_metrics is not None and train_dataset is not None:\n        # 'all_extracted_features' đã được tính toán ở đầu hàm cho data_loader hiện tại\n        loaded_models[0].eval() # Ensure eval mode for feature extraction\n        train_features_for_adjustment, train_global_indices_for_adjustment = extract_features(\n            loaded_models[0], DataLoader(train_dataset, batch_size=cfg.BATCH_SIZE, shuffle=False, num_workers=2), cfg.DEVICE\n        )\n\n        final_rejection_scores = adjust_confidence_with_training_dynamics(\n            cfg, all_extracted_features, final_rejection_scores, \n            train_features_for_adjustment, train_global_indices_for_adjustment, final_overall_learning_metrics\n        )\n    elif cfg.ENABLE_TRAINING_DYNAMICS:\n        print(\"Cảnh báo: ENABLE_TRAINING_DYNAMICS bật nhưng final_overall_learning_metrics hoặc train_dataset không được cung cấp.\")\n\n    # Clear CUDA cache at the very end of the function\n    if cfg.DEVICE.type == 'cuda':\n        torch.cuda.empty_cache()\n\n    return (all_predictions, final_rejection_scores, all_labels,\n            all_original_indices, all_ensemble_individual_probs_stacked,\n            all_odin_scores_raw, all_energy_scores_raw)\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-29T18:05:16.004041Z","iopub.execute_input":"2025-06-29T18:05:16.004269Z","iopub.status.idle":"2025-06-29T18:05:16.035001Z","shell.execute_reply.started":"2025-06-29T18:05:16.004254Z","shell.execute_reply":"2025-06-29T18:05:16.03432Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 10. Rejection Mechanism và Adaptive Threshold Optimization\n\nPhần này bao gồm:\n- Tìm ngưỡng từ chối tối ưu trên validation set\n- Tính toán ECE (Expected Calibration Error)\n- Tối ưu hóa đa mục tiêu (accuracy, rejection rate, ECE)\n- Categorize rejected cases (failure, OOD, ambiguous)\n","metadata":{"vscode":{"languageId":"raw"}}},{"cell_type":"code","source":"# --- ECE Calculation ---\ndef calculate_ece(model_predictions, rejection_scores, true_labels, num_bins=10):\n    \"\"\"\n    Tính toán Expected Calibration Error (ECE).\n    \"\"\"\n    if len(rejection_scores) == 0:\n        return 0.0\n\n    bins = np.linspace(0., 1., num_bins + 1)\n    ece = 0.0\n    total_samples = len(true_labels)\n\n    for i in range(num_bins):\n        lower_bound = bins[i]\n        upper_bound = bins[i+1]\n        # FIX: Added np.nan_to_num to handle potential NaNs from previous calculations before comparison\n        mask = (np.nan_to_num(rejection_scores, nan=-np.inf) >= lower_bound) & (np.nan_to_num(rejection_scores, nan=-np.inf) < upper_bound)\n        if i == num_bins - 1: # Bao gồm 1.0 trong bin cuối cùng\n            mask = (np.nan_to_num(rejection_scores, nan=-np.inf) >= lower_bound) & (np.nan_to_num(rejection_scores, nan=-np.inf) <= upper_bound)\n\n        bin_samples_indices = np.where(mask)[0]\n        bin_count = len(bin_samples_indices)\n\n        if bin_count > 0:\n            bin_accuracy = accuracy_score(true_labels[bin_samples_indices], model_predictions[bin_samples_indices])\n            bin_rejection_score_mean = np.mean(rejection_scores[bin_samples_indices])\n            ece += (bin_count / total_samples) * np.abs(bin_accuracy - bin_rejection_score_mean)\n    return ece\n\ndef find_optimal_rejection_threshold(rejection_scores, original_model_predictions, true_labels, cfg):\n    \"\"\"\n    Tìm ngưỡng độ tin cậy tối ưu trên tập validation\n    để đáp ứng độ chính xác mục tiêu trên các trường hợp được chấp nhận, tỷ lệ từ chối mục tiêu và tối ưu hóa ECE.\n    \"\"\"\n    thresholds = np.linspace(0.0, 1.0, 1000) \n    best_threshold = 0.0\n    min_deviation = float('inf')\n\n    print(\"\\n--- Tìm Ngưỡng Từ Chối Tối ưu trên Tập Validation ---\")\n    results = []\n    # FIX: Added np.nan_to_num to handle potential NaNs in rejection_scores before thresholding\n    rejection_scores_clean = np.nan_to_num(rejection_scores, nan=-np.inf) # Treat NaN as low score (rejected)\n\n    for threshold in tqdm(thresholds, desc=\"Đánh giá ngưỡng\"):\n        # `rejection_scores` là điểm thống nhất, điểm cao hơn nghĩa là được chấp nhận\n        accepted_indices = rejection_scores_clean >= threshold \n        \n        num_total = len(true_labels)\n        num_accepted = np.sum(accepted_indices)\n        \n        current_coverage = num_accepted / num_total\n        current_rejection_rate = 1.0 - current_coverage\n\n        current_accuracy_on_accepted = 0.0\n        current_ece_on_accepted = 0.0\n\n        if num_accepted > 0:\n            accepted_predictions = original_model_predictions[accepted_indices]\n            accepted_true_labels = true_labels[accepted_indices]\n            accepted_rejection_scores = rejection_scores[accepted_indices] # Use original scores for ECE calc\n\n            current_accuracy_on_accepted = accuracy_score(accepted_true_labels, accepted_predictions)\n            current_ece_on_accepted = calculate_ece(accepted_predictions, accepted_rejection_scores, accepted_true_labels)\n\n\n        # Tính độ lệch so với mục tiêu, sử dụng trọng số có thể cấu hình\n        accuracy_deviation = max(0, cfg.TARGET_ACCEPTED_ACCURACY - current_accuracy_on_accepted) * cfg.ACCURACY_DEVIATION_WEIGHT\n        rejection_deviation = abs(current_rejection_rate - cfg.TARGET_REJECTION_RATE) * cfg.REJECTION_RATE_DEVIATION_WEIGHT\n        # Giảm thiểu ECE trên tập chấp nhận (ECE thấp hơn là tốt hơn)\n        ece_deviation = current_ece_on_accepted * cfg.ECE_DEVIATION_WEIGHT\n\n        deviation = accuracy_deviation + rejection_deviation + ece_deviation\n\n        results.append({\n            'threshold': threshold,\n            'accuracy_on_accepted': current_accuracy_on_accepted,\n            'rejection_rate': current_rejection_rate,\n            'ece_on_accepted': current_ece_on_accepted,\n            'deviation': deviation\n        })\n\n    results_df = pd.DataFrame(results)\n    # Lọc các ngưỡng thực tế cung cấp một số độ phủ\n    results_df = results_df[results_df['rejection_rate'] < 1.0]\n\n    if not results_df.empty:\n        # Tìm hàng có tổng độ lệch tối thiểu\n        best_row_idx = results_df['deviation'].idxmin()\n        best_threshold_info = results_df.loc[best_row_idx]\n        best_threshold = best_threshold_info['threshold']\n        min_deviation = best_threshold_info['deviation']\n\n        print(f\"Tìm thấy ngưỡng tối ưu: {best_threshold:.4f}\")\n        print(f\"  Độ chính xác trên các trường hợp được chấp nhận: {best_threshold_info['accuracy_on_accepted']:.4f}\")\n        print(f\"  Tỷ lệ từ chối: {best_threshold_info['rejection_rate']:.4f}\")\n        print(f\"  ECE trên các trường hợp được chấp nhận: {best_threshold_info['ece_on_accepted']:.4f}\")\n        print(f\"  Tổng độ lệch: {best_threshold_info['deviation']:.4f}\")\n    else:\n        print(\"Không thể tìm thấy ngưỡng phù hợp, mặc định là 0.5. Vui lòng kiểm tra dữ liệu và mục tiêu cấu hình của bạn.\")\n        best_threshold = 0.5\n\n    return best_threshold, results_df\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-29T18:05:16.035777Z","iopub.execute_input":"2025-06-29T18:05:16.035994Z","iopub.status.idle":"2025-06-29T18:05:16.051223Z","shell.execute_reply.started":"2025-06-29T18:05:16.03597Z","shell.execute_reply":"2025-06-29T18:05:16.050631Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 11. Evaluation Metrics và Performance Analysis\n\nCác metrics được tính toán:\n- **Accuracy**: Overall và trên accepted cases\n- **Coverage/Rejection Rate**: Tỷ lệ samples được chấp nhận/từ chối\n- **Calibration**: ECE, NLL, Brier Score\n- **Discrimination**: AUROC, AUPR cho correctness prediction\n- **Risk-Coverage**: AURC và Risk-Coverage curves\n- **F1-Score**: Cho rejection task\n","metadata":{"vscode":{"languageId":"raw"}}},{"cell_type":"code","source":"# --- Metrics Đánh giá ---\ndef calculate_metrics(model_predictions, rejection_scores, true_labels, rejection_threshold, verbose=True):\n    \"\"\"\n    Tính toán các chỉ số phân loại chọn lọc khác nhau.\n    `rejection_scores` là điểm thống nhất, điểm cao hơn nghĩa là được chấp nhận.\n    \"\"\"\n    accepted_indices = rejection_scores >= rejection_threshold\n    rejected_indices = rejection_scores < rejection_threshold\n\n    num_total = len(true_labels)\n    num_accepted = np.sum(accepted_indices)\n    num_rejected = np.sum(rejected_indices)\n\n    # Độ phủ (Coverage)\n    coverage = num_accepted / num_total\n    rejection_rate = num_rejected / num_total\n\n    # Độ chính xác trên các Trường hợp được Chấp nhận (Rủi ro)\n    if num_accepted > 0:\n        accepted_predictions = model_predictions[accepted_indices]\n        accepted_true_labels = true_labels[accepted_indices]\n        accuracy_accepted = accuracy_score(accepted_true_labels, accepted_predictions)\n        risk = 1.0 - accuracy_accepted\n        # NLL và Brier Score trên các mẫu được chấp nhận\n        all_possible_labels = np.unique(true_labels) # Get all unique labels from the original true_labels\n        nll_accepted = log_loss(accepted_true_labels, rejection_scores[accepted_indices], labels=all_possible_labels)\n        brier_accepted = brier_score_loss(accepted_true_labels, rejection_scores[accepted_indices])\n    else:\n        accuracy_accepted = 0.0 \n        risk = 1.0\n        nll_accepted = np.nan\n        brier_accepted = np.nan\n\n    # Độ chính xác tổng thể (để so sánh)\n    overall_accuracy = accuracy_score(true_labels, model_predictions)\n\n    if verbose:\n        print(f\"\\n--- Kết quả Đánh giá (Ngưỡng={rejection_threshold:.4f}) ---\")\n        print(f\"Độ chính xác tổng thể (Tất cả các mẫu): {overall_accuracy:.4f}\")\n        print(f\"Độ phủ: {coverage:.4f} ({num_accepted} mẫu được chấp nhận)\")\n        print(f\"Tỷ lệ từ chối: {rejection_rate:.4f} ({num_rejected} mẫu bị từ chối)\")\n        print(f\"Độ chính xác trên các Trường hợp được chấp nhận: {accuracy_accepted:.4f}\")\n        print(f\"Rủi ro trên các Trường hợp được chấp nhận: {risk:.4f}\")\n\n    # Chỉ số hiệu chỉnh (ECE - Expected Calibration Error)\n    ece = calculate_ece(model_predictions, rejection_scores, true_labels)\n    if verbose:\n        print(f\"\\nExpected Calibration Error (ECE): {ece:.4f}\")\n        print(f\"Negative Log-Likelihood (NLL) trên các mẫu được chấp nhận: {nll_accepted:.4f}\")\n        print(f\"Brier Score trên các mẫu được chấp nhận: {brier_accepted:.4f}\")\n\n    # AUROC và AUPR calculations\n    is_correct = (model_predictions == true_labels).astype(int)\n    \n    if len(np.unique(true_labels)) > 1: \n        fpr, tpr, roc_thresholds = roc_curve(is_correct, rejection_scores)\n        auroc = auc(fpr, tpr)\n        if verbose:\n            print(f\"AUROC (Điểm từ chối là điểm cho tính đúng đắn): {auroc:.4f}\")\n    else:\n        auroc = np.nan\n\n    if len(np.unique(is_correct)) > 1:\n        precision_correct, recall_correct, _ = precision_recall_curve(is_correct, rejection_scores)\n        aupr_correct = auc(recall_correct, precision_correct)\n        if verbose:\n            print(f\"AUPR (Điểm từ chối là điểm cho tính đúng đắn): {aupr_correct:.4f}\")\n    else:\n        aupr_correct = np.nan\n\n    # AURC calculation\n    sorted_indices = np.argsort(rejection_scores)\n    sorted_predictions = model_predictions[sorted_indices]\n    sorted_labels = true_labels[sorted_indices]\n\n    risks = []\n    coverages = []\n    for i_idx in range(num_total):\n        current_accepted_preds = sorted_predictions[i_idx:]\n        current_accepted_labels = sorted_labels[i_idx:]\n        current_coverage = (num_total - i_idx) / num_total\n\n        if (num_total - i_idx) == 0:\n            current_risk = 1.0 \n        else:\n            num_correct = np.sum(current_accepted_preds == current_accepted_labels)\n            current_risk = 1.0 - (num_correct / (num_total - i_idx)) \n\n        coverages.append(current_coverage)\n        risks.append(current_risk)\n\n    coverages = coverages[::-1]\n    risks = risks[::-1]\n    aurc = np.trapz(risks, coverages)\n    \n    if verbose:\n        print(f\"Diện tích dưới Đường cong Risk-Coverage (AURC): {aurc:.4f}\")\n\n    # ✅ FIXED: Added F1-Score calculation for rejection task\n    # F1-Score for rejection task: ability to correctly identify rejected samples\n    is_rejected = (rejection_scores < rejection_threshold).astype(int)\n    is_incorrect = (model_predictions != true_labels).astype(int)\n    \n    if len(np.unique(is_incorrect)) > 1:\n        f1_rejection = f1_score(is_incorrect, is_rejected)\n        if verbose:\n            print(f\"F1-Score (Rejection Task - Identifying Incorrect Predictions): {f1_rejection:.4f}\")\n    else:\n        f1_rejection = np.nan\n\n    return {\n        'overall_accuracy': overall_accuracy,\n        'accuracy_on_accepted': accuracy_accepted,\n        'coverage': coverage,\n        'rejection_rate': rejection_rate,\n        'risk': risk,\n        'ece': ece,\n        'nll_accepted': nll_accepted,\n        'brier_accepted': brier_accepted,\n        'auroc_correct_incorrect': auroc,\n        'aupr_correct_incorrect': aupr_correct,\n        'aurc': aurc,\n        'f1_rejection': f1_rejection  # ✅ FIXED: Added F1-score to returned metrics\n    }\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-29T18:05:16.05213Z","iopub.execute_input":"2025-06-29T18:05:16.052405Z","iopub.status.idle":"2025-06-29T18:05:16.06744Z","shell.execute_reply.started":"2025-06-29T18:05:16.052389Z","shell.execute_reply":"2025-06-29T18:05:16.066791Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 12. XAI Visualization với Grad-CAM++\n\nTạo explainability visualizations cho các trường hợp khác nhau:\n- **Accepted & Correct**: Cases được model chấp nhận và dự đoán đúng\n- **Failure Rejected**: Cases bị từ chối vì model sẽ dự đoán sai\n- **Ambiguous Rejected**: Cases bị từ chối do uncertainty cao  \n- **OOD Rejected**: Cases bị từ chối vì được identify là Out-of-Distribution\n\nSử dụng Grad-CAM++ để tạo ensemble heatmaps cho mỗi case type.\n","metadata":{"vscode":{"languageId":"raw"}}},{"cell_type":"code","source":"# --- Hàm Trực quan hóa XAI ---\n\n# Hàm trợ giúp để tiền xử lý hình ảnh cho các phương pháp XAI\ndef preprocess_for_xai(image_path, cfg):\n    \"\"\"Tải hình ảnh và áp dụng các phép biến đổi cần thiết cho các phương pháp XAI.\"\"\"\n    img = Image.open(image_path).convert('RGB')\n    \n    # Hình ảnh gốc để phủ heatmap (mảng numpy float 0-1)\n    original_img_for_display = np.array(img.resize(cfg.IMAGE_SIZE)) / 255.0\n\n    # Hình ảnh đã biến đổi để làm đầu vào mô hình (tensor, được chuẩn hóa)\n    transform = transforms.Compose([\n        transforms.Resize(cfg.IMAGE_SIZE),\n        transforms.ToTensor(),\n        transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) # Chuẩn hóa ImageNet\n    ])\n    input_tensor = transform(img).unsqueeze(0).to(cfg.DEVICE) # Thêm chiều batch\n\n    return original_img_for_display, input_tensor\n\n\ndef visualize_ensemble_grad_cam(ensemble_models, image_path, original_img_for_display, input_tensor, \n                               true_label, ensemble_pred, ensemble_rejection_score, rejection_status, \n                               case_type, original_idx, save_dir, cam_method=GradCAMPlusPlus):\n    \"\"\"Tạo và hiển thị heatmap Grad-CAM++ trung bình cho ensemble.\"\"\"\n    print(f\"Tạo Grad-CAM++ trung bình của Ensemble cho Trường hợp {case_type} (Chỉ mục gốc: {original_idx})...\")\n    \n    all_grayscale_cams = []\n    for model in ensemble_models:\n        model.eval() # Đảm bảo mô hình ở chế độ eval\n        cam_instance = cam_method(model=model, target_layers=[model.target_layer])\n        targets = [ClassifierOutputTarget(ensemble_pred)] # Mục tiêu là lớp được dự đoán bởi ensemble\n        \n        grayscale_cam = cam_instance(input_tensor=input_tensor, targets=targets) # Truyền targets vào đây\n        all_grayscale_cams.append(grayscale_cam[0, :]) # Lưu trữ chỉ heatmap (đã loại bỏ chiều batch)\n\n    # Trung bình các heatmaps\n    if len(all_grayscale_cams) > 0:\n        averaged_grayscale_cam = np.mean(all_grayscale_cams, axis=0)\n    else:\n        averaged_grayscale_cam = np.zeros_like(original_img_for_display[:,:,0]) # Fallback to black if no CAMs\n\n    cam_image = show_cam_on_image(original_img_for_display, averaged_grayscale_cam, use_rgb=True)\n\n    plt.figure(figsize=(10, 5))\n    plt.subplot(1, 2, 1)\n    plt.imshow(original_img_for_display)\n    plt.title(f\"Ảnh gốc\\\\nĐúng: {true_label}, Dự đoán Ensemble: {ensemble_pred}\")\n    plt.axis('off')\n\n    plt.subplot(1, 2, 2)\n    plt.imshow(cam_image)\n    plt.title(f\"Ensemble Grad-CAM++ (Mục tiêu: Lớp {ensemble_pred})\\\\nĐiểm Từ Chối: {ensemble_rejection_score:.2f}, {rejection_status}\")\n    plt.axis('off')\n\n    sanitized_case_type = case_type.replace('/', '_').replace(' ', '_').replace('(', '').replace(')', '')\n    plt.suptitle(f\"Trường hợp {sanitized_case_type} (Chỉ mục: {original_idx}) - Giải thích Ensemble Grad-CAM++\")\n    plt.tight_layout(rect=[0, 0.03, 1, 0.95])\n    \n    # Lưu biểu đồ và sau đó đóng nó\n    filepath = os.path.join(save_dir, f'ensemble_grad_cam_idx_{original_idx}_{sanitized_case_type}.png')\n    plt.savefig(filepath)\n    plt.show()\n    plt.close() # Đóng biểu đồ để tránh các vấn đề về bộ nhớ\n    print(f\"Giải thích Ensemble Grad-CAM++ cho chỉ mục {original_idx} đã lưu vào: {filepath}\")\n    print(\"Giải thích: Heatmap trung bình cho thấy các vùng đồng thuận cho dự đoán của ensemble.\")\n\n\ndef visualize_xai_examples(cfg, ensemble_models, test_dataset, test_results, rejection_threshold):\n    \"\"\"\n    Chọn các trường hợp đại diện và tạo các hình ảnh XAI cho chúng bằng cách sử dụng Grad-CAM++.\n    \"\"\"\n    print(\"\\\\n--- Tạo Hình ảnh XAI cho các Trường hợp được Chọn ---\")\n\n    # Đảm bảo thư mục lưu XAI tồn tại\n    os.makedirs(cfg.XAI_SAVE_DIR, exist_ok=True)\n\n    test_model_predictions = test_results['model_preds']\n    test_rejection_scores = test_results['rejection_scores']\n    test_true_labels = test_results['true_labels']\n    test_original_indices = test_results['original_indices']\n    rejected_categories = test_results['rejected_categories']\n\n    # Lọc các loại trường hợp khác nhau\n    accepted_mask = test_rejection_scores >= rejection_threshold\n    \n    # Được chấp nhận và Đúng\n    accepted_correct_indices = np.where((accepted_mask) & (test_model_predictions == test_true_labels))[0]\n    \n    # Ánh xạ từ chỉ mục tập dữ liệu gốc đến vị trí của nó trong các mảng test_results phẳng\n    test_original_idx_to_pos = {idx: i for i, idx in enumerate(test_original_indices)}\n\n    # Danh sách để lưu trữ đường dẫn của các hình ảnh XAI đã tạo\n    generated_xai_image_paths = []\n\n    # --- Ví dụ về các trường hợp khác nhau ---\n    cases_to_visualize = [\n        (\"Accepted_Correct\", accepted_correct_indices, \"Được chấp nhận & Đúng\"),\n        (\"Failure_Rejected\", rejected_categories['failure_rejection_indices'], \"Bị từ chối Lỗi\"), \n        (\"Unknown_Ambiguous\", rejected_categories['unknown_ambiguous_indices'], \"Bị từ chối Không rõ/Mơ hồ\"),\n        (\"Potential_OOD\", rejected_categories['potential_ood_indices'], \"Bị từ chối OOD tiềm năng\")\n    ]\n\n    for case_name, case_indices, case_description in cases_to_visualize:\n        print(f\"\\\\n--- Ví dụ: {case_description} ---\")\n        if len(case_indices) > 0:\n            # Lấy case đầu tiên\n            if case_name == \"Accepted_Correct\":\n                sample_idx_in_test_data = case_indices[0]\n                original_idx = test_original_indices[sample_idx_in_test_data]\n            else:\n                original_idx = case_indices[0]\n                sample_idx_in_test_data = test_original_idx_to_pos[original_idx]\n            \n            image_path = test_dataset.image_paths[test_dataset.original_indices_map[original_idx]]\n            true_label = test_true_labels[sample_idx_in_test_data]\n            model_pred = test_model_predictions[sample_idx_in_test_data]\n            rejection_score = test_rejection_scores[sample_idx_in_test_data]\n            \n            original_img_for_display, input_tensor = preprocess_for_xai(image_path, cfg)\n            \n            print(f\"Đã chọn {case_description} (Chỉ mục gốc: {original_idx})\")\n            \n            filepath_case = os.path.join(cfg.XAI_SAVE_DIR, f'ensemble_grad_cam_idx_{original_idx}_{case_name}.png')\n            \n            rejection_status = \"Được chấp nhận\" if case_name == \"Accepted_Correct\" else f\"Bị từ chối ({case_description.split(' ')[-1]})\"\n            \n            visualize_ensemble_grad_cam(ensemble_models, image_path, original_img_for_display, input_tensor, \n                                      true_label, model_pred, rejection_score, rejection_status, \n                                      case_description, original_idx, cfg.XAI_SAVE_DIR)\n            generated_xai_image_paths.append(filepath_case)\n        else:\n            print(f\"Không tìm thấy trường hợp {case_description} để trực quan hóa XAI.\")\n        \n    print(f\"\\\\nTất cả các hình ảnh XAI đã lưu vào: {cfg.XAI_SAVE_DIR}\")\n    return generated_xai_image_paths\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-29T18:05:16.068227Z","iopub.execute_input":"2025-06-29T18:05:16.068459Z","iopub.status.idle":"2025-06-29T18:05:16.085965Z","shell.execute_reply.started":"2025-06-29T18:05:16.068444Z","shell.execute_reply":"2025-06-29T18:05:16.085312Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 13. Baseline Experiments và Main Execution\n\nĐịnh nghĩa và chạy tất cả các baseline experiments:\n\n### Baseline 0: Current Ensemble (Temperature Scaling)\n### Baseline A.1: Ensemble + MCDO  \n### Baseline A.2.1: Ensemble + Isotonic Regression\n### Baseline A.2.2: Ensemble + Beta Calibration\n### Baseline A.2.3: Ensemble + Label Smoothing\n### Baseline B.1.1: Ensemble + ODIN (Basic)\n### Baseline B.1.2: Ensemble + ODIN (Combined)  \n### Baseline B.2.1: Ensemble + Energy Score (Basic)\n### Baseline B.2.2: Ensemble + Energy Score (Combined)\n### Baseline B.3: Ensemble + Training Dynamics Insights\n\nMỗi baseline sẽ được đánh giá trên các metrics khác nhau và tạo comparative analysis.\n","metadata":{"vscode":{"languageId":"raw"}}},{"cell_type":"code","source":"# ✅ FIXED: Placeholder cell đã được làm sạch và thay thế bằng main run_baseline function\n\n# ✅ FIXED: Updated run_baseline to match main.py signature and logic\n\ndef run_baseline(config_name, mcdo_enable, label_smoothing_enable, \n                 calibration_method, ood_detection_method='none',\n                 combine_ood_with_disagreement=False,\n                 enable_training_dynamics=False):\n    \"\"\"\n    Chạy một cấu hình baseline cụ thể - Updated to match main.py structure.\n    \"\"\"\n    print(f\"\\\\n{'='*20}\\\\nBắt đầu chạy Baseline: {config_name}\\\\n{'='*20}\")\n\n    # Reset Config về trạng thái mặc định trước mỗi lần chạy\n    global cfg\n    cfg = Config() \n    set_seed(cfg.RANDOM_SEED)\n\n    # Cấu hình các cờ cho baseline hiện tại\n    cfg.MCDO_ENABLE = mcdo_enable\n    cfg.LABEL_SMOOTHING_ENABLE = label_smoothing_enable\n    cfg.ENABLE_TRAINING_DYNAMICS = enable_training_dynamics\n\n    # 1. Chuẩn bị dữ liệu\n    train_loader, val_loader, test_loader, train_dataset, val_dataset, test_dataset = prepare_datasets(cfg)\n\n    # 2. Huấn luyện các mô hình Ensemble cho baseline này (TRAINING RIÊNG CHO MỖI BASELINE)\n    print(f\"🔥 Bắt đầu training ensemble cho {config_name}...\")\n    print(f\"📋 Config: MCDO={cfg.MCDO_ENABLE}, LabelSmoothing={cfg.LABEL_SMOOTHING_ENABLE}, TrainingDynamics={cfg.ENABLE_TRAINING_DYNAMICS}\")\n    \n    final_overall_learning_metrics = None\n    if cfg.ENABLE_TRAINING_DYNAMICS:\n        print(\"📊 Training với Thu thập Training Dynamics...\")\n        final_overall_learning_metrics = train_ensemble(cfg, train_loader, val_loader, train_dataset)\n    else:\n        print(\"⚡ Training ensemble chuẩn...\")\n        _ = train_ensemble(cfg, train_loader, val_loader, train_dataset)\n    \n    print(f\"✅ Hoàn thành training cho {config_name}\")\n    \n    # Clear CUDA cache after training ensemble\n    if cfg.DEVICE.type == 'cuda':\n        torch.cuda.empty_cache()\n\n    # 3. Lấy Dự đoán Ensemble và Điểm Từ Chối (cho Tập Validation)\n    print(f\"\\\\nLấy dự đoán và điểm từ chối cho Tập Validation (Sử dụng hiệu chỉnh: {calibration_method}, OOD: {ood_detection_method}, Kết hợp bất đồng: {combine_ood_with_disagreement})...\")\n    val_model_predictions, val_rejection_scores, val_true_labels, val_original_indices, _, _, _ = (\n        get_rejection_scores_and_predictions(cfg, val_loader, cfg.MODEL_SAVE_DIR, \n                                            final_overall_learning_metrics, train_dataset, \n                                            calibration_method=calibration_method,\n                                            ood_detection_method=ood_detection_method,\n                                            combine_ood_with_disagreement=combine_ood_with_disagreement))\n\n    # Clear CUDA cache after val predictions\n    if cfg.DEVICE.type == 'cuda':\n        torch.cuda.empty_cache()\n\n    # 4. Cơ chế Từ chối thích ứng: Tìm ngưỡng tối ưu trên Tập Validation\n    best_rejection_threshold, _ = find_optimal_rejection_threshold(\n        val_rejection_scores, val_model_predictions, val_true_labels, cfg\n    )\n    print(f\"Ngưỡng từ chối cuối cùng được chọn: {best_rejection_threshold:.4f}\")\n\n    # 5. Đánh giá trên Tập Test bằng cách sử dụng ngưỡng đã học\n    print(f\"\\\\n--- Đánh giá trên Tập Test (Sử dụng hiệu chỉnh: {calibration_method}, OOD: {ood_detection_method}, Kết hợp bất đồng: {combine_ood_with_disagreement}) ---\")\n    (test_model_predictions, test_rejection_scores, test_true_labels, test_original_indices,\n     all_ensemble_individual_probs_test, test_odin_scores, test_energy_scores) = (\n        get_rejection_scores_and_predictions(cfg, test_loader, cfg.MODEL_SAVE_DIR, \n                                            final_overall_learning_metrics, train_dataset, \n                                            calibration_method=calibration_method,\n                                            ood_detection_method=ood_detection_method,\n                                            combine_ood_with_disagreement=combine_ood_with_disagreement))\n\n    test_metrics = calculate_metrics(test_model_predictions, test_rejection_scores, test_true_labels, best_rejection_threshold, verbose=True)\n\n\n    if hasattr(best_rejection_threshold, 'item'):\n        best_rejection_threshold = best_rejection_threshold.item()\n    elif hasattr(best_rejection_threshold, '__len__') and len(best_rejection_threshold) > 1:\n        best_rejection_threshold = float(best_rejection_threshold)\n    \n    rejected_categories_info = categorize_rejected_cases(\n        test_rejection_scores, test_model_predictions, test_true_labels, test_original_indices,\n        best_rejection_threshold, all_ensemble_individual_probs_test, \n        all_odin_scores=test_odin_scores, all_energy_scores=test_energy_scores,\n        ood_detection_method=ood_detection_method\n    )\n\n\n    \n    print(\"\\\\n--- Tóm tắt Phân loại Trường hợp bị Từ chối ---\")\n    print(f\"Số lượng Trường hợp Từ chối Lỗi: {len(rejected_categories_info['failure_rejection_indices'])}\")\n    print(f\"Số lượng Trường hợp Từ chối Không rõ/Mơ hồ: {len(rejected_categories_info['unknown_ambiguous_indices'])}\") \n    print(f\"Số lượng Trường hợp Từ chối OOD tiềm năng: {len(rejected_categories_info['potential_ood_indices'])}\") \n\n    # 7. Tạo Hình ảnh XAI cho các trường hợp quan trọng\n    loaded_ensemble_models_for_xai = []\n    for i_idx in range(cfg.NUM_ENSEMBLE_MODELS):\n        model = BaseClassifier(num_classes=2, dropout_rate=(cfg.MCDO_DROPOUT_RATE if cfg.MCDO_ENABLE else 0.0)).to(cfg.DEVICE)\n        model.load_state_dict(torch.load(os.path.join(cfg.MODEL_SAVE_DIR, f'best_model_ensemble_{i_idx}.pth')))\n        model.eval() \n        loaded_ensemble_models_for_xai.append(model)\n        \n    test_results_for_xai = {\n        'model_preds': test_model_predictions,\n        'rejection_scores': test_rejection_scores,\n        'true_labels': test_true_labels,\n        'original_indices': test_original_indices,\n        'rejected_categories': rejected_categories_info\n    }\n    \n    print(f\"Đảm bảo thư mục lưu XAI tồn tại: {cfg.XAI_SAVE_DIR}\")\n    os.makedirs(cfg.XAI_SAVE_DIR, exist_ok=True)\n    visualize_xai_examples(cfg, loaded_ensemble_models_for_xai, test_dataset, test_results_for_xai, best_rejection_threshold)\n\n    # ✅ FIXED: 8. Trực quan hóa các đường cong hiệu suất riêng cho baseline này\n    print(\"\\\\n--- Trực quan hóa các đường cong hiệu suất ---\")\n    plot_individual_baseline_charts(test_model_predictions, test_rejection_scores, test_true_labels, config_name, cfg.XAI_SAVE_DIR)\n\n    print(f\"\\\\n{'='*20}\\\\nHoàn tất chạy Baseline: {config_name}\\\\n{'='*20}\")\n    \n    # ✅ FIXED: Comprehensive cleanup để tránh memory leaks giữa các baselines\n    print(\"🧹 Cleaning up memory và models...\")\n    \n    # Xóa models từ memory\n    for model in loaded_ensemble_models_for_xai:\n        del model\n    del loaded_ensemble_models_for_xai\n    \n    # Xóa các mô hình đã lưu để đảm bảo sạch sẽ cho lần chạy tiếp theo\n    if os.path.exists(cfg.MODEL_SAVE_DIR):\n        shutil.rmtree(cfg.MODEL_SAVE_DIR)\n        os.makedirs(cfg.MODEL_SAVE_DIR)\n    \n    # Force garbage collection và clear CUDA cache\n    import gc\n    gc.collect()\n    if cfg.DEVICE.type == 'cuda':\n        torch.cuda.empty_cache()\n    \n    print(\"✅ Memory cleanup hoàn tất\")\n\n    # ✅ FIXED: Trả về structure giống main.py với tất cả metrics cần thiết\n    return {\n        'metrics': {\n            'Config Name': config_name,\n            'Overall Accuracy': test_metrics['overall_accuracy'],\n            'Accuracy on Accepted': test_metrics['accuracy_on_accepted'],\n            'Coverage': test_metrics['coverage'],\n            'Rejection Rate': test_metrics['rejection_rate'],\n            'Risk': test_metrics['risk'],\n            'ECE': test_metrics['ece'],\n            'NLL Accepted': test_metrics['nll_accepted'],\n            'Brier Accepted': test_metrics['brier_accepted'],\n            'AUROC': test_metrics['auroc_correct_incorrect'],\n            'AUPR': test_metrics['aupr_correct_incorrect'],\n            'AURC': test_metrics['aurc'],\n            'F1 Rejection': test_metrics['f1_rejection'],\n            'Failure Rejected Count': len(rejected_categories_info['failure_rejection_indices']),\n            'Unknown/Ambiguous Rejected Count': len(rejected_categories_info['unknown_ambiguous_indices']),\n            'Potential OOD Rejected Count': len(rejected_categories_info['potential_ood_indices'])\n        },\n        'raw_data': {\n            'predictions': test_model_predictions,\n            'rejection_scores': test_rejection_scores,\n            'true_labels': test_true_labels\n        }\n    }\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-29T18:05:16.086824Z","iopub.execute_input":"2025-06-29T18:05:16.087012Z","iopub.status.idle":"2025-06-29T18:05:16.103572Z","shell.execute_reply.started":"2025-06-29T18:05:16.086998Z","shell.execute_reply":"2025-06-29T18:05:16.103088Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def create_xai_visualizations_for_rejected_cases(cfg, rejected_categories, test_dataset, baseline_name):\n    \"\"\"\n    Tạo XAI visualizations cho một số rejected cases từ mỗi category.\n    \"\"\"\n    print(f\"🎨 Creating XAI visualizations for {baseline_name}...\")\n    \n    # Load model đầu tiên cho XAI visualization\n    model = BaseClassifier(num_classes=2, dropout_rate=0.0).to(cfg.DEVICE)\n    model.load_state_dict(torch.load(os.path.join(cfg.MODEL_SAVE_DIR, 'best_model_ensemble_0.pth')))\n    model.eval()\n    \n    # Tạo thư mục cho baseline này\n    baseline_xai_dir = os.path.join(cfg.XAI_SAVE_DIR, baseline_name)\n    os.makedirs(baseline_xai_dir, exist_ok=True)\n    \n    # Lấy vài samples từ mỗi category để visualize\n    categories_to_visualize = ['failure', 'ood', 'ambiguous']\n    max_samples_per_category = 3\n    \n    for category in categories_to_visualize:\n        if category in rejected_categories and len(rejected_categories[category]) > 0:\n            print(f\"📊 Visualizing {category} cases...\")\n            \n            # Lấy một vài samples từ category này\n            samples_to_viz = rejected_categories[category][:max_samples_per_category]\n            \n            for i, sample_info in enumerate(samples_to_viz):\n                try:\n                    # Lấy sample từ dataset\n                    global_idx = sample_info['global_idx']\n                    \n                    # Tìm local index trong test_dataset\n                    local_idx = None\n                    for j, dataset_global_idx in enumerate(test_dataset.global_indices):\n                        if dataset_global_idx == global_idx:\n                            local_idx = j\n                            break\n                    \n                    if local_idx is not None:\n                        # Lấy sample\n                        image, label, _ = test_dataset[local_idx]\n                        image_tensor = image.unsqueeze(0).to(cfg.DEVICE)\n                        \n                        # Tạo Grad-CAM visualization\n                        cam = GradCAMPlusPlus(model=model, target_layers=[model.target_layer])\n                        \n                        # Tạo target cho class được dự đoán\n                        predicted_class = sample_info['predicted_label']\n                        targets = [ClassifierOutputTarget(predicted_class)]\n                        \n                        # Generate CAM\n                        grayscale_cam = cam(input_tensor=image_tensor, targets=targets)\n                        grayscale_cam = grayscale_cam[0, :]\n                        \n                        # Convert tensor to PIL Image for visualization\n                        img_np = image.permute(1, 2, 0).cpu().numpy()\n                        img_np = (img_np * np.array([0.229, 0.224, 0.225])) + np.array([0.485, 0.456, 0.406])\n                        img_np = np.clip(img_np, 0, 1)\n                        \n                        # Create visualization\n                        visualization = show_cam_on_image(img_np, grayscale_cam, use_rgb=True)\n                        \n                        # Save visualization\n                        plt.figure(figsize=(12, 4))\n                        \n                        plt.subplot(1, 3, 1)\n                        plt.imshow(img_np)\n                        plt.title(f'Original Image\\nTrue: {label}, Pred: {predicted_class}')\n                        plt.axis('off')\n                        \n                        plt.subplot(1, 3, 2)\n                        plt.imshow(grayscale_cam, cmap='hot')\n                        plt.title('Grad-CAM++')\n                        plt.axis('off')\n                        \n                        plt.subplot(1, 3, 3)\n                        plt.imshow(visualization)\n                        plt.title(f'Overlay\\nConfidence: {sample_info[\"confidence\"]:.3f}')\n                        plt.axis('off')\n                        \n                        plt.tight_layout()\n                        save_path = os.path.join(baseline_xai_dir, f'{category}_case_{i+1}_idx_{global_idx}.png')\n                        plt.savefig(save_path, dpi=150, bbox_inches='tight')\n                        plt.close()\n                        \n                except Exception as e:\n                    print(f\"⚠️ Error creating visualization for {category} case {i+1}: {e}\")\n                    continue\n    \n    print(f\"✅ XAI visualizations saved to {baseline_xai_dir}\")\n    return baseline_xai_dir\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-29T18:05:16.104339Z","iopub.execute_input":"2025-06-29T18:05:16.104628Z","iopub.status.idle":"2025-06-29T18:05:16.122208Z","shell.execute_reply.started":"2025-06-29T18:05:16.104606Z","shell.execute_reply":"2025-06-29T18:05:16.121627Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# # ✅ FIXED: Main execution - Mỗi baseline train độc lập hoàn toàn\n\n# print(\"🚀 BẮT ĐẦU EXPLAINABLE LEARNING TO REJECT - TRAINING ĐỘC LẬP CHO MỖI BASELINE\")\n# print(\"=\"*80)\n\n# # Xóa tất cả các mô hình đã lưu từ các lần chạy trước đó\n# if os.path.exists(cfg.MODEL_SAVE_DIR):\n#     shutil.rmtree(cfg.MODEL_SAVE_DIR)\n# os.makedirs(cfg.MODEL_SAVE_DIR)\n\n# # Xóa tất cả các hình ảnh XAI đã lưu từ các lần chạy trước đó\n# if os.path.exists(cfg.XAI_SAVE_DIR):\n#     shutil.rmtree(cfg.XAI_SAVE_DIR)\n# os.makedirs(cfg.XAI_SAVE_DIR)\n\n# all_baseline_results = []\n\n# print(\"\\\\n📝 CHIẾN LƯỢC TRAINING:\")\n# print(\"✅ Mỗi baseline sẽ train ensemble models từ đầu\")\n# print(\"✅ Các baseline khác nhau có architecture/loss function khác nhau:\")\n# print(\"   - Baseline A.1: MCDO dropout layers\")\n# print(\"   - Baseline A.2.3: Label Smoothing loss\")\n# print(\"   - Baseline B.3: Training dynamics collection\")\n# print(\"   - Các baseline khác: Standard training với calibration methods khác nhau\")\n# print(\"\\\\n🔥 BẮT ĐẦU TRAINING TẤT CẢ BASELINES...\")\n# print(\"=\"*80)\n\n# # ⚡ Baseline 0: Current Ensemble Baseline (Temperature Scaling mặc định)\n# print(\"\\\\n🔄 [1/10] Baseline 0 - Current Ensemble (Temperature Scaling)\")\n# print(\"⏱️  Ước tính thời gian: ~15-20 phút\")\n# results_b0 = run_baseline(\n#     config_name='Baseline 0 - Current Ensemble (Temperature Scaling)',\n#     mcdo_enable=False,\n#     label_smoothing_enable=False,\n#     calibration_method='temperature_scaling',\n#     ood_detection_method='none',\n#     combine_ood_with_disagreement=False,\n#     enable_training_dynamics=False\n# )\n# all_baseline_results.append(results_b0)\n# print(\"✅ [1/10] Hoàn thành Baseline 0\")\n\n# # ⚡ Baseline A.1: Ensemble + MCDO \n# print(\"\\\\n🔄 [2/10] Baseline A.1 - Ensemble + MCDO (Cần train lại với dropout layers)\")\n# print(\"⏱️  Ước tính thời gian: ~18-25 phút\")\n# results_a1 = run_baseline(\n#     config_name='Baseline A.1 - Ensemble + MCDO',\n#     mcdo_enable=True,\n#     label_smoothing_enable=False,\n#     calibration_method='temperature_scaling',\n#     ood_detection_method='none',\n#     combine_ood_with_disagreement=False,\n#     enable_training_dynamics=False\n# )\n# all_baseline_results.append(results_a1)\n# print(\"✅ [2/10] Hoàn thành Baseline A.1\")\n\n# # ⚡ Baseline A.2.1: Ensemble + Isotonic Regression\n# print(\"\\\\n🔄 [3/10] Baseline A.2.1 - Ensemble + Isotonic Regression\")\n# print(\"⏱️  Ước tính thời gian: ~15-20 phút\")\n# results_a21 = run_baseline(\n#     config_name='Baseline A.2.1 - Ensemble + Isotonic Regression',\n#     mcdo_enable=False,\n#     label_smoothing_enable=False,\n#     calibration_method='isotonic_regression',\n#     ood_detection_method='none',\n#     combine_ood_with_disagreement=False,\n#     enable_training_dynamics=False\n# )\n# all_baseline_results.append(results_a21)\n# print(\"✅ [3/10] Hoàn thành Baseline A.2.1\")\n\n# # ⚡ Baseline A.2.2: Ensemble + Beta Calibration\n# print(\"\\\\n🔄 [4/10] Baseline A.2.2 - Ensemble + Beta Calibration\")\n# print(\"⏱️  Ước tính thời gian: ~15-20 phút\")\n# results_a22 = run_baseline(\n#     config_name='Baseline A.2.2 - Ensemble + Beta Calibration',\n#     mcdo_enable=False,\n#     label_smoothing_enable=False,\n#     calibration_method='beta_calibration',\n#     ood_detection_method='none',\n#     combine_ood_with_disagreement=False,\n#     enable_training_dynamics=False\n# )\n# all_baseline_results.append(results_a22)\n# print(\"✅ [4/10] Hoàn thành Baseline A.2.2\")\n\n# # ⚡ Baseline A.2.3: Ensemble + Label Smoothing (Cần train lại với loss function khác)\n# print(\"\\\\n🔄 [5/10] Baseline A.2.3 - Ensemble + Label Smoothing (Cần train lại với Label Smoothing loss)\")\n# print(\"⏱️  Ước tính thời gian: ~16-22 phút\")\n# results_a23 = run_baseline( \n#     config_name='Baseline A.2.3 - Ensemble + Label Smoothing',\n#     mcdo_enable=False,\n#     label_smoothing_enable=True,\n#     calibration_method='temperature_scaling',\n#     ood_detection_method='none',\n#     combine_ood_with_disagreement=False,\n#     enable_training_dynamics=False\n# )\n# all_baseline_results.append(results_a23)\n# print(\"✅ [5/10] Hoàn thành Baseline A.2.3\")\n\n# # 🚀 --- Nhóm B: Nâng cao Phương pháp Phân biệt & OOD Detection ---\n\n# # ⚡ Baseline B.1.1: Ensemble + ODIN (OOD Discrimination - Basic)\n# print(\"\\\\n🔄 [6/10] Baseline B.1.1 - Ensemble + ODIN (Basic)\")\n# print(\"⏱️  Ước tính thời gian: ~15-20 phút\")\n# results_b11 = run_baseline(\n#     config_name='Baseline B.1.1 - Ensemble + ODIN (Basic)',\n#     mcdo_enable=False,\n#     label_smoothing_enable=False,\n#     calibration_method='temperature_scaling',\n#     ood_detection_method='odin',\n#     combine_ood_with_disagreement=False,\n#     enable_training_dynamics=False\n# )\n# all_baseline_results.append(results_b11)\n# print(\"✅ [6/10] Hoàn thành Baseline B.1.1\")\n\n# # ⚡ Baseline B.1.2: Ensemble + ODIN (OOD Discrimination - Combined)\n# print(\"\\\\n🔄 [7/10] Baseline B.1.2 - Ensemble + ODIN (Combined)\")\n# print(\"⏱️  Ước tính thời gian: ~15-20 phút\")\n# results_b12 = run_baseline(\n#     config_name='Baseline B.1.2 - Ensemble + ODIN (Combined)',\n#     mcdo_enable=False,\n#     label_smoothing_enable=False,\n#     calibration_method='temperature_scaling',\n#     ood_detection_method='odin',\n#     combine_ood_with_disagreement=True,\n#     enable_training_dynamics=False\n# )\n# all_baseline_results.append(results_b12)\n# print(\"✅ [7/10] Hoàn thành Baseline B.1.2\")\n\n# # ⚡ Baseline B.2.1: Ensemble + Energy Score (OOD Discrimination - Basic)\n# print(\"\\\\n🔄 [8/10] Baseline B.2.1 - Ensemble + Energy Score (Basic)\")\n# print(\"⏱️  Ước tính thời gian: ~15-20 phút\")\n# results_b21 = run_baseline(\n#     config_name='Baseline B.2.1 - Ensemble + Energy Score (Basic)',\n#     mcdo_enable=False,\n#     label_smoothing_enable=False,\n#     calibration_method='temperature_scaling',\n#     ood_detection_method='energy',\n#     combine_ood_with_disagreement=False,\n#     enable_training_dynamics=False\n# )\n# all_baseline_results.append(results_b21)\n# print(\"✅ [8/10] Hoàn thành Baseline B.2.1\")\n\n# # ⚡ Baseline B.2.2: Ensemble + Energy Score (OOD Discrimination - Combined)\n# print(\"\\\\n🔄 [9/10] Baseline B.2.2 - Ensemble + Energy Score (Combined)\")\n# print(\"⏱️  Ước tính thời gian: ~15-20 phút\")\n# results_b22 = run_baseline(\n#     config_name='Baseline B.2.2 - Ensemble + Energy Score (Combined)',\n#     mcdo_enable=False,\n#     label_smoothing_enable=False,\n#     calibration_method='temperature_scaling',\n#     ood_detection_method='energy',\n#     combine_ood_with_disagreement=True,\n#     enable_training_dynamics=False\n# )\n# all_baseline_results.append(results_b22)\n# print(\"✅ [9/10] Hoàn thành Baseline B.2.2\")\n\n# # ⚡ Baseline B.3: Ensemble + Training Dynamics Insights (Cần train lại để thu thập dynamics)\n# print(\"\\\\n🔄 [10/10] Baseline B.3 - Training Dynamics Insights (Cần train lại để thu thập training dynamics)\")\n# print(\"⏱️  Ước tính thời gian: ~20-30 phút (lâu nhất vì thu thập metrics)\")\n# results_b3 = run_baseline(\n#     config_name='Baseline B.3 - Ensemble + Training Dynamics Insights',\n#     mcdo_enable=False,\n#     label_smoothing_enable=False,\n#     calibration_method='temperature_scaling',\n#     ood_detection_method='none',\n#     combine_ood_with_disagreement=False,\n#     enable_training_dynamics=True\n# )\n# all_baseline_results.append(results_b3)\n# print(\"✅ [10/10] Hoàn thành Baseline B.3\")\n\n# print(\"\\\\n🎉 HOÀN THÀNH TẤT CẢ 10 BASELINES!\")\n# print(\"⏱️  Tổng thời gian ước tính đã chạy: ~2.5-3.5 giờ\")\n\n# print(\"\\\\n📊 TẠO PHÂN TÍCH SO SÁNH VÀ VISUALIZATIONS...\")\n# print(\"=\"*80)\n\n# # ✅ FIXED: Hiển thị kết quả đã thu thập trong DataFrame với tất cả metrics\n# metrics_only_results = [res['metrics'] for res in all_baseline_results]\n# final_results_df = pd.DataFrame(metrics_only_results)\n\n# print(\"\\\\n📋 BẢNG SO SÁNH KẾT QUẢ TỔNG QUAN:\")\n# print(\"🎯 Mỗi baseline đã được train độc lập với architecture/loss riêng biệt\")\n# print(\"-\" * 120)\n# # In DataFrame dưới dạng Markdown\n# print(final_results_df.round(4).to_markdown(index=False))\n\n# print(\"\\\\n📊 TẠO BIỂU ĐỒ SO SÁNH CHO TẤT CẢ BASELINES...\")\n# # ✅ FIXED: Plotting all comparative graphs như main.py\n# plot_all_calibration_curves(all_baseline_results, cfg.XAI_SAVE_DIR)\n# plot_all_roc_curves(all_baseline_results, cfg.XAI_SAVE_DIR)\n# plot_all_pr_curves(all_baseline_results, cfg.XAI_SAVE_DIR)\n# plot_all_risk_coverage_curves(all_baseline_results, cfg.XAI_SAVE_DIR)\n\n# print(\"\\\\n🎨 TỔNG HỢP XAI VISUALIZATIONS...\")\n# print(f\"📁 Tất cả biểu đồ và XAI explanations đã được lưu vào: {cfg.XAI_SAVE_DIR}\")\n# print(\"📝 Mỗi baseline có:\")\n# print(\"   ✅ Individual performance charts (reliability, ROC, PR)\")\n# print(\"   ✅ XAI explanations cho 4 case types (accepted, failure, ambiguous, OOD)\")\n# print(\"   ✅ Comparative analysis với các baselines khác\")\n\n# print(\"\\\\n🏁 HOÀN TẤT TOÀN BỘ EXPERIMENT!\")\n# print(\"=\"*80)\n# print(\"🎉 Thành công train và đánh giá 10 baselines độc lập\")\n# print(\"📊 Đã tạo comprehensive comparison và analysis\")\n# print(\"🔍 XAI explanations đã được tạo cho tất cả baselines\")\n# print(f\"⏱️  Tổng thời gian thực tế: [Sẽ hiển thị khi chạy xong]\")\n# print(\"=\"*80)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-29T18:05:16.122953Z","iopub.execute_input":"2025-06-29T18:05:16.123175Z","iopub.status.idle":"2025-06-29T18:05:16.137816Z","shell.execute_reply.started":"2025-06-29T18:05:16.123152Z","shell.execute_reply":"2025-06-29T18:05:16.137296Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## ✅ **ĐÃ CẬP NHẬT: INDEPENDENT TRAINING STRATEGY**\n\n### 🚀 **Chiến lược Training Độc lập cho Mỗi Baseline**\n\n**Thay đổi quan trọng**: Mỗi baseline giờ đây sẽ train ensemble models từ đầu hoàn toàn, không reuse models từ baseline khác.\n\n#### **Lý do cần Training Độc lập:**\n\n1. **Baseline A.1 (MCDO)**: Cần models với dropout layers enabled trong architecture\n2. **Baseline A.2.3 (Label Smoothing)**: Cần train với Label Smoothing Loss thay vì CrossEntropy Loss  \n3. **Baseline B.3 (Training Dynamics)**: Cần thu thập training dynamics metadata trong quá trình training\n4. **Các baselines khác**: Mặc dù architecture giống nhau, nhưng training độc lập đảm bảo fair comparison\n\n#### **Training Timeline (Ước tính):**\n\n| Baseline | Đặc biệt | Thời gian ước tính |\n|----------|----------|-------------------|\n| Baseline 0 | Standard Training | ~15-20 phút |\n| **Baseline A.1** | **MCDO Dropout** | **~18-25 phút** |\n| Baseline A.2.1 | Isotonic Regression | ~15-20 phút |\n| Baseline A.2.2 | Beta Calibration | ~15-20 phút |\n| **Baseline A.2.3** | **Label Smoothing Loss** | **~16-22 phút** |\n| Baseline B.1.1 | ODIN Basic | ~15-20 phút |\n| Baseline B.1.2 | ODIN Combined | ~15-20 phút |\n| Baseline B.2.1 | Energy Score Basic | ~15-20 phút |\n| Baseline B.2.2 | Energy Score Combined | ~15-20 phút |\n| **Baseline B.3** | **Training Dynamics** | **~20-30 phút** |\n\n**📊 Tổng thời gian**: ~2.5-3.5 giờ (tùy thuộc GPU và dataset size)\n\n#### **Output cho Mỗi Baseline:**\n\n✅ **Independent ensemble models** trained từ đầu  \n✅ **Individual performance charts**: Reliability, ROC, PR curves  \n✅ **XAI explanations**: 4 case types (accepted, failure, ambiguous, OOD)  \n✅ **Detailed metrics**: 16 metrics bao gồm F1-rejection, AURC, ECE, etc.  \n\n#### **Comparative Analysis:**\n\n✅ **All reliability diagrams** on one plot  \n✅ **All ROC curves** comparison  \n✅ **All PR curves** comparison  \n✅ **All risk-coverage curves** comparison  \n✅ **Comprehensive results table** với tất cả metrics  \n\n### 🎯 **Cách Chạy:**\n\n```python\n# Chạy tất cả cells từ 1-29 để thực hiện complete experiment\n# Hoặc chạy từng baseline riêng lẻ nếu muốn test\n```\n\n**⚠️ Lưu ý**: Training độc lập đảm bảo scientific rigor nhưng tốn thời gian hơn!\n","metadata":{"vscode":{"languageId":"raw"}}},{"cell_type":"markdown","source":"## 14. Case Categorization và Visualization Functions\n\nPhân loại các trường hợp bị từ chối và tạo các visualization curves:\n- **categorize_rejected_cases**: Phân loại rejected cases thành failure, OOD, ambiguous\n- **Plotting functions**: Calibration curve, ROC curve, PR curve, Risk-Coverage curve\n- **Comparative plotting**: So sánh tất cả baselines trên cùng một đồ thị\n","metadata":{"vscode":{"languageId":"raw"}}},{"cell_type":"code","source":"def categorize_rejected_cases(rejection_scores, model_predictions, true_labels, original_indices, rejection_threshold, \n                             all_ensemble_individual_probs_stacked, all_odin_scores=None, all_energy_scores=None, \n                             ood_detection_method='none'):\n    \"\"\"\n    Phân loại các trường hợp bị từ chối.\n    - 'Failure Rejection': Các trường hợp bị từ chối do điểm từ chối thấp VÀ dự đoán của mô hình không chính xác.\n    - 'Potential OOD Rejected Cases': Các trường hợp bị từ chối do điểm từ chối thấp VÀ được xác định là OOD tiềm năng.\n    - 'Unknown/Ambiguous': Các trường hợp bị từ chối do điểm từ chối thấp, dự đoán đúng, và không được xác định là OOD tiềm năng.\n    \"\"\"\n    rejected_mask = rejection_scores < rejection_threshold\n    \n    rejected_indices = original_indices[rejected_mask]\n    rejected_model_preds = model_predictions[rejected_mask]\n    rejected_true_labels = true_labels[rejected_mask]\n    rejected_rejection_scores = rejection_scores[rejected_mask]\n\n    failure_rejection_indices = []\n    unknown_ambiguous_indices = [] \n    potential_ood_indices = [] \n    \n    # Tạo ánh xạ từ chỉ mục gốc (toàn cục) đến vị trí phẳng của nó trong mảng `original_indices`\n    original_to_flat_pos_map = {original_idx: flat_pos for flat_pos, original_idx in enumerate(original_indices)}\n\n    print(f\"\\\\n--- Phân loại các Trường hợp bị Từ chối ({len(rejected_indices)} bị từ chối) ---\")\n\n    # If energy scores are used for OOD classification, calculate the threshold now\n    energy_ood_threshold = None\n    if ood_detection_method == 'energy' and all_energy_scores is not None and all_energy_scores.size > 0:\n        energy_ood_threshold = np.percentile(all_energy_scores, cfg.ENERGY_CLASSIFY_PERCENTILE_THRESHOLD)\n        print(f\"Ngưỡng phân loại OOD Energy: {energy_ood_threshold:.4f}\")\n\n    for i_idx, rejected_original_idx in enumerate(rejected_indices):\n        current_pred = rejected_model_preds[i_idx]\n        current_true = rejected_true_labels[i_idx]\n        current_rejection_score = rejected_rejection_scores[i_idx]\n\n        if rejected_original_idx not in original_to_flat_pos_map:\n            print(f\"Cảnh báo: Chỉ mục gốc {rejected_original_idx} không tìm thấy. Bỏ qua phân loại cho mẫu này.\")\n            continue \n\n        flat_pos_in_full_data = original_to_flat_pos_map[rejected_original_idx]\n        \n        is_potential_ood = False\n\n        if ood_detection_method == 'odin' and all_odin_scores is not None:\n            if all_odin_scores[flat_pos_in_full_data] < cfg.ODIN_CLASSIFY_THRESHOLD:\n                is_potential_ood = True\n        elif ood_detection_method == 'energy' and all_energy_scores is not None and energy_ood_threshold is not None:\n            if all_energy_scores[flat_pos_in_full_data] < energy_ood_threshold:\n                is_potential_ood = True\n        else: # Default OOD classification using confidence and ensemble variance\n            if all_ensemble_individual_probs_stacked.size > 0:\n                individual_probs_for_this_sample = all_ensemble_individual_probs_stacked[flat_pos_in_full_data, :, :] \n                ensemble_predicted_class = current_pred\n                variance_disagreement = 0.0\n                if (individual_probs_for_this_sample.size > 0 and \n                    ensemble_predicted_class < individual_probs_for_this_sample.shape[1] and \n                    ensemble_predicted_class >= 0):\n                    variance_disagreement = np.var(individual_probs_for_this_sample[:, ensemble_predicted_class])\n                else:\n                    if individual_probs_for_this_sample.size > 0:\n                        variance_disagreement = np.max(np.var(individual_probs_for_this_sample, axis=0))\n                    else:\n                        variance_disagreement = 0.0 \n\n                if current_rejection_score < cfg.OOD_CONFIDENCE_THRESHOLD and variance_disagreement > cfg.OOD_VARIANCE_THRESHOLD:\n                    is_potential_ood = True\n\n        # Phân loại\n        if current_pred != current_true:\n            failure_rejection_indices.append(rejected_original_idx)\n        elif is_potential_ood:\n            potential_ood_indices.append(rejected_original_idx)\n        else: # Dự đoán đúng nhưng bị từ chối do sự không chắc chắn/bất đồng chung trong phân phối\n            unknown_ambiguous_indices.append(rejected_original_idx)\n\n    return {\n        'rejected_data_df': pd.DataFrame({\n            'original_idx': rejected_indices,\n            'model_pred': rejected_model_preds,\n            'true_label': rejected_true_labels,\n            'rejection_score': rejected_rejection_scores\n        }),\n        'failure_rejection_indices': failure_rejection_indices,\n        'unknown_ambiguous_indices': unknown_ambiguous_indices, \n        'potential_ood_indices': potential_ood_indices \n    }\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-29T18:05:16.138564Z","iopub.execute_input":"2025-06-29T18:05:16.139079Z","iopub.status.idle":"2025-06-29T18:05:16.15349Z","shell.execute_reply.started":"2025-06-29T18:05:16.139054Z","shell.execute_reply":"2025-06-29T18:05:16.152822Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ✅ FIXED: All plotting functions updated to match main.py\n\ndef plot_calibration_curve(model_predictions, rejection_scores, true_labels, num_bins=10, save_path=None):\n    \"\"\"\n    Vẽ biểu đồ độ tin cậy (Reliability Diagram) để đánh giá khả năng hiệu chỉnh.\n    \"\"\"\n    if len(rejection_scores) == 0:\n        print(\"Không có dữ liệu điểm để vẽ biểu đồ hiệu chỉnh.\")\n        return\n\n    bins = np.linspace(0., 1., num_bins + 1)\n    bin_accuracies = []\n    bin_rejection_scores = []\n    bin_counts = []\n\n    for i in range(num_bins):\n        lower_bound = bins[i]\n        upper_bound = bins[i+1]\n        mask = (rejection_scores >= lower_bound) & (rejection_scores < upper_bound)\n        if i == num_bins - 1:\n            mask = (rejection_scores >= lower_bound) & (rejection_scores <= upper_bound)\n\n        bin_samples_indices = np.where(mask)[0]\n        bin_count = len(bin_samples_indices)\n\n        if bin_count > 0:\n            bin_accuracy = accuracy_score(true_labels[bin_samples_indices], model_predictions[bin_samples_indices])\n            bin_rejection_score_mean = np.mean(rejection_scores[bin_samples_indices])\n            bin_accuracies.append(bin_accuracy)\n            bin_rejection_scores.append(bin_rejection_score_mean)\n            bin_counts.append(bin_count)\n        else:\n            bin_accuracies.append(np.nan)\n            bin_rejection_scores.append(np.nan)\n            bin_counts.append(0)\n\n    # Lọc các bin rỗng\n    valid_bins_mask = ~np.isnan(bin_accuracies)\n    bin_accuracies = np.array(bin_accuracies)[valid_bins_mask]\n    bin_rejection_scores = np.array(bin_rejection_scores)[valid_bins_mask]\n\n    plt.figure(figsize=(7, 7))\n    plt.plot([0, 1], [0, 1], linestyle='--', color='gray', label='Hiệu chỉnh hoàn hảo')\n    plt.plot(bin_rejection_scores, bin_accuracies, marker='o', linestyle='-', color='blue', label='Mô hình')\n    \n    # Vẽ các thanh biểu đồ\n    for i_idx in range(len(bin_rejection_scores)):\n        plt.plot([bin_rejection_scores[i_idx], bin_rejection_scores[i_idx]], [bin_rejection_scores[i_idx], bin_accuracies[i_idx]],\n                 color='red' if bin_accuracies[i_idx] < bin_rejection_scores[i_idx] else 'green', linestyle='-', linewidth=2)\n\n    plt.xlabel(\"Điểm trung bình (Score)\")\n    plt.ylabel(\"Độ chính xác (Accuracy)\")\n    plt.title(\"Biểu đồ độ tin cậy (Reliability Diagram)\")\n    plt.grid(True)\n    plt.legend()\n    plt.xlim([0, 1])\n    plt.ylim([0, 1])\n    if save_path:\n        plt.savefig(save_path)\n        print(f\"Biểu đồ độ tin cậy đã lưu vào: {save_path}\")\n    plt.show()\n    plt.close()\n\ndef plot_roc_curve(model_predictions, rejection_scores, true_labels, save_path=None):\n    \"\"\"\n    Vẽ đường cong ROC (Receiver Operating Characteristic).\n    \"\"\"\n    if len(rejection_scores) == 0:\n        print(\"Không có dữ liệu điểm để vẽ đường cong ROC.\")\n        return\n\n    is_correct = (model_predictions == true_labels).astype(int)\n    \n    if len(np.unique(is_correct)) < 2:\n        print(\"Không đủ biến thể trong các dự đoán đúng/sai để vẽ đường cong ROC.\")\n        return\n\n    fpr, tpr, thresholds = roc_curve(is_correct, rejection_scores)\n    roc_auc = auc(fpr, tpr)\n\n    plt.figure(figsize=(8, 6))\n    plt.plot(fpr, tpr, color='darkorange', lw=2, label=f'Đường cong ROC (AUC = {roc_auc:.2f})')\n    plt.plot([0, 1], [0, 1], color='navy', lw=2, linestyle='--', label='Ngẫu nhiên')\n    plt.xlim([0.0, 1.0])\n    plt.ylim([0.0, 1.05])\n    plt.xlabel('Tỷ lệ dương tính giả (False Positive Rate)')\n    plt.ylabel('Tỷ lệ dương tính thật (True Positive Rate)')\n    plt.title('Đường cong ROC')\n    plt.legend(loc=\"lower right\")\n    plt.grid(True)\n    if save_path:\n        plt.savefig(save_path)\n        print(f\"Đường cong ROC đã lưu vào: {save_path}\")\n    plt.show()\n    plt.close()\n\ndef plot_pr_curve(model_predictions, rejection_scores, true_labels, save_path=None):\n    \"\"\"\n    Vẽ đường cong PR (Precision-Recall).\n    \"\"\"\n    if len(rejection_scores) == 0:\n        print(\"Không có dữ liệu điểm để vẽ đường cong PR.\")\n        return\n\n    is_correct = (model_predictions == true_labels).astype(int)\n\n    if len(np.unique(is_correct)) < 2:\n        print(\"Không đủ biến thể trong các dự đoán đúng/sai để vẽ đường cong PR.\")\n        return\n\n    precision, recall, thresholds = precision_recall_curve(is_correct, rejection_scores)\n    pr_auc = auc(recall, precision)\n\n    plt.figure(figsize=(8, 6))\n    plt.plot(recall, precision, color='purple', lw=2, label=f'Đường cong PR (AUC = {pr_auc:.2f})')\n    plt.xlabel('Độ thu hồi (Recall)')\n    plt.ylabel('Độ chính xác (Precision)')\n    plt.title('Đường cong Precision-Recall')\n    plt.legend(loc=\"lower left\")\n    plt.grid(True)\n    plt.xlim([0.0, 1.0])\n    plt.ylim([0.0, 1.05])\n    if save_path:\n        plt.savefig(save_path)\n        print(f\"Đường cong PR đã lưu vào: {save_path}\")\n    plt.show()\n    plt.close()\n\n# ✅ FIXED: Individual plotting functions for each baseline\ndef plot_individual_baseline_charts(model_predictions, rejection_scores, true_labels, config_name, save_dir):\n    \"\"\"\n    Plot individual performance charts for a single baseline.\n    \"\"\"\n    # Create subdirectory for this baseline\n    baseline_dir = os.path.join(save_dir, config_name.replace(' ', '_').replace('/', '_'))\n    os.makedirs(baseline_dir, exist_ok=True)\n    \n    print(f\"🔄 Tạo biểu đồ cho {config_name}...\")\n    \n    # Reliability Diagram\n    plot_calibration_curve(model_predictions, rejection_scores, true_labels, \n                          save_path=os.path.join(baseline_dir, f'reliability_diagram_{config_name.replace(\" \", \"_\")}.png'))\n    \n    # ROC Curve\n    plot_roc_curve(model_predictions, rejection_scores, true_labels, \n                  save_path=os.path.join(baseline_dir, f'roc_curve_{config_name.replace(\" \", \"_\")}.png'))\n    \n    # PR Curve\n    plot_pr_curve(model_predictions, rejection_scores, true_labels, \n                 save_path=os.path.join(baseline_dir, f'pr_curve_{config_name.replace(\" \", \"_\")}.png'))\n\n# ✅ FIXED: Comprehensive comparison plotting to match main.py\ndef plot_all_calibration_curves(all_results, save_dir, num_bins=10):\n    \"\"\"\n    Plots reliability diagrams for all baselines on a single figure.\n    \"\"\"\n    plt.figure(figsize=(10, 10))\n    plt.plot([0, 1], [0, 1], linestyle='--', color='gray', label='Hiệu chỉnh hoàn hảo')\n\n    for result in all_results:\n        config_name = result['metrics']['Config Name']\n        preds = result['raw_data']['predictions']\n        rejection_scores = result['raw_data']['rejection_scores']\n        labels = result['raw_data']['true_labels']\n\n        if len(rejection_scores) == 0:\n            print(f\"Không có dữ liệu độ tin cậy để vẽ biểu đồ hiệu chỉnh cho {config_name}.\")\n            continue\n\n        bins = np.linspace(0., 1., num_bins + 1)\n        bin_accuracies = []\n        bin_rejection_scores = []\n\n        for i_idx in range(num_bins):\n            lower_bound = bins[i_idx]\n            upper_bound = bins[i_idx+1]\n            mask = (rejection_scores >= lower_bound) & (rejection_scores < upper_bound)\n            if i_idx == num_bins - 1:\n                mask = (rejection_scores >= lower_bound) & (rejection_scores <= upper_bound)\n\n            bin_samples_indices = np.where(mask)[0]\n            if len(bin_samples_indices) > 0:\n                bin_accuracy = accuracy_score(labels[bin_samples_indices], preds[bin_samples_indices])\n                bin_rejection_score_mean = np.mean(rejection_scores[bin_samples_indices])\n                bin_accuracies.append(bin_accuracy)\n                bin_rejection_scores.append(bin_rejection_score_mean)\n            else:\n                bin_accuracies.append(np.nan)\n                bin_rejection_scores.append(np.nan)\n        \n        valid_bins_mask = ~np.isnan(bin_accuracies)\n        bin_accuracies = np.array(bin_accuracies)[valid_bins_mask]\n        bin_rejection_scores = np.array(bin_rejection_scores)[valid_bins_mask]\n\n        plt.plot(bin_rejection_scores, bin_accuracies, marker='o', linestyle='-', label=config_name)\n\n    plt.xlabel(\"Điểm trung bình (Score)\")\n    plt.ylabel(\"Độ chính xác (Accuracy)\")\n    plt.title(\"Biểu đồ Độ tin cậy cho các Baseline\")\n    plt.grid(True)\n    plt.legend(loc='upper left', bbox_to_anchor=(1, 1))\n    plt.xlim([0, 1])\n    plt.ylim([0, 1])\n    plt.tight_layout()\n    save_path = os.path.join(save_dir, 'all_reliability_diagrams.png')\n    plt.savefig(save_path)\n    print(f\"Đã lưu Biểu đồ Độ tin cậy tổng hợp vào: {save_path}\")\n    plt.show()\n    plt.close()\n\ndef plot_all_roc_curves(all_results, save_dir):\n    \"\"\"\n    Plots ROC curves for all baselines on a single figure.\n    \"\"\"\n    plt.figure(figsize=(10, 8))\n    plt.plot([0, 1], [0, 1], color='navy', lw=2, linestyle='--', label='Ngẫu nhiên')\n\n    for result in all_results:\n        config_name = result['metrics']['Config Name']\n        preds = result['raw_data']['predictions']\n        rejection_scores = result['raw_data']['rejection_scores']\n        labels = result['raw_data']['true_labels']\n\n        if len(rejection_scores) == 0:\n            print(f\"Không có dữ liệu điểm để vẽ đường cong ROC cho {config_name}.\")\n            continue\n        \n        is_correct = (preds == labels).astype(int)\n        if len(np.unique(is_correct)) < 2:\n            print(f\"Không đủ biến thể đúng/sai để vẽ đường cong ROC cho {config_name}.\")\n            continue\n\n        fpr, tpr, _ = roc_curve(is_correct, rejection_scores)\n        roc_auc = auc(fpr, tpr)\n        plt.plot(fpr, tpr, lw=2, label=f'{config_name} (AUC = {roc_auc:.2f})')\n\n    plt.xlim([0.0, 1.0])\n    plt.ylim([0.0, 1.05])\n    plt.xlabel('Tỷ lệ dương tính giả (False Positive Rate)')\n    plt.ylabel('Tỷ lệ dương tính thật (True Positive Rate)')\n    plt.title('Đường cong ROC cho các Baseline')\n    plt.legend(loc=\"lower right\", bbox_to_anchor=(1, 0))\n    plt.grid(True)\n    plt.tight_layout()\n    save_path = os.path.join(save_dir, 'all_roc_curves.png')\n    plt.savefig(save_path)\n    print(f\"Đã lưu Đường cong ROC tổng hợp vào: {save_path}\")\n    plt.show()\n    plt.close()\n\ndef plot_all_pr_curves(all_results, save_dir):\n    \"\"\"\n    Plots Precision-Recall curves for all baselines on a single figure.\n    \"\"\"\n    plt.figure(figsize=(10, 8))\n\n    for result in all_results:\n        config_name = result['metrics']['Config Name']\n        preds = result['raw_data']['predictions']\n        rejection_scores = result['raw_data']['rejection_scores']\n        labels = result['raw_data']['true_labels']\n\n        if len(rejection_scores) == 0:\n            print(f\"Không có dữ liệu điểm để vẽ đường cong PR cho {config_name}.\")\n            continue\n        \n        is_correct = (preds == labels).astype(int)\n        if len(np.unique(is_correct)) < 2:\n            print(f\"Không đủ biến thể đúng/sai để vẽ đường cong PR cho {config_name}.\")\n            continue\n\n        precision, recall, _ = precision_recall_curve(is_correct, rejection_scores)\n        pr_auc = auc(recall, precision)\n        plt.plot(recall, precision, lw=2, label=f'{config_name} (AUC = {pr_auc:.2f})')\n\n    plt.xlabel('Độ thu hồi (Recall)')\n    plt.ylabel('Độ chính xác (Precision)')\n    plt.title('Đường cong Precision-Recall cho các Baseline')\n    plt.legend(loc=\"lower left\", bbox_to_anchor=(0, 0))\n    plt.grid(True)\n    plt.xlim([0.0, 1.0])\n    plt.ylim([0.0, 1.05])\n    plt.tight_layout()\n    save_path = os.path.join(save_dir, 'all_pr_curves.png')\n    plt.savefig(save_path)\n    print(f\"Đã lưu Đường cong Precision-Recall tổng hợp vào: {save_path}\")\n    plt.show()\n    plt.close()\n\ndef plot_all_risk_coverage_curves(all_results, save_dir):\n    \"\"\"\n    Plots Risk-Coverage curves for all baselines on a single figure.\n    \"\"\"\n    plt.figure(figsize=(10, 8))\n\n    for result in all_results:\n        config_name = result['metrics']['Config Name']\n        rejection_scores = result['raw_data']['rejection_scores']\n        preds = result['raw_data']['predictions']\n        labels = result['raw_data']['true_labels']\n\n        num_total = len(labels)\n        if num_total == 0:\n            print(f\"Không có dữ liệu để vẽ đường cong Rủi ro-Độ phủ cho {config_name}.\")\n            continue\n\n        # Sort samples by score in increasing order to simulate increasing rejection\n        sorted_indices = np.argsort(rejection_scores)\n        sorted_preds = preds[sorted_indices]\n        sorted_labels = labels[sorted_indices]\n\n        risks = []\n        coverages = []\n        for i_idx in range(num_total):\n            current_accepted_preds = sorted_preds[i_idx:]\n            current_accepted_labels = sorted_labels[i_idx:]\n            current_coverage = (num_total - i_idx) / num_total\n\n            if (num_total - i_idx) == 0:\n                current_risk = 1.0 \n            else:\n                num_correct = np.sum(current_accepted_preds == current_accepted_labels)\n                current_risk = 1.0 - (num_correct / (num_total - i_idx)) \n\n            coverages.append(current_coverage)\n            risks.append(current_risk)\n\n        # Reverse lists so coverage goes from 0 to 1\n        coverages = np.array(coverages[::-1]) * 100 \n        risks = np.array(risks[::-1]) * 100       \n\n        plt.plot(coverages, risks, lw=2, label=f'{config_name} (AURC = {result[\"metrics\"][\"AURC\"]:.4f})')\n\n    plt.xlabel(\"Độ phủ (%)\")\n    plt.ylabel(\"Rủi ro (%)\")\n    plt.title(\"Đường cong Rủi ro-Độ phủ cho các Baseline\")\n    plt.grid(True)\n    plt.legend(loc='upper right', bbox_to_anchor=(1, 1))\n    plt.xlim([0, 100])\n    plt.ylim([0, 100])\n    plt.tight_layout()\n    save_path = os.path.join(save_dir, 'all_risk_coverage_curves.png')\n    plt.savefig(save_path)\n    print(f\"Đã lưu Đường cong Rủi ro-Độ phủ tổng hợp vào: {save_path}\")\n    plt.show()\n    plt.close()\n    bin_accuracies = []\n    bin_rejection_scores = [] # Use scores here as confidence\n    bin_counts = []\n\n    for i in range(num_bins):\n        lower_bound = bins[i]\n        upper_bound = bins[i+1]\n        mask = (rejection_scores >= lower_bound) & (rejection_scores < upper_bound)\n        if i == num_bins - 1:\n            mask = (rejection_scores >= lower_bound) & (rejection_scores <= upper_bound)\n\n        bin_samples_indices = np.where(mask)[0]\n        bin_count = len(bin_samples_indices)\n\n        if bin_count > 0:\n            bin_accuracy = accuracy_score(true_labels[bin_samples_indices], model_predictions[bin_samples_indices])\n            bin_rejection_score_mean = np.mean(rejection_scores[bin_samples_indices])\n            bin_accuracies.append(bin_accuracy)\n            bin_rejection_scores.append(bin_rejection_score_mean)\n            bin_counts.append(bin_count)\n        else:\n            bin_accuracies.append(np.nan)\n            bin_rejection_scores.append(np.nan)\n            bin_counts.append(0)\n\n    # Lọc các bin rỗng\n    valid_bins_mask = ~np.isnan(bin_accuracies)\n    bin_accuracies = np.array(bin_accuracies)[valid_bins_mask]\n    bin_rejection_scores = np.array(bin_rejection_scores)[valid_bins_mask]\n\n    plt.figure(figsize=(7, 7))\n    plt.plot([0, 1], [0, 1], linestyle='--', color='gray', label='Hiệu chỉnh hoàn hảo')\n    plt.plot(bin_rejection_scores, bin_accuracies, marker='o', linestyle='-', color='blue', label='Mô hình')\n    \n    # Vẽ các thanh biểu đồ\n    for i_idx in range(len(bin_rejection_scores)):\n        plt.plot([bin_rejection_scores[i_idx], bin_rejection_scores[i_idx]], [bin_rejection_scores[i_idx], bin_accuracies[i_idx]],\n                 color='red' if bin_accuracies[i_idx] < bin_rejection_scores[i_idx] else 'green', linestyle='-', linewidth=2)\n\n    plt.xlabel(\"Điểm trung bình (Score)\")\n    plt.ylabel(\"Độ chính xác (Accuracy)\")\n    plt.title(\"Biểu đồ độ tin cậy (Reliability Diagram)\")\n    plt.grid(True)\n    plt.legend()\n    plt.xlim([0, 1])\n    plt.ylim([0, 1])\n    if save_path:\n        plt.savefig(save_path)\n        print(f\"Biểu đồ độ tin cậy đã lưu vào: {save_path}\")\n    plt.show()\n    plt.close()\n\ndef plot_roc_curve(model_predictions, rejection_scores, true_labels, save_path=None):\n    \"\"\"\n    Vẽ đường cong ROC (Receiver Operating Characteristic).\n    Sử dụng 'rejection_scores' (điểm từ chối hoặc độ tin cậy đã xử lý) làm điểm số để phân biệt đúng/sai.\n    \"\"\"\n    if len(rejection_scores) == 0:\n        print(\"Không có dữ liệu điểm để vẽ đường cong ROC.\")\n        return\n\n    is_correct = (model_predictions == true_labels).astype(int)\n    \n    if len(np.unique(is_correct)) < 2:\n        print(\"Không đủ biến thể trong các dự đoán đúng/sai để vẽ đường cong ROC.\")\n        return\n\n    fpr, tpr, thresholds = roc_curve(is_correct, rejection_scores)\n    roc_auc = auc(fpr, tpr)\n\n    plt.figure(figsize=(8, 6))\n    plt.plot(fpr, tpr, color='darkorange', lw=2, label=f'Đường cong ROC (AUC = {roc_auc:.2f})')\n    plt.plot([0, 1], [0, 1], color='navy', lw=2, linestyle='--', label='Ngẫu nhiên')\n    plt.xlim([0.0, 1.0])\n    plt.ylim([0.0, 1.05])\n    plt.xlabel('Tỷ lệ dương tính giả (False Positive Rate)')\n    plt.ylabel('Tỷ lệ dương tính thật (True Positive Rate)')\n    plt.title('Đường cong đặc trưng hoạt động của bộ thu (ROC Curve)')\n    plt.legend(loc=\"lower right\")\n    plt.grid(True)\n    if save_path:\n        plt.savefig(save_path)\n        print(f\"Đường cong ROC đã lưu vào: {save_path}\")\n    plt.show()\n    plt.close()\n\ndef plot_pr_curve(model_predictions, rejection_scores, true_labels, save_path=None):\n    \"\"\"\n    Vẽ đường cong PR (Precision-Recall).\n    \"\"\"\n    if len(rejection_scores) == 0:\n        print(\"Không có dữ liệu điểm để vẽ đường cong PR.\")\n        return\n\n    is_correct = (model_predictions == true_labels).astype(int)\n\n    if len(np.unique(is_correct)) < 2:\n        print(\"Không đủ biến thể trong các dự đoán đúng/sai để vẽ đường cong PR.\")\n        return\n\n    precision, recall, thresholds = precision_recall_curve(is_correct, rejection_scores)\n    pr_auc = auc(recall, precision)\n\n    plt.figure(figsize=(8, 6))\n    plt.plot(recall, precision, color='purple', lw=2, label=f'Đường cong PR (AUC = {pr_auc:.2f})')\n    plt.xlabel('Độ thu hồi (Recall)')\n    plt.ylabel('Độ chính xác (Precision)')\n    plt.title('Đường cong chính xác-độ thu hồi (Precision-Recall Curve)')\n    plt.legend(loc=\"lower left\")\n    plt.grid(True)\n    plt.xlim([0.0, 1.0])\n    plt.ylim([0.0, 1.05])\n    if save_path:\n        plt.savefig(save_path)\n        print(f\"Đường cong PR đã lưu vào: {save_path}\")\n    plt.show()\n    plt.close()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-29T18:05:16.154515Z","iopub.execute_input":"2025-06-29T18:05:16.154775Z","iopub.status.idle":"2025-06-29T18:05:16.198845Z","shell.execute_reply.started":"2025-06-29T18:05:16.154754Z","shell.execute_reply":"2025-06-29T18:05:16.198321Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def plot_individual_baseline_charts(model_predictions, rejection_scores, true_labels, baseline_name, save_dir):\n    \"\"\"\n    Tạo các biểu đồ phân tích performance cho một baseline riêng lẻ.\n    \"\"\"\n    print(f\"📊 Creating performance charts for {baseline_name}...\")\n    \n    # Create subfolder for this baseline\n    baseline_charts_dir = os.path.join(save_dir, f\"{baseline_name.replace(' ', '_')}_charts\")\n    os.makedirs(baseline_charts_dir, exist_ok=True)\n    \n    # 1. Calibration Reliability Diagram\n    plt.figure(figsize=(10, 6))\n    \n    # Calculate bins for calibration\n    n_bins = 10\n    bin_boundaries = np.linspace(0, 1, n_bins + 1)\n    bin_lowers = bin_boundaries[:-1]\n    bin_uppers = bin_boundaries[1:]\n    \n    accuracies = []\n    confidences = []\n    \n    is_correct = (model_predictions == true_labels).astype(float)\n    \n    for bin_lower, bin_upper in zip(bin_lowers, bin_uppers):\n        in_bin = (rejection_scores > bin_lower) & (rejection_scores <= bin_upper)\n        prop_in_bin = in_bin.mean()\n        \n        if prop_in_bin > 0:\n            accuracy_in_bin = is_correct[in_bin].mean()\n            avg_confidence_in_bin = rejection_scores[in_bin].mean()\n            accuracies.append(accuracy_in_bin)\n            confidences.append(avg_confidence_in_bin)\n        else:\n            accuracies.append(0)\n            confidences.append((bin_lower + bin_upper) / 2)\n    \n    plt.subplot(1, 2, 1)\n    plt.plot([0, 1], [0, 1], 'k--', label='Perfect Calibration')\n    plt.plot(confidences, accuracies, 'ro-', label=f'{baseline_name}')\n    plt.xlabel('Mean Predicted Confidence')\n    plt.ylabel('Accuracy')\n    plt.title('Reliability Diagram')\n    plt.legend()\n    plt.grid(True)\n    \n    # 2. Confidence Histogram\n    plt.subplot(1, 2, 2)\n    plt.hist(rejection_scores, bins=20, alpha=0.7, edgecolor='black')\n    plt.xlabel('Confidence Score')\n    plt.ylabel('Frequency')\n    plt.title('Confidence Distribution')\n    plt.grid(True)\n    \n    plt.tight_layout()\n    plt.savefig(os.path.join(baseline_charts_dir, f'{baseline_name}_calibration.png'), dpi=150, bbox_inches='tight')\n    plt.close()\n    \n    # 3. Risk-Coverage Curve\n    plt.figure(figsize=(8, 6))\n    \n    # Sort by confidence (descending)\n    sorted_indices = np.argsort(rejection_scores)[::-1]\n    sorted_predictions = model_predictions[sorted_indices]\n    sorted_labels = true_labels[sorted_indices]\n    \n    coverages = []\n    risks = []\n    \n    n_samples = len(sorted_predictions)\n    for i in range(n_samples):\n        coverage = (i + 1) / n_samples\n        accepted_preds = sorted_predictions[:i+1]\n        accepted_labels = sorted_labels[:i+1]\n        \n        if len(accepted_preds) > 0:\n            accuracy = np.mean(accepted_preds == accepted_labels)\n            risk = 1 - accuracy\n        else:\n            risk = 1.0\n            \n        coverages.append(coverage)\n        risks.append(risk)\n    \n    plt.plot(coverages, risks, 'b-', linewidth=2, label=f'{baseline_name}')\n    plt.xlabel('Coverage')\n    plt.ylabel('Risk')\n    plt.title('Risk-Coverage Curve')\n    plt.grid(True)\n    plt.legend()\n    \n    plt.savefig(os.path.join(baseline_charts_dir, f'{baseline_name}_risk_coverage.png'), dpi=150, bbox_inches='tight')\n    plt.close()\n    \n    # 4. ROC Curve for Correctness Prediction\n    plt.figure(figsize=(8, 6))\n    \n    if len(np.unique(is_correct)) > 1:\n        fpr, tpr, _ = roc_curve(is_correct, rejection_scores)\n        roc_auc = auc(fpr, tpr)\n        \n        plt.plot(fpr, tpr, 'b-', linewidth=2, label=f'{baseline_name} (AUC = {roc_auc:.3f})')\n        plt.plot([0, 1], [0, 1], 'k--', label='Random')\n        plt.xlabel('False Positive Rate')\n        plt.ylabel('True Positive Rate')\n        plt.title('ROC Curve - Correctness Prediction')\n        plt.legend()\n        plt.grid(True)\n    else:\n        plt.text(0.5, 0.5, 'Cannot compute ROC\\n(only one class)', \n                horizontalalignment='center', verticalalignment='center', \n                transform=plt.gca().transAxes, fontsize=14)\n    \n    plt.savefig(os.path.join(baseline_charts_dir, f'{baseline_name}_roc.png'), dpi=150, bbox_inches='tight')\n    plt.close()\n    \n    print(f\"✅ Charts saved to {baseline_charts_dir}\")\n    return baseline_charts_dir\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-29T18:05:16.19942Z","iopub.execute_input":"2025-06-29T18:05:16.199577Z","iopub.status.idle":"2025-06-29T18:05:16.212547Z","shell.execute_reply.started":"2025-06-29T18:05:16.199564Z","shell.execute_reply":"2025-06-29T18:05:16.211926Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def plot_risk_coverage_curve(model_predictions, rejection_scores, true_labels, save_path=None):\n    \"\"\"\n    Vẽ đường cong Risk-Coverage.\n    \"\"\"\n    if len(rejection_scores) == 0:\n        print(\"Không có dữ liệu điểm để vẽ đường cong Risk-Coverage.\")\n        return\n\n    sorted_indices = np.argsort(rejection_scores)\n    sorted_predictions = model_predictions[sorted_indices]\n    sorted_labels = true_labels[sorted_indices]\n\n    risks = []\n    coverages = []\n    num_total = len(true_labels)\n\n    for i_idx in range(num_total):\n        current_accepted_preds = sorted_predictions[i_idx:]\n        current_accepted_labels = sorted_labels[i_idx:]\n        current_coverage = (num_total - i_idx) / num_total\n\n        if (num_total - i_idx) == 0:\n            current_risk = 1.0  \n        else:\n            num_correct = np.sum(current_accepted_preds == current_accepted_labels)\n            current_risk = 1.0 - (num_correct / (num_total - i_idx)) \n\n        coverages.append(current_coverage)\n        risks.append(current_risk)\n\n    # Đảo ngược để có coverage từ thấp đến cao\n    coverages = coverages[::-1]\n    risks = risks[::-1]\n\n    plt.figure(figsize=(8, 6))\n    plt.plot(coverages, risks, color='red', lw=2, marker='o', markersize=2, label='Đường cong Risk-Coverage')\n    plt.xlabel('Độ phủ (Coverage)')\n    plt.ylabel('Rủi ro (Risk)')\n    plt.title('Đường cong Risk-Coverage')\n    plt.legend()\n    plt.grid(True)\n    plt.xlim([0.0, 1.0])\n    plt.ylim([0.0, 1.0])\n    if save_path:\n        plt.savefig(save_path)\n        print(f\"Đường cong Risk-Coverage đã lưu vào: {save_path}\")\n    plt.show()\n    plt.close()\n\ndef plot_comparison_metrics(all_baseline_results, save_path=None):\n    \"\"\"\n    Vẽ biểu đồ so sánh giữa tất cả các baseline.\n    \"\"\"\n    if not all_baseline_results:\n        print(\"Không có kết quả baseline để so sánh.\")\n        return\n\n    baseline_names = list(all_baseline_results.keys())\n    metrics_to_compare = ['accuracy_on_accepted', 'coverage', 'ece', 'auroc_correct_incorrect', 'aurc', 'f1_rejection']  # ✅ FIXED: Added f1_rejection\n\n    fig, axes = plt.subplots(2, 3, figsize=(18, 12))\n    axes = axes.flatten()\n\n    for i_idx, metric in enumerate(metrics_to_compare):\n        if i_idx >= len(axes):\n            break\n        \n        values = [all_baseline_results[baseline].get(metric, np.nan) for baseline in baseline_names]\n        valid_data = [(name, val) for name, val in zip(baseline_names, values) if not np.isnan(val)]\n        \n        if valid_data:\n            names, vals = zip(*valid_data)\n            ax = axes[i_idx]\n            bars = ax.bar(range(len(names)), vals, color=plt.cm.Set3(np.linspace(0, 1, len(names))))\n            ax.set_xlabel('Baseline')\n            ax.set_ylabel(metric.replace('_', ' ').title())\n            ax.set_title(f'So sánh {metric.replace(\"_\", \" \").title()}')\n            ax.set_xticks(range(len(names)))\n            ax.set_xticklabels(names, rotation=45, ha='right')\n            ax.grid(True, alpha=0.3)\n            \n            # Thêm giá trị trên đầu các thanh\n            for j_idx, (bar, val) in enumerate(zip(bars, vals)):\n                ax.text(bar.get_x() + bar.get_width()/2., bar.get_height() + 0.01,\n                       f'{val:.3f}', ha='center', va='bottom', fontsize=9)\n        else:\n            axes[i_idx].text(0.5, 0.5, f'Không có dữ liệu cho {metric}', \n                           ha='center', va='center', transform=axes[i_idx].transAxes)\n\n    # Ẩn subplot cuối cùng nếu không được sử dụng\n    if len(metrics_to_compare) < len(axes):\n        axes[-1].axis('off')\n\n    plt.tight_layout()\n    if save_path:\n        plt.savefig(save_path, dpi=300, bbox_inches='tight')\n        print(f\"Biểu đồ so sánh đã lưu vào: {save_path}\")\n    plt.show()\n    plt.close()\n\ndef plot_risk_coverage_comparison(all_baseline_results, all_baseline_test_results, save_path=None):\n    \"\"\"\n    Vẽ so sánh đường cong Risk-Coverage cho tất cả các baseline.\n    \"\"\"\n    plt.figure(figsize=(12, 8))\n    \n    colors = plt.cm.Set1(np.linspace(0, 1, len(all_baseline_test_results)))\n    \n    for i_idx, (baseline_name, test_results) in enumerate(all_baseline_test_results.items()):\n        model_preds = test_results['model_preds']\n        rejection_scores = test_results['rejection_scores']\n        true_labels = test_results['true_labels']\n        \n        if len(rejection_scores) == 0:\n            continue\n            \n        sorted_indices = np.argsort(rejection_scores)\n        sorted_predictions = model_preds[sorted_indices]\n        sorted_labels = true_labels[sorted_indices]\n\n        risks = []\n        coverages = []\n        num_total = len(true_labels)\n\n        for j_idx in range(num_total):\n            current_accepted_preds = sorted_predictions[j_idx:]\n            current_accepted_labels = sorted_labels[j_idx:]\n            current_coverage = (num_total - j_idx) / num_total\n\n            if (num_total - j_idx) == 0:\n                current_risk = 1.0  \n            else:\n                num_correct = np.sum(current_accepted_preds == current_accepted_labels)\n                current_risk = 1.0 - (num_correct / (num_total - j_idx)) \n\n            coverages.append(current_coverage)\n            risks.append(current_risk)\n\n        coverages = coverages[::-1]\n        risks = risks[::-1]\n        \n        plt.plot(coverages, risks, color=colors[i_idx], lw=2, label=baseline_name, alpha=0.8)\n\n    plt.xlabel('Độ phủ (Coverage)')\n    plt.ylabel('Rủi ro (Risk)')\n    plt.title('So sánh Đường cong Risk-Coverage của tất cả Baseline')\n    plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left')\n    plt.grid(True, alpha=0.3)\n    plt.xlim([0.0, 1.0])\n    plt.ylim([0.0, 1.0])\n    \n    if save_path:\n        plt.savefig(save_path, dpi=300, bbox_inches='tight')\n        print(f\"So sánh đường cong Risk-Coverage đã lưu vào: {save_path}\")\n    plt.show()\n    plt.close()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-29T18:05:16.213284Z","iopub.execute_input":"2025-06-29T18:05:16.213508Z","iopub.status.idle":"2025-06-29T18:05:16.232434Z","shell.execute_reply.started":"2025-06-29T18:05:16.213487Z","shell.execute_reply":"2025-06-29T18:05:16.231846Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 15. Main Baseline Execution Function\n\nHàm chính để chạy một baseline cụ thể với các cấu hình khác nhau:\n- Thiết lập configuration flags cho từng baseline\n- Chạy ensemble training (nếu cần)\n- Thực hiện inference và tính rejection scores\n- Tìm optimal threshold trên validation set\n- Đánh giá performance trên test set\n- Tạo visualizations và XAI explanations\n","metadata":{"vscode":{"languageId":"raw"}}},{"cell_type":"code","source":"# ❌ REMOVED: Redundant run_baseline function definition\n# ✅ The correct run_baseline function with independent training signature is already defined in an earlier cell\n# This old function was overriding the correct one, causing the TypeError\n\nprint(\"⚠️  THÔNG BÁO: Function definition này đã được loại bỏ vì trùng lặp\")\nprint(\"✅ Function run_baseline đúng đã được định nghĩa ở cell trước đó\")\nprint(\"📝 Signature đúng: run_baseline(config_name, mcdo_enable, label_smoothing_enable, ...)\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-29T18:05:16.233104Z","iopub.execute_input":"2025-06-29T18:05:16.233342Z","iopub.status.idle":"2025-06-29T18:05:16.246712Z","shell.execute_reply.started":"2025-06-29T18:05:16.233322Z","shell.execute_reply":"2025-06-29T18:05:16.246098Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 🔧 Main Baseline Execution Function\n\ndef run_baseline(config_name, mcdo_enable, label_smoothing_enable, calibration_method, \n                ood_detection_method, combine_ood_with_disagreement, enable_training_dynamics):\n    \"\"\"\n    Chạy một baseline với cấu hình đã cho và trả về kết quả đánh giá.\n    \"\"\"\n    print(f\"\\n🚀 Starting {config_name}\")\n    print(\"-\" * 80)\n    \n    # Cập nhật config cho baseline này\n    cfg.MCDO_ENABLE = mcdo_enable\n    cfg.LABEL_SMOOTHING_ENABLE = label_smoothing_enable\n    cfg.ENABLE_TRAINING_DYNAMICS = enable_training_dynamics\n    \n    # Tải dữ liệu\n    train_loader, val_loader, test_loader, train_dataset, val_dataset, test_dataset = prepare_datasets(cfg)\n    \n    # Training Ensemble\n    print(f\"🔥 Training ensemble for {config_name}...\")\n    overall_learning_metrics = train_ensemble(cfg, train_loader, val_loader)\n    \n    # Validation: Tìm ngưỡng tối ưu\n    print(f\"🎯 Finding optimal threshold on validation set...\")\n    val_predictions, val_rejection_scores, val_labels, val_indices, \\\n    val_ensemble_probs_stacked, val_odin_scores, val_energy_scores = get_rejection_scores_and_predictions(\n        cfg, val_loader, cfg.MODEL_SAVE_DIR, overall_learning_metrics, train_dataset,\n        calibration_method=calibration_method,\n        ood_detection_method=ood_detection_method,\n        combine_ood_with_disagreement=combine_ood_with_disagreement\n    )\n    \n    optimal_threshold, val_threshold_results = find_optimal_rejection_threshold(\n        val_rejection_scores, val_predictions, val_labels, cfg\n    )\n    \n    # Test: Áp dụng ngưỡng tối ưu\n    print(f\"📊 Evaluating on test set...\")\n    test_predictions, test_rejection_scores, test_labels, test_indices, \\\n    test_ensemble_probs_stacked, test_odin_scores, test_energy_scores = get_rejection_scores_and_predictions(\n        cfg, test_loader, cfg.MODEL_SAVE_DIR, overall_learning_metrics, train_dataset,\n        calibration_method=calibration_method,\n        ood_detection_method=ood_detection_method,\n        combine_ood_with_disagreement=combine_ood_with_disagreement\n    )\n    \n    # Tính toán metrics trên test set\n    test_metrics = calculate_metrics(\n        test_predictions, test_rejection_scores, test_labels, optimal_threshold, verbose=True\n    )\n    \n    # Categorize rejected cases\n\n    rejected_categories = categorize_rejected_cases(\n        test_rejection_scores, test_predictions, test_labels, test_indices,\n        optimal_threshold, test_ensemble_probs_stacked, test_odin_scores, test_energy_scores,\n        ood_detection_method\n    )\n    \n    # Tạo XAI visualizations (chọn vài samples)\n    create_xai_visualizations_for_rejected_cases(\n        cfg, rejected_categories, test_dataset, config_name.replace(' ', '_').replace('-', '_')\n    )\n    \n    # Tính các metrics bổ sung\n    overall_accuracy = accuracy_score(test_labels, test_predictions)\n    accepted_indices = test_rejection_scores >= optimal_threshold\n    num_accepted = np.sum(accepted_indices)\n    num_total = len(test_labels)\n    coverage = num_accepted / num_total\n    rejection_rate = 1.0 - coverage\n    \n    if num_accepted > 0:\n        accepted_predictions = test_predictions[accepted_indices]\n        accepted_true_labels = test_labels[accepted_indices]\n        accuracy_accepted = accuracy_score(accepted_true_labels, accepted_predictions)\n        f1_accepted = f1_score(accepted_true_labels, accepted_predictions, average='weighted', zero_division=0)\n    else:\n        accuracy_accepted = 0.0\n        f1_accepted = 0.0\n    \n    # Tính ECE\n    ece = calculate_ece(test_predictions, test_rejection_scores, test_labels)\n    \n    # Tính AUROC và AUPR cho correctness prediction\n    correctness = (test_predictions == test_labels).astype(int)\n    if len(np.unique(correctness)) > 1:\n        auroc_correctness = roc_auc_score(correctness, test_rejection_scores)\n        precision_correctness, recall_correctness, _ = precision_recall_curve(correctness, test_rejection_scores)\n        aupr_correctness = auc(recall_correctness, precision_correctness)\n    else:\n        auroc_correctness = 0.5\n        aupr_correctness = 0.5\n    \n    # Tính AURC (Area Under Risk-Coverage curve)\n    sorted_indices = np.argsort(test_rejection_scores)[::-1]  # Sort by confidence (descending)\n    sorted_correctness = correctness[sorted_indices]\n    cumulative_risk = np.cumsum(1 - sorted_correctness) / np.arange(1, len(sorted_correctness) + 1)\n    coverage_points = np.arange(1, len(sorted_correctness) + 1) / len(sorted_correctness)\n    aurc = auc(coverage_points, cumulative_risk)\n    \n    metrics = {\n        'Baseline': config_name,\n        'Overall_Accuracy': overall_accuracy,\n        'Accuracy_Accepted': accuracy_accepted,\n        'F1_Accepted': f1_accepted,\n        'Coverage': coverage,\n        'Rejection_Rate': rejection_rate,\n        'ECE': ece,\n        'AUROC_Correctness': auroc_correctness,\n        'AUPR_Correctness': aupr_correctness,\n        'AURC': aurc,\n        'Optimal_Threshold': optimal_threshold,\n        'Num_Failure_Cases': len(rejected_categories.get('failure', [])),\n        'Num_OOD_Cases': len(rejected_categories.get('ood', [])),\n        'Num_Ambiguous_Cases': len(rejected_categories.get('ambiguous', []))\n    }\n    \n    print(f\"✅ Completed {config_name}\")\n    print(f\"📈 Key Results - Accuracy: {accuracy_accepted:.3f}, Coverage: {coverage:.3f}, ECE: {ece:.3f}\")\n    \n    return {\n        'metrics': metrics,\n        'test_predictions': test_predictions,\n        'test_rejection_scores': test_rejection_scores,\n        'test_labels': test_labels,\n        'optimal_threshold': optimal_threshold,\n        'rejected_categories': rejected_categories,\n        'val_threshold_results': val_threshold_results\n    }\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-29T18:05:16.247455Z","iopub.execute_input":"2025-06-29T18:05:16.247689Z","iopub.status.idle":"2025-06-29T18:05:16.263341Z","shell.execute_reply.started":"2025-06-29T18:05:16.247669Z","shell.execute_reply":"2025-06-29T18:05:16.262707Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 16. Main Execution\n\nPhần chính để chạy tất cả các experiments:\n\n1. **Data Loading**: Load và prepare datasets\n2. **Initial Training**: Train ensemble models cơ bản\n3. **Baseline Experiments**: Chạy tất cả 10+ baselines\n4. **Comparative Analysis**: So sánh kết quả và tạo visualizations\n5. **XAI Generation**: Tạo explanations cho các cases đại diện\n\n**Lưu ý**: Đây là notebook hoàn chỉnh, bạn có thể chạy từng cell hoặc chạy toàn bộ để thực hiện complete experiment.\n","metadata":{"vscode":{"languageId":"raw"}}},{"cell_type":"markdown","source":"## 🎯 Hướng dẫn Sử dụng Notebook\n\n### Cách chạy Notebook:\n\n**Tùy chọn 1: Chạy toàn bộ**\n```python\n# Chạy tất cả cells từ đầu đến cuối để thực hiện complete experiment\n# Ước tính thời gian: 2-4 giờ (tùy thuộc vào GPU và data size)\n```\n\n**Tùy chọn 2: Chạy từng phần**\n1. **Cells 1-7**: Setup và data loading (bắt buộc)\n2. **Cells 8-12**: Training ensemble cơ bản (bắt buộc)\n3. **Cells 13-27**: Utility functions (bắt buộc)\n4. **Cell 37**: Chạy main execution để test tất cả baselines\n\n**Tùy chọn 3: Chạy baseline cụ thể**\n```python\n# Thay vì chạy tất cả baselines, bạn có thể test từng baseline:\nbaseline_result = run_baseline(\n    \"Baseline A.1 (Ensemble + MCDO)\", \n    cfg, train_loader, val_loader, test_loader,\n    train_dataset, val_dataset, test_dataset\n)\n```\n\n### Customization:\n\n**Thay đổi dataset path:**\n```python\n# Trong Cell 4 (Configuration), sửa đường dẫn:\ncfg.DATA_DIR = '/path/to/your/covid-ct-dataset'\n```\n\n**Điều chỉnh hyperparameters:**\n```python\n# Trong Cell 4, có thể điều chỉnh:\ncfg.NUM_ENSEMBLE_MODELS = 5  # Tăng số models trong ensemble\ncfg.BATCH_SIZE = 32          # Tăng batch size nếu có GPU mạnh\ncfg.NUM_EPOCHS_PER_MODEL = 10 # Tăng epochs để training tốt hơn\n```\n\n**Chọn baselines cụ thể:**\n```python\n# Trong Cell 37, có thể comment/uncomment baselines không muốn chạy:\nselected_baselines = [\n    \"Baseline 0 (Current Ensemble)\",\n    \"Baseline A.1 (Ensemble + MCDO)\",\n    # \"Baseline A.2.1 (Ensemble + Isotonic Regression)\",  # Skip này\n]\n```\n\n### Output Files:\n- **Models**: `/kaggle/working/models/` - Trained ensemble models\n- **Visualizations**: `/kaggle/working/xai_visualizations/` - XAI explanations\n- **Comparison Charts**: `/kaggle/working/baseline_comparison.png`\n- **Risk-Coverage**: `/kaggle/working/risk_coverage_comparison.png`\n\n### 🚨 Lưu ý quan trọng:\n1. Cần GPU để training hiệu quả (ít nhất 8GB VRAM)\n2. Toàn bộ experiment có thể mất 2-4 giờ\n3. Cần ít nhất 16GB RAM để xử lý ensemble và features\n4. Dataset COVID-19 CT cần được download từ Kaggle trước\n\n### 📞 Troubleshooting:\n- **OOM Error**: Giảm `BATCH_SIZE` và `NUM_ENSEMBLE_MODELS`\n- **Training lâu**: Giảm `NUM_EPOCHS_PER_MODEL` \n- **Missing dataset**: Kiểm tra `cfg.DATA_DIR` path\n- **CUDA Error**: Thêm `torch.cuda.empty_cache()` sau mỗi baseline\n","metadata":{"vscode":{"languageId":"raw"}}},{"cell_type":"code","source":"# 🚀 MAIN EXECUTION - Chạy tất cả 10 baselines với independent training\n\nprint(\"🚀 BẮT ĐẦU EXPLAINABLE LEARNING TO REJECT - INDEPENDENT TRAINING\")\nprint(\"=\"*80)\n\n# Xóa tất cả models và visualizations từ runs trước đó\nif os.path.exists(cfg.MODEL_SAVE_DIR):\n    shutil.rmtree(cfg.MODEL_SAVE_DIR)\nos.makedirs(cfg.MODEL_SAVE_DIR)\n\nif os.path.exists(cfg.XAI_SAVE_DIR):\n    shutil.rmtree(cfg.XAI_SAVE_DIR)\nos.makedirs(cfg.XAI_SAVE_DIR)\n\nall_baseline_results = []\n\nprint(\"\\n📝 CHIẾN LƯỢC: Mỗi baseline train ensemble độc lập với config riêng\")\nprint(\"⏱️  Ước tính thời gian tổng: ~2.5-3.5 giờ\")\nprint(\"=\"*80)\n\n# Baseline 0: Current Ensemble (Temperature Scaling)\nprint(\"\\n🔄 [1/10] Baseline 0 - Current Ensemble\")\nresults_b0 = run_baseline(\n    config_name='Baseline 0 - Current Ensemble (Temperature Scaling)',\n    mcdo_enable=False,\n    label_smoothing_enable=False,\n    calibration_method='temperature_scaling',\n    ood_detection_method='none',\n    combine_ood_with_disagreement=False,\n    enable_training_dynamics=False\n)\nall_baseline_results.append(results_b0)\nprint(\"✅ [1/10] Completed Baseline 0\")\n\n# Baseline A.1: Ensemble + MCDO\nprint(\"\\n🔄 [2/10] Baseline A.1 - Ensemble + MCDO\")\nresults_a1 = run_baseline(\n    config_name='Baseline A.1 - Ensemble + MCDO',\n    mcdo_enable=True,\n    label_smoothing_enable=False,\n    calibration_method='temperature_scaling',\n    ood_detection_method='none',\n    combine_ood_with_disagreement=False,\n    enable_training_dynamics=False\n)\nall_baseline_results.append(results_a1)\nprint(\"✅ [2/10] Completed Baseline A.1\")\n\n# Baseline A.2.1: Ensemble + Isotonic Regression\nprint(\"\\n🔄 [3/10] Baseline A.2.1 - Isotonic Regression\")\nresults_a21 = run_baseline(\n    config_name='Baseline A.2.1 - Ensemble + Isotonic Regression',\n    mcdo_enable=False,\n    label_smoothing_enable=False,\n    calibration_method='isotonic_regression',\n    ood_detection_method='none',\n    combine_ood_with_disagreement=False,\n    enable_training_dynamics=False\n)\nall_baseline_results.append(results_a21)\nprint(\"✅ [3/10] Completed Baseline A.2.1\")\n\n# Baseline A.2.2: Ensemble + Beta Calibration\nprint(\"\\n🔄 [4/10] Baseline A.2.2 - Beta Calibration\")\nresults_a22 = run_baseline(\n    config_name='Baseline A.2.2 - Ensemble + Beta Calibration',\n    mcdo_enable=False,\n    label_smoothing_enable=False,\n    calibration_method='beta_calibration',\n    ood_detection_method='none',\n    combine_ood_with_disagreement=False,\n    enable_training_dynamics=False\n)\nall_baseline_results.append(results_a22)\nprint(\"✅ [4/10] Completed Baseline A.2.2\")\n\n# Baseline A.2.3: Ensemble + Label Smoothing\nprint(\"\\n🔄 [5/10] Baseline A.2.3 - Label Smoothing\")\nresults_a23 = run_baseline(\n    config_name='Baseline A.2.3 - Ensemble + Label Smoothing',\n    mcdo_enable=False,\n    label_smoothing_enable=True,\n    calibration_method='temperature_scaling',\n    ood_detection_method='none',\n    combine_ood_with_disagreement=False,\n    enable_training_dynamics=False\n)\nall_baseline_results.append(results_a23)\nprint(\"✅ [5/10] Completed Baseline A.2.3\")\n\n# Baseline B.1.1: Ensemble + ODIN (Basic)\nprint(\"\\n🔄 [6/10] Baseline B.1.1 - ODIN Basic\")\nresults_b11 = run_baseline(\n    config_name='Baseline B.1.1 - Ensemble + ODIN (Basic)',\n    mcdo_enable=False,\n    label_smoothing_enable=False,\n    calibration_method='temperature_scaling',\n    ood_detection_method='odin',\n    combine_ood_with_disagreement=False,\n    enable_training_dynamics=False\n)\nall_baseline_results.append(results_b11)\nprint(\"✅ [6/10] Completed Baseline B.1.1\")\n\n# Baseline B.1.2: Ensemble + ODIN (Combined)\nprint(\"\\n🔄 [7/10] Baseline B.1.2 - ODIN Combined\")\nresults_b12 = run_baseline(\n    config_name='Baseline B.1.2 - Ensemble + ODIN (Combined)',\n    mcdo_enable=False,\n    label_smoothing_enable=False,\n    calibration_method='temperature_scaling',\n    ood_detection_method='odin',\n    combine_ood_with_disagreement=True,\n    enable_training_dynamics=False\n)\nall_baseline_results.append(results_b12)\nprint(\"✅ [7/10] Completed Baseline B.1.2\")\n\n# Baseline B.2.1: Ensemble + Energy Score (Basic)\nprint(\"\\n🔄 [8/10] Baseline B.2.1 - Energy Score Basic\")\nresults_b21 = run_baseline(\n    config_name='Baseline B.2.1 - Ensemble + Energy Score (Basic)',\n    mcdo_enable=False,\n    label_smoothing_enable=False,\n    calibration_method='temperature_scaling',\n    ood_detection_method='energy',\n    combine_ood_with_disagreement=False,\n    enable_training_dynamics=False\n)\nall_baseline_results.append(results_b21)\nprint(\"✅ [8/10] Completed Baseline B.2.1\")\n\n# Baseline B.2.2: Ensemble + Energy Score (Combined)\nprint(\"\\n🔄 [9/10] Baseline B.2.2 - Energy Score Combined\")\nresults_b22 = run_baseline(\n    config_name='Baseline B.2.2 - Ensemble + Energy Score (Combined)',\n    mcdo_enable=False,\n    label_smoothing_enable=False,\n    calibration_method='temperature_scaling',\n    ood_detection_method='energy',\n    combine_ood_with_disagreement=True,\n    enable_training_dynamics=False\n)\nall_baseline_results.append(results_b22)\nprint(\"✅ [9/10] Completed Baseline B.2.2\")\n\n# Baseline B.3: Ensemble + Training Dynamics\nprint(\"\\n🔄 [10/10] Baseline B.3 - Training Dynamics\")\nresults_b3 = run_baseline(\n    config_name='Baseline B.3 - Ensemble + Training Dynamics Insights',\n    mcdo_enable=False,\n    label_smoothing_enable=False,\n    calibration_method='temperature_scaling',\n    ood_detection_method='none',\n    combine_ood_with_disagreement=False,\n    enable_training_dynamics=True\n)\nall_baseline_results.append(results_b3)\nprint(\"✅ [10/10] Completed Baseline B.3\")\n\nprint(\"\\n🎉 HOÀN THÀNH TẤT CẢ 10 BASELINES!\")\nprint(\"=\"*80)\n\n# Show comparison results\nmetrics_only_results = [res['metrics'] for res in all_baseline_results]\nfinal_results_df = pd.DataFrame(metrics_only_results)\n\nprint(\"\\n📊 BẢNG SO SÁNH KẾT QUẢ TỔNG QUAN:\")\nprint(\"-\" * 120)\nprint(final_results_df.round(4).to_markdown(index=False))\n\nprint(\"\\n📊 TẠO BIỂU ĐỒ SO SÁNH...\")\nplot_all_calibration_curves(all_baseline_results, cfg.XAI_SAVE_DIR)\nplot_all_roc_curves(all_baseline_results, cfg.XAI_SAVE_DIR)\nplot_all_pr_curves(all_baseline_results, cfg.XAI_SAVE_DIR)\nplot_all_risk_coverage_curves(all_baseline_results, cfg.XAI_SAVE_DIR)\n\nprint(\"\\n🎉 HOÀN TẤT TOÀN BỘ EXPERIMENT!\")\nprint(f\"📁 Results saved to: {cfg.XAI_SAVE_DIR}\")\nprint(\"=\"*80)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-29T18:05:16.264097Z","iopub.execute_input":"2025-06-29T18:05:16.264337Z","iopub.status.idle":"2025-06-29T20:44:57.341371Z","shell.execute_reply.started":"2025-06-29T18:05:16.264317Z","shell.execute_reply":"2025-06-29T20:44:57.339639Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}