{"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\nimport numpy as np # linear algebra\nimport 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\nimport os\nfor 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\nimport 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-09-02T19:30:26.642523Z","iopub.execute_input":"2026-09-02T19:30:26.643212Z","iopub.status.idle":"2026-09-02T19:30:27.133599Z","shell.execute_reply.started":"2026-09-02T19:30:26.643181Z","shell.execute_reply":"2026-09-02T19:30:27.132990Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import tifffile as tiff\nimport os\nimport matplotlib.pyplot as plt\n\npath = r\"/kaggle/input/competitions/beyond-visible-spectrum-ai-for-agriculture-P1/train/Health\"\nfile = os.listdir(path)[1]\n\nimg = tiff.imread(os.path.join(path, file))\n\nprint(\"Shape:\", img.shape)\nprint(\"Dtype:\", img.dtype)\nprint(\"Min:\", img.min())\nprint(\"Max:\", img.max())\n\nplt.imshow(img[:,:,100])\nplt.colorbar()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-02T19:57:18.263584Z","iopub.execute_input":"2026-09-02T19:57:18.264199Z","iopub.status.idle":"2026-09-02T19:57:18.439382Z","shell.execute_reply.started":"2026-09-02T19:57:18.264167Z","shell.execute_reply":"2026-09-02T19:57:18.438730Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport numpy as np\nimport tifffile as tiff\nimport torch\nfrom torch.utils.data import Dataset\n\nclass HSIDataset(Dataset):\n    def __init__(self, root_dir, classes=[\"Health\",\"Rust\",\"Other\"]):\n        self.samples = []\n        self.class_to_idx = {\n            \"Health\":0,\n            \"Rust\":1,\n            \"Other\":2\n        }\n\n        for class_name in classes:\n            class_path = os.path.join(root_dir, class_name)\n\n            if not os.path.exists(class_path):\n                print(f\"[Warning]: {class_path} doesnt exist\")\n                continue\n\n            for file in os.listdir(class_path):\n                if file.endswith(\".tif\") or file.endswith(\".tiff\"):\n                    file_path = os.path.join(class_path, file)\n                    self.samples.append(\n                        (file_path, self.class_to_idx[class_name])\n                    )\n\n        print(f\"[Loaded]: {len(self.samples)} samples from {root_dir}\")\n\n    def __len__(self):\n        return len(self.samples)\n\n    def __getitem__(self, idx):\n        file_path, label = self.samples[idx]\n\n        img = tiff.imread(file_path)\n        img = img.astype(np.float32)\n\n        # print(f\"[Shape]: {img.shape} | H W B\")\n\n        if img.ndim == 3:\n\n            if img.shape[-1] <= 126:\n                img = np.transpose(img, (2, 0, 1))\n\n        if img.shape[0] > 125:\n            img = img[:125]\n        elif img.shape[0] < 125:\n            padding = np.zeros(\n                (125 - img.shape[0], img.shape[1], img.shape[2]),\n                dtype=np.float32\n            )\n            img = np.concatenate((img, padding), axis=0)\n        \n        img_min = img.min()\n        img_max = img.max()\n\n        img = (img - img_min) / (img_max - img_min + 1e-8)\n           \n        img = torch.tensor(img, dtype=torch.float32)\n        label = torch.tensor(label, dtype=torch.long)\n\n        return img, label","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-02T19:57:18.514669Z","iopub.execute_input":"2026-09-02T19:57:18.515566Z","iopub.status.idle":"2026-09-02T19:57:18.525657Z","shell.execute_reply.started":"2026-09-02T19:57:18.515527Z","shell.execute_reply":"2026-09-02T19:57:18.524639Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch \nimport torch.nn as nn\n\nclass ResNetBlock(nn.Module):\n    def __init__(self, in_channels, out_channels):\n        super(ResNetBlock, self).__init__()\n\n        self.conv1, self.bn1 = nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=1, padding=0), nn.BatchNorm2d(out_channels)\n        self.conv2, self.bn2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, stride=1, padding=1), nn.BatchNorm2d(out_channels)\n        self.conv3, self.bn3 = nn.Conv2d(out_channels, out_channels, kernel_size=1, stride=1, padding=0), nn.BatchNorm2d(out_channels)\n        self.relu = nn.ReLU()\n\n        if in_channels != out_channels:\n            self.shortcut = nn.Sequential(\n                nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=1),\n                nn.BatchNorm2d(out_channels)\n            )\n        else:\n            self.shortcut = nn.Identity()\n\n    def forward(self, x):\n        i = self.shortcut(x)\n\n        out = self.relu(x)\n        \n        out = self.conv1(out)\n        out = self.bn1(out)\n        out = self.relu(out)\n\n        out = self.conv2(out)\n        out = self.bn2(out)\n        out = self.relu(out)\n\n        out = self.conv3(out)\n        out = self.bn3(out)\n        out = out + i\n        out = self.relu(out)\n\n        return out\n        ","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-02T19:57:18.697614Z","iopub.execute_input":"2026-09-02T19:57:18.697872Z","iopub.status.idle":"2026-09-02T19:57:18.705852Z","shell.execute_reply.started":"2026-09-02T19:57:18.697851Z","shell.execute_reply":"2026-09-02T19:57:18.704822Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class HSIResNet(nn.Module):\n    def __init__(self, input_channels, num_classes = 3):\n        super(HSIResNet, self).__init__()\n\n        self.initial_conv = nn.Sequential(\n            nn.Conv2d(input_channels, 32, kernel_size=3, padding=1),\n            nn.BatchNorm2d(32),\n            nn.ReLU()\n        )\n        self.block1 = ResNetBlock(32, 32)\n        self.block2 = ResNetBlock(32, 64)\n        self.block3 = ResNetBlock(64, 128)\n        self.block4 = ResNetBlock(128, 256)\n\n        self.avg_pool = nn.AdaptiveAvgPool2d((1, 1))\n        self.classifier = nn.Linear(256, num_classes)\n\n    def forward(self, x):\n        x = self.initial_conv(x)\n        \n        x = self.block1(x)\n        x = self.block2(x)\n        x = self.block3(x)\n        x = self.block4(x)\n\n        x = self.avg_pool(x)\n        x = torch.flatten(x, 1)\n        x = self.classifier(x)\n\n        return x","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-02T19:57:18.849753Z","iopub.execute_input":"2026-09-02T19:57:18.850168Z","iopub.status.idle":"2026-09-02T19:57:18.856755Z","shell.execute_reply.started":"2026-09-02T19:57:18.850140Z","shell.execute_reply":"2026-09-02T19:57:18.855995Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Train","metadata":{}},{"cell_type":"code","source":"import torch \nimport torch.nn as nn\nimport torch.optim as optim\n\nfrom torch.utils.data import DataLoader, Subset\nfrom sklearn.model_selection import train_test_split\n\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(f\"[Device]: {device}\")\n\nTRAIN_PATH = r\"/kaggle/input/competitions/beyond-visible-spectrum-ai-for-agriculture-P1/train\"\nVAL_PATH = r\"/kaggle/input/competitions/beyond-visible-spectrum-ai-for-agriculture-P1/val/val\"\nBATCH_SIZE = 16\n\nfull_ds = HSIDataset(TRAIN_PATH, classes=[\"Health\", \"Rust\", \"Other\"])\nprint(f\"Length of full ds = {len(full_ds)}\")\n\nlabels = [ label for _, label in full_ds.samples ]\n\ninds = list(range(len(full_ds)))\n\ntrain_inds, val_inds = train_test_split(inds, test_size=0.20, random_state=42, stratify=labels)\n\ntrain_ds = Subset(full_ds,train_inds)\nval_ds = Subset(full_ds,val_inds)\n\nprint(f\"Training samples:   {len(train_ds)}\")\nprint(f\"Validation samples: {len(val_ds)}\")\n\n\ntrain_ld = DataLoader(train_ds, batch_size=BATCH_SIZE, shuffle=True, num_workers=0, pin_memory=True)\nval_ld = DataLoader(val_ds, batch_size=BATCH_SIZE, shuffle=False, num_workers=0, pin_memory=True)\n\nsample_img, sample_label = full_ds[0]\ninp_channels = sample_img.shape[0]\nprint(\"== Input Shape:\", sample_img.shape)\nprint(\"== Spectral Bands:\", inp_channels)\nprint(\"== Sample Label:\", sample_label)\n\nmodel = HSIResNet(input_channels=inp_channels, num_classes=3).to(device)\ncriterion = nn.CrossEntropyLoss()\noptimizer = optim.Adam(model.parameters(), lr=0.0001)\n\nEPOCHS = 30\nbest_val_acc = 0\n\nfor epoch in range(EPOCHS):\n    model.train()\n\n    running_loss = 0\n    correct = 0\n    total = 0\n\n    for imgs, labels in train_ld:\n        imgs = imgs.to(device)\n        labels = labels.to(device)\n\n        optimizer.zero_grad()\n        outputs = model(imgs)\n        loss = criterion(outputs, labels)\n        loss.backward()\n        optimizer.step()\n\n        running_loss += loss.item()\n        _, predict = torch.max(outputs, dim=1)\n        total += labels.size(0)\n\n        correct += (predict == labels).sum().item()\n\n    train_acc = (100 * correct / total)\n    train_loss = running_loss / len(train_ld)\n\n    model.eval()\n\n    val_running_loss = 0\n    val_correct = 0\n    val_total = 0\n\n    with torch.no_grad():\n        for imgs, labels in val_ld:\n            imgs = imgs.to(device)\n            labels = labels.to(device)\n\n            outputs = model(imgs)\n            loss = criterion(outputs, labels)\n\n            val_running_loss += loss.item()\n\n            _, predict = torch.max(outputs, dim=1)\n\n            val_total += labels.size(0)\n            val_correct += (predict == labels).sum().item()\n\n    val_acc = (100 * val_correct / val_total)\n    val_loss = val_running_loss / len(val_ld)\n\n    print(f\"Epoch: [{epoch+1}/{EPOCHS}] == Train Loss: {train_loss:.4f} == Train Acc: {train_acc:.2f}% == Val Loss: {val_loss:.4f} == Val Acc: {val_acc:.2f}%\")\n\n    if val_acc > best_val_acc:\n        best_val_acc = val_acc\n        torch.save(model.state_dict(), \"best_hyperspectral_resnet.pth\")\n\ntorch.save(model.state_dict(), \"hyperspectral_resnet.pth\")\nprint(\"[SAVED] Model\")\nprint(f\"[BEST VAL ACC]: {best_val_acc:.2f}%\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-02T19:57:23.243153Z","iopub.execute_input":"2026-09-02T19:57:23.243417Z","iopub.status.idle":"2026-09-02T20:00:08.627806Z","shell.execute_reply.started":"2026-09-02T19:57:23.243394Z","shell.execute_reply":"2026-09-02T20:00:08.626973Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from collections import Counter\n\ndataset_labels = [label for _, label in full_ds.samples]\n\nprint(Counter(dataset_labels))\n\nfor class_name, class_idx in full_ds.class_to_idx.items():\n    print(class_name, \":\", dataset_labels.count(class_idx))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-02T20:01:33.494510Z","iopub.execute_input":"2026-09-02T20:01:33.495342Z","iopub.status.idle":"2026-09-02T20:01:33.500650Z","shell.execute_reply.started":"2026-09-02T20:01:33.495309Z","shell.execute_reply":"2026-09-02T20:01:33.499999Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nfrom sklearn.metrics import confusion_matrix, classification_report\n\nmodel.load_state_dict(\n    torch.load(\"best_hyperspectral_resnet.pth\", map_location=device)\n)\n\nmodel.eval()\n\nall_preds = []\nall_labels = []\n\nwith torch.no_grad():\n\n    for imgs, labels in val_ld:\n\n        imgs = imgs.to(device)\n\n        outputs = model(imgs)\n\n        _, preds = torch.max(outputs, dim=1)\n\n        all_preds.extend(preds.cpu().numpy())\n        all_labels.extend(labels.numpy())\n\nprint(\"Confusion Matrix:\")\nprint(confusion_matrix(all_labels, all_preds))\n\nprint(\"\\nClassification Report:\")\nprint(classification_report(\n    all_labels,\n    all_preds,\n    target_names=[\"Health\", \"Rust\", \"Other\"]\n))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-02T20:01:50.328046Z","iopub.execute_input":"2026-09-02T20:01:50.328495Z","iopub.status.idle":"2026-09-02T20:01:51.254370Z","shell.execute_reply.started":"2026-09-02T20:01:50.328466Z","shell.execute_reply":"2026-09-02T20:01:51.253651Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}