{"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":"none","dataSources":[{"sourceType":"competition","sourceId":24800,"datasetId":1042002,"databundleVersionId":1831594}],"dockerImageVersionId":31040,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import glob\nimport cv2\nimport random\nimport numpy as np\nimport torch\nfrom torch.utils.data import Dataset, DataLoader, random_split\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\n\n# 1. Load toàn bộ paths\ndef load_all_paths():\n    pos_paths = glob.glob('/kaggle/working/small_train/pCT/*.[jp][pn]g')\n    neg_paths = glob.glob('/kaggle/working/small_train/nCT/*.[jp][pn]g')\n    all_paths = pos_paths + neg_paths\n    all_labels = [1]*len(pos_paths) + [0]*len(neg_paths)\n    print(f\"Tổng số ảnh pCT: {len(pos_paths)}\")\n    print(f\"Tổng số ảnh nCT: {len(neg_paths)}\")\n    return all_paths, all_labels\n\n# 2. Dataset class\nclass CTScanDataset(Dataset):\n    def __init__(self, paths, labels, transform=None):\n        self.paths = paths\n        self.labels = labels\n        self.transform = transform\n\n    def __len__(self):\n        return len(self.paths)\n\n    def __getitem__(self, idx):\n        img_path = self.paths[idx]\n        label = self.labels[idx]\n        img = cv2.imread(img_path)\n        if img is None:\n            print(f\"⚠️ Lỗi ảnh: {img_path}, dùng ảnh trắng.\")\n            img = np.zeros((224, 224, 3), dtype=np.uint8)\n        else:\n            img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n        if self.transform:\n            augmented = self.transform(image=img)\n            img = augmented['image']\n        return img, label\n\n# 3. Transform\ntransform = A.Compose([\n    A.Resize(224, 224),\n    A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n    ToTensorV2()\n])\n\n# 4. Load paths và chọn ngẫu nhiên 100 ảnh\nall_paths, all_labels = load_all_paths()\ncombined = list(zip(all_paths, all_labels))\nrandom.shuffle(combined)\n\n# Lấy tối đa 100 ảnh\nsubset = combined[:min(100, len(combined))]\nsubset_paths, subset_labels = zip(*subset)\n\n# 5. Dataset\ndataset = CTScanDataset(list(subset_paths), list(subset_labels), transform=transform)\n\nprint(\"Tổng số ảnh dùng:\", len(dataset))\nassert len(dataset) > 0, \"❌ Dataset rỗng!\"\n\n# 6. Tách train / val / test (70/10/20)\ntotal = len(dataset)\ntrain_len = int(0.7 * total)\nval_len = int(0.1 * total)\ntest_len = total - train_len - val_len\n\ntrain_set, val_set, test_set = random_split(dataset, [train_len, val_len, test_len])\n\n# 7. DataLoader\nbatch_size = 16\ntrain_loader = DataLoader(train_set, batch_size=batch_size, shuffle=True, num_workers=2)\nval_loader = DataLoader(val_set, batch_size=batch_size, shuffle=False, num_workers=2)\ntest_loader = DataLoader(test_set, batch_size=batch_size, shuffle=False, num_workers=2)\n\n# 8. In kết quả\nprint(f\"Train size: {len(train_set)} | Val size: {len(val_set)} | Test size: {len(test_set)}\")\nprint(f\"Train batches: {len(train_loader)}\")\n\n# 9. Test thử 1 batch\nfor images, labels in train_loader:\n    print(\"Batch size:\", images.shape, labels.shape)\n    break\n\nprint(\"✅ Đã load dữ liệu, chia tập và tạo DataLoader thành công!\")\n","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-06-01T19:04:01.954267Z","iopub.execute_input":"2025-06-01T19:04:01.954697Z","iopub.status.idle":"2025-06-01T19:04:02.241459Z","shell.execute_reply.started":"2025-06-01T19:04:01.954666Z","shell.execute_reply":"2025-06-01T19:04:02.239857Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!pip install grad-cam","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-01T18:54:30.388476Z","iopub.execute_input":"2025-06-01T18:54:30.388857Z","iopub.status.idle":"2025-06-01T18:54:33.318325Z","shell.execute_reply.started":"2025-06-01T18:54:30.388828Z","shell.execute_reply":"2025-06-01T18:54:33.317061Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import warnings\nwarnings.filterwarnings(\"ignore\")\n\nimport numpy as np\nimport matplotlib.pyplot as plt\nimport cv2\nimport random\nimport glob\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\nimport torchvision.transforms as transforms\nimport torchvision.models as models\nfrom pytorch_grad_cam import GradCAM\nfrom pytorch_grad_cam.utils.model_targets import ClassifierOutputTarget\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\n\n# Thiết lập device\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\n# 1. Tải và phân chia dữ liệu\ndef load_data_paths():\n    # Fix: Add wildcard to glob to get all .jpg files in the directories\n    train_positive = glob.glob('/kaggle/working/small_train/pCT/*.jpg')\n    train_negative = glob.glob('/kaggle/working/small_train/nCT/*.jpg')\n\n    # Check if the directories contain files\n    if not train_positive:\n        raise ValueError(\"No positive CT images found in /kaggle/working/small_train/pCT/\")\n    if not train_negative:\n        raise ValueError(\"No negative CT images found in /kaggle/working/small_train/nCT/\")\n\n    random.shuffle(train_positive)\n    random.shuffle(train_negative)\n    \n    train_positive_split = train_positive[:int(len(train_positive) * 0.7)]\n    val_positive_split = train_positive[int(len(train_positive) * 0.7):int(len(train_positive) * 0.8)]\n    test_positive_split = train_positive[int(len(train_positive) * 0.8):]\n    \n    train_negative_split = train_negative[:int(len(train_negative) * 0.7)]\n    val_negative_split = train_negative[int(len(train_negative) * 0.7):int(len(train_negative) * 0.8)]\n    test_negative_split = train_negative[int(len(train_negative) * 0.8):]\n    \n    print(f\"Number of train positive CT: {len(train_positive_split)}\")\n    print(f\"Number of train negative CT: {len(train_negative_split)}\")\n    print(f\"Number of val positive CT: {len(val_positive_split)}\")\n    print(f\"Number of val negative CT: {len(val_negative_split)}\")\n    print(f\"Number of test positive CT: {len(test_positive_split)}\")\n    print(f\"Number of test negative CT: {len(test_negative_split)}\")\n    \n    return train_positive_split, train_negative_split, val_positive_split, val_negative_split, test_positive_split, test_negative_split\n\n# 2. Tạo Custom Dataset\nclass CTScanDataset(Dataset):\n    def __init__(self, positive_paths, negative_paths, transform=None):\n        self.paths = positive_paths + negative_paths\n        # Fix: Correct the labeling to match previous code (pCT → 1, nCT → 0)\n        self.labels = [1] * len(positive_paths) + [0] * len(negative_paths)\n        self.transform = transform\n    \n    def __len__(self):\n        return len(self.paths)\n    \n    def __getitem__(self, idx):\n        img_path = self.paths[idx]\n        label = self.labels[idx]\n        \n        img = cv2.imread(img_path)\n        if img is None:\n            print(f\"⚠️ Lỗi ảnh: {img_path}\")\n            img = np.zeros((224, 224, 3), dtype=np.uint8)\n        img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n        \n        if self.transform:\n            augmented = self.transform(image=img)\n            img = augmented['image']\n        \n        return img, label\n\n# Định nghĩa transform\ntrain_transform = A.Compose([\n    A.Resize(224, 224),\n    A.RandomBrightnessContrast(brightness_limit=0.2, contrast_limit=0.2, p=0.5),\n    A.Rotate(limit=40, p=0.5),\n    A.HorizontalFlip(p=0.5),\n    A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n    ToTensorV2()\n])\n\nval_test_transform = A.Compose([\n    A.Resize(224, 224),\n    A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n    ToTensorV2()\n])\n\n# Tạo dataset và dataloader\ntrain_pos_paths, train_neg_paths, val_pos_paths, val_neg_paths, test_pos_paths, test_neg_paths = load_data_paths()\n\n# Create datasets and check if they are empty\ntrain_dataset = CTScanDataset(train_pos_paths, train_neg_paths, transform=train_transform)\nif len(train_dataset) == 0:\n    raise ValueError(\"Train dataset is empty!\")\n\nval_dataset = CTScanDataset(val_pos_paths, val_neg_paths, transform=val_test_transform)\nif len(val_dataset) == 0:\n    raise ValueError(\"Validation dataset is empty!\")\n\ntest_dataset = CTScanDataset(test_pos_paths, test_neg_paths, transform=val_test_transform)\nif len(test_dataset) == 0:\n    raise ValueError(\"Test dataset is empty!\")\n\nbatch_size = 16\ntrain_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True, num_workers=4)\nval_loader = DataLoader(val_dataset, batch_size=batch_size, shuffle=False, num_workers=4)\ntest_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False, num_workers=4)\n\n# Test the DataLoader\nfor images, labels in train_loader:\n    print(\"Train batch size:\", images.shape, labels.shape)\n    break\n\nprint(\"✅ Successfully created DataLoaders!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-01T18:54:34.986682Z","iopub.execute_input":"2025-06-01T18:54:34.986972Z","iopub.status.idle":"2025-06-01T18:54:35.382211Z","shell.execute_reply.started":"2025-06-01T18:54:34.98695Z","shell.execute_reply":"2025-06-01T18:54:35.38022Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 3. Định nghĩa mô hình MobileNetV2\nclass MobileNetV2Classifier(nn.Module):\n    def __init__(self, num_classes=2):\n        super(MobileNetV2Classifier, self).__init__()\n        self.model = models.mobilenet_v2(pretrained=True)\n        self.model.classifier[1] = nn.Linear(self.model.last_channel, num_classes)\n    \n    def forward(self, x):\n        return self.model(x)\n\n# Khởi tạo mô hình\nmodel = MobileNetV2Classifier(num_classes=2).to(device)\n\n# Định nghĩa loss và optimizer\ncriterion = nn.CrossEntropyLoss()\noptimizer = optim.Adam(model.parameters(), lr=0.001)\n\n# 4. Huấn luyện mô hình\ndef train_model(model, train_loader, criterion, optimizer, num_epochs=10):\n    model.train()\n    for epoch in range(num_epochs):\n        running_loss = 0.0\n        correct = 0\n        total = 0\n        for images, labels in train_loader:\n            images, labels = images.to(device), labels.to(device)\n            optimizer.zero_grad()\n            outputs = model(images)\n            loss = criterion(outputs, labels)\n            loss.backward()\n            optimizer.step()\n            running_loss += loss.item()\n            _, predicted = torch.max(outputs.data, 1)\n            total += labels.size(0)\n            correct += (predicted == labels).sum().item()\n        epoch_loss = running_loss / len(train_loader)\n        epoch_acc = 100 * correct / total\n        print(f'Epoch [{epoch+1}/{num_epochs}], Loss: {epoch_loss:.4f}, Accuracy: {epoch_acc:.2f}%')\n\n# 5. Đánh giá mô hình và thu thập nhãn\ndef evaluate_model_and_collect_labels(model, loader):\n    model.eval()\n    true_labels = []\n    pred_labels = []\n    all_activated_outputs = []\n    with torch.no_grad():\n        for images, labels in loader:\n            images, labels = images.to(device), labels.to(device)\n            outputs = model(images)\n            activated_outputs = F.softmax(outputs, dim=1)\n            _, predicted = torch.max(outputs.data, 1)\n            true_labels.extend(labels.cpu().numpy())\n            pred_labels.extend(predicted.cpu().numpy())\n            all_activated_outputs.extend(activated_outputs.cpu().numpy())\n    return np.array(true_labels), np.array(pred_labels), np.array(all_activated_outputs)\n\n# Huấn luyện và thu thập nhãn\ntrain_model(model, train_loader, criterion, optimizer, num_epochs=10)\ntrue_labels, pred_labels, all_activated_outputs = evaluate_model_and_collect_labels(model, test_loader)\nval_true_labels, val_pred_labels, val_activated_outputs = evaluate_model_and_collect_labels(model, val_loader)\n\n# Hàm trích xuất đặc trưng từ mask Grad-CAM\ndef extract_mask_features(grayscale_cam):\n    threshold = np.percentile(grayscale_cam, 80)\n    important_area = np.sum(grayscale_cam > threshold) / grayscale_cam.size\n    y, x = np.indices(grayscale_cam.shape)\n    mask = grayscale_cam > threshold\n    dispersion = 0\n    if np.sum(mask) > 0:\n        centroid_x = np.mean(x[mask])\n        centroid_y = np.mean(y[mask])\n        dispersion = np.mean((x[mask] - centroid_x)**2 + (y[mask] - centroid_y)**2)\n    contrast = grayscale_cam.max() - grayscale_cam.min()\n    return important_area, dispersion, contrast\n\n# Hàm tính ngưỡng từ chối cho đặc trưng mask\ndef compute_mask_thresholds(dataset, model, target_layer, rejection_rate=0.2):\n    model.eval()\n    cam = GradCAM(model=model, target_layers=target_layer)\n    areas, dispersions, contrasts = [], [], []\n    for idx in range(len(dataset)):\n        img, label = dataset[idx]\n        img_tensor = img.unsqueeze(0).to(device)\n        with torch.no_grad():\n            outputs = model(img_tensor)\n            probs = F.softmax(outputs, dim=1).cpu().numpy()[0]\n            predicted_label = np.argmax(probs)\n        targets = [ClassifierOutputTarget(predicted_label)]\n        grayscale_cam = cam(input_tensor=img_tensor, targets=targets)[0, :]\n        area, dispersion, contrast = extract_mask_features(grayscale_cam)\n        areas.append(area)\n        dispersions.append(dispersion)\n        contrasts.append(contrast)\n    R_area = np.percentile(areas, rejection_rate * 100)\n    R_dispersion = np.percentile(dispersions, (1 - rejection_rate) * 100)\n    R_contrast = np.percentile(contrasts, rejection_rate * 100)\n    return R_area, R_dispersion, R_contrast\n\n# Hàm từ chối dựa trên TU (phiên bản cũ)\ndef threshold_reject(y_true, y_pred_proba, rejection_rate=0.1):\n    uncertainty = -np.sum(y_pred_proba * np.log2(y_pred_proba + 1e-10), axis=1)\n    sorted_uncertainty = np.sort(uncertainty)[::-1]\n    n_samples = len(y_true)\n    n_reject = int(rejection_rate * n_samples)\n    R = sorted_uncertainty[n_reject - 1] if n_reject > 0 else sorted_uncertainty[0]\n    return R\n\ndef reject_decision(y_pred_proba, R):\n    uncertainty = -np.sum(y_pred_proba * np.log2(y_pred_proba + 1e-10))\n    return uncertainty <= R\n\n# Hàm từ chối dựa trên TU + Mask (phiên bản mới)\ndef threshold_reject_with_mask(y_true, y_pred_proba, dataset, model, target_layer, rejection_rate=0.1):\n    R_entropy = threshold_reject(y_true, y_pred_proba, rejection_rate)\n    R_area, R_dispersion, R_contrast = compute_mask_thresholds(dataset, model, target_layer, rejection_rate=0.2)\n    return R_entropy, R_area, R_dispersion, R_contrast\n\ndef reject_decision_with_mask(y_pred_proba, grayscale_cam, R_entropy, R_area, R_dispersion, R_contrast, w1=0.4, w2=0.4, w3=0.1, w4=0.1):\n    uncertainty = -np.sum(y_pred_proba * np.log2(y_pred_proba + 1e-10))\n    area, dispersion, contrast = extract_mask_features(grayscale_cam)\n    \n    # Chuẩn hóa các giá trị\n    entropy_norm = uncertainty / R_entropy if R_entropy > 0 else uncertainty\n    area_norm = (R_area - area) / R_area if R_area > 0 and area < R_area else 0\n    dispersion_norm = dispersion / R_dispersion if R_dispersion > 0 else dispersion\n    contrast_norm = (R_contrast - contrast) / R_contrast if R_contrast > 0 and contrast < R_contrast else 0\n    \n    # Tính điểm tổng hợp\n    score = w1 * entropy_norm + w2 * area_norm + w3 * dispersion_norm + w4 * contrast_norm\n    \n    # Debugging: In thông tin score cho một số mẫu\n    if random.random() < 0.01:  # In ngẫu nhiên 1% mẫu\n        print(f\"Score: {score:.4f}, Entropy: {entropy_norm:.4f}, Area: {area_norm:.4f}, Dispersion: {dispersion_norm:.4f}, Contrast: {contrast_norm:.4f}\")\n    \n    return score <= 0.5  # Stricter threshold\n\n# Hàm chạy và so sánh hai chiến lược từ chối\ndef compare_rejection_strategies(true_labels, pred_labels, all_activated_outputs, dataset, val_dataset, model, target_layer, rejection_rate=0.09):\n    mau_sai = np.sum(true_labels != pred_labels)\n    total_samples = len(true_labels)\n    original_error_rate = mau_sai / total_samples\n    \n    # Chiến lược TU cũ\n    R_entropy = threshold_reject(true_labels, all_activated_outputs, rejection_rate)\n    reject_count_old = 0\n    reject_true_sample_old = 0\n    reject_false_sample_old = 0\n    \n    for i, proba in enumerate(all_activated_outputs):\n        decision = reject_decision(proba, R_entropy)\n        if not decision:\n            reject_count_old += 1\n            if true_labels[i] == pred_labels[i]:\n                reject_true_sample_old += 1\n            else:\n                reject_false_sample_old += 1\n    \n    error_rate_old = (mau_sai - reject_false_sample_old) / (total_samples - reject_count_old) if (total_samples - reject_count_old) > 0 else 0\n    risk_reduction_old = (original_error_rate - error_rate_old) / original_error_rate if original_error_rate > 0 else 0\n    \n    # Chiến lược TU + Mask\n    R_entropy, R_area, R_dispersion, R_contrast = threshold_reject_with_mask(val_true_labels, val_activated_outputs, val_dataset, model, target_layer, rejection_rate)\n    reject_count_new = 0\n    reject_true_sample_new = 0\n    reject_false_sample_new = 0\n    cam = GradCAM(model=model, target_layers=target_layer)\n    \n    for i, (img, label) in enumerate(dataset):\n        img_tensor = img.unsqueeze(0).to(device)\n        with torch.no_grad():\n            outputs = model(img_tensor)\n            probs = F.softmax(outputs, dim=1).cpu().numpy()[0]\n            predicted_label = np.argmax(probs)\n        \n        targets = [ClassifierOutputTarget(predicted_label)]\n        grayscale_cam = cam(input_tensor=img_tensor, targets=targets)[0, :]\n        \n        decision = reject_decision_with_mask(probs, grayscale_cam, R_entropy, R_area, R_dispersion, R_contrast)\n        if not decision:\n            reject_count_new += 1\n            if true_labels[i] == pred_labels[i]:\n                reject_true_sample_new += 1\n            else:\n                reject_false_sample_new += 1\n    \n    error_rate_new = (mau_sai - reject_false_sample_new) / (total_samples - reject_count_new) if (total_samples - reject_count_new) > 0 else 0\n    risk_reduction_new = (original_error_rate - error_rate_new) / original_error_rate if original_error_rate > 0 else 0\n    \n    # In kết quả so sánh\n    print(\"=== So sánh chiến lược từ chối ===\")\n    print(\"Chiến lược TU cũ:\")\n    print(f\"Ngưỡng từ chối (R_entropy): {R_entropy:.4f}\")\n    print(f\"Từ chối: {reject_count_old}\")\n    print(f\"Tỷ lệ mẫu bị từ chối: {reject_count_old/total_samples:.4f}\")\n    print(f\"Tỷ lệ lỗi trên các mẫu không bị từ chối: {error_rate_old:.4f}\")\n    print(f\"Hiệu suất giảm thiểu rủi ro: {risk_reduction_old:.4f}\")\n    \n    print(\"\\nChiến lược TU + Mask:\")\n    print(f\"Ngưỡng từ chối (R_entropy, R_area, R_dispersion, R_contrast): {R_entropy:.4f}, {R_area:.4f}, {R_dispersion:.4f}, {R_contrast:.4f}\")\n    print(f\"Từ chối: {reject_count_new}\")\n    print(f\"Tỷ lệ mẫu bị từ chối: {reject_count_new/total_samples:.4f}\")\n    print(f\"Tỷ lệ lỗi trên các mẫu không bị từ chối: {error_rate_new:.4f}\")\n    print(f\"Hiệu suất giảm thiểu rủi ro: {risk_reduction_new:.4f}\")\n    \n    # Vẽ biểu đồ so sánh trade-off Coverage vs. Accuracy\n    thresholds = np.linspace(0, 0.5, 26)\n    coverage_old, accuracy_old = [], []\n    coverage_new, accuracy_new = [], []\n    \n    for threshold in thresholds:\n        # TU cũ\n        R = threshold_reject(true_labels, all_activated_outputs, threshold)\n        reject_count = sum(1 for proba in all_activated_outputs if not reject_decision(proba, R))\n        reject_false = sum(1 for i, proba in enumerate(all_activated_outputs) if not reject_decision(proba, R) and true_labels[i] != pred_labels[i])\n        coverage_old.append(1 - reject_count / total_samples if total_samples > 0 else 0)\n        accuracy_old.append(1 - (mau_sai - reject_false) / (total_samples - reject_count + 1e-10))\n        \n        # TU + Mask\n        R_entropy, R_area, R_dispersion, R_contrast = threshold_reject_with_mask(val_true_labels, val_activated_outputs, val_dataset, model, target_layer, threshold)\n        reject_count = 0\n        reject_false = 0\n        cam = GradCAM(model=model, target_layers=target_layer)\n        for i, (img, label) in enumerate(dataset):\n            img_tensor = img.unsqueeze(0).to(device)\n            with torch.no_grad():\n                outputs = model(img_tensor)\n                probs = F.softmax(outputs, dim=1).cpu().numpy()[0]\n                predicted_label = np.argmax(probs)\n            \n            targets = [ClassifierOutputTarget(predicted_label)]\n            grayscale_cam = cam(input_tensor=img_tensor, targets=targets)[0, :]\n            if not reject_decision_with_mask(probs, grayscale_cam, R_entropy, R_area, R_dispersion, R_contrast):\n                reject_count += 1\n                if true_labels[i] != pred_labels[i]:\n                    reject_false += 1\n        coverage_new.append(1 - reject_count / total_samples if total_samples > 0 else 0)\n        accuracy_new.append(1 - (mau_sai - reject_false) / (total_samples - reject_count + 1e-10))\n    \n    plt.figure(figsize=(10, 6))\n    plt.plot(coverage_old, accuracy_old, label='TU Old', marker='o')\n    plt.plot(coverage_new, accuracy_new, label='TU + Mask', marker='s')\n    plt.title(\"Coverage vs. Accuracy Trade-off\")\n    plt.xlabel(\"Coverage (Tỷ lệ mẫu không bị từ chối)\")\n    plt.ylabel(\"Accuracy (Độ chính xác trên mẫu không bị từ chối)\")\n    plt.legend()\n    plt.grid(True)\n    plt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-01T18:55:37.146314Z","iopub.execute_input":"2025-06-01T18:55:37.146631Z"}},"outputs":[],"execution_count":null}]}