{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"codemirror_mode":{"name":"ipython","version":3},"file_extension":".py","mimetype":"text/x-python","name":"python","nbconvert_exporter":"python","pygments_lexer":"ipython3","version":"3.10.13"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceId":91196,"databundleVersionId":11565823,"sourceType":"competition"}],"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true},"papermill":{"default_parameters":{},"duration":35265.370426,"end_time":"2025-04-12T04:18:24.491981","environment_variables":{},"exception":null,"input_path":"__notebook__.ipynb","output_path":"__notebook__.ipynb","parameters":{},"start_time":"2025-04-11T18:30:39.121555","version":"2.5.0"}},"nbformat_minor":4,"nbformat":4,"cells":[{"id":"e76c598c","cell_type":"markdown","source":"## Simple baseline with Sentinel-2 data — ResNet-18 + Binary Cross Entropy\n\nThe occurrence of different types of organisms, whether plants or animals, is generally associated with the characteristics of the environment or ecosystem in which they live. This relationship between the presence of species and their habitat is often interdependent and can be affected by various factors, such as climate, which is another modality we provide.\n\nTo demonstrate the performance while using just the _image data_, i.e., Sentinel Image Patches, we provide a straightforward baseline that is based on a slighly modified ResNet-18 and Binary Cross Entropy. \nAs described above, the satellite patches provide an image-like modalities that captures habitats and other aspects of the locality.\n\nConsidering the significant extent for enhancing performance of this baseline, we encourage you to experiment with various techniques, architectures, losses, etc.\n\n#### **Have Fun!**","metadata":{"papermill":{"duration":0.004282,"end_time":"2025-04-11T18:30:41.945879","exception":false,"start_time":"2025-04-11T18:30:41.941597","status":"completed"},"tags":[]}},{"id":"55c27752","cell_type":"code","source":"import os\nimport torch\nimport tqdm\nimport rasterio\nimport numpy as np\nimport pandas as pd\nimport albumentations as A\nimport torchvision.models as models\nimport torchvision.transforms as transforms\nimport torch.nn as nn\nfrom PIL import Image\nfrom torch.utils.data import Dataset, DataLoader\nfrom torch.optim.lr_scheduler import CosineAnnealingLR\nfrom sklearn.metrics import precision_recall_fscore_support","metadata":{"ExecuteTime":{"end_time":"2024-04-30T21:25:07.29831Z","start_time":"2024-04-30T21:25:05.354584Z"},"execution":{"iopub.execute_input":"2025-04-11T18:30:41.955742Z","iopub.status.busy":"2025-04-11T18:30:41.955014Z","iopub.status.idle":"2025-04-11T18:30:51.632223Z","shell.execute_reply":"2025-04-11T18:30:51.631404Z"},"papermill":{"duration":9.684855,"end_time":"2025-04-11T18:30:51.634701","exception":false,"start_time":"2025-04-11T18:30:41.949846","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"e91674b2","cell_type":"markdown","source":"## Data description\n\nThe Sentinel-2 data was acquired through the Sentinel2 satellite program and pre-processed by [Ecodatacube](https://stac.ecodatacube.eu/) to produce raster files scaled to the entire European continent and projected into a unique CRS. \nEach TIFF file corresponds to a unique observation location (via \"surveyId\"). To load the patches for a selected observation, take the \"surveyId\" from any occurrence CSV and load it following this rule --> '…/CD/AB/XXXXABCD.jpeg'. For example, the image location for the surveyId 3018575 is \"./75/85/3018575.tiff\". For all \"surveyId\" with less than four digits, you can use a similar rule. For a \"surveyId\" 1 is \"./1/1.tiff\".\nThe data can simply be loaded using the following method:\n\n```python\ndef construct_patch_path(output_path, survey_id):\n    \"\"\"Construct the patch file path based on survey_id as './CD/AB/XXXXABCD.tiff'\"\"\"\n    path = output_path\n    for d in (str(survey_id)[-2:], str(survey_id)[-4:-2]):\n        path = os.path.join(path, d)\n\n    path = os.path.join(path, f\"{survey_id}.tiff\")\n\n    return path\n```\n\n**For more information about data processing, normalization, and visualization, please refer to the following notebook**: [Kaggle Notebook](https://www.kaggle.com/code/picekl/sentinel-2-data-processing-and-normalization).\n\n**References:**\n- *Traceability (lineage): The dataset was produced entirely by mosaicking and seasonally aggregating imagery from the Sentinel-2 Level-2A product (https://sentinels.copernicus.eu/web/sentinel/user-guides/sentinel-2-msi/product-types/level-2a)*\n- *Ecodatacube.eu: Analysis-ready open environmental data cube for Europe (https://doi.org/10.21203/rs.3.rs-2277090/v3)*","metadata":{"execution":{"iopub.execute_input":"2024-05-01T13:30:07.054038Z","iopub.status.busy":"2024-05-01T13:30:07.053659Z","iopub.status.idle":"2024-05-01T13:30:07.058148Z","shell.execute_reply":"2024-05-01T13:30:07.057269Z","shell.execute_reply.started":"2024-05-01T13:30:07.054008Z"},"papermill":{"duration":0.003839,"end_time":"2025-04-11T18:30:51.642722","exception":false,"start_time":"2025-04-11T18:30:51.638883","status":"completed"},"tags":[]}},{"id":"5213a77a","cell_type":"markdown","source":"## Prepare custom dataset loader\n\nWe have to slightly update the Dataset to provide the relevant data in the appropriate format.","metadata":{"papermill":{"duration":0.00379,"end_time":"2025-04-11T18:30:51.650496","exception":false,"start_time":"2025-04-11T18:30:51.646706","status":"completed"},"tags":[]}},{"id":"122e2fee","cell_type":"code","source":"def construct_patch_path(data_path, survey_id):\n    \"\"\"Construct the patch file path based on plot_id as './CD/AB/XXXXABCD.jpeg'\"\"\"\n    path = data_path\n    for d in (str(survey_id)[-2:], str(survey_id)[-4:-2]):\n        path = os.path.join(path, d)\n\n    path = os.path.join(path, f\"{survey_id}.tiff\")\n\n    return path\n\ndef quantile_normalize(band, low=2, high=98):\n    sorted_band = np.sort(band.flatten())\n    quantiles = np.percentile(sorted_band, np.linspace(low, high, len(sorted_band)))\n    normalized_band = np.interp(band.flatten(), sorted_band, quantiles).reshape(band.shape)\n    \n    min_val, max_val = np.min(normalized_band), np.max(normalized_band)\n    \n    # Prevent division by zero if min_val == max_val\n    if max_val == min_val:\n        return np.zeros_like(normalized_band, dtype=np.float32)  # Return an array of zeros\n\n    # Perform normalization (min-max scaling)\n    return ((normalized_band - min_val) / (max_val - min_val)).astype(np.float32)\n\nclass TrainDataset(Dataset):\n    def __init__(self, data_dir, metadata, transform=None):\n        self.transform = transform\n        self.data_dir = data_dir\n        self.metadata = metadata\n        self.metadata = self.metadata.dropna(subset=\"speciesId\").reset_index(drop=True)\n        self.metadata['speciesId'] = self.metadata['speciesId'].astype(int)\n        self.label_dict = self.metadata.groupby('surveyId')['speciesId'].apply(list).to_dict()\n        \n        self.metadata = self.metadata.drop_duplicates(subset=\"surveyId\").reset_index(drop=True)\n\n    def __len__(self):\n        return len(self.metadata)\n\n    def __getitem__(self, idx):\n        \n        survey_id = self.metadata.surveyId[idx]\n        species_ids = self.label_dict.get(survey_id, [])  # Get list of species IDs for the survey ID\n        label = torch.zeros(num_classes)  # Initialize label tensor\n        for species_id in species_ids:\n            label_id = species_id\n            label[label_id] = 1  # Set the corresponding class index to 1 for each species\n        \n        # Read TIFF files (multispectral bands)\n        tiff_path = construct_patch_path(self.data_dir, survey_id)\n        with rasterio.open(tiff_path) as dataset:\n            image = dataset.read(out_dtype=np.float32)  # Read all bands\n            image = np.array([quantile_normalize(band) for band in image])  # Apply quantile normalization\n\n        image = np.transpose(image, (1, 2, 0))  # Convert to HWC format\n        image = self.transform(image)\n\n        return image, label, survey_id\n    \nclass TestDataset(TrainDataset):\n    def __init__(self, data_dir, metadata, transform=None):\n        self.transform = transform\n        self.data_dir = data_dir\n        self.metadata = metadata\n        \n    def __getitem__(self, idx):\n        \n        survey_id = self.metadata.surveyId[idx]\n        \n        # Read TIFF files (multispectral bands)\n        tiff_path = construct_patch_path(self.data_dir, survey_id)\n        with rasterio.open(tiff_path) as dataset:\n            image = dataset.read(out_dtype=np.float32)  # Read all bands\n            image = np.array([quantile_normalize(band) for band in image])  # Apply quantile normalization\n\n        image = np.transpose(image, (1, 2, 0))  # Convert to HWC format\n        \n        image = self.transform(image)\n        return image, survey_id","metadata":{"ExecuteTime":{"end_time":"2024-04-30T21:25:32.627928Z","start_time":"2024-04-30T21:25:32.612131Z"},"collapsed":false,"execution":{"iopub.execute_input":"2025-04-11T18:30:51.660248Z","iopub.status.busy":"2025-04-11T18:30:51.659488Z","iopub.status.idle":"2025-04-11T18:30:51.674577Z","shell.execute_reply":"2025-04-11T18:30:51.673551Z"},"jupyter":{"outputs_hidden":false},"papermill":{"duration":0.022281,"end_time":"2025-04-11T18:30:51.676595","exception":false,"start_time":"2025-04-11T18:30:51.654314","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"201a02b1","cell_type":"markdown","source":"### Load metadata and prepare data loaders","metadata":{"papermill":{"duration":0.003709,"end_time":"2025-04-11T18:30:51.684240","exception":false,"start_time":"2025-04-11T18:30:51.680531","status":"completed"},"tags":[]}},{"id":"5b5fc9db","cell_type":"code","source":"# Dataset and DataLoader\nbatch_size = 32\n\ntransform = transforms.Compose([\n    transforms.ToTensor(),\n    transforms.Normalize(mean=(0.5, 0.5, 0.5, 0.5), std=(0.5, 0.5, 0.5, 0.5)),\n])\n\n# Load Training metadata\ntrain_data_path = \"/kaggle/input/geolifeclef-2025/SatelitePatches/PA-train\"\ntrain_metadata_path = \"/kaggle/input/geolifeclef-2025/GLC25_PA_metadata_train.csv\"\ntrain_metadata = pd.read_csv(train_metadata_path)\ntrain_dataset = TrainDataset(train_data_path, train_metadata, transform=transform)\ntrain_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True, num_workers=4)\n\n# Load Test metadata\ntest_data_path = \"/kaggle/input/geolifeclef-2025/SatelitePatches/PA-test/\"\ntest_metadata_path = \"/kaggle/input/geolifeclef-2025/GLC25_PA_metadata_test.csv\"\ntest_metadata = pd.read_csv(test_metadata_path)\ntest_dataset = TestDataset(test_data_path, test_metadata, transform=transform)\ntest_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False, num_workers=4)","metadata":{"ExecuteTime":{"end_time":"2024-04-30T21:25:34.532017Z","start_time":"2024-04-30T21:25:32.615562Z"},"collapsed":false,"execution":{"iopub.execute_input":"2025-04-11T18:30:51.693628Z","iopub.status.busy":"2025-04-11T18:30:51.693293Z","iopub.status.idle":"2025-04-11T18:30:57.431246Z","shell.execute_reply":"2025-04-11T18:30:57.429814Z"},"jupyter":{"outputs_hidden":false},"papermill":{"duration":5.745402,"end_time":"2025-04-11T18:30:57.433545","exception":false,"start_time":"2025-04-11T18:30:51.688143","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"757e8b59","cell_type":"markdown","source":"## Modify pretrained ResNet-18 model\n\nTo fully use all the R,G,B and NIR channels, we have to modify the input layer of the standard ResNet-18.\nThat is all :)","metadata":{"papermill":{"duration":0.004179,"end_time":"2025-04-11T18:30:57.441900","exception":false,"start_time":"2025-04-11T18:30:57.437721","status":"completed"},"tags":[]}},{"id":"877d7197","cell_type":"code","source":"# Check if cuda is available\ndevice = torch.device(\"cpu\")\n\nif torch.cuda.is_available():\n    device = torch.device(\"cuda\")\n    print(\"DEVICE = CUDA\")\n\n# Hyperparameters\nlearning_rate = 0.0001\nnum_epochs = 25\npositive_weigh_factor = 1.0\nnum_classes = 11255 # Number of all unique classes within the PO and PA data.","metadata":{"ExecuteTime":{"end_time":"2024-04-30T21:25:31.014067Z","start_time":"2024-04-30T21:25:31.01006Z"},"collapsed":false,"execution":{"iopub.execute_input":"2025-04-11T18:30:57.450727Z","iopub.status.busy":"2025-04-11T18:30:57.450448Z","iopub.status.idle":"2025-04-11T18:30:57.528529Z","shell.execute_reply":"2025-04-11T18:30:57.527509Z"},"jupyter":{"outputs_hidden":false},"papermill":{"duration":0.084742,"end_time":"2025-04-11T18:30:57.530654","exception":false,"start_time":"2025-04-11T18:30:57.445912","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"85a7f756","cell_type":"code","source":"model = models.resnet18(weights=\"IMAGENET1K_V1\")\nmodel.conv1 = nn.Conv2d(4, 64, kernel_size=(7, 7), stride=(2, 2))\nmodel.fc = nn.Linear(in_features=512, out_features=num_classes, bias=True)\nmodel.to(device)\n\noptimizer = torch.optim.AdamW(model.parameters(), lr=learning_rate)\nscheduler = CosineAnnealingLR(optimizer, T_max=25, verbose=True)","metadata":{"execution":{"iopub.execute_input":"2025-04-11T18:30:57.539829Z","iopub.status.busy":"2025-04-11T18:30:57.539525Z","iopub.status.idle":"2025-04-11T18:30:58.466528Z","shell.execute_reply":"2025-04-11T18:30:58.465645Z"},"papermill":{"duration":0.933587,"end_time":"2025-04-11T18:30:58.468395","exception":false,"start_time":"2025-04-11T18:30:57.534808","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"b7946956","cell_type":"code","source":"def set_seed(seed):\n    # Set seed for Python's built-in random number generator\n    torch.manual_seed(seed)\n    # Set seed for numpy\n    np.random.seed(seed)\n    # Set seed for CUDA if available\n    if torch.cuda.is_available():\n        torch.cuda.manual_seed_all(seed)\n        # Set cuDNN's random number generator seed for deterministic behavior\n        torch.backends.cudnn.deterministic = True\n        torch.backends.cudnn.benchmark = False\n\nset_seed(77)","metadata":{"execution":{"iopub.execute_input":"2025-04-11T18:30:58.478049Z","iopub.status.busy":"2025-04-11T18:30:58.477780Z","iopub.status.idle":"2025-04-11T18:30:58.482847Z","shell.execute_reply":"2025-04-11T18:30:58.482065Z"},"papermill":{"duration":0.011755,"end_time":"2025-04-11T18:30:58.484569","exception":false,"start_time":"2025-04-11T18:30:58.472814","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"1dd18b4b","cell_type":"markdown","source":"## Training Loop\n\nNothing special, just a standard Pytorch training loop.","metadata":{"papermill":{"duration":0.003826,"end_time":"2025-04-11T18:30:58.492394","exception":false,"start_time":"2025-04-11T18:30:58.488568","status":"completed"},"tags":[]}},{"id":"146d3632","cell_type":"code","source":"print(f\"Training for {num_epochs} epochs started.\")\n\nfor epoch in range(num_epochs):\n    model.train()\n    for batch_idx, (data, targets, _) in enumerate(train_loader):\n\n        data = data.to(device)\n        targets = targets.to(device)\n\n        optimizer.zero_grad()\n        outputs = model(data)\n\n        pos_weight = targets*positive_weigh_factor  # All positive weights are equal to 10\n        criterion = torch.nn.BCEWithLogitsLoss(pos_weight=pos_weight)\n        loss = criterion(outputs, targets)\n\n        loss.backward()\n        optimizer.step()\n\n        if batch_idx % 348 == 0:\n            print(f\"Epoch {epoch+1}/{num_epochs}, Batch {batch_idx}/{len(train_loader)}, Loss: {loss.item()}\")\n\n    scheduler.step()\n    print(\"Scheduler:\",scheduler.state_dict())\n\n# Save the trained model\nmodel.eval()\ntorch.save(model.state_dict(), \"resnet18-with-sentinel2.pth\")","metadata":{"ExecuteTime":{"start_time":"2024-04-30T21:25:34.536634Z"},"collapsed":false,"execution":{"iopub.execute_input":"2025-04-11T18:30:58.502389Z","iopub.status.busy":"2025-04-11T18:30:58.501748Z","iopub.status.idle":"2025-04-12T04:14:09.901308Z","shell.execute_reply":"2025-04-12T04:14:09.900211Z"},"is_executing":true,"jupyter":{"outputs_hidden":false},"papermill":{"duration":34991.420477,"end_time":"2025-04-12T04:14:09.917095","exception":false,"start_time":"2025-04-11T18:30:58.496618","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"994c12d7","cell_type":"markdown","source":"## Test Loop\n\nAgain, nothing special, just a standard inference.","metadata":{"papermill":{"duration":0.013546,"end_time":"2025-04-12T04:14:09.944423","exception":false,"start_time":"2025-04-12T04:14:09.930877","status":"completed"},"tags":[]}},{"id":"65a38793","cell_type":"code","source":"with torch.no_grad():\n    all_predictions = []\n    surveys = []\n    top_k_indices = None\n    for data, surveyID in tqdm.tqdm(test_loader, total=len(test_loader)):\n\n        data = data.to(device)\n        \n        outputs = model(data)\n        predictions = torch.sigmoid(outputs).cpu().numpy()\n\n        # Sellect top-25 values as predictions\n        top_25 = np.argsort(-predictions, axis=1)[:, :25] \n        if top_k_indices is None:\n            top_k_indices = top_25\n        else:\n            top_k_indices = np.concatenate((top_k_indices, top_25), axis=0)\n\n        surveys.extend(surveyID.cpu().numpy())","metadata":{"collapsed":false,"execution":{"iopub.execute_input":"2025-04-12T04:14:09.972328Z","iopub.status.busy":"2025-04-12T04:14:09.971988Z","iopub.status.idle":"2025-04-12T04:18:21.383060Z","shell.execute_reply":"2025-04-12T04:18:21.381992Z"},"jupyter":{"outputs_hidden":false},"papermill":{"duration":251.427356,"end_time":"2025-04-12T04:18:21.384873","exception":false,"start_time":"2025-04-12T04:14:09.957517","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"67773ed5","cell_type":"markdown","source":"## Save prediction file! 🎉🥳🙌🤗","metadata":{"papermill":{"duration":0.026488,"end_time":"2025-04-12T04:18:21.438850","exception":false,"start_time":"2025-04-12T04:18:21.412362","status":"completed"},"tags":[]}},{"id":"50ff556b","cell_type":"code","source":"data_concatenated = [' '.join(map(str, row)) for row in top_k_indices]\n\npd.DataFrame(\n    {'surveyId': surveys,\n     'predictions': data_concatenated,\n    }).to_csv(\"submission.csv\", index = False)","metadata":{"execution":{"iopub.execute_input":"2025-04-12T04:18:21.495048Z","iopub.status.busy":"2025-04-12T04:18:21.494708Z","iopub.status.idle":"2025-04-12T04:18:21.673108Z","shell.execute_reply":"2025-04-12T04:18:21.672421Z"},"papermill":{"duration":0.209123,"end_time":"2025-04-12T04:18:21.675144","exception":false,"start_time":"2025-04-12T04:18:21.466021","status":"completed"},"tags":[]},"outputs":[],"execution_count":null}]}