{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"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":24800,"databundleVersionId":1831594,"sourceType":"competition"},{"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":"# Download Dependency","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-07-08T12:36:12.435801Z","iopub.execute_input":"2025-07-08T12:36:12.436385Z","iopub.status.idle":"2025-07-08T12:38:06.953841Z","shell.execute_reply.started":"2025-07-08T12:36:12.436346Z","shell.execute_reply":"2025-07-08T12:38:06.95306Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\nimport numpy as np\nimport os\nimport shutil\n\n# Đường dẫn đến dữ liệu gốc trên Kaggle\nKAGGLE_INPUT_PATH = '/kaggle/input/vinbigdata-chest-xray-abnormalities-detection/'\nTRAIN_CSV_PATH = os.path.join(KAGGLE_INPUT_PATH, 'train.csv')\nTRAIN_IMAGES_PATH = os.path.join(KAGGLE_INPUT_PATH, 'train/')\n\n# Đường dẫn thư mục đầu ra\nOUTPUT_BASE_PATH = '/kaggle/working/'\nABNORMAL_PATH = os.path.join(OUTPUT_BASE_PATH, 'abnormal/')\nNON_ABNORMAL_PATH = os.path.join(OUTPUT_BASE_PATH, 'non_abnormal/')\n\n# Số lượng ảnh tối đa muốn lấy cho mỗi loại\nMAX_IMAGES_PER_CATEGORY = 500\n\n# Tạo các thư mục đầu ra nếu chúng chưa tồn tại\nos.makedirs(ABNORMAL_PATH, exist_ok=True)\nos.makedirs(NON_ABNORMAL_PATH, exist_ok=True)\n\nprint(f\"Đọc file CSV từ: {TRAIN_CSV_PATH}\")\n# Đọc file train.csv\ndf = pd.read_csv(TRAIN_CSV_PATH)\nprint(f\"Đã đọc {len(df)} dòng từ train.csv.\")\n\n# Lấy tất cả các image_id duy nhất\nall_image_ids = df['image_id'].unique()\nprint(f\"Tổng số ảnh duy nhất: {len(all_image_ids)}\")\n\n# Xác định image_id của các ảnh \"không bệnh\" (chỉ có class_id = 14)\n# Group theo image_id và kiểm tra xem có bất kỳ class_id nào khác 14 không\nimage_labels = df.groupby('image_id')['class_id'].apply(list).reset_index()\n\nnon_abnormal_image_ids = []\nabnormal_image_ids = []\n\nfor index, row in image_labels.iterrows():\n    image_id = row['image_id']\n    class_ids = row['class_id']\n    \n    # Nếu tất cả các nhãn cho ảnh này đều là 14 (\"No finding\")\n    if all(cid == 14 for cid in class_ids):\n        non_abnormal_image_ids.append(image_id)\n    else:\n        abnormal_image_ids.append(image_id)\n\nprint(f\"Tổng số ảnh 'không bệnh' (non_abnormal) tìm thấy: {len(non_abnormal_image_ids)}\")\nprint(f\"Tổng số ảnh 'có bệnh' (abnormal) tìm thấy: {len(abnormal_image_ids)}\")\n\nprint(f\"\\nBắt đầu sao chép tối đa {MAX_IMAGES_PER_CATEGORY} ảnh cho mỗi loại...\")\n\n# Sao chép ảnh \"không bệnh\"\ncopied_non_abnormal_count = 0\nfor image_id in non_abnormal_image_ids:\n    if copied_non_abnormal_count >= MAX_IMAGES_PER_CATEGORY:\n        break # Dừng lại nếu đã đủ số lượng\n    \n    src_path = os.path.join(TRAIN_IMAGES_PATH, f\"{image_id}.dicom\")\n    dst_path = os.path.join(NON_ABNORMAL_PATH, f\"{image_id}.dicom\")\n    try:\n        shutil.copy(src_path, dst_path)\n        copied_non_abnormal_count += 1\n    except FileNotFoundError:\n        # print(f\"Cảnh báo: Không tìm thấy file {src_path}. Bỏ qua.\")\n        pass # Bỏ qua cảnh báo để tránh spam console\n    except Exception as e:\n        print(f\"Lỗi khi sao chép {src_path} đến {dst_path}: {e}\")\n\nprint(f\"Đã sao chép {copied_non_abnormal_count} ảnh vào thư mục non_abnormal.\")\n\n# Sao chép ảnh \"có bệnh\"\ncopied_abnormal_count = 0\nfor image_id in abnormal_image_ids:\n    if copied_abnormal_count >= MAX_IMAGES_PER_CATEGORY:\n        break # Dừng lại nếu đã đủ số lượng\n    \n    src_path = os.path.join(TRAIN_IMAGES_PATH, f\"{image_id}.dicom\")\n    dst_path = os.path.join(ABNORMAL_PATH, f\"{image_id}.dicom\")\n    try:\n        shutil.copy(src_path, dst_path)\n        copied_abnormal_count += 1\n    except FileNotFoundError:\n        # print(f\"Cảnh báo: Không tìm thấy file {src_path}. Bỏ qua.\")\n        pass # Bỏ qua cảnh báo để tránh spam console\n    except Exception as e:\n        print(f\"Lỗi khi sao chép {src_path} đến {dst_path}: {e}\")\n\nprint(f\"Đã sao chép {copied_abnormal_count} ảnh vào thư mục abnormal.\")\n\nprint(\"\\nQuá trình phân chia dữ liệu hoàn tất!\")\nprint(f\"Kiểm tra các thư mục: {ABNORMAL_PATH} và {NON_ABNORMAL_PATH}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-08T12:38:06.955277Z","iopub.execute_input":"2025-07-08T12:38:06.955512Z","iopub.status.idle":"2025-07-08T12:41:29.133574Z","shell.execute_reply.started":"2025-07-08T12:38:06.955488Z","shell.execute_reply":"2025-07-08T12:41:29.1325Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport pydicom\nfrom PIL import Image\nimport numpy as np\nfrom tqdm.notebook import tqdm\n\n# Đường dẫn đến các thư mục đã phân chia (từ bước trước)\nABNORMAL_DICOM_PATH = '/kaggle/working/abnormal/'\nNON_ABNORMAL_DICOM_PATH = '/kaggle/working/non_abnormal/'\n\n# Đường dẫn mới để lưu ảnh đã chuyển đổi\nABNORMAL_PNG_PATH = '/kaggle/working/abnormal_png/'\nNON_ABNORMAL_PNG_PATH = '/kaggle/working/non_abnormal_png/'\n\n# Tạo các thư mục mới nếu chúng chưa tồn tại\nos.makedirs(ABNORMAL_PNG_PATH, exist_ok=True)\nos.makedirs(NON_ABNORMAL_PNG_PATH, exist_ok=True)\n\ndef dicom_to_png(dicom_dir, png_dir):\n    \"\"\"\n    Chuyển đổi tất cả các file DICOM trong một thư mục sang định dạng PNG.\n    \"\"\"\n    print(f\"Bắt đầu chuyển đổi DICOM từ '{dicom_dir}' sang PNG trong '{png_dir}'...\")\n    dicom_files = [f for f in os.listdir(dicom_dir) if f.endswith('.dicom')]\n    \n    for dicom_file in tqdm(dicom_files, desc=f\"Chuyển đổi từ {os.path.basename(dicom_dir)}\"):\n        dicom_path = os.path.join(dicom_dir, dicom_file)\n        png_filename = dicom_file.replace('.dicom', '.png')\n        png_path = os.path.join(png_dir, png_filename)\n        \n        try:\n            dicom_data = pydicom.dcmread(dicom_path)\n            \n            # Kiểm tra xem pixel_array có sẵn không\n            if 'PixelData' in dicom_data:\n                # Chuyển đổi dữ liệu pixel sang mảng numpy\n                pixel_array = dicom_data.pixel_array\n                \n                # Chuẩn hóa giá trị pixel về 0-255 (nếu cần) và chuyển sang uint8\n                # Một số hình ảnh DICOM có thể có độ sâu bit cao hơn\n                if pixel_array.dtype != np.uint8:\n                    pixel_array = (pixel_array - pixel_array.min()) / (pixel_array.max() - pixel_array.min())\n                    pixel_array = (pixel_array * 255).astype(np.uint8)\n                \n                # Nếu là ảnh grayscale 2D, chuyển thành RGB để tương thích với các mô hình CNN\n                if len(pixel_array.shape) == 2:\n                    image = Image.fromarray(pixel_array).convert('RGB')\n                elif len(pixel_array.shape) == 3 and pixel_array.shape[2] == 1: # Grayscale với 3rd dim\n                     image = Image.fromarray(pixel_array[:,:,0]).convert('RGB')\n                else: # Đã là RGB hoặc có nhiều kênh\n                    image = Image.fromarray(pixel_array)\n\n                image.save(png_path)\n            else:\n                print(f\"Cảnh báo: File '{dicom_file}' không chứa PixelData.\")\n        except Exception as e:\n            print(f\"Lỗi khi xử lý file '{dicom_file}': {e}\")\n\n# Chạy chuyển đổi\ndicom_to_png(ABNORMAL_DICOM_PATH, ABNORMAL_PNG_PATH)\ndicom_to_png(NON_ABNORMAL_DICOM_PATH, NON_ABNORMAL_PNG_PATH)\n\nprint(\"\\nĐã hoàn tất chuyển đổi DICOM sang PNG.\")\nprint(f\"Ảnh abnormal đã chuyển đổi được lưu tại: {ABNORMAL_PNG_PATH}\")\nprint(f\"Ảnh non_abnormal đã chuyển đổi được lưu tại: {NON_ABNORMAL_PNG_PATH}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-08T12:41:29.134855Z","iopub.execute_input":"2025-07-08T12:41:29.135311Z","iopub.status.idle":"2025-07-08T13:20:54.190435Z","shell.execute_reply.started":"2025-07-08T12:41:29.135276Z","shell.execute_reply":"2025-07-08T13:20:54.189682Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Newest Version","metadata":{}},{"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 # For nice progress bars in Kaggle notebooks\n\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\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\nfrom scipy.special import softmax # For converting logits to probabilities\nfrom sklearn.metrics.pairwise import cosine_similarity # For similarity-based confidence adjustment\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\n\n# Removed LIME and SHAP imports as requested.\n# import lime\n# import lime.lime_image\n# import shap\n\n# --- 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/working/' # Giữ nguyên hoặc điều chỉnh nếu bạn muốn một thư mục gốc khác\n        self.ABNORMAL_DIR = os.path.join(self.DATA_DIR, '/kaggle/working/abnormal_png') # Đã chuyển đổi\n        self.NON_ABNORMAL_DIR = os.path.join(self.DATA_DIR, '/kaggle/working/non_abnormal_png') # Đã chuyển đổi\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 = 32\n        self.NUM_EPOCHS_PER_MODEL = 2 # Tăng số epoch để huấn luyện tốt hơn\n        self.LEARNING_RATE = 1e-4\n        self.NUM_ENSEMBLE_MODELS = 2 # Số lượng mô hình trong ensemble (Thay đổi từ 1 thành 3 để dynamic gating có ý nghĩa)\n\n        # Cấu hình Monte Carlo Dropout (MCDO)\n        self.MCDO_ENABLE = False      # Bật/tắt Monte Carlo Dropout\n        self.MCDO_DROPOUT_RATE = 0.3   # Tỷ lệ dropout cho MCDO\n        self.MCDO_NUM_RUNS = 10        # Số lần chạy forward pass cho MCDO để ước tính độ bất định\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        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        # 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)\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\n        # Bật/tắt Weighted Logits theo Độ tin cậy (ECE)\n        self.USE_WEIGHTED_ENSEMBLE = True \n        \n        # Bật/tắt Dynamic Ensemble Selection (chỉ chọn model tốt nhất cho từng mẫu)\n        # Lưu ý: Đặt NUM_ENSEMBLE_MODELS = 3 để logic chọn 2/3 hoạt động đúng\n        self.USE_DYNAMIC_SELECTION = True\n        self.DYNAMIC_SELECTION_COUNT = 2 # Chọn 2 model tốt nhất\n\n        # Hằng số nhỏ để tránh chia cho 0 khi tính trọng số từ ECE\n        self.EPSILON_ECE = 1e-8\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\n        # Temperature Scaling\n        self.ENABLE_TEMPERATURE_SCALING = True\n        self.TEMPERATURE_CALIBRATION_SPLIT = 0.1 # Tỷ lệ dữ liệu test được dùng để hiệu chỉnh nhiệt độ\n        self.CALIBRATION_EPOCHS = 20 # Số epoch cho huấn luyện hiệu chỉnh nhiệt độ\n        self.CALIBRATION_LR = 0.01 # Tốc độ học cho hiệu chỉnh nhiệt độ\n\n        # Training Dynamics Analysis\n        self.ENABLE_TRAINING_DYNAMICS = True # Bật/tắt phân tích động lực học huấn luyện\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\n# --- 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 Abnormal\n    abnormal_paths = [os.path.join(cfg.ABNORMAL_DIR, f) for f in os.listdir(cfg.ABNORMAL_DIR) if f.endswith('.png') or f.endswith('.jpg')]\n    all_image_paths_raw.extend(abnormal_paths)\n    all_labels_raw.extend([1] * len(abnormal_paths)) # 1 cho 'có bệnh'\n    \n    # Thu thập hình ảnh Non-Abnormal\n    non_abnormal_paths = [os.path.join(cfg.NON_ABNORMAL_DIR, f) for f in os.listdir(cfg.NON_ABNORMAL_DIR) if f.endswith('.png') or f.endswith('.jpg')]\n    all_image_paths_raw.extend(non_abnormal_paths)\n    all_labels_raw.extend([0] * len(non_abnormal_paths)) # 0 cho 'không bệnh'\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(abnormal_paths)}, Hình ảnh Non-COVID: {len(non_abnormal_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\n\n# --- 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        # Chèn các lớp Dropout cho MCDO. Đối với ResNet, thường tốt sau các khối conv hoặc trước FC.\n        # Ở đây, thêm sau avgpool trước FC.\n        feature_extractor_layers = list(self.model.children())[:-1]\n        \n        # Thêm lớp dropout nếu dropout_rate dương và MCDO được bật\n        if self.dropout_rate > 0:\n            # Tìm chỉ mục của lớp AdaptiveAvgPool2d để chèn dropout sau nó\n            # Đây là một vị trí phổ biến để chèn dropout trước lớp tuyến tính cuối cùng\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# --- 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):\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    criterion = nn.CrossEntropyLoss()\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        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 i in range(len(history)):\n            if history[i]['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[i:]):\n                    first_correct_epoch = history[i]['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\n\n# --- 4. Thực thi huấn luyện Ensemble ---\ndef train_ensemble(cfg, train_loader, val_loader, train_dataset):\n    \"\"\"\n    Điều phối việc huấn luyện nhiều bộ phân loại cơ bản cho ensemble.\n    Thu thập và tổng hợp động lực huấn luyện chi tiết từ mỗi mô hình.\n    \"\"\"\n    # Khởi tạo một từ điển để lưu trữ động lực huấn luyện tổng thể cho mỗi mẫu\n    # Giá trị sẽ là danh sách các metrics từ giai đoạn huấn luyện của mỗi mô hình ensemble\n    overall_sample_learning_metrics = {global_idx: {'avg_correct_confidence': [], 'first_correct_epoch': [], 'consistency': []}\n                                       for global_idx in train_dataset.global_indices}\n\n\n    for i in range(cfg.NUM_ENSEMBLE_MODELS):\n        set_seed(cfg.RANDOM_SEED + i) # Sử dụng các seed khác nhau để đa dạng hóa ensemble\n        # Truyền dropout_rate từ cấu hình nếu MCDO được bật\n        model = BaseClassifier(num_classes=2, dropout_rate=(cfg.MCDO_DROPOUT_RATE if cfg.MCDO_ENABLE else 0.0))\n        # Huấn luyện mô hình và lấy các learning metrics riêng lẻ của nó\n        single_model_learning_metrics = train_model(\n            model, train_loader, val_loader, cfg.NUM_EPOCHS_PER_MODEL, cfg.LEARNING_RATE, cfg.DEVICE, i\n        )\n        # Tổng hợp các learning metrics qua các mô hình ensemble\n        for global_idx, metrics in single_model_learning_metrics.items():\n            overall_sample_learning_metrics[global_idx]['avg_correct_confidence'].append(metrics['avg_correct_confidence'])\n            overall_sample_learning_metrics[global_idx]['first_correct_epoch'].append(metrics['first_correct_epoch'])\n            overall_sample_learning_metrics[global_idx]['consistency'].append(metrics['consistency'])\n\n    # Xử lý các metrics tổng hợp để có được 'overall difficulty' cuối cùng cho mỗi mẫu huấn luyện\n    final_overall_learning_metrics = {}\n    for global_idx, metrics_lists in overall_sample_learning_metrics.items():\n        final_overall_learning_metrics[global_idx] = {\n            'mean_avg_correct_confidence': np.mean(metrics_lists['avg_correct_confidence']) if metrics_lists['avg_correct_confidence'] else 0.0,\n            'mean_first_correct_epoch': np.mean(metrics_lists['first_correct_epoch']) if metrics_lists['first_correct_epoch'] else cfg.NUM_EPOCHS_PER_MODEL, # Nếu không bao giờ học được, sử dụng epoch tối đa\n            'mean_consistency': np.mean(metrics_lists['consistency']) if metrics_lists['consistency'] else 0.0\n        }\n    \n    print(\"\\n--- Huấn luyện Ensemble Hoàn tất ---\")\n    return final_overall_learning_metrics\n\n\n# --- 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\n\ndef 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, test_confidences,\n                                             train_features, train_global_indices, final_overall_learning_metrics):\n    \"\"\"\n    Điều chỉnh độ tin cậy 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    Độ tin cậy 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 độ tin cậy của tập test với động lực huấn luyện...\")\n    adjusted_confidences = np.copy(test_confidences)\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_confidences\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\n        # Giảm độ tin cậy 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 độ tin cậy lớn hơn\n        adjustment_factor = 1.0 - (cfg.TRAINING_DYNAMICS_CONF_PENALTY * normalized_difficulty)\n        \n        adjusted_confidences[i] *= adjustment_factor\n        adjusted_confidences[i] = max(0.0, adjusted_confidences[i]) # Đảm bảo độ tin cậy không âm\n\n    return adjusted_confidences\n\n\ndef get_confidence_and_predictions(cfg, data_loader, ensemble_models_dir, final_overall_learning_metrics=None, train_dataset=None, train_features=None, train_indices=None):\n    \"\"\"\n    Tính toán điểm độ tin cậy và dự đoán của ensemble cho các mẫu, tích hợp Monte Carlo Dropout (MCDO).\n    Áp dụng hiệu chỉnh nhiệt độ và kết hợp sự bất đồng của ensemble (bao gồm MCDO).\n    Tùy chọn áp dụng điều chỉnh dựa trên động lực huấn luyện nếu `final_overall_learning_metrics` và `train_dataset` được cung cấp.\n\n    Trả về:\n    - all_predictions: Các dự đoán ensemble cuối cùng\n    - all_confidences: Điểm độ tin cậy đã điều chỉnh cuối cùng\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_predictions: Danh sách các danh sách, mỗi danh sách con chứa các dự đoán cho một mô hình ensemble (đã làm phẳng).\n    - all_ensemble_individual_probs_stacked: Mảng NumPy (num_samples, total_runs_per_sample, num_classes) của các xác suất mô hình riêng lẻ từ tất cả các chạy (ensemble + MCDO).\n    \"\"\"\n    all_predictions = []\n    all_confidences = []\n    all_labels = []\n    all_original_indices = []\n    \n    # Lưu trữ các dự đoán mô hình ensemble riêng lẻ (danh sách phẳng cho mỗi mô hình)\n    # Lưu ý: Nếu MCDO bật, đây chỉ là dự đoán trung bình của MCDO cho mỗi mô hình ensemble\n    all_ensemble_individual_predictions = [[] for _ in range(cfg.NUM_ENSEMBLE_MODELS)]\n    \n    # Lưu trữ TẤT CẢ các xác suất từ MỌI chạy (ensemble models x MCDO runs) cho mỗi mẫu\n    # Cấu trúc: list of lists, where each inner list contains batch_size x (num_classes) arrays\n    raw_probs_from_all_runs_per_batch = [] \n\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        # Đảm bảo dropout_rate được truyền trong constructor để kích hoạt MCDO\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        # Đặt lại chế độ eval sau khi load.\n        # Đối với MCDO, chúng ta sẽ đặt lại thành train() trong vòng lặp dự đoán.\n        model.eval() \n        loaded_models.append(model)\n\n    temp_scaler = TemperatureScaler()\n    temp_scaler.to(cfg.DEVICE)\n\n    print(\"Hiệu chỉnh bộ hiệu chỉnh nhiệt độ trên các logits trung bình của ensemble từ trình tải dữ liệu hiện tại...\")\n    # --- Thu thập tất cả các logits trung bình của ensemble từ data_loader hiện tại để hiệu chỉnh ---\n    data_loader_ensemble_avg_logits = []\n    data_loader_labels_for_calibration = []\n    with torch.no_grad():\n        for inputs_batch_cal, labels_batch_cal, _ in tqdm(data_loader, desc=\"Thu thập Logits để hiệu chỉnh\"):\n            inputs_batch_cal = inputs_batch_cal.to(cfg.DEVICE)\n            \n            ensemble_logits_batch_cal_runs = [] # Logits cho batch hiện tại từ tất cả các chạy ensemble/MCDO\n            \n            for model in loaded_models:\n                if cfg.MCDO_ENABLE:\n                    model.train() # Kích hoạt dropout cho MCDO\n                    mcd_logits_for_current_model = []\n                    for _ in range(cfg.MCDO_NUM_RUNS):\n                        mcd_logits_for_current_model.append(model(inputs_batch_cal))\n                    ensemble_logits_batch_cal_runs.append(torch.mean(torch.stack(mcd_logits_for_current_model), dim=0))\n                    model.eval() # Tắt dropout sau các chạy MCDO cho mô hình này\n                else:\n                    ensemble_logits_batch_cal_runs.append(model(inputs_batch_cal))\n\n            # This is still averaging all models for calibration. This is correct as calibration happens\n            # on the overall ensemble output.\n            avg_logits_batch_cal = torch.stack(ensemble_logits_batch_cal_runs).mean(dim=0) # Trung bình hóa ensemble\n            \n            data_loader_ensemble_avg_logits.append(avg_logits_batch_cal.cpu())\n            data_loader_labels_for_calibration.append(labels_batch_cal.cpu())\n    \n    if data_loader_ensemble_avg_logits: # Kiểm tra xem danh sách có trống không\n        data_loader_ensemble_avg_logits_all = torch.cat(data_loader_ensemble_avg_logits).to(cfg.DEVICE)\n        data_loader_labels_for_calibration_all = torch.cat(data_loader_labels_for_calibration).to(cfg.DEVICE)\n        # Hiệu chỉnh bộ hiệu chỉnh nhiệt độ bằng cách sử dụng các logits trung bình của ensemble\n        temp_scaler.calibrate(data_loader_ensemble_avg_logits_all, data_loader_labels_for_calibration_all, cfg.DEVICE)\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 nhiệt độ.\")\n\n\n    # --- Trích xuất đặc trưng từ data_loader hiện tại (tập val/test) *một lần* ---\n    # `all_extracted_features` cần được định nghĩa ở đây cho phạm vi của hàm này.\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    # Sử dụng mô hình đầu tiên để trích xuất đặc trưng, đặt nó về eval() để dropout không ảnh hưởng đến đặc trưng\n    loaded_models[0].eval() \n    all_extracted_features, extracted_original_indices = extract_features(\n        loaded_models[0], data_loader, cfg.DEVICE \n    )\n\n    print(\"Tạo dự đoán và độ tin cậy...\")\n    with torch.no_grad(): # Tắt grad tổng thể, nhưng bật lại cho MCDO models.train()\n        for inputs, labels, original_indices_batch in tqdm(data_loader, desc=\"Dự đoán\"):\n            inputs, labels = inputs.to(cfg.DEVICE), labels.to(cfg.DEVICE)\n            \n            # This will store logits for each model for the current batch\n            # Shape: (num_ensemble_models, batch_size, num_classes)\n            individual_model_logits_for_batch = [] \n            \n            # This will store probabilities from all individual runs (models * MCDO runs)\n            current_batch_all_probs_runs = []\n\n            for model_idx, model in enumerate(loaded_models):\n                if cfg.MCDO_ENABLE:\n                    model.train() # Kích hoạt dropout cho MCDO\n                    mcd_logits_for_current_model = []\n                    for _ in range(cfg.MCDO_NUM_RUNS):\n                        logits_run = model(inputs)\n                        mcd_logits_for_current_model.append(logits_run)\n                        # Store raw probabilities for disagreement calculation later\n                        current_batch_all_probs_runs.append(softmax(logits_run.detach().cpu().numpy(), axis=1)) \n                    # Average MCDO logits for this specific model\n                    avg_mcd_logits = torch.mean(torch.stack(mcd_logits_for_current_model), dim=0)\n                    individual_model_logits_for_batch.append(avg_mcd_logits)\n                    # For individual prediction tracking, use the averaged MCDO probs\n                    avg_mcd_probs = softmax(avg_mcd_logits.detach().cpu().numpy(), axis=1)\n                    all_ensemble_individual_predictions[model_idx].extend(np.argmax(avg_mcd_probs, axis=1))\n                    model.eval() # Tắt dropout sau các chạy MCDO cho mô hình này\n                else:\n                    logits = model(inputs)\n                    individual_model_logits_for_batch.append(logits)\n                    current_batch_all_probs_runs.append(softmax(logits.detach().cpu().numpy(), axis=1))\n                    all_ensemble_individual_predictions[model_idx].extend(np.argmax(softmax(logits.detach().cpu().numpy(), axis=1), axis=1))\n\n            # Stack individual model logits to (batch_size, num_ensemble_models, num_classes)\n            individual_model_logits_for_batch_stacked = torch.stack(individual_model_logits_for_batch, dim=1)\n            \n            # Prepare a tensor to store the gated ensemble logits for the current batch\n            gated_ensemble_logits_for_batch = torch.zeros_like(individual_model_logits_for_batch_stacked[:, 0, :])\n\n            # Apply dynamic selection per sample in the batch\n            if cfg.USE_DYNAMIC_SELECTION and cfg.NUM_ENSEMBLE_MODELS > 1 and cfg.DYNAMIC_SELECTION_COUNT < cfg.NUM_ENSEMBLE_MODELS:\n                for sample_idx in range(inputs.size(0)):\n                    # Get logits for all models for the current sample\n                    sample_individual_logits = individual_model_logits_for_batch_stacked[sample_idx, :, :] # (NUM_ENSEMBLE_MODELS, num_classes)\n                    \n                    # Convert logits to probabilities to determine confidence for selection\n                    sample_individual_probs = softmax(sample_individual_logits.detach().cpu().numpy(), axis=1)\n                    \n                    # Get the confidence for the predicted class for each model\n                    # This assumes we want to select models that are most confident in their *own* prediction\n                    model_confidences = np.max(sample_individual_probs, axis=1)\n                    \n                    # Sort models by confidence in descending order and get their original indices\n                    sorted_model_indices = np.argsort(model_confidences)[::-1]\n                    \n                    # Select the top DYNAMIC_SELECTION_COUNT models\n                    selected_model_indices = sorted_model_indices[:cfg.DYNAMIC_SELECTION_COUNT]\n                    \n                    # Gather logits from selected models for this sample\n                    selected_logits = sample_individual_logits[selected_model_indices, :]\n                    \n                    # Average the logits from the selected models\n                    gated_ensemble_logits_for_batch[sample_idx, :] = torch.mean(selected_logits, dim=0)\n            else:\n                # If dynamic selection is off or not applicable, average all models (original behavior)\n                gated_ensemble_logits_for_batch = individual_model_logits_for_batch_stacked.mean(dim=1)\n\n            # Nối các xác suất từ tất cả các chạy vào danh sách tổng thể\n            if current_batch_all_probs_runs:\n                # Each element in current_batch_all_probs_runs is a (batch_size, num_classes) array\n                num_total_runs_per_sample = cfg.NUM_ENSEMBLE_MODELS * (cfg.MCDO_NUM_RUNS if cfg.MCDO_ENABLE else 1)\n                \n                # Stack them: result is (num_total_runs_per_sample, batch_size, num_classes)\n                stacked_for_disagreement_temp = np.stack(current_batch_all_probs_runs, axis=0)\n                # Transpose to (batch_size, num_total_runs_per_sample, num_classes)\n                stacked_for_disagreement = np.transpose(stacked_for_disagreement_temp, (1, 0, 2))\n                raw_probs_from_all_runs_per_batch.append(stacked_for_disagreement)\n            \n            # Use the gated_ensemble_logits_for_batch for temperature scaling\n            calibrated_logits_ensemble = temp_scaler.forward(gated_ensemble_logits_for_batch) \n            \n            # Chuyển đổi logits đã hiệu chỉnh thành xác suất và dự đoán\n            calibrated_probs_ensemble = softmax(calibrated_logits_ensemble.detach().cpu().numpy(), axis=1)\n            predicted_labels_ensemble = np.argmax(calibrated_probs_ensemble, axis=1)\n            confidences_ensemble = np.max(calibrated_probs_ensemble, axis=1)\n\n            all_predictions.extend(predicted_labels_ensemble)\n            all_confidences.extend(confidences_ensemble)\n            all_labels.extend(labels.cpu().numpy())\n            all_original_indices.extend(original_indices_batch.cpu().numpy())\n\n    all_predictions = np.array(all_predictions)\n    all_confidences = np.array(all_confidences)\n    all_labels = np.array(all_labels)\n    all_original_indices = np.array(all_original_indices)\n    \n    # Nối tất cả các mảng xác suất thô từ các chạy lại với nhau\n    all_ensemble_individual_probs_stacked = np.concatenate(raw_probs_from_all_runs_per_batch, axis=0)\n\n    # Áp dụng điều chỉnh động lực huấn luyện nếu được bật và dữ liệu được cung cấp\n    if cfg.ENABLE_TRAINING_DYNAMICS and final_overall_learning_metrics is not None:\n        # train_features và train_indices được truyền trực tiếp vào hàm này\n        if train_features is not None and train_indices is not None:\n            all_confidences = adjust_confidence_with_training_dynamics(\n                cfg, all_extracted_features, all_confidences,\n                train_features, train_indices, final_overall_learning_metrics\n            )\n        else:\n            print(\"Cảnh báo: Không có đặc trưng huấn luyện hoặc chỉ mục được cung cấp cho điều chỉnh động lực huấn luyện.\")\n\n\n    # Tính toán sự bất đồng của ensemble (Ensemble Disagreement)\n    # Đây là độ lệch chuẩn của các xác suất dự đoán trên tất cả các mô hình ensemble và các chạy MCDO.\n    # Một độ lệch chuẩn cao hơn cho thấy sự bất đồng lớn hơn.\n    # all_ensemble_individual_probs_stacked: (num_samples, total_runs_per_sample, num_classes)\n    # Lấy xác suất của lớp được dự đoán bởi ensemble cuối cùng\n    # Lấy index của lớp được dự đoán bởi ensemble cuối cùng\n    predicted_classes_indices = all_predictions\n    \n    # Tạo một mảng để lưu trữ xác suất của lớp được dự đoán bởi ensemble cho mỗi chạy riêng lẻ\n    probs_of_predicted_class_per_run = np.zeros((all_ensemble_individual_probs_stacked.shape[0], all_ensemble_individual_probs_stacked.shape[1]))\n    \n    for i in range(all_ensemble_individual_probs_stacked.shape[0]): # Duyệt qua từng mẫu\n        for j in range(all_ensemble_individual_probs_stacked.shape[1]): # Duyệt qua từng chạy (mô hình + MCDO)\n            # Lấy xác suất của lớp được dự đoán bởi ensemble cuối cùng cho chạy này\n            probs_of_predicted_class_per_run[i, j] = all_ensemble_individual_probs_stacked[i, j, predicted_classes_indices[i]]\n\n    # Tính độ lệch chuẩn của xác suất của lớp được dự đoán trên các chạy\n    disagreement_scores = np.std(probs_of_predicted_class_per_run, axis=1)\n\n    # Áp dụng hình phạt bất đồng: Độ bất đồng cao hơn -> độ tin cậy thấp hơn\n    # Sử dụng một hàm giảm dần, ví dụ: 1 / (1 + factor * disagreement)\n    disagreement_penalty = 1.0 / (1.0 + cfg.DISAGREEMENT_PENALTY_FACTOR * disagreement_scores)\n    all_confidences *= disagreement_penalty\n    all_confidences = np.clip(all_confidences, 0.0, 1.0) # Đảm bảo độ tin cậy nằm trong [0, 1]\n\n    return all_predictions, all_confidences, all_labels, all_original_indices, all_ensemble_individual_predictions, all_ensemble_individual_probs_stacked\n\n\n# --- 6. Các hàm đánh giá hiệu suất ---\ndef calculate_ece(confidences, predictions, true_labels, num_bins=10):\n    \"\"\"\n    Tính toán Expected Calibration Error (ECE).\n    Chia các dự đoán thành các bin dựa trên độ tin cậy, sau đó tính toán\n    độ chính xác và độ tin cậy trung bình cho mỗi bin.\n    \"\"\"\n    bin_boundaries = np.linspace(0, 1, num_bins + 1)\n    ece = 0.0\n    total_samples = len(confidences)\n\n    if total_samples == 0:\n        return 0.0\n\n    for i in range(num_bins):\n        lower_bound = bin_boundaries[i]\n        upper_bound = bin_boundaries[i+1]\n        \n        # Tìm các mẫu trong bin hiện tại\n        in_bin = (confidences > lower_bound) & (confidences <= upper_bound)\n        \n        if np.any(in_bin):\n            bin_confidences = confidences[in_bin]\n            bin_predictions = predictions[in_bin]\n            bin_true_labels = true_labels[in_bin]\n            \n            # Tính độ chính xác và độ tin cậy trung bình cho bin\n            accuracy_in_bin = np.mean(bin_predictions == bin_true_labels)\n            avg_confidence_in_bin = np.mean(bin_confidences)\n            \n            # Trọng số của bin (tỷ lệ mẫu trong bin)\n            bin_weight = len(bin_confidences) / total_samples\n            \n            ece += bin_weight * np.abs(accuracy_in_bin - avg_confidence_in_bin)\n            \n    return ece\n\n\ndef plot_calibration_curve(predictions, confidences, true_labels, num_bins=10, save_path=None):\n    \"\"\"\n    Vẽ biểu đồ đường cong hiệu chỉnh (reliability diagram).\n    \"\"\"\n    bin_boundaries = np.linspace(0, 1, num_bins + 1)\n    accuracies = []\n    avg_confidences = []\n\n    for i in range(num_bins):\n        lower_bound = bin_boundaries[i]\n        upper_bound = bin_boundaries[i+1]\n        \n        in_bin = (confidences > lower_bound) & (confidences <= upper_bound)\n        \n        if np.any(in_bin):\n            bin_predictions = predictions[in_bin]\n            bin_true_labels = true_labels[in_bin]\n            bin_confidences = confidences[in_bin]\n\n            accuracies.append(np.mean(bin_predictions == bin_true_labels))\n            avg_confidences.append(np.mean(bin_confidences))\n        else:\n            accuracies.append(np.nan) # Sử dụng NaN để không vẽ điểm nếu bin trống\n            avg_confidences.append(np.nan)\n\n    plt.figure(figsize=(8, 6))\n    plt.plot([0, 1], [0, 1], linestyle='--', color='gray', label='Perfectly Calibrated')\n    plt.plot(avg_confidences, accuracies, marker='o', linestyle='-', color='blue', label='Model')\n    plt.xlabel('Average Confidence (Bin)')\n    plt.ylabel('Accuracy (Bin)')\n    plt.title('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    plt.show()\n\n\ndef plot_roc_curve(predictions, confidences, true_labels, save_path=None):\n    \"\"\"\n    Vẽ biểu đồ đường cong ROC và tính toán AUC.\n    \"\"\"\n    fpr, tpr, _ = roc_curve(true_labels, confidences)\n    roc_auc = auc(fpr, tpr)\n\n    plt.figure(figsize=(8, 6))\n    plt.plot(fpr, tpr, color='darkorange', lw=2, label=f'ROC curve (area = {roc_auc:.2f})')\n    plt.plot([0, 1], [0, 1], color='navy', lw=2, linestyle='--')\n    plt.xlim([0.0, 1.0])\n    plt.ylim([0.0, 1.05])\n    plt.xlabel('False Positive Rate')\n    plt.ylabel('True Positive Rate')\n    plt.title('Receiver Operating Characteristic (ROC) Curve')\n    plt.legend(loc=\"lower right\")\n    plt.grid(True)\n    if save_path:\n        plt.savefig(save_path)\n    plt.show()\n\n\ndef plot_precision_recall_curve(predictions, confidences, true_labels, save_path=None):\n    \"\"\"\n    Vẽ biểu đồ đường cong Precision-Recall.\n    \"\"\"\n    precision, recall, _ = precision_recall_curve(true_labels, confidences)\n\n    plt.figure(figsize=(8, 6))\n    plt.plot(recall, precision, color='b', alpha=0.7, label='Precision-Recall curve')\n    plt.xlabel('Recall')\n    plt.ylabel('Precision')\n    plt.title('Precision-Recall Curve')\n    plt.grid(True)\n    plt.legend(loc=\"lower left\")\n    plt.xlim([0.0, 1.0])\n    plt.ylim([0.0, 1.05])\n    if save_path:\n        plt.savefig(save_path)\n    plt.show()\n\n\ndef plot_confusion_matrix(predictions, true_labels, class_names=['Non-Abnormal', 'Abnormal'], save_path=None):\n    \"\"\"\n    Vẽ ma trận nhầm lẫn.\n    \"\"\"\n    cm = confusion_matrix(true_labels, predictions)\n    plt.figure(figsize=(8, 6))\n    plt.imshow(cm, interpolation='nearest', cmap=plt.cm.Blues)\n    plt.title('Confusion Matrix')\n    plt.colorbar()\n    tick_marks = np.arange(len(class_names))\n    plt.xticks(tick_marks, class_names, rotation=45)\n    plt.yticks(tick_marks, class_names)\n\n    fmt = 'd'\n    thresh = cm.max() / 2.\n    for i in range(cm.shape[0]):\n        for j in range(cm.shape[1]):\n            plt.text(j, i, format(cm[i, j], fmt),\n                     ha=\"center\", va=\"center\",\n                     color=\"white\" if cm[i, j] > thresh else \"black\")\n    plt.ylabel('True label')\n    plt.xlabel('Predicted label')\n    plt.tight_layout()\n    if save_path:\n        plt.savefig(save_path)\n    plt.show()\n\n\n# --- 7. Phân loại có chọn lọc và XAI ---\ndef optimize_rejection_threshold(cfg, confidences, true_labels):\n    \"\"\"\n    Tìm ngưỡng từ chối tối ưu dựa trên mục tiêu độ chính xác và tỷ lệ từ chối.\n    Sử dụng tìm kiếm lưới để đánh giá các ngưỡng khác nhau và chọn ngưỡng tốt nhất\n    dựa trên hàm mục tiêu kết hợp.\n    \"\"\"\n    best_threshold = 0.0\n    min_objective_value = float('inf')\n    \n    # Các ngưỡng tiềm năng để thử\n    threshold_candidates = np.linspace(0.0, 1.0, 100) # 100 điểm từ 0 đến 1\n\n    results = []\n\n    for threshold in tqdm(threshold_candidates, desc=\"Tối ưu hóa ngưỡng từ chối\"):\n        accepted_mask = confidences >= threshold\n        rejected_mask = confidences < threshold\n\n        num_accepted = np.sum(accepted_mask)\n        num_rejected = np.sum(rejected_mask)\n        \n        rejection_rate = num_rejected / len(confidences) if len(confidences) > 0 else 0.0\n\n        accepted_accuracy = 0.0\n        ece_accepted = 0.0\n\n        if num_accepted > 0:\n            accepted_confidences = confidences[accepted_mask]\n            accepted_predictions = (confidences[accepted_mask] >= 0.5).astype(int) # Dự đoán dựa trên độ tin cậy\n            accepted_true_labels = true_labels[accepted_mask]\n            \n            accepted_accuracy = accuracy_score(accepted_true_labels, accepted_predictions)\n            ece_accepted = calculate_ece(accepted_confidences, accepted_predictions, accepted_true_labels)\n        \n        # Tính toán độ lệch so với mục tiêu\n        accuracy_deviation = np.abs(accepted_accuracy - cfg.TARGET_ACCEPTED_ACCURACY)\n        rejection_rate_deviation = np.abs(rejection_rate - cfg.TARGET_REJECTION_RATE)\n        \n        # Hàm mục tiêu: Giảm thiểu độ lệch, với trọng số cho mỗi thành phần\n        # ECE được thêm vào để khuyến khích hiệu chỉnh tốt trong tập chấp nhận\n        objective_value = (cfg.ACCURACY_DEVIATION_WEIGHT * accuracy_deviation +\n                          cfg.REJECTION_RATE_DEVIATION_WEIGHT * rejection_rate_deviation +\n                          cfg.ECE_DEVIATION_WEIGHT * ece_accepted)\n        \n        results.append({\n            'threshold': threshold,\n            'accepted_accuracy': accepted_accuracy,\n            'rejection_rate': rejection_rate,\n            'ece_accepted': ece_accepted,\n            'objective_value': objective_value\n        })\n\n        if objective_value < min_objective_value:\n            min_objective_value = objective_value\n            best_threshold = threshold\n            \n    print(f\"Ngưỡng từ chối tối ưu: {best_threshold:.4f}\")\n    best_result = [r for r in results if r['threshold'] == best_threshold][0]\n    print(f\"  Độ chính xác được chấp nhận: {best_result['accepted_accuracy']:.4f}\")\n    print(f\"  Tỷ lệ từ chối: {best_result['rejection_rate']:.4f}\")\n    print(f\"  ECE trên tập được chấp nhận: {best_result['ece_accepted']:.4f}\")\n    print(f\"  Giá trị mục tiêu: {best_result['objective_value']:.4f}\")\n\n    return best_threshold, results\n\n\ndef visualize_xai_examples(cfg, models, dataset, test_results, rejection_threshold, num_examples=5):\n    \"\"\"\n    Trực quan hóa các ví dụ được chấp nhận và từ chối cùng với Grad-CAM.\n    \"\"\"\n    print(\"\\n--- Trực quan hóa các ví dụ XAI ---\")\n    \n    model_preds = test_results['model_preds']\n    confidences = test_results['confidences']\n    true_labels = test_results['true_labels']\n    original_indices = test_results['original_indices']\n    rejected_categories = test_results['rejected_categories']\n\n    # Lấy các chỉ mục của các mẫu được chấp nhận/từ chối\n    accepted_mask = confidences >= rejection_threshold\n    rejected_mask = confidences < rejection_threshold\n\n    accepted_indices_in_test_results = np.where(accepted_mask)[0]\n    rejected_indices_in_test_results = np.where(rejected_mask)[0]\n\n    # Chọn ngẫu nhiên các ví dụ từ mỗi loại\n    selected_accepted_indices = np.random.choice(accepted_indices_in_test_results, min(num_examples, len(accepted_indices_in_test_results)), replace=False)\n    selected_rejected_indices = np.random.choice(rejected_indices_in_test_results, min(num_examples, len(rejected_indices_in_test_results)), replace=False)\n\n    # Đảm bảo có ít nhất một mô hình để sử dụng Grad-CAM\n    if not models:\n        print(\"Không có mô hình nào được cung cấp để tạo Grad-CAM. Bỏ qua trực quan hóa Grad-CAM.\")\n        return\n\n    # Sử dụng mô hình đầu tiên trong ensemble cho Grad-CAM\n    # Đảm bảo mô hình ở chế độ eval() cho Grad-CAM\n    grad_cam_model = models[0]\n    grad_cam_model.eval() \n    target_layer = grad_cam_model.target_layer\n    # Đã sửa lỗi: Loại bỏ tham số use_cuda\n    cam = GradCAMPlusPlus(model=grad_cam_model, target_layers=[target_layer])\n\n    # Hàm trợ giúp để hiển thị hình ảnh và Grad-CAM\n    def show_image_with_cam(ax, image_tensor, heatmap, title):\n        # Chuyển đổi tensor hình ảnh trở lại định dạng hiển thị\n        img_np = image_tensor.permute(1, 2, 0).cpu().numpy()\n        # Chuẩn hóa lại hình ảnh nếu nó được chuẩn hóa\n        mean = np.array([0.485, 0.456, 0.406])\n        std = np.array([0.229, 0.224, 0.225])\n        img_np = std * img_np + mean\n        img_np = np.clip(img_np, 0, 1)\n\n        # Tạo hình ảnh với heatmap\n        cam_image = show_cam_on_image(img_np, heatmap, use_rgb=True)\n        ax.imshow(cam_image)\n        ax.set_title(title)\n        ax.axis('off')\n\n    # Trực quan hóa các ví dụ được chấp nhận\n    print(\"\\n--- Ví dụ được chấp nhận (Accept) ---\")\n    plt.figure(figsize=(num_examples * 3, 4))\n    for i, test_idx in enumerate(selected_accepted_indices):\n        original_global_idx = original_indices[test_idx]\n        # Tìm chỉ mục cục bộ trong test_dataset\n        local_idx_in_test_dataset = dataset.original_indices_map[original_global_idx]\n        \n        image_tensor, true_label, _ = dataset[local_idx_in_test_dataset]\n        \n        # Thêm chiều batch\n        input_tensor = image_tensor.unsqueeze(0).to(cfg.DEVICE)\n        \n        predicted_label = model_preds[test_idx]\n        confidence = confidences[test_idx]\n        \n        # Tạo heatmap Grad-CAM\n        targets = [ClassifierOutputTarget(predicted_label)] # Mục tiêu là lớp dự đoán\n        grayscale_cam = cam(input_tensor=input_tensor, targets=targets)\n        grayscale_cam = grayscale_cam[0, :] # Lấy heatmap đầu tiên\n\n        ax = plt.subplot(1, num_examples, i + 1)\n        title = (f\"True: {true_label}\\nPred: {predicted_label}\\n\"\n                 f\"Conf: {confidence:.2f}\")\n        show_image_with_cam(ax, image_tensor, grayscale_cam, title)\n    plt.tight_layout()\n    plt.savefig(os.path.join(cfg.XAI_SAVE_DIR, 'accepted_examples_cam.png'))\n    plt.show()\n\n    # Trực quan hóa các ví dụ bị từ chối\n    print(\"\\n--- Ví dụ bị từ chối (Reject) ---\")\n    plt.figure(figsize=(num_examples * 3, 4))\n    for i, test_idx in enumerate(selected_rejected_indices):\n        original_global_idx = original_indices[test_idx]\n        # Tìm chỉ mục cục bộ trong test_dataset\n        local_idx_in_test_dataset = dataset.original_indices_map[original_global_idx]\n\n        image_tensor, true_label, _ = dataset[local_idx_in_test_dataset]\n        \n        # Thêm chiều batch\n        input_tensor = image_tensor.unsqueeze(0).to(cfg.DEVICE)\n\n        predicted_label = model_preds[test_idx]\n        confidence = confidences[test_idx]\n        rejection_reason = \"Unknown\"\n        # Tìm lý do từ chối cụ thể nếu có\n        for reason, indices in rejected_categories.items():\n            if test_idx in indices:\n                rejection_reason = reason\n                break\n\n        # Tạo heatmap Grad-CAM\n        targets = [ClassifierOutputTarget(predicted_label)] # Mục tiêu là lớp dự đoán\n        grayscale_cam = cam(input_tensor=input_tensor, targets=targets)\n        grayscale_cam = grayscale_cam[0, :] # Lấy heatmap đầu tiên\n\n        ax = plt.subplot(1, num_examples, i + 1)\n        title = (f\"True: {true_label}\\nPred: {predicted_label}\\n\"\n                 f\"Conf: {confidence:.2f}\\nRejected: {rejection_reason}\")\n        show_image_with_cam(ax, image_tensor, grayscale_cam, title)\n    plt.tight_layout()\n    plt.savefig(os.path.join(cfg.XAI_SAVE_DIR, 'rejected_examples_cam.png'))\n    plt.show()\n\n\n# --- Hàm chính để chạy toàn bộ quy trình ---\ndef run_selective_classification_pipeline(cfg):\n    \"\"\"\n    Chạy toàn bộ quy trình phân loại có chọn lọc:\n    1. Chuẩn bị dữ liệu.\n    2. Huấn luyện ensemble các mô hình.\n    3. Ước tính độ tin cậy và dự đoán của ensemble với hiệu chỉnh nhiệt độ và MCDO.\n    4. Tối ưu hóa ngưỡng từ chối.\n    5. Đánh giá hiệu suất phân loại có chọn lọc.\n    6. Trực quan hóa các ví dụ XAI.\n    \"\"\"\n    print(\"--- Bắt đầu quy trình phân loại có chọn lọc ---\")\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 ensemble các mô hình\n    # Động lực huấn luyện được thu thập trong quá trình này\n    final_overall_learning_metrics = train_ensemble(cfg, train_loader, val_loader, train_dataset)\n\n    # 3. Ước tính độ tin cậy và dự đoán của ensemble\n    # Sử dụng test_loader để có được dự đoán trên tập test độc lập\n    print(\"\\n--- Đánh giá hiệu suất Ensemble trên tập Test ---\")\n    \n    # Tạo một DataLoader cho tập huấn luyện để trích xuất đặc trưng\n    train_features_loader = DataLoader(train_dataset, batch_size=cfg.BATCH_SIZE, shuffle=False, num_workers=2)\n\n    # Tải lại các mô hình để đảm bảo chúng ở đúng chế độ (eval) và có thể được sử dụng cho XAI và trích xuất đặc trưng\n    loaded_models_for_feature_extraction = []\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(cfg.MODEL_SAVE_DIR, f'best_model_ensemble_{i}.pth')))\n        model.eval() \n        loaded_models_for_feature_extraction.append(model)\n\n    # Trích xuất đặc trưng huấn luyện một lần ở đây\n    # Sử dụng mô hình đầu tiên để trích xuất đặc trưng\n    train_features_for_adjustment, train_indices_for_adjustment = extract_features(\n        loaded_models_for_feature_extraction[0], train_features_loader, cfg.DEVICE\n    )\n\n    # Truyền train_features_for_adjustment và train_indices_for_adjustment vào get_confidence_and_predictions\n    test_model_predictions, test_confidences, test_true_labels, test_original_indices, \\\n    test_ensemble_individual_predictions, test_ensemble_individual_probs_stacked = \\\n        get_confidence_and_predictions(cfg, test_loader, cfg.MODEL_SAVE_DIR, \n                                       final_overall_learning_metrics, \n                                       train_dataset, # Truyền train_dataset để có thể truy cập global_indices\n                                       train_features=train_features_for_adjustment, \n                                       train_indices=train_indices_for_adjustment)\n\n\n    # 4. Tối ưu hóa ngưỡng từ chối\n    print(\"\\n--- Tối ưu hóa ngưỡng từ chối ---\")\n    best_rejection_threshold, rejection_optimization_results = optimize_rejection_threshold(\n        cfg, test_confidences, test_true_labels\n    )\n\n    # 5. Đánh giá hiệu suất phân loại có chọn lọc\n    print(\"\\n--- Đánh giá hiệu suất phân loại có chọn lọc ---\")\n    accepted_mask = test_confidences >= best_rejection_threshold\n    rejected_mask = test_confidences < best_rejection_threshold\n\n    accepted_predictions = test_model_predictions[accepted_mask]\n    accepted_confidences = test_confidences[accepted_mask]\n    accepted_true_labels = test_true_labels[accepted_mask]\n\n    rejected_predictions = test_model_predictions[rejected_mask]\n    rejected_confidences = test_confidences[rejected_mask]\n    rejected_true_labels = test_true_labels[rejected_mask]\n    \n    num_total_samples = len(test_confidences)\n    num_accepted_samples = len(accepted_confidences)\n    num_rejected_samples = len(rejected_confidences)\n\n    print(f\"Tổng số mẫu: {num_total_samples}\")\n    print(f\"Số mẫu được chấp nhận: {num_accepted_samples} ({num_accepted_samples/num_total_samples:.2%})\")\n    print(f\"Số mẫu bị từ chối: {num_rejected_samples} ({num_rejected_samples/num_total_samples:.2%})\")\n\n    if num_accepted_samples > 0:\n        accepted_accuracy = accuracy_score(accepted_true_labels, accepted_predictions)\n        accepted_ece = calculate_ece(accepted_confidences, accepted_predictions, accepted_true_labels)\n        print(f\"Độ chính xác trên các mẫu được chấp nhận: {accepted_accuracy:.4f}\")\n        print(f\"ECE trên các mẫu được chấp nhận: {accepted_ece:.4f}\")\n        plot_confusion_matrix(accepted_predictions, accepted_true_labels, save_path=os.path.join(cfg.XAI_SAVE_DIR, 'confusion_matrix_accepted.png'))\n    else:\n        print(\"Không có mẫu nào được chấp nhận.\")\n\n    if num_rejected_samples > 0:\n        rejected_accuracy = accuracy_score(rejected_true_labels, rejected_predictions)\n        print(f\"Độ chính xác trên các mẫu bị từ chối (chỉ để tham khảo): {rejected_accuracy:.4f}\")\n        \n        # Phân loại các mẫu bị từ chối theo lý do (độ tin cậy thấp, bất đồng cao, OOD)\n        rejected_categories_info = {\n            'low_confidence': np.where((test_confidences < best_rejection_threshold) & (test_confidences < cfg.OOD_CONFIDENCE_THRESHOLD))[0],\n            'high_disagreement': np.where((test_confidences < best_rejection_threshold) & (test_confidences >= cfg.OOD_CONFIDENCE_THRESHOLD))[0], # Placeholder for now, need actual disagreement metric\n            # OOD (Out-of-Distribution) detection heuristic\n            'ood_potential': np.where((test_confidences < cfg.OOD_CONFIDENCE_THRESHOLD) & \n                                      (np.std(test_ensemble_individual_probs_stacked[:, :, 1], axis=1) > cfg.OOD_VARIANCE_THRESHOLD))[0] # Variance of abnormal class probability\n        }\n        test_results_for_xai = {\n            'model_preds': test_model_predictions,\n            'confidences': test_confidences,\n            'true_labels': test_true_labels,\n            'original_indices': test_original_indices,\n            'rejected_categories': rejected_categories_info\n        }\n    else:\n        print(\"Không có mẫu nào bị từ chối.\")\n        rejected_categories_info = {}\n        test_results_for_xai = {\n            'model_preds': test_model_predictions,\n            'confidences': test_confidences,\n            'true_labels': test_true_labels,\n            'original_indices': test_original_indices,\n            'rejected_categories': {}\n        }\n    \n    # 6. Trực quan hóa các ví dụ XAI\n    # Tải lại các mô hình để đảm bảo chúng ở đúng chế độ (eval) và có thể được sử dụng cho XAI\n    loaded_ensemble_models_for_xai = []\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(cfg.MODEL_SAVE_DIR, f'best_model_ensemble_{i}.pth')))\n        model.eval() # Đảm bảo mô hình ở chế độ eval() cho Grad-CAM một khi dự đoán đã được xác định.\n        loaded_ensemble_models_for_xai.append(model)\n        \n    test_results_for_xai = {\n        'model_preds': test_model_predictions,\n        'confidences': test_confidences,\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    # 8. Trực quan hóa các đường cong hiệu suất\n    print(\"\\n--- Trực quan hóa các đường cong hiệu suất ---\")\n    plot_calibration_curve(test_model_predictions, test_confidences, test_true_labels, \n                           save_path=os.path.join(cfg.XAI_SAVE_DIR, 'reliability_diagram.png'))\n    plot_roc_curve(test_model_predictions, test_confidences, test_true_labels,\n                   save_path=os.path.join(cfg.XAI_SAVE_DIR, 'roc_curve.png'))\n    plot_precision_recall_curve(test_model_predictions, test_confidences, test_true_labels,\n                                save_path=os.path.join(cfg.XAI_SAVE_DIR, 'precision_recall_curve.png'))\n    plot_confusion_matrix(test_model_predictions, test_true_labels, save_path=os.path.join(cfg.XAI_SAVE_DIR, 'confusion_matrix_overall.png'))\n\n    print(\"\\n--- Quy trình phân loại có chọn lọc hoàn tất ---\")\n\n\n# Chạy pipeline\nif __name__ == '__main__':\n    run_selective_classification_pipeline(cfg)\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-08T15:24:08.871562Z","iopub.execute_input":"2025-07-08T15:24:08.872237Z","iopub.status.idle":"2025-07-08T15:31:23.16362Z","shell.execute_reply.started":"2025-07-08T15:24:08.872211Z","shell.execute_reply":"2025-07-08T15:31:23.162788Z"}},"outputs":[],"execution_count":null}]}