{"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":"# ── INSTALL ──────────────────────────────────────────────────────────────\nimport subprocess, sys, os\nos.environ[\"PYTORCH_ALLOC_CONF\"] = \"expandable_segments:True\"\nfor p in [\"timm\", \"scikit-learn\"]:\n    subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", p])\n\n# ── IMPORT ───────────────────────────────────────────────────────────────\nimport random, time, json, warnings, gc\nfrom pathlib import Path\nfrom collections import Counter, defaultdict\nfrom typing import List, Dict\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nfrom PIL import Image\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader, WeightedRandomSampler\nfrom torchvision import transforms\nimport timm\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score\nfrom tqdm import tqdm\nwarnings.filterwarnings('ignore')\n\n# ── PATHS & CONFIG ────────────────────────────────────────────────────────\nPNG_DIR  = \"/kaggle/input/datasets/theoviel/rsna-abdominal-trauma-detection-png-pt2\"\nCSV_PATH = \"/kaggle/input/competitions/rsna-2023-abdominal-trauma-detection/train_2024.csv\"\nOUT_DIR  = \"/kaggle/working/outputs\"\nos.makedirs(OUT_DIR, exist_ok=True)\n\nSEED=42; N_PATIENTS=200; NUM_SLICES=8; IMG_SIZE=128\nBATCH_SIZE=2; NUM_EPOCHS=10; LR=1e-4; WEIGHT_DECAY=1e-4\nMODEL_1=\"efficientnet_b3\"; MODEL_2=\"resnet50\"\nORGAN_LABELS=[\"liver\",\"spleen\",\"kidney\"]\nBINARY_LABELS=[\"bowel\",\"extravasation\"]\nALL_TARGETS=ORGAN_LABELS+BINARY_LABELS\nDEVICE=torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\ndef set_seed(s):\n    random.seed(s); np.random.seed(s); torch.manual_seed(s)\n    if torch.cuda.is_available(): torch.cuda.manual_seed_all(s)\nset_seed(SEED)\nprint(f\"✅ Device: {DEVICE}\")\n\n# ── BUILD INDEX ───────────────────────────────────────────────────────────\nprint(\"📂 Indexing files...\")\npid_files = defaultdict(list)\nfor fname in os.listdir(PNG_DIR):\n    if fname.endswith('.png'):\n        pid = int(fname.split('_')[0])\n        pid_files[pid].append(fname)\nfor pid in pid_files:\n    pid_files[pid].sort()\navailable_pids = sorted(pid_files.keys())\nprint(f\"✅ Bệnh nhân có ảnh: {len(available_pids)}\")\n\n# ── ĐỌC CSV ──────────────────────────────────────────────────────────────\ndf = pd.read_csv(CSV_PATH)\ndf = df[df['patient_id'].isin(available_pids)].copy()\ncols = df.columns.tolist()\nprint(f\"✅ CSV: {len(df)} rows | Columns: {cols}\")\n\ndef get_organ_label(row, org):\n    if f\"{org}_healthy\" in cols:\n        if row.get(f\"{org}_healthy\", 1) == 1: return 0\n        if row.get(f\"{org}_low\", 0) == 1: return 1\n        return 2\n    return 0\n\ndef get_binary_label(row, name):\n    for c in [f\"{name}_injury\", f\"{name}_2\", name]:\n        if c in cols: return int(row.get(c, 0))\n    return 0\n\n# ── SUBSET ────────────────────────────────────────────────────────────────\nN = min(N_PATIENTS, len(df))\nnp.random.seed(SEED)\nh_col = \"liver_healthy\" if \"liver_healthy\" in cols else None\nif h_col:\n    inj = df[df[h_col]==0][\"patient_id\"].values\n    hlt = df[df[h_col]==1][\"patient_id\"].values\n    n_inj = min(N//2, len(inj))\n    n_hlt = min(N-n_inj, len(hlt))\n    sel = np.concatenate([np.random.choice(inj, n_inj, replace=False),\n                          np.random.choice(hlt, n_hlt, replace=False)])\nelse:\n    sel = np.random.choice(df[\"patient_id\"].values, N, replace=False)\n\ndf_subset = df[df[\"patient_id\"].isin(sel)].copy()\nprint(f\"✅ Subset: {len(df_subset)} bệnh nhân\")\n\nrecords = []\nfor _, row in df_subset.iterrows():\n    records.append({\n        \"patient_id\"   : int(row[\"patient_id\"]),\n        \"liver\"        : get_organ_label(row, \"liver\"),\n        \"spleen\"       : get_organ_label(row, \"spleen\"),\n        \"kidney\"       : get_organ_label(row, \"kidney\"),\n        \"bowel\"        : get_binary_label(row, \"bowel\"),\n        \"extravasation\": get_binary_label(row, \"extravasation\"),\n    })\n\nprint(\"📊 Phân bố nhãn:\")\nfor t in ALL_TARGETS:\n    print(f\"   {t:15s}: {dict(Counter(r[t] for r in records))}\")\n\n# ── CLASS WEIGHTS ─────────────────────────────────────────────────────────\ndef compute_class_weights(records, target, n_classes):\n    counts = Counter(r[target] for r in records)\n    total  = len(records)\n    return torch.tensor([total/(n_classes*(counts.get(c,1))) for c in range(n_classes)], dtype=torch.float)\n\ndef binary_pos_weight(records, target):\n    counts = Counter(r[target] for r in records)\n    return torch.tensor([counts.get(0,1)/counts.get(1,1)], dtype=torch.float)\n\nliver_w  = compute_class_weights(records, \"liver\",  3)\nspleen_w = compute_class_weights(records, \"spleen\", 3)\nkidney_w = compute_class_weights(records, \"kidney\", 3)\nbowel_w  = binary_pos_weight(records, \"bowel\")\nextrav_w = binary_pos_weight(records, \"extravasation\")\n\nprint(f\"\\n⚖️  Class weights:\")\nprint(f\"   liver  : {[round(x,2) for x in liver_w.tolist()]}\")\nprint(f\"   spleen : {[round(x,2) for x in spleen_w.tolist()]}\")\nprint(f\"   kidney : {[round(x,2) for x in kidney_w.tolist()]}\")\nprint(f\"   bowel  pos_weight: {bowel_w.item():.1f}\")\nprint(f\"   extrav pos_weight: {extrav_w.item():.1f}\")\n\n# ── WEIGHTED SAMPLER ──────────────────────────────────────────────────────\ndef make_sampler(records):\n    weights = [3.0 if (r[\"liver\"]>0 or r[\"spleen\"]>0 or r[\"kidney\"]>0\n                       or r[\"bowel\"]>0 or r[\"extravasation\"]>0) else 1.0\n               for r in records]\n    return WeightedRandomSampler(weights=weights, num_samples=len(weights), replacement=True)\n\n# ── DATASET ───────────────────────────────────────────────────────────────\ndef load_slices(pid, n_slices, img_size):\n    files = pid_files.get(pid, [])\n    if not files:\n        return np.zeros((n_slices, img_size, img_size), dtype=np.float32)\n    total = len(files)\n    idxs  = np.linspace(0, total-1, n_slices, dtype=int)\n    vol   = []\n    for i in idxs:\n        try:\n            img = Image.open(f\"{PNG_DIR}/{files[i]}\").convert(\"L\")\n            img = img.resize((img_size, img_size), Image.BILINEAR)\n            vol.append(np.array(img, dtype=np.float32)/255.0)\n        except:\n            vol.append(np.zeros((img_size, img_size), dtype=np.float32))\n    return np.stack(vol)\n\nclass RSNADataset(Dataset):\n    def __init__(self, records, transform=None):\n        self.records=records; self.transform=transform\n    def __len__(self): return len(self.records)\n    def __getitem__(self, idx):\n        rec = self.records[idx]\n        vol = torch.from_numpy(load_slices(rec[\"patient_id\"], NUM_SLICES, IMG_SIZE))\n        vol = vol.unsqueeze(1).repeat(1,3,1,1)\n        if self.transform:\n            vol = torch.stack([self.transform(vol[s]) for s in range(vol.shape[0])])\n        return vol, {\n            \"liver\"        : torch.tensor(rec[\"liver\"],          dtype=torch.long),\n            \"spleen\"       : torch.tensor(rec[\"spleen\"],         dtype=torch.long),\n            \"kidney\"       : torch.tensor(rec[\"kidney\"],         dtype=torch.long),\n            \"bowel\"        : torch.tensor(rec[\"bowel\"],          dtype=torch.float),\n            \"extravasation\": torch.tensor(rec[\"extravasation\"],  dtype=torch.float),\n        }\n\n# ── MODEL ─────────────────────────────────────────────────────────────────\nclass AbdominalTraumaModel(nn.Module):\n    def __init__(self, backbone_name, pretrained=True):\n        super().__init__()\n        bb = timm.create_model(backbone_name, pretrained=pretrained)\n        n_feat = (bb.classifier.in_features if hasattr(bb,\"classifier\")\n                  else bb.fc.in_features if hasattr(bb,\"fc\") else bb.num_features)\n        self.backbone    = nn.Sequential(*list(bb.children())[:-2])\n        self.pool        = nn.AdaptiveAvgPool2d((1,1))\n        self.dropout     = nn.Dropout(0.3)\n        self.head_liver  = nn.Linear(n_feat, 3)\n        self.head_spleen = nn.Linear(n_feat, 3)\n        self.head_kidney = nn.Linear(n_feat, 3)\n        self.head_bowel  = nn.Linear(n_feat, 1)\n        self.head_extrav = nn.Linear(n_feat, 1)\n    def forward(self, x):\n        B,S,C,H,W = x.shape\n        x = x.view(B*S,C,H,W)\n        x = self.pool(self.backbone(x)).view(B*S,-1)\n        x = self.dropout(x.view(B,S,-1).mean(1))\n        return {\"liver\":self.head_liver(x), \"spleen\":self.head_spleen(x),\n                \"kidney\":self.head_kidney(x), \"bowel\":self.head_bowel(x),\n                \"extravasation\":self.head_extrav(x)}\n\n# ── WEIGHTED LOSS ──────────────────────────────────────────────────────────\nclass RSNAWeightedLoss(nn.Module):\n    def __init__(self, device):\n        super().__init__()\n        self.ce_liver   = nn.CrossEntropyLoss(weight=liver_w.to(device))\n        self.ce_spleen  = nn.CrossEntropyLoss(weight=spleen_w.to(device))\n        self.ce_kidney  = nn.CrossEntropyLoss(weight=kidney_w.to(device))\n        self.bce_bowel  = nn.BCEWithLogitsLoss(pos_weight=bowel_w.to(device))\n        self.bce_extrav = nn.BCEWithLogitsLoss(pos_weight=extrav_w.to(device))\n    def forward(self, out, tgt):\n        loss  = self.ce_liver(out[\"liver\"],   tgt[\"liver\"])\n        loss += self.ce_spleen(out[\"spleen\"], tgt[\"spleen\"])\n        loss += self.ce_kidney(out[\"kidney\"], tgt[\"kidney\"])\n        loss += self.bce_bowel(out[\"bowel\"].squeeze(-1),          tgt[\"bowel\"])\n        loss += self.bce_extrav(out[\"extravasation\"].squeeze(-1), tgt[\"extravasation\"])\n        return loss / 5\n\n# ── DATALOADER ────────────────────────────────────────────────────────────\ntrain_recs, val_recs = train_test_split(records, test_size=0.2, random_state=SEED)\ntrain_ds = RSNADataset(train_recs, transforms.Compose([\n    transforms.RandomHorizontalFlip(0.5),\n    transforms.ColorJitter(0.2, 0.2),\n]))\nval_ds = RSNADataset(val_recs)\nsampler = make_sampler(train_recs)\ntrain_loader = DataLoader(train_ds, batch_size=BATCH_SIZE, sampler=sampler,\n                          num_workers=2, pin_memory=True)\nval_loader   = DataLoader(val_ds,   batch_size=BATCH_SIZE, shuffle=False,\n                          num_workers=2, pin_memory=True)\nprint(f\"✅ Train: {len(train_recs)} | Val: {len(val_recs)}\")\n\n# ── TRAINING FUNCTIONS ────────────────────────────────────────────────────\ndef train_epoch(model, loader, opt, crit, device):\n    model.train(); total=0.0\n    for vol, lbl in tqdm(loader, desc=\"Train\", leave=False):\n        vol=vol.to(device); lbl={k:v.to(device) for k,v in lbl.items()}\n        opt.zero_grad(); loss=crit(model(vol),lbl); loss.backward(); opt.step()\n        total+=loss.item()\n    return total/len(loader)\n\ndef eval_epoch(model, loader, crit, device):\n    model.eval(); total=0.0\n    store={k:{\"p\":[],\"l\":[]} for k in ALL_TARGETS}\n    with torch.no_grad():\n        for vol, lbl in tqdm(loader, desc=\"Val\", leave=False):\n            vol=vol.to(device); lbl={k:v.to(device) for k,v in lbl.items()}\n            out=model(vol); total+=crit(out,lbl).item()\n            for o in ORGAN_LABELS:\n                store[o][\"p\"].extend(torch.argmax(out[o],1).cpu().numpy())\n                store[o][\"l\"].extend(lbl[o].cpu().numpy())\n            for b in BINARY_LABELS:\n                store[b][\"p\"].extend((torch.sigmoid(out[b].squeeze(-1))>0.5).cpu().numpy().astype(int))\n                store[b][\"l\"].extend(lbl[b].cpu().numpy().astype(int))\n    metrics={}\n    for k in ALL_TARGETS:\n        p,l=np.array(store[k][\"p\"]),np.array(store[k][\"l\"])\n        metrics[k]={\"acc\":accuracy_score(l,p),\n                    \"prec\":precision_score(l,p,average=\"weighted\",zero_division=0),\n                    \"rec\":recall_score(l,p,average=\"weighted\",zero_division=0),\n                    \"f1\":f1_score(l,p,average=\"weighted\",zero_division=0)}\n    return total/len(loader), metrics\n\n# ── MAIN TRAINING LOOP ────────────────────────────────────────────────────\nresults={}; model_metrics={}; train_curves={}\ntorch.cuda.empty_cache(); gc.collect()\n\nfor model_name in [MODEL_1, MODEL_2]:\n    print(f\"\\n{'='*60}\\n🤖 {model_name.upper()}\\n{'='*60}\")\n    torch.cuda.empty_cache(); gc.collect()\n    model     = AbdominalTraumaModel(model_name, pretrained=True).to(DEVICE)\n    criterion = RSNAWeightedLoss(DEVICE)\n    optimizer = optim.AdamW(model.parameters(), lr=LR, weight_decay=WEIGHT_DECAY)\n    scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=NUM_EPOCHS)\n    n_params  = sum(p.numel() for p in model.parameters())\n    size_mb   = n_params*4/(1024**2)\n    print(f\"   Params: {n_params:,} ({size_mb:.1f} MB)\")\n    best_loss=float(\"inf\"); best_mets=None; t_losses=[]; v_losses=[]\n    start=time.time()\n    for epoch in range(NUM_EPOCHS):\n        tl=train_epoch(model,train_loader,optimizer,criterion,DEVICE)\n        vl,mets=eval_epoch(model,val_loader,criterion,DEVICE)\n        scheduler.step(); t_losses.append(tl); v_losses.append(vl)\n        avg_acc=np.mean([mets[k][\"acc\"] for k in ALL_TARGETS])\n        avg_f1 =np.mean([mets[k][\"f1\"]  for k in ALL_TARGETS])\n        print(f\"   Epoch {epoch+1}/{NUM_EPOCHS} | train={tl:.4f} | val={vl:.4f} | acc={avg_acc:.4f} | f1={avg_f1:.4f}\")\n        if vl<best_loss:\n            best_loss=vl; best_mets=mets\n            torch.save(model.state_dict(), f\"{OUT_DIR}/best_{model_name}.pt\")\n    elapsed=time.time()-start\n    avg_acc=np.mean([best_mets[k][\"acc\"] for k in ALL_TARGETS])\n    avg_f1 =np.mean([best_mets[k][\"f1\"]  for k in ALL_TARGETS])\n    print(f\"\\n   ✅ Time: {elapsed:.1f}s | Best Acc: {avg_acc:.4f} | Best F1: {avg_f1:.4f}\")\n    results[model_name]={\"Params (M)\":n_params/1e6,\"Size (MB)\":size_mb,\n                         \"Train Time (s)\":elapsed,\"Accuracy\":avg_acc,\"F1-Score\":avg_f1}\n    model_metrics[model_name]=best_mets\n    train_curves[model_name]={\"train\":t_losses,\"val\":v_losses}\n    del model, criterion, optimizer, scheduler\n    torch.cuda.empty_cache(); gc.collect()\n\n# ── RESULTS ───────────────────────────────────────────────────────────────\nprint(\"\\n📋 BẢNG SO SÁNH\\n\"+\"=\"*70)\nprint(pd.DataFrame(results).T.round(4).to_string())\nprint(\"=\"*70)\nmax_t=max(results[m][\"Train Time (s)\"] for m in results)\nmax_s=max(results[m][\"Size (MB)\"]      for m in results)\nfor m in results:\n    results[m][\"Overall Score\"]=(\n        results[m][\"Accuracy\"]*0.4+results[m][\"F1-Score\"]*0.3+\n        (1-results[m][\"Train Time (s)\"]/max_t)*0.15+\n        (1-results[m][\"Size (MB)\"]/max_s)*0.15)\nfor m in [MODEL_1,MODEL_2]:\n    print(f\"   {m.upper():20s}: Overall Score = {results[m]['Overall Score']:.4f}\")\n\nprint(\"\\n\"+\"=\"*75+\"\\n📊 CHI TIẾT PER TARGET\\n\"+\"=\"*75)\nfor t in ALL_TARGETS:\n    m1=model_metrics[MODEL_1][t]; m2=model_metrics[MODEL_2][t]\n    d=m1[\"acc\"]-m2[\"acc\"]; w=MODEL_1 if d>=0 else MODEL_2\n    print(f\"\\n🎯 {t.upper()}\")\n    print(f\"   {'Model':20s} {'Acc':>8} {'Prec':>8} {'Rec':>8} {'F1':>8}\")\n    print(f\"   {'-'*54}\")\n    print(f\"   {MODEL_1:20s} {m1['acc']:>8.4f} {m1['prec']:>8.4f} {m1['rec']:>8.4f} {m1['f1']:>8.4f}\")\n    print(f\"   {MODEL_2:20s} {m2['acc']:>8.4f} {m2['prec']:>8.4f} {m2['rec']:>8.4f} {m2['f1']:>8.4f}\")\n    print(f\"   → {w.upper()} tốt hơn {abs(d)*100:.2f}%\")\n\n# ── PLOTS ──────────────────────────────────────────────────────────────────\nfig,axes=plt.subplots(2,2,figsize=(14,10))\ncolors=[\"#2ecc71\",\"#3498db\"]; short=[\"EfficientNet-B3\",\"ResNet-50\"]\nfor ax,metric in zip([axes[0,0],axes[0,1]],[\"Accuracy\",\"F1-Score\"]):\n    vals=[results[m][metric] for m in [MODEL_1,MODEL_2]]\n    bars=ax.bar(short,vals,color=colors,alpha=0.85,edgecolor=\"black\")\n    ax.set_title(metric,fontweight=\"bold\",fontsize=13); ax.set_ylim(0,1)\n    [ax.text(b.get_x()+b.get_width()/2,v+0.01,f\"{v:.4f}\",ha=\"center\",fontweight=\"bold\") for b,v in zip(bars,vals)]\n    ax.grid(axis=\"y\",alpha=0.3)\nax=axes[1,0]; x=np.arange(len(ALL_TARGETS)); w=0.35\nfor i,(mn,lbl,c) in enumerate(zip([MODEL_1,MODEL_2],short,colors)):\n    accs=[model_metrics[mn][t][\"acc\"] for t in ALL_TARGETS]\n    bars=ax.bar(x+(i-0.5)*w,accs,w,label=lbl,color=c,alpha=0.85)\n    [ax.text(b.get_x()+b.get_width()/2,v+0.01,f\"{v:.2f}\",ha=\"center\",fontsize=8) for b,v in zip(bars,accs)]\nax.set_xticks(x); ax.set_xticklabels([t.capitalize() for t in ALL_TARGETS])\nax.set_title(\"Per-Target Accuracy\",fontweight=\"bold\",fontsize=13)\nax.set_ylim(0,1.15); ax.legend(); ax.grid(axis=\"y\",alpha=0.3)\nax=axes[1,1]\nfor mn,lbl,c in zip([MODEL_1,MODEL_2],short,colors):\n    ax.plot(train_curves[mn][\"val\"],  label=f\"{lbl} (val)\",  color=c,marker=\"o\",ms=5)\n    ax.plot(train_curves[mn][\"train\"],label=f\"{lbl} (train)\",color=c,linestyle=\"--\",alpha=0.5)\nax.set_title(\"Loss Curves\",fontweight=\"bold\",fontsize=13)\nax.set_xlabel(\"Epoch\"); ax.set_ylabel(\"Loss\"); ax.legend(fontsize=8); ax.grid(alpha=0.3)\nplt.suptitle(f\"RSNA 2023 — EfficientNet-B3 vs ResNet-50\\n\"\n             f\"({len(records)} bệnh nhân thật · {NUM_EPOCHS} epochs · IMG {IMG_SIZE}×{IMG_SIZE} · Weighted Loss)\",\n             fontsize=12,fontweight=\"bold\",y=1.01)\nplt.tight_layout()\nplt.savefig(f\"{OUT_DIR}/comparison_weighted.png\",dpi=150,bbox_inches=\"tight\")\nplt.show()\n\n# ── SAVE JSON ─────────────────────────────────────────────────────────────\nsummary={\"config\":{\"seed\":SEED,\"n_patients\":len(records),\"train\":len(train_recs),\n                   \"val\":len(val_recs),\"num_slices\":NUM_SLICES,\"img_size\":IMG_SIZE,\n                   \"batch_size\":BATCH_SIZE,\"num_epochs\":NUM_EPOCHS,\"lr\":LR,\n                   \"optimizer\":\"AdamW\",\"scheduler\":\"CosineAnnealingLR\",\n                   \"loss\":\"WeightedCE+WeightedBCE+WeightedSampler\",\n                   \"data_type\":\"REAL PNG — RSNA 2023 Kaggle (pt2, 400 patients)\"},\n         \"results\":{m:{k:float(v) for k,v in results[m].items()} for m in results},\n         \"per_target\":{m:{t:{k:float(v) for k,v in model_metrics[m][t].items()}\n                          for t in ALL_TARGETS} for m in model_metrics}}\nwith open(f\"{OUT_DIR}/results_weighted.json\",\"w\") as f:\n    json.dump(summary,f,indent=2)\nwinner=max(results,key=lambda m:results[m][\"Overall Score\"])\nprint(f\"\\n🥇 MÔ HÌNH TỐI ƯU: {winner.upper()}\")\nprint(f\"✅ Saved: {OUT_DIR}/results_weighted.json\")\nprint(f\"✅ Saved: {OUT_DIR}/comparison_weighted.png\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-27T13:00:36.169878Z","iopub.execute_input":"2026-08-27T13:00:36.170264Z","iopub.status.idle":"2026-08-27T13:03:30.765214Z","shell.execute_reply.started":"2026-08-27T13:00:36.170236Z","shell.execute_reply":"2026-08-27T13:03:30.76428Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport shutil\nimport zipfile\n\nprint(\"\\n\" + \"=\"*70 + \"\\n📦 ĐÓNG GÓI DỮ LIỆU NỘP BÀI\\n\" + \"=\"*70)\n\n# Định nghĩa các thư mục đích theo chuẩn yêu cầu\nSUBMISSION_DIR    = \"/kaggle/working\"\nOUTPUT_IMAGES_DIR = os.path.join(SUBMISSION_DIR, \"Output_images\")\nRESULTS_DIR       = os.path.join(SUBMISSION_DIR, \"results\")\nMODELS_TEMP_DIR   = os.path.join(SUBMISSION_DIR, \"saved_models_temp\")\nZIP_FILE_PATH     = os.path.join(SUBMISSION_DIR, \"saved_models.zip\")\n\nos.makedirs(OUTPUT_IMAGES_DIR, exist_ok=True)\nos.makedirs(RESULTS_DIR, exist_ok=True)\nos.makedirs(MODELS_TEMP_DIR, exist_ok=True)\n\nplot_path = f\"{OUT_DIR}/comparison_weighted.png\"\nif os.path.exists(plot_path):\n    shutil.copy(plot_path, os.path.join(OUTPUT_IMAGES_DIR, \"comparison_weighted.png\"))\n    print(\"✅ Đã chép biểu đồ vào Output_images/\")\n\njson_path = f\"{OUT_DIR}/results_weighted.json\"\nif os.path.exists(json_path):\n    shutil.copy(json_path, os.path.join(RESULTS_DIR, \"results_weighted.json\"))\n    print(\"✅ Đã chép file kết quả JSON vào results/\")\n\nmodel_files_exist = False\nfor m_name in [MODEL_1, MODEL_2]:\n    model_path = f\"{OUT_DIR}/best_{m_name}.pt\"\n    if os.path.exists(model_path):\n        shutil.copy(model_path, os.path.join(MODELS_TEMP_DIR, f\"best_{m_name}.pt\"))\n        model_files_exist = True\n\nif model_files_exist:\n    with zipfile.ZipFile(ZIP_FILE_PATH, 'w', zipfile.ZIP_DEFLATED) as zipf:\n        for root, _, files in os.walk(MODELS_TEMP_DIR):\n            for file in files:\n                file_path = os.path.join(root, file)\n                # Chỉ nén tên file, không nén đường dẫn thư mục gốc\n                zipf.write(file_path, arcname=file)\n    print(f\"✅ Đã nén thành công các file mô hình (.pt) vào {ZIP_FILE_PATH}\")\n    \n    # Xóa thư mục tạm sau khi nén xong\n    shutil.rmtree(MODELS_TEMP_DIR)\nelse:\n    print(\"⚠️ Không tìm thấy file mô hình (.pt) nào để nén!\")\n\nprint(\"\\n🎉 Đã chuẩn bị xong! Bạn hãy vào tab 'Output' trên Kaggle để tải về 2 thư mục 'Output_images', 'results' và file 'saved_models.zip'.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-27T13:03:30.766813Z","iopub.execute_input":"2026-08-27T13:03:30.767117Z","iopub.status.idle":"2026-08-27T13:03:37.601814Z","shell.execute_reply.started":"2026-08-27T13:03:30.76709Z","shell.execute_reply":"2026-08-27T13:03:37.601111Z"}},"outputs":[],"execution_count":null}]}