{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[],"dockerImageVersionId":28755,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# This Python 3 environment comes with many helpful analytics libraries installed\n# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python\n# For example, here's several helpful packages to load\n\n# import numpy as np # linear algebra\n# import pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\n\n# Input data files are available in the read-only \"../input/\" directory\n# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory\n\n# import os\n# for dirname, _, filenames in os.walk('/kaggle/input'):\n#    for filename in filenames:\n#        print(os.path.join(dirname, filename))\n\n# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using \"Save & Run All\" \n# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session\n\n# Use the kagglehub client library to attach Kaggle resources like competitions, datasets, and models to your session\n# Learn more about kagglehub: https://github.com/Kaggle/kagglehub/blob/main/README.md\n\n# import kagglehub\n# kagglehub.dataset_download('<owner>/<dataset-slug>')","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-08-10T09:45:35.300158Z","iopub.execute_input":"2026-08-10T09:45:35.300416Z","iopub.status.idle":"2026-08-10T09:45:35.305399Z","shell.execute_reply.started":"2026-08-10T09:45:35.300386Z","shell.execute_reply":"2026-08-10T09:45:35.30462Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"**Datasets:**\n\n'/kaggle/input/competitions/rsna-knee-abnormality-detection'\n'/kaggle/input/competitions/rsna-knee-abnormality-detection/test_series'\n'/kaggle/input/competitions/rsna-knee-abnormality-detection/train_series'\n'/kaggle/input/competitions/rsna-knee-abnormality-detection/sample_submission.csv'\n'/kaggle/input/competitions/rsna-knee-abnormality-detection/test.csv'\n'/kaggle/input/competitions/rsna-knee-abnormality-detection/test_series.csv'\n'/kaggle/input/competitions/rsna-knee-abnormality-detection/train.csv'\n'/kaggle/input/competitions/rsna-knee-abnormality-detection/train_series.csv'","metadata":{}},{"cell_type":"code","source":"import numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\nimport os\n\n# 1. Tạo danh sách chứa các đường dẫn file\nfile_paths = []\n\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    for filename in filenames:\n        file_paths.append(os.path.join(dirname, filename))\n\n# 2. Tạo DataFrame từ danh sách và xuất thành file CSV\ndf_files = pd.DataFrame(file_paths, columns=['file_path'])\ndf_files.to_csv('file_list.csv', index=False)\n\nimport kagglehub\n# kagglehub.dataset_download('<owner>/<dataset-slug>')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-10T09:45:35.30681Z","iopub.execute_input":"2026-08-10T09:45:35.307092Z","iopub.status.idle":"2026-08-10T09:49:41.153714Z","shell.execute_reply.started":"2026-08-10T09:45:35.30706Z","shell.execute_reply":"2026-08-10T09:49:41.153147Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"**Link Datasets:**\n\n**The files containing the links are located within this file:**\n(Nằm trong file chứa các link:)\n\n'/kaggle/input/datasets/trnhquangminh140/rsna-knee-abnormality-detection-link-datasets/file_list_dataset.txt'\n\n**And the links within the file are similar to the following:**\n(và các link bên trong file tương tự như sau:) \n\n'/kaggle/input/competitions/rsna-knee-abnormality-detection/sample_submission.csv',\n'/kaggle/input/competitions/rsna-knee-abnormality-detection/test_series.csv',\n'/kaggle/input/competitions/rsna-knee-abnormality-detection/train_series.csv',\n'/kaggle/input/competitions/rsna-knee-abnormality-detection/train.csv',\n'/kaggle/input/competitions/rsna-knee-abnormality-detection/test.csv',\n\n'/kaggle/input/competitions/rsna-knee-abnormality-detection/test_series/1.2.826.0.1.3680043.8.498.10062861783145312629332250977456991776/1.2.826.0.1.3680043.8.498.10818107180155454609278933308651876447/1.2.826.0.1.3680043.8.498.43758349334868121588037306541272735960.dcm'\n\n'/kaggle/input/competitions/rsna-knee-abnormality-detection/test_series/1.2.826.0.1.3680043.8.498.10062861783145312629332250977456991776/1.2.826.0.1.3680043.8.498.10818107180155454609278933308651876447/1.2.826.0.1.3680043.8.498.98300178100494838040275925015059424504.dcm'\n\n'/kaggle/input/competitions/rsna-knee-abnormality-detection/test_series/1.2.826.0.1.3680043.8.498.10062861783145312629332250977456991776/1.2.826.0.1.3680043.8.498.10818107180155454609278933308651876447/1.2.826.0.1.3680043.8.498.24211175366460976919052329259009027034.dcm'\n\n'/kaggle/input/competitions/rsna-knee-abnormality-detection/test_series/1.2.826.0.1.3680043.8.498.10062861783145312629332250977456991776/1.2.826.0.1.3680043.8.498.10818107180155454609278933308651876447/1.2.826.0.1.3680043.8.498.62702218416158351740972867662762507972.dcm'\n\n\n\n**And there are many links, including files; the dataset file_list_dataset.txt has a size of up to 211MB.**\n(và nhiều link lắm, file chứ link dataset file_list_dataset.txt có dung lượng đến 211MB.)\n\n","metadata":{}},{"cell_type":"code","source":"# ==============================================================================\n# RSNA KNEE ABNORMALITY DETECTION - FULL WORKING END-TO-END PIPELINE\n# Author: Trịnh Quang Minh (trnhquangminh140)\n# ==============================================================================\n\nimport os\nimport sys\nimport gc\nimport re\nimport glob\nimport math\nimport random\nimport warnings\nimport numpy as np\nimport pandas as pd\nfrom pathlib import Path\nfrom tqdm import tqdm\n\n# Visualizations / Hiển thị hình ảnh\nimport cv2\n\n# PyTorch & Medical Imaging Libraries\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader\nfrom torch.cuda.amp import GradScaler, autocast\n\n# Tải tự động các thư viện cần thiết nếu chưa có\ntry:\n    import pydicom\nexcept ImportError:\n    os.system(\"pip install -q pydicom timm albumentations\")\n    import pydicom\n\ntry:\n    import timm\nexcept ImportError:\n    os.system(\"pip install -q timm\")\n    import timm\n\nwarnings.filterwarnings('ignore')\n\n# ------------------------------------------------------------------------------\n# 1. CONFIGURATION & REPRODUCIBILITY / CẤU HÌNH & TÍNH TÁI LẬP\n# ------------------------------------------------------------------------------\nclass Config:\n    SEED = 42\n    \n    # Đường dẫn thư mục dữ liệu trên Kaggle\n    DATA_DIR = '/kaggle/input/competitions/rsna-knee-abnormality-detection'\n    OUTPUT_DIR = '/kaggle/working/models'\n    \n    # Thông số ảnh & thể tích MRI\n    IMG_SIZE = 256\n    NUM_SLICES_PER_SERIES = 16  # Số lát cắt chuẩn hóa cho mỗi chuỗi MRI (2.5D Input)\n    \n    # Thông số huấn luyện\n    BATCH_SIZE = 8\n    NUM_WORKERS = 2\n    EPOCHS = 3\n    LR = 3e-4\n    WEIGHT_DECAY = 1e-2\n    DEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n    \n    # 12 Nhãn chẩn đoán bất thường khớp gối\n    LABELS = [\n        'ACL', 'MCL', 'Medial Meniscus', 'Lateral Meniscus',\n        'Medial OA', 'Lateral OA', 'PF OA', 'Effusion',\n        'Synovitis', \"Baker's\", 'Contusion', 'Fracture'\n    ]\n\ndef seed_everything(seed=42):\n    random.seed(seed)\n    os.environ['PYTHONHASHSEED'] = str(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed(seed)\n    torch.cuda.manual_seed_all(seed)\n    torch.backends.cudnn.deterministic = True\n\nseed_everything(Config.SEED)\nos.makedirs(Config.OUTPUT_DIR, exist_ok=True)\n\nprint(f\"=== Running on Device / Đang chạy trên thiết bị: {Config.DEVICE} ===\")\nif Config.DEVICE.type == 'cpu':\n    print(\"⚠️ LƯU Ý: Bạn đang chạy trên CPU! Hãy bật GPU trong 'Session options' trên Kaggle để tăng tốc.\")\n\n# ------------------------------------------------------------------------------\n# 2. DICOM LOADER / TẢI VÀ TIỀN XỬ LÝ ẢNH DICOM\n# ------------------------------------------------------------------------------\ndef load_dicom_array(fpath, target_size=Config.IMG_SIZE):\n    \"\"\"\n    Đọc file DICOM an toàn, thực hiện Rescale & Normalization về [0, 1]\n    \"\"\"\n    try:\n        dcm = pydicom.dcmread(fpath)\n        img = dcm.pixel_array.astype(np.float32)\n        \n        # Rescale Slope & Intercept\n        slope = getattr(dcm, 'RescaleSlope', 1)\n        intercept = getattr(dcm, 'RescaleIntercept', 0)\n        img = img * slope + intercept\n        \n        # Min-Max Normalization\n        img_min, img_max = img.min(), img.max()\n        if img_max > img_min:\n            img = (img - img_min) / (img_max - img_min)\n        else:\n            img = np.zeros_like(img)\n            \n        img = cv2.resize(img, (target_size, target_size))\n        return img\n    except Exception as e:\n        return np.zeros((target_size, target_size), dtype=np.float32)\n\n# ------------------------------------------------------------------------------\n# 3. DATASET & DATALOADER DEFINITION / ĐỊNH NGHĨA DATASET\n# ------------------------------------------------------------------------------\nclass RSNAKneeDataset(Dataset):\n    def __init__(self, df, series_df, base_path, is_train=True):\n        self.df = df\n        self.series_df = series_df\n        self.base_path = base_path\n        self.is_train = is_train\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        study_uid = row['StudyInstanceUID']\n        \n        # Tìm tất cả chuỗi series thuộc ca bệnh (StudyInstanceUID)\n        study_series = self.series_df[self.series_df['StudyInstanceUID'] == study_uid] if self.series_df is not None else pd.DataFrame()\n        \n        volume_slices = []\n        folder_prefix = 'train_series' if self.is_train else 'test_series'\n        \n        if not study_series.empty:\n            for _, s_row in study_series.iterrows():\n                series_uid = s_row['SeriesInstanceUID']\n                series_dir = os.path.join(self.base_path, folder_prefix, study_uid, series_uid)\n                if os.path.exists(series_dir):\n                    dcm_files = sorted(glob.glob(os.path.join(series_dir, \"*.dcm\")))\n                    if len(dcm_files) > 0:\n                        # Lấy mẫu đều các lát cắt trong chuỗi\n                        step = max(1, len(dcm_files) // 4)\n                        selected = dcm_files[::step][:4]\n                        for f in selected:\n                            volume_slices.append(load_dicom_array(f))\n        \n        # Padding hoặc Clipping để đưa về đúng số lượng kênh C=16 (2.5D Input)\n        target_depth = Config.NUM_SLICES_PER_SERIES\n        if len(volume_slices) == 0:\n            volume_slices = [np.zeros((Config.IMG_SIZE, Config.IMG_SIZE), dtype=np.float32) for _ in range(target_depth)]\n        elif len(volume_slices) < target_depth:\n            pad_count = target_depth - len(volume_slices)\n            volume_slices.extend([volume_slices[-1]] * pad_count)\n        else:\n            volume_slices = volume_slices[:target_depth]\n            \n        # Chuyển đổi thành Tensor 3D (C, H, W)\n        volume_tensor = torch.tensor(np.stack(volume_slices), dtype=torch.float32)\n        \n        if self.is_train:\n            labels = row[Config.LABELS].values.astype(np.float32)\n            return volume_tensor, torch.tensor(labels, dtype=torch.float32), study_uid\n        else:\n            return volume_tensor, study_uid\n\n# ------------------------------------------------------------------------------\n# 4. NEURAL NETWORK ARCHITECTURE / MÔ HÌNH PRETRAINED BACKBONE\n# ------------------------------------------------------------------------------\nclass RSNAKneeModel(nn.Module):\n    def __init__(self, num_classes=12, in_channels=16, backbone_name='resnet34'):\n        super(RSNAKneeModel, self).__init__()\n        \n        # Lớp chuyển đổi đầu vào 16 kênh (slices) về 3 kênh chuẩn của ImageNet\n        self.stem = nn.Conv2d(in_channels, 3, kernel_size=1, bias=False)\n        \n        # Tải mô hình Pretrained từ timm\n        self.backbone = timm.create_model(backbone_name, pretrained=True, num_classes=0)\n        in_features = self.backbone.num_features\n        \n        # Classifier Head cho 12 nhãn\n        self.classifier = nn.Sequential(\n            nn.Dropout(0.3),\n            nn.Linear(in_features, num_classes)\n        )\n\n    def forward(self, x):\n        x = self.stem(x)\n        features = self.backbone(x)\n        logits = self.classifier(features)\n        return logits\n\n# ------------------------------------------------------------------------------\n# 5. TRAINING & VALIDATION LOOPS / HÀM HUẤN LUYỆN\n# ------------------------------------------------------------------------------\ndef train_one_epoch(model, dataloader, criterion, optimizer, scaler, device):\n    model.train()\n    running_loss = 0.0\n    for images, labels, _ in tqdm(dataloader, desc=\"Training\"):\n        images, labels = images.to(device), labels.to(device)\n        optimizer.zero_grad()\n        \n        with autocast(enabled=(device.type == 'cuda')):\n            outputs = model(images)\n            loss = criterion(outputs, labels)\n            \n        scaler.scale(loss).backward()\n        scaler.step(optimizer)\n        scaler.update()\n        \n        running_loss += loss.item() * images.size(0)\n    return running_loss / len(dataloader.dataset)\n\ndef validate(model, dataloader, criterion, device):\n    model.eval()\n    running_loss = 0.0\n    with torch.no_grad():\n        for images, labels, _ in tqdm(dataloader, desc=\"Validation\"):\n            images, labels = images.to(device), labels.to(device)\n            outputs = model(images)\n            loss = criterion(outputs, labels)\n            running_loss += loss.item() * images.size(0)\n    return running_loss / len(dataloader.dataset)\n\n# ------------------------------------------------------------------------------\n# 6. MAIN PIPELINE / QUY TRÌNH CHẠY CHÍNH\n# ------------------------------------------------------------------------------\ndef run_pipeline():\n    print(\"\\n--- [Step 1] Loading Metadata / Tải dữ liệu metadata ---\")\n    train_csv_path = os.path.join(Config.DATA_DIR, 'train.csv')\n    train_series_csv_path = os.path.join(Config.DATA_DIR, 'train_series.csv')\n    test_csv_path = os.path.join(Config.DATA_DIR, 'test.csv')\n    test_series_csv_path = os.path.join(Config.DATA_DIR, 'test_series.csv')\n    \n    # Kiểm tra sự tồn tại của dữ liệu\n    if os.path.exists(train_csv_path):\n        train_df = pd.read_csv(train_csv_path)\n        train_series_df = pd.read_csv(train_series_csv_path) if os.path.exists(train_series_csv_path) else None\n        print(f\"Loaded train.csv with {len(train_df)} samples.\")\n    else:\n        print(\"⚠️ Warning: Không tìm thấy train.csv! Khởi tạo dữ liệu mẫu để kiểm tra pipeline.\")\n        train_df = pd.DataFrame({'StudyInstanceUID': ['sample_1', 'sample_2']})\n        for lbl in Config.LABELS:\n            train_df[lbl] = 0.0\n        train_series_df = None\n\n    if os.path.exists(test_csv_path):\n        test_df = pd.read_csv(test_csv_path)\n        test_series_df = pd.read_csv(test_series_csv_path) if os.path.exists(test_series_csv_path) else None\n        print(f\"Loaded test.csv with {len(test_df)} samples.\")\n    else:\n        test_df = pd.DataFrame({'StudyInstanceUID': ['sample_test_1', 'sample_test_2']})\n        test_series_df = None\n\n    # Tạo DataLoader cho Training\n    train_dataset = RSNAKneeDataset(train_df, train_series_df, base_path=Config.DATA_DIR, is_train=True)\n    train_loader = DataLoader(\n        train_dataset, \n        batch_size=Config.BATCH_SIZE, \n        shuffle=True, \n        num_workers=Config.NUM_WORKERS,\n        pin_memory=True if Config.DEVICE.type == 'cuda' else False\n    )\n\n    # Khởi tạo Model, Loss, Optimizer\n    model = RSNAKneeModel(num_classes=len(Config.LABELS), in_channels=Config.NUM_SLICES_PER_SERIES)\n    model.to(Config.DEVICE)\n    \n    criterion = nn.BCEWithLogitsLoss()\n    optimizer = optim.AdamW(model.parameters(), lr=Config.LR, weight_decay=Config.WEIGHT_DECAY)\n    scaler = GradScaler(enabled=(Config.DEVICE.type == 'cuda'))\n\n    print(\"\\n--- [Step 2] Training Model / Tiến hành huấn luyện ---\")\n    for epoch in range(Config.EPOCHS):\n        print(f\"\\nEpoch {epoch+1}/{Config.EPOCHS}\")\n        train_loss = train_one_epoch(model, train_loader, criterion, optimizer, scaler, Config.DEVICE)\n        print(f\"Epoch {epoch+1} - Train Loss: {train_loss:.4f}\")\n\n    # Lưu checkpoint\n    model_save_path = os.path.join(Config.OUTPUT_DIR, 'rsna_knee_model.pth')\n    torch.save(model.state_dict(), model_save_path)\n    print(f\"\\nModel checkpoint saved at: {model_save_path}\")\n\n    # Tiến hành suy luận tập Test (Inference)\n    print(\"\\n--- [Step 3] Generating Test Predictions / Dự đoán tập Test ---\")\n    test_dataset = RSNAKneeDataset(test_df, test_series_df, base_path=Config.DATA_DIR, is_train=False)\n    test_loader = DataLoader(test_dataset, batch_size=Config.BATCH_SIZE, shuffle=False, num_workers=Config.NUM_WORKERS)\n    \n    model.eval()\n    results = []\n    \n    with torch.no_grad():\n        for images, uids in tqdm(test_loader, desc=\"Inference\"):\n            images = images.to(Config.DEVICE)\n            outputs = model(images)\n            probs = torch.sigmoid(outputs).cpu().numpy()\n            \n            for uid, prob in zip(uids, probs):\n                res_row = {'StudyInstanceUID': uid}\n                for idx, label in enumerate(Config.LABELS):\n                    res_row[label] = float(np.round(prob[idx], 4))\n                results.append(res_row)\n\n    sub_df = pd.DataFrame(results)\n    submission_path = 'submission.csv'\n    sub_df.to_csv(submission_path, index=False)\n    \n    print(f\"\\n✅ Đã lưu tệp submission.csv thành công!\")\n    print(\"\\n5 dòng đầu tiên của submission.csv:\")\n    print(sub_df.head())\n\nif __name__ == '__main__':\n    run_pipeline()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-10T09:49:41.15493Z","iopub.execute_input":"2026-08-10T09:49:41.155425Z","iopub.status.idle":"2026-08-10T10:19:32.560459Z","shell.execute_reply.started":"2026-08-10T09:49:41.155398Z","shell.execute_reply":"2026-08-10T10:19:32.559433Z"}},"outputs":[],"execution_count":null}]}