{"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-10T12:18:04.458862Z","iopub.execute_input":"2026-08-10T12:18:04.459562Z","iopub.status.idle":"2026-08-10T12:18:04.464029Z","shell.execute_reply.started":"2026-08-10T12:18:04.459532Z","shell.execute_reply":"2026-08-10T12:18:04.462894Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# import os\n\n# Đường dẫn thư mục mô hình của bạn trên Kaggle\n# MODEL_DIR = '/kaggle/input/models/trnhquangminh140/rsna-knee-model-minh'\n\n# MODEL_DIR = '/kaggle/input/models/trnhquangminh140/rsna-knee-model-zip'\n\n\n# print(f'=== DANH SÁCH TỆP TRONG MÔ HÌNH: {MODEL_DIR} ===\\n')\n\n# if os.path.exists(MODEL_DIR):\n#  file_count = 0\n#  for root, dirs, files in os.walk(MODEL_DIR):\n#    for file in files:\n#      full_path = os.path.join(root, file)\n#      print(f\"'{full_path}',\")\n#      file_count += 1\n\n#  print(f'\\nTổng cộng: {file_count} tệp được tìm thấy.')\n# else:\n#  print(f'❌ Thư mục không tồn tại: {MODEL_DIR}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-10T12:18:04.46436Z","iopub.execute_input":"2026-08-10T12:18:04.464561Z","iopub.status.idle":"2026-08-10T12:18:04.472233Z","shell.execute_reply.started":"2026-08-10T12:18:04.464541Z","shell.execute_reply":"2026-08-10T12:18:04.471387Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# RSNA KNEE ABNORMALITY DETECTION - FULL OFFLINE PIPELINE (CPU/GPU SAFE)\n# Author: Trịnh Quang Minh (trnhquangminh140) - Fixed Edition\n# =============================================================================\n\nimport os\nimport sys\nimport gc\nimport re\nimport glob\nimport math\nimport random\nimport zipfile\nimport warnings\nfrom pathlib import Path\n\nimport cv2\nimport numpy as np\nimport pandas as pd\nfrom tqdm import tqdm\n\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\nimport timm\n\n# Kiểm tra thư viện pydicom\ntry:\n    import pydicom\n    HAS_PYDICOM = True\nexcept ImportError:\n    HAS_PYDICOM = False\n    print(\"⚠️ Warning: pydicom chưa được cài đặt. Tự động chuyển sang OpenCV fallback.\")\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    # Đường dẫn thư mục mô hình đã giải nén trên Kaggle\n    PRETRAINED_MODEL_PATH = '/kaggle/input/models/trnhquangminh140/rsna-knee-model-zip/pytorch/default/1/rsna_knee_model'\n    \n    # Thông số ảnh & thể tích MRI (Giảm IMG_SIZE nếu muốn chạy nhanh hơn trên CPU)\n    IMG_SIZE = 256\n    NUM_SLICES_PER_SERIES = 16  \n    \n    # Thông số huấn luyện / Suy luận\n    BATCH_SIZE = 8\n    NUM_WORKERS = 2\n    EPOCHS = 3\n    LR = 1e-4                   # Giảm nhẹ Learning Rate để tránh nổ gradient\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. Code đã được tối ưu hóa để không bị lỗi NaN.\")\n\n# ------------------------------------------------------------------------------\n# 2. HELPER TO LOAD UNZIPPED PYTORCH MODEL DIRECTORY\n# ------------------------------------------------------------------------------\ndef load_offline_model(model_path, device):\n    \"\"\"Đóng gói thư mục đã giải nén thành file Zip đúng chuẩn cấu trúc PyTorch\"\"\"\n    if os.path.isfile(model_path):\n        return torch.load(model_path, map_location=device, weights_only=False)\n        \n    elif os.path.isdir(model_path):\n        print(f\"📦 Phát hiện thư mục mô hình giải nén. Đang đóng gói lại theo chuẩn PyTorch Zip Archive...\")\n        temp_zip = \"/tmp/temp_model_repacked.pth\"\n        if os.path.exists(temp_zip):\n            os.remove(temp_zip)\n            \n        folder_name = os.path.basename(os.path.normpath(model_path))\n        \n        with zipfile.ZipFile(temp_zip, 'w', zipfile.ZIP_STORED) as zipf:\n            for root, _, files in os.walk(model_path):\n                for file in files:\n                    full_path = os.path.join(root, file)\n                    rel_path = os.path.relpath(full_path, model_path)\n                    arcname = os.path.join(folder_name, rel_path)\n                    zipf.write(full_path, arcname=arcname)\n                    \n        loaded_obj = torch.load(temp_zip, map_location=device, weights_only=False)\n        \n        if os.path.exists(temp_zip):\n            os.remove(temp_zip)\n            \n        return loaded_obj\n    else:\n        raise FileNotFoundError(f\"Không tìm thấy file hoặc thư mục mô hình tại: {model_path}\")\n\n# ------------------------------------------------------------------------------\n# 3. DICOM LOADER (KHẮC PHỤC LỖI CHIA CHO 0 & TRÁNH NAN)\n# ------------------------------------------------------------------------------\ndef load_dicom_array(fpath, target_size=Config.IMG_SIZE):\n    \"\"\"Đọc file DICOM an toàn offline, chống sinh ra NaN\"\"\"\n    if HAS_PYDICOM:\n        try:\n            dcm = pydicom.dcmread(fpath)\n            img = dcm.pixel_array.astype(np.float32)\n            \n            slope = getattr(dcm, 'RescaleSlope', 1)\n            intercept = getattr(dcm, 'RescaleIntercept', 0)\n            img = img * slope + intercept\n            \n            img_min, img_max = img.min(), img.max()\n            if img_max > img_min:\n                img = (img - img_min) / (img_max - img_min + 1e-6)\n            else:\n                img = np.zeros_like(img)\n                \n            return cv2.resize(img, (target_size, target_size))\n        except Exception:\n            pass\n\n    # Fallback OpenCV\n    try:\n        img = cv2.imread(fpath, cv2.IMREAD_GRAYSCALE)\n        if img is not None:\n            img = img.astype(np.float32)\n            img_min, img_max = img.min(), img.max()\n            if img_max > img_min:\n                img = (img - img_min) / (img_max - img_min + 1e-6)\n            else:\n                img = np.zeros_like(img)\n            return cv2.resize(img, (target_size, target_size))\n    except Exception:\n        pass\n        \n    return np.zeros((target_size, target_size), dtype=np.float32)\n\n# ------------------------------------------------------------------------------\n# 4. DATASET & DATALOADER DEFINITION\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        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                        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        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        volume_tensor = torch.tensor(np.stack(volume_slices), dtype=torch.float32)\n        # Khử NaN nếu có trong tensor\n        volume_tensor = torch.nan_to_num(volume_tensor, nan=0.0)\n\n        if self.is_train:\n            labels = row[Config.LABELS].values.astype(np.float32)\n            labels = np.nan_to_num(labels, nan=0.0) # Điền 0.0 nếu nhãn bị NaN\n            return volume_tensor, torch.tensor(labels, dtype=torch.float32), study_uid\n        else:\n            return volume_tensor, study_uid\n\n# ------------------------------------------------------------------------------\n# 5. NEURAL NETWORK ARCHITECTURE\n# ------------------------------------------------------------------------------\nclass RSNAKneeModel(nn.Module):\n    def __init__(self, num_classes=12, in_channels=16, backbone_name='resnet34'):\n        super(RSNAKneeModel, self).__init__()\n        self.stem = nn.Conv2d(in_channels, 3, kernel_size=1, bias=False)\n        self.backbone = timm.create_model(backbone_name, pretrained=False, num_classes=0)\n        in_features = self.backbone.num_features\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# 6. TRAINING LOOPS (THÊM GRADIENT CLIPPING & XỬ LÝ NAN LOSS)\n# ------------------------------------------------------------------------------\ndef train_one_epoch(model, dataloader, criterion, optimizer, scaler, device):\n    model.train()\n    running_loss = 0.0\n    valid_batches = 0\n    \n    for images, labels, _ in tqdm(dataloader, desc=\"Training\"):\n        images, labels = images.to(device), labels.to(device)\n        optimizer.zero_grad()\n\n        # Tắt autocast nếu chạy trên CPU để tránh lỗi kiểu dữ liệu\n        is_cuda = (device.type == 'cuda')\n        with autocast(enabled=is_cuda):\n            outputs = model(images)\n            loss = criterion(outputs, labels)\n\n        # Kiểm tra nếu loss bị NaN thì bỏ qua batch này\n        if torch.isnan(loss) or torch.isinf(loss):\n            continue\n\n        if is_cuda:\n            scaler.scale(loss).backward()\n            scaler.unscale_(optimizer)\n            torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) # Clip Gradient chống nổ loss\n            scaler.step(optimizer)\n            scaler.update()\n        else:\n            loss.backward()\n            torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)\n            optimizer.step()\n\n        running_loss += loss.item() * images.size(0)\n        valid_batches += images.size(0)\n        \n    return running_loss / (valid_batches + 1e-8)\n\n# ------------------------------------------------------------------------------\n# 7. MAIN PIPELINE\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    if os.path.exists(train_csv_path):\n        train_df = pd.read_csv(train_csv_path)\n        # BƯỚC QUAN TRỌNG: Sửa các ô trống (NaN) trong file train.csv thành 0.0\n        train_df[Config.LABELS] = train_df[Config.LABELS].fillna(0.0)\n        \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 dataframe giả định.\")\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    # Khởi tạo mô hình\n    model = RSNAKneeModel(num_classes=len(Config.LABELS), in_channels=Config.NUM_SLICES_PER_SERIES)\n\n    # Tải trọng số mô hình đã huấn luyện trước\n    if os.path.exists(Config.PRETRAINED_MODEL_PATH):\n        print(f\"Loading offline model from: {Config.PRETRAINED_MODEL_PATH}\")\n        loaded_obj = load_offline_model(Config.PRETRAINED_MODEL_PATH, device=Config.DEVICE)\n        \n        if isinstance(loaded_obj, dict):\n            if 'state_dict' in loaded_obj:\n                model.load_state_dict(loaded_obj['state_dict'])\n            else:\n                model.load_state_dict(loaded_obj)\n        elif isinstance(loaded_obj, nn.Module):\n            model = loaded_obj\n            \n        print(\"✅ Loaded offline model weights successfully!\")\n    else:\n        print(f\"⚠️ Warning: Không tìm thấy mô hình tại {Config.PRETRAINED_MODEL_PATH}\")\n\n    model.to(Config.DEVICE)\n\n    # Tạo DataLoader\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=(Config.DEVICE.type == 'cuda')\n    )\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] Fine-tuning / Training Model ---\")\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 dự đoán (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(\n        test_dataset, \n        batch_size=Config.BATCH_SIZE, \n        shuffle=False, \n        num_workers=Config.NUM_WORKERS\n    )\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            # Khử giá trị NaN trong dự đoán nếu có\n            probs = np.nan_to_num(probs, nan=0.0)\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-10T12:18:04.630234Z","iopub.execute_input":"2026-08-10T12:18:04.630403Z","iopub.status.idle":"2026-08-10T12:39:12.434927Z","shell.execute_reply.started":"2026-08-10T12:18:04.630389Z","shell.execute_reply":"2026-08-10T12:39:12.433642Z"}},"outputs":[],"execution_count":null}]}