{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"pygments_lexer":"ipython3","nbconvert_exporter":"python","version":"3.6.4","file_extension":".py","codemirror_mode":{"name":"ipython","version":3},"name":"python","mimetype":"text/x-python"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":6799,"databundleVersionId":4225553,"sourceType":"competition"}],"dockerImageVersionId":31090,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os\n\nROOT = \"/kaggle/input/imagenet-object-localization-challenge/ILSVRC/Data/CLS-LOC\"\nprint(\"ROOT exists:\", os.path.exists(ROOT))\nprint(\"train exists:\", os.path.isdir(os.path.join(ROOT, \"train\")))\nprint(\"val   exists:\", os.path.isdir(os.path.join(ROOT, \"val\")))\n\nif os.path.isdir(os.path.join(ROOT, \"train\")):\n    n_classes_train = len([d for d in os.listdir(os.path.join(ROOT, \"train\")) \n                           if os.path.isdir(os.path.join(ROOT, \"train\", d))])\n    n_classes_val   = len([d for d in os.listdir(os.path.join(ROOT, \"val\")) \n                           if os.path.isdir(os.path.join(ROOT, \"val\", d))])\n    print(\"train classes:\", n_classes_train, \"| val classes:\", n_classes_val)  # mong đợi ≈ 1000\n\n    # liệt kê thử vài lớp đầu\n    print(\"sample train classes:\", sorted(os.listdir(os.path.join(ROOT, \"train\")))[:5])\n","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =========================================================\n# VGG16-BN on ImageNet-2012 — FAST/LARGE SUBSET (Kaggle-ready)\n# - Subset cân bằng: 200 lớp × 400 ảnh/lớp từ train/ (không cần val.txt)\n# - Tách 10% subset làm validation (đúng transform)\n# - Optim: AdamW + Warmup(5 epoch) → Cosine, GradClip, AMP (torch.amp)\n# - Regularization: MixUp (phase-out cuối kỳ), Label Smoothing, ColorJitter nhẹ\n# - Metrics: Top-1 / Top-5 / mIoU + 2 ảnh mẫu (đúng/sai) ở best epoch\n# - Checkpoint load an toàn với PyTorch >= 2.6\n# =========================================================\n\nimport os, random, time, math\nfrom collections import defaultdict\nimport numpy as np\nimport torch, torch.nn as nn, torch.optim as optim\nfrom torch.optim.lr_scheduler import SequentialLR, LinearLR, CosineAnnealingLR\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms\nfrom PIL import Image, ImageFile\nfrom glob import glob\nimport matplotlib.pyplot as plt\n\n# ------------ Robust PIL ------------\nImageFile.LOAD_TRUNCATED_IMAGES = True\n\n# -------------------- Config --------------------\nSEED = 1337\nrandom.seed(SEED); np.random.seed(SEED); torch.manual_seed(SEED); torch.cuda.manual_seed_all(SEED)\ntry: torch.set_float32_matmul_precision(\"high\")\nexcept: pass\nDEVICE = \"cuda\" if torch.cuda.is_available() else \"cpu\"\ntorch.backends.cudnn.benchmark = True\nprint(\"Device:\", DEVICE)\n\nCFG = {\n    \"DATA_ROOT\": \"/kaggle/input/imagenet-object-localization-challenge/ILSVRC/Data/CLS-LOC\",\n\n    # SUBSET lớn: có thể hạ xuống nếu IO/GPU yếu (vd: 150 × 200)\n    \"SUBSET_CLASSES\": 200,\n    \"IMGS_PER_CLASS\": 400,\n    \"VAL_RATIO\": 0.10,\n\n    \"MODEL\": \"vgg16_bn\",        # 'vgg16' | 'vgg16_bn'\n    \"PRETRAINED\": True,         # fine-tune từ weights chuẩn\n    \"OPTIM\": \"auto\",            # 'auto' | 'adamw' | 'sgd' (auto: AdamW khi PRETRAINED)\n\n    \"BATCH_SIZE\": 64,           # nếu OOM → 48 hoặc 32\n    \"EPOCHS\": 40,\n    \"LR\": 1e-4,                 # fine-tune AdamW\n    \"WEIGHT_DECAY\": 1e-4,\n    \"MOMENTUM\": 0.9,            # cho SGD nếu dùng\n\n    \"IMG_SIZE\": 224,\n    \"NUM_WORKERS\": 4,           # IO cao → 4 là hợp lý trên Kaggle T4\n    \"PREFETCH_FACTOR\": 2,\n    \"LOG_EVERY\": 200,           # in ETA mỗi 200 batch\n\n    \"LABEL_SMOOTH\": 0.02,\n    \"MIXUP_ALPHA\": 0.3,         # mạnh, sẽ phase-out về 0 ở 6 epoch cuối\n    \"MIXUP_PHASEOUT_EPOCHS\": 6, # giảm dần alpha -> 0 ở 6 epoch cuối\n    \"CLIP_GRAD_NORM\": 1.0,\n\n    \"WARMUP_EPOCHS\": 5,         # warmup LR 5 epoch rồi mới cosine\n}\n\nTRAIN_DIR = os.path.join(CFG[\"DATA_ROOT\"], \"train\")\nassert os.path.isdir(TRAIN_DIR), f\"Missing train dir: {TRAIN_DIR}\"\n\n# -------------------- Transforms --------------------\nIMAGENET_MEAN = [0.485, 0.456, 0.406]\nIMAGENET_STD  = [0.229, 0.224, 0.225]\ntrain_tfms = transforms.Compose([\n    transforms.RandomResizedCrop(CFG[\"IMG_SIZE\"], scale=(0.08, 1.0), ratio=(3/4, 4/3)),\n    transforms.RandomHorizontalFlip(),\n    transforms.ColorJitter(0.1, 0.1, 0.1, 0.05),  # nhẹ vì MixUp cao\n    transforms.ToTensor(),\n    transforms.Normalize(IMAGENET_MEAN, IMAGENET_STD),\n])\nval_tfms = transforms.Compose([\n    transforms.Resize(256),\n    transforms.CenterCrop(CFG[\"IMG_SIZE\"]),\n    transforms.ToTensor(),\n    transforms.Normalize(IMAGENET_MEAN, IMAGENET_STD),\n])\n\n# -------------------- Build balanced subset --------------------\ndef list_class_dirs(train_dir):\n    return sorted([d for d in os.listdir(train_dir) if os.path.isdir(os.path.join(train_dir, d))])\n\nALL_CLASSES = list_class_dirs(TRAIN_DIR)\nrng = np.random.default_rng(SEED)\nif CFG[\"SUBSET_CLASSES\"] is not None and CFG[\"SUBSET_CLASSES\"] < len(ALL_CLASSES):\n    chosen = rng.choice(ALL_CLASSES, size=CFG[\"SUBSET_CLASSES\"], replace=False)\n    CLASSES_WNID = sorted(list(chosen))\nelse:\n    CLASSES_WNID = ALL_CLASSES\n\nclass_to_idx = {wnid: i for i, wnid in enumerate(CLASSES_WNID)}\nidx_to_wnid = {i: wnid for wnid, i in class_to_idx.items()}\nNUM_CLASSES = len(CLASSES_WNID)\nprint(f\"[SUBSET] Using: {NUM_CLASSES} classes × {CFG['IMGS_PER_CLASS']} imgs/class\")\n\ndef collect_images_for_class(wnid, limit=None):\n    folder = os.path.join(TRAIN_DIR, wnid)\n    files = []\n    for ext in (\"*.JPEG\",\"*.jpeg\",\"*.jpg\",\"*.png\",\"*.bmp\"):\n        files += glob(os.path.join(folder, ext))\n    rng.shuffle(files)\n    return files[:limit] if (limit is not None) else files\n\ntrain_items, val_items = [], []\nfor wnid in CLASSES_WNID:\n    paths = collect_images_for_class(wnid, CFG[\"IMGS_PER_CLASS\"])\n    if len(paths) == 0:\n        continue\n    n_val = max(1, int(len(paths) * CFG[\"VAL_RATIO\"]))\n    val_p = paths[:n_val]; train_p = paths[n_val:]\n    y = class_to_idx[wnid]\n    train_items += [(p, y) for p in train_p]\n    val_items   += [(p, y) for p in val_p]\n\nprint(f\"[DATA] train images: {len(train_items)} | val images: {len(val_items)}\")\n\n# -------------------- Simple dataset --------------------\nclass SimpleImageDataset(Dataset):\n    def __init__(self, items, transform):\n        self.items = items\n        self.transform = transform\n    def __len__(self): return len(self.items)\n    def __getitem__(self, i):\n        path, y = self.items[i]\n        img = Image.open(path).convert(\"RGB\")\n        if self.transform: img = self.transform(img)\n        return img, y\n\ntrain_ds = SimpleImageDataset(train_items, train_tfms)\nval_ds   = SimpleImageDataset(val_items,   val_tfms)\n\n# -------------------- DataLoaders (safe prefetch) --------------------\ndef make_loader(ds, train=True):\n    kwargs = dict(\n        batch_size=CFG[\"BATCH_SIZE\"],\n        shuffle=train,\n        num_workers=CFG[\"NUM_WORKERS\"],\n        pin_memory=True,\n        persistent_workers=(CFG[\"NUM_WORKERS\"] > 0),\n    )\n    if CFG[\"NUM_WORKERS\"] > 0 and \"prefetch_factor\" in DataLoader.__init__.__code__.co_varnames:\n        kwargs[\"prefetch_factor\"] = CFG[\"PREFETCH_FACTOR\"]\n    return DataLoader(ds, **kwargs)\n\ntrain_loader = make_loader(train_ds, train=True)\nval_loader   = make_loader(val_ds,   train=False)\nprint(f\"[DATA] steps/epoch: train={len(train_loader)} | val={len(val_loader)}\")\n\n# -------------------- Model --------------------\nfrom torchvision.models import vgg16, vgg16_bn\n# chọn weights enum; fallback cho torchvision cũ\ntry:\n    from torchvision.models import VGG16_Weights, VGG16_BN_Weights\n    if CFG[\"MODEL\"] == \"vgg16_bn\":\n        weights = VGG16_BN_Weights.IMAGENET1K_V1 if CFG[\"PRETRAINED\"] else None\n        model = vgg16_bn(weights=weights)\n    else:\n        weights = VGG16_Weights.IMAGENET1K_V1 if CFG[\"PRETRAINED\"] else None\n        model = vgg16(weights=weights)\nexcept Exception:\n    model = vgg16_bn(pretrained=bool(CFG[\"PRETRAINED\"])) if CFG[\"MODEL\"]==\"vgg16_bn\" \\\n            else vgg16(pretrained=bool(CFG[\"PRETRAINED\"]))\n\n# thay head theo NUM_CLASSES\nin_features = model.classifier[-1].in_features\nmodel.classifier[-1] = nn.Linear(in_features, NUM_CLASSES)\nmodel = model.to(DEVICE)\n\n# -------------------- Loss / Optim / Scheduler / AMP --------------------\ncriterion = nn.CrossEntropyLoss(label_smoothing=CFG[\"LABEL_SMOOTH\"])\n\ndef pick_optimizer():\n    if CFG[\"OPTIM\"].lower() == \"auto\":\n        return \"adamw\" if CFG[\"PRETRAINED\"] else \"sgd\"\n    return CFG[\"OPTIM\"].lower()\n\nOPT = pick_optimizer()\nif OPT == \"adamw\":\n    optimizer = optim.AdamW(model.parameters(), lr=CFG[\"LR\"], weight_decay=CFG[\"WEIGHT_DECAY\"])\n    print(\"Using AdamW\")\nelif OPT == \"sgd\":\n    optimizer = optim.SGD(model.parameters(), lr=CFG[\"LR\"], momentum=CFG[\"MOMENTUM\"],\n                          weight_decay=CFG[\"WEIGHT_DECAY\"], nesterov=True)\n    print(\"Using SGD+Momentum\")\nelse:\n    raise ValueError(\"OPTIM must be 'auto' | 'adamw' | 'sgd'\")\n\n# Warmup (5e) → Cosine\nWARMUP_EPOCHS = CFG[\"WARMUP_EPOCHS\"]\ncosine_epochs = max(1, CFG[\"EPOCHS\"] - WARMUP_EPOCHS)\nwarmup = LinearLR(optimizer, start_factor=1e-2, end_factor=1.0, total_iters=WARMUP_EPOCHS)\ncosine = CosineAnnealingLR(optimizer, T_max=cosine_epochs)\nscheduler = SequentialLR(optimizer, schedulers=[warmup, cosine], milestones=[WARMUP_EPOCHS])\n\n# AMP (ưu tiên torch.amp; fallback cuda.amp nếu quá cũ)\ntry:\n    autocast = torch.amp.autocast\n    GradScaler = torch.amp.GradScaler\n    scaler = GradScaler('cuda', enabled=(DEVICE==\"cuda\"))\nexcept AttributeError:\n    autocast = torch.cuda.amp.autocast\n    GradScaler = torch.cuda.amp.GradScaler\n    scaler = GradScaler(enabled=(DEVICE==\"cuda\"))\n\n# -------------------- MixUp --------------------\ndef maybe_mixup(inputs, targets, alpha):\n    if alpha is None or alpha <= 0:\n        return inputs, targets, None, False\n    lam = np.random.beta(alpha, alpha)\n    idx = torch.randperm(inputs.size(0), device=inputs.device)\n    mixed = lam * inputs + (1 - lam) * inputs[idx]\n    return mixed, (targets, targets[idx], lam), lam, True\n\ndef apply_mix_criterion(criterion, pred, y_a, y_b, lam):\n    return lam * criterion(pred, y_a) + (1 - lam) * criterion(pred, y_b)\n\ndef current_mixup_alpha(epoch, total_epochs, base_alpha, phaseout_last):\n    # epoch: 1..EPOCHS\n    if base_alpha <= 0 or phaseout_last <= 0: return base_alpha\n    remain = total_epochs - epoch + 1\n    if remain > phaseout_last: return base_alpha\n    factor = max(0.0, (remain - 1) / max(1, phaseout_last - 1))  # tuyến tính về 0\n    return base_alpha * factor\n\n# -------------------- Metrics --------------------\ndef accuracy_from_logits(logits, targets):\n    preds = logits.argmax(1)\n    return (preds == targets).float().mean().item()\n\ndef topk_accuracy_from_logits(logits, targets, k=5):\n    topk = logits.topk(k, dim=1).indices\n    return topk.eq(targets.view(-1,1)).any(dim=1).float().mean().item()\n\ndef iou_from_confmat(C):\n    K = C.shape[0]; ious = []\n    for c in range(K):\n        TP = C[c, c]; FP = C[:, c].sum() - TP; FN = C[c, :].sum() - TP\n        den = TP + FP + FN\n        ious.append(float(\"nan\") if den == 0 else TP / den)\n    return np.array(ious)\n\n# -------------------- Train / Eval --------------------\ndef train_one_epoch(model, loader, epoch, total_epochs):\n    model.train()\n    total_loss = total_acc = n = 0\n    t0 = time.time()\n\n    alpha_now = current_mixup_alpha(epoch, total_epochs, CFG[\"MIXUP_ALPHA\"], CFG[\"MIXUP_PHASEOUT_EPOCHS\"])\n    for step, (images, labels) in enumerate(loader, 1):\n        images = images.to(DEVICE, non_blocking=True)\n        labels = labels.to(DEVICE, non_blocking=True)\n\n        optimizer.zero_grad(set_to_none=True)\n        inputs, tgt_mix, lam, mixed = maybe_mixup(images, labels, alpha_now)\n\n        with autocast('cuda', enabled=(DEVICE==\"cuda\")):\n            outputs = model(inputs)\n            if mixed:\n                y_a, y_b, lam_ = tgt_mix\n                loss = apply_mix_criterion(criterion, outputs, y_a, y_b, lam_)\n            else:\n                loss = criterion(outputs, labels)\n\n        scaler.scale(loss).backward()\n        if CFG[\"CLIP_GRAD_NORM\"] and CFG[\"CLIP_GRAD_NORM\"] > 0:\n            scaler.unscale_(optimizer)\n            nn.utils.clip_grad_norm_(model.parameters(), CFG[\"CLIP_GRAD_NORM\"])\n        scaler.step(optimizer); scaler.update()\n\n        bs = labels.size(0)\n        total_loss += loss.item() * bs\n        total_acc  += accuracy_from_logits(outputs.detach(), labels) * bs\n        n += bs\n\n        if step % CFG[\"LOG_EVERY\"] == 0 or step == len(loader):\n            elapsed = time.time() - t0\n            eta = elapsed / step * (len(loader) - step)\n            print(f\"  step {step}/{len(loader)} | loss {loss.item():.4f} | \"\n                  f\"elapsed {elapsed/60:.1f}m | ETA {eta/60:.1f}m | mixup α={alpha_now:.3f}\")\n\n    return total_loss / n, total_acc / n\n\n@torch.no_grad()\ndef evaluate(model, loader):\n    model.eval()\n    totL = totA1 = totA5 = n = 0\n    C = np.zeros((NUM_CLASSES, NUM_CLASSES), dtype=np.int64)\n    for images, labels in loader:\n        images = images.to(DEVICE, non_blocking=True)\n        labels = labels.to(DEVICE, non_blocking=True)\n        with autocast('cuda', enabled=(DEVICE==\"cuda\")):\n            outputs = model(images)\n            loss = criterion(outputs, labels)\n        bs = labels.size(0)\n        totL += loss.item() * bs\n        totA1 += accuracy_from_logits(outputs, labels) * bs\n        totA5 += topk_accuracy_from_logits(outputs, labels, k=5) * bs\n        n += bs\n        preds = outputs.argmax(1).cpu().numpy()\n        for p, t in zip(preds, labels.cpu().numpy()):\n            C[t, p] += 1\n    L = totL / n; A1 = totA1 / n; A5 = totA5 / n\n    ious = iou_from_confmat(C); miou = np.nanmean(ious); maxiou = np.nanmax(ious)\n    return L, A1, A5, ious, miou, maxiou, C\n\ndef show_two_samples(loader, model, title=\"Samples\"):\n    model.eval()\n    images, labels = next(iter(loader))\n    images = images.to(DEVICE)\n    with torch.no_grad():\n        with autocast('cuda', enabled=(DEVICE==\"cuda\")):\n            outputs = model(images)\n    preds = outputs.argmax(1).cpu().numpy()\n    labels_np = labels.numpy()\n    images_cpu = images.cpu()\n    mean = torch.tensor(IMAGENET_MEAN)[:,None,None]\n    std  = torch.tensor(IMAGENET_STD)[:,None,None]\n    idxc = idxw = None\n    for i in range(len(labels_np)):\n        if preds[i]==labels_np[i] and idxc is None: idxc=i\n        if preds[i]!=labels_np[i] and idxw is None: idxw=i\n        if idxc is not None and idxw is not None: break\n    idxs = [i for i in (idxc, idxw) if i is not None]\n    if not idxs:\n        print(\"No suitable samples in this batch.\"); return\n    for i in idxs:\n        img = torch.clamp(images_cpu[i]*std+mean, 0, 1)\n        plt.figure(); plt.imshow(np.transpose(img.numpy(), (1,2,0)))\n        plt.title(f\"{title} | pred={preds[i]} (wnid={idx_to_wnid.get(preds[i],'?')}) \"\n                  f\"vs true={labels_np[i]} (wnid={idx_to_wnid.get(labels_np[i],'?')})\")\n        plt.axis(\"off\"); plt.show()\n\n# -------------------- Train Loop --------------------\nhistory = {\"train_loss\":[], \"train_acc\":[], \"val_loss\":[], \"val_acc\":[], \"val_top5\":[], \"val_miou\":[], \"val_max_iou\":[]}\nbest_epoch, best_miou = 0, -1.0\n\nprint(f\"\\n[RUN] epochs={CFG['EPOCHS']}, LR={CFG['LR']}, WD={CFG['WEIGHT_DECAY']}, \"\n      f\"MixUp={CFG['MIXUP_ALPHA']} (phase-out {CFG['MIXUP_PHASEOUT_EPOCHS']}e), \"\n      f\"LS={CFG['LABEL_SMOOTH']}\")\n\nfor epoch in range(1, CFG[\"EPOCHS\"]+1):\n    print(f\"\\n===== Epoch {epoch}/{CFG['EPOCHS']} =====\")\n    t0 = time.time()\n    trL, trA = train_one_epoch(model, train_loader, epoch, CFG[\"EPOCHS\"])\n    vaL, vaA1, vaA5, vaIOUs, vaMIoU, vaMaxIoU, vaC = evaluate(model, val_loader)\n    scheduler.step()\n\n    history[\"train_loss\"].append(trL); history[\"train_acc\"].append(trA)\n    history[\"val_loss\"].append(vaL);   history[\"val_acc\"].append(vaA1)\n    history[\"val_top5\"].append(vaA5);  history[\"val_miou\"].append(vaMIoU); history[\"val_max_iou\"].append(vaMaxIoU)\n\n    dt = time.time() - t0\n    print(f\"Epoch {epoch} | {dt/60:.1f}m | \"\n          f\"Train L/A={trL:.4f}/{trA:.4f} | Val L/A1/A5={vaL:.4f}/{vaA1:.4f}/{vaA5:.4f} \"\n          f\"| mIoU {vaMIoU:.4f} maxIoU {vaMaxIoU:.4f}\")\n\n    if vaMIoU > best_miou:\n        best_miou, best_epoch = vaMIoU, epoch\n        torch.save({\"epoch\":epoch, \"model\":model.state_dict(), \"history\":history},\n                   \"best_vgg_fast_imagenet.pth\")\n        # thêm weights-only cho lần load sau thật sạch\n        torch.save(model.state_dict(), \"best_vgg_weights_only.pth\")\n        print(\"  → Saved new best (by val mIoU)\")\n\nprint(\"Best epoch:\", best_epoch, \"| best mIoU:\", best_miou)\n\n# -------------------- Load best & visualize --------------------\n# Cách 1: load full (PyTorch >= 2.6 cần weights_only=False)\nckpt = torch.load(\"best_vgg_fast_imagenet.pth\", map_location=DEVICE, weights_only=False)\nmodel.load_state_dict(ckpt[\"model\"])\nhistory = ckpt.get(\"history\", history)\nbe = ckpt[\"epoch\"]\nprint(\"Best epoch checkpoint:\", be)\n\n# (Tuỳ chọn) Cách 2: load weights-only (nếu muốn)\n# state = torch.load(\"best_vgg_weights_only.pth\", map_location=DEVICE, weights_only=True)\n# model.load_state_dict(state)\n\nval_loss, val_acc1, val_acc5, val_ious, val_miou, val_max_iou, _ = evaluate(model, val_loader)\nprint(f\"Validation | loss {val_loss:.4f} | Top1 {val_acc1:.4f} | Top5 {val_acc5:.4f} \"\n      f\"| mIoU {val_miou:.4f} | maxIoU {val_max_iou:.4f}\")\nshow_two_samples(val_loader, model, title=f\"Best Epoch {be} Samples\")\n\n# -------------------- Plots --------------------\nepochs = range(1, len(history[\"train_loss\"])+1)\nplt.figure(); plt.plot(epochs, history[\"train_loss\"], label=\"Train Loss\"); plt.plot(epochs, history[\"val_loss\"], label=\"Val Loss\")\nplt.xlabel(\"Epoch\"); plt.ylabel(\"Loss\"); plt.title(\"Learning Curve - Loss\"); plt.legend(); plt.show()\n\nplt.figure(); plt.plot(epochs, history[\"train_acc\"], label=\"Train Top-1\"); plt.plot(epochs, history[\"val_acc\"], label=\"Val Top-1\"); plt.plot(epochs, history[\"val_top5\"], label=\"Val Top-5\")\nplt.xlabel(\"Epoch\"); plt.ylabel(\"Accuracy\"); plt.title(\"Learning Curve - Accuracy\"); plt.legend(); plt.show()\n\nplt.figure(); plt.plot(epochs, history[\"val_miou\"], label=\"Val mIoU\")\nplt.xlabel(\"Epoch\"); plt.ylabel(\"mIoU\"); plt.title(\"Validation mIoU per Epoch\"); plt.legend(); plt.show()\n","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}