{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.11","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":91844,"databundleVersionId":11361821,"sourceType":"competition"}],"dockerImageVersionId":31012,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"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","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-04-12T21:56:29.926938Z","iopub.execute_input":"2025-04-12T21:56:29.927221Z","iopub.status.idle":"2025-04-12T21:56:29.931403Z","shell.execute_reply.started":"2025-04-12T21:56:29.927199Z","shell.execute_reply":"2025-04-12T21:56:29.930534Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Proprocess metadata","metadata":{}},{"cell_type":"code","source":"import pandas as pd\nimport ast\n\nmetadata = pd.read_csv(\"/kaggle/input/birdclef-2025/train.csv\")\nmetadata.head()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-20T10:44:59.808784Z","iopub.execute_input":"2025-04-20T10:44:59.808941Z","iopub.status.idle":"2025-04-20T10:45:00.252243Z","shell.execute_reply.started":"2025-04-20T10:44:59.808926Z","shell.execute_reply":"2025-04-20T10:45:00.251445Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def parse_secondary_labels(val):\n    if isinstance(val, str):\n        parsed = ast.literal_eval(val)\n        if isinstance(parsed, list):\n            return [s for s in parsed if s] \n    return [] ","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-20T10:45:00.252989Z","iopub.execute_input":"2025-04-20T10:45:00.253247Z","iopub.status.idle":"2025-04-20T10:45:00.257804Z","shell.execute_reply.started":"2025-04-20T10:45:00.253228Z","shell.execute_reply":"2025-04-20T10:45:00.256884Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"metadata[\"secondary_labels_list\"] = metadata[\"secondary_labels\"].apply(parse_secondary_labels)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-20T10:45:00.259590Z","iopub.execute_input":"2025-04-20T10:45:00.259812Z","iopub.status.idle":"2025-04-20T10:45:00.498922Z","shell.execute_reply.started":"2025-04-20T10:45:00.259796Z","shell.execute_reply":"2025-04-20T10:45:00.498164Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\n\ntaxonomy = pd.read_csv(\"/kaggle/input/birdclef-2025/taxonomy.csv\")\n\nlabel_list = taxonomy['primary_label'].tolist()\nlabel_to_idx = {label: idx for idx, label in enumerate(label_list)}\nidx_to_label = {idx: label for label, idx in label_to_idx.items()}\n\nprint(\"Total classes:\", len(label_list))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-20T10:45:00.499632Z","iopub.execute_input":"2025-04-20T10:45:00.499863Z","iopub.status.idle":"2025-04-20T10:45:00.510803Z","shell.execute_reply.started":"2025-04-20T10:45:00.499841Z","shell.execute_reply":"2025-04-20T10:45:00.510098Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"metadata[\"parsed_labels\"] = metadata.apply(\n    lambda row: list(set([row[\"primary_label\"]] + row[\"secondary_labels_list\"])),\n    axis=1\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-20T10:45:00.511601Z","iopub.execute_input":"2025-04-20T10:45:00.511856Z","iopub.status.idle":"2025-04-20T10:45:00.764122Z","shell.execute_reply.started":"2025-04-20T10:45:00.511834Z","shell.execute_reply":"2025-04-20T10:45:00.763392Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Create dataset","metadata":{}},{"cell_type":"code","source":"import os\nimport torch\nimport pandas as pd\nfrom pathlib import Path\nimport numpy as np\n\nimport librosa\nfrom torch.utils.data import Dataset, DataLoader\nfrom sklearn.metrics import roc_auc_score","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-21T11:15:44.505641Z","iopub.execute_input":"2025-04-21T11:15:44.506443Z","iopub.status.idle":"2025-04-21T11:15:49.219060Z","shell.execute_reply.started":"2025-04-21T11:15:44.506405Z","shell.execute_reply":"2025-04-21T11:15:49.218282Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"T = 1 + int((32000 * 5 - 2048) / 512)\nT","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-21T11:14:00.918332Z","iopub.execute_input":"2025-04-21T11:14:00.918577Z","iopub.status.idle":"2025-04-21T11:14:00.923913Z","shell.execute_reply.started":"2025-04-21T11:14:00.918560Z","shell.execute_reply":"2025-04-21T11:14:00.922983Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class BirdCLEFDatasetOnTheFly(Dataset):\n    def __init__(self, df, label_to_idx, audio_root, sr=32000, n_mels=128, target_len=309, random_crop=False):\n        self.df = df.reset_index(drop=True)\n        self.label_to_idx = label_to_idx\n        self.audio_root = audio_root\n        self.sr = sr\n        self.n_mels = n_mels\n        self.target_len = target_len\n        self.random_crop = random_crop\n\n    def __len__(self):\n        return len(self.df)\n\n\n    def audio_to_logmelspec(self, path):\n        y, _ = librosa.load(path, sr=self.sr, mono=True)\n\n        if self.random_crop:\n            segment_samples = int(self.sr * 5.0)\n            if len(y) > segment_samples:\n                max_start = len(y) - segment_samples\n                start = np.random.randint(0, max_start)\n                y = y[start:start + segment_samples]\n            else:\n                y = np.pad(y, (0, max(0, segment_samples - len(y))))\n\n        mel = librosa.feature.melspectrogram(\n            y=y,\n            sr=self.sr,\n            n_fft=2048,\n            hop_length=512,\n            n_mels=self.n_mels,\n            fmin=20,\n            fmax=16000\n        )\n        logmel = librosa.power_to_db(mel).astype(np.float32)\n        return logmel\n\n    def pad_or_crop(self, logmel):\n        _, t = logmel.shape\n        if t < self.target_len:\n            pad_width = self.target_len - t\n            logmel = np.pad(logmel, ((0, 0), (0, pad_width)), mode='constant')\n        else:\n            logmel = logmel[:, :self.target_len]\n        return logmel\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        \n        # Путь к аудиофайлу\n        audio_path = os.path.join(self.audio_root, row[\"filename\"])\n        \n        # Генерация log-mel на лету\n        logmel = self.audio_to_logmelspec(audio_path)\n        logmel = self.pad_or_crop(logmel)\n        mel_tensor = torch.tensor(logmel, dtype=torch.float32).unsqueeze(0)  # [1, 128, T]\n\n        # Multi-hot вектор\n        label_vec = torch.zeros(len(self.label_to_idx))\n        for label in row[\"parsed_labels\"]:\n            if label in self.label_to_idx:\n                label_vec[self.label_to_idx[label]] = 1.0\n\n        return mel_tensor, label_vec","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-21T11:15:37.759728Z","iopub.execute_input":"2025-04-21T11:15:37.760342Z","iopub.status.idle":"2025-04-21T11:15:37.831052Z","shell.execute_reply.started":"2025-04-21T11:15:37.760312Z","shell.execute_reply":"2025-04-21T11:15:37.829934Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# x, y = dataset[17]\n# print(x.shape)  # [1, 128, 1312]\n# print(y.sum())  # >= 1","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-20T10:45:04.680877Z","iopub.execute_input":"2025-04-20T10:45:04.681088Z","iopub.status.idle":"2025-04-20T10:45:04.697056Z","shell.execute_reply.started":"2025-04-20T10:45:04.681073Z","shell.execute_reply":"2025-04-20T10:45:04.696215Z"},"jupyter":{"source_hidden":true}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Split train val","metadata":{}},{"cell_type":"code","source":"!pip install -q iterative-stratification","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-20T10:45:04.699612Z","iopub.execute_input":"2025-04-20T10:45:04.699872Z","iopub.status.idle":"2025-04-20T10:45:08.796193Z","shell.execute_reply.started":"2025-04-20T10:45:04.699844Z","shell.execute_reply":"2025-04-20T10:45:08.795344Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from iterstrat.ml_stratifiers import MultilabelStratifiedKFold\nimport numpy as np\nimport pandas as pd\n\ndef multilabel_stratified_train_test_split(df, label_to_idx, test_size=0.2, random_state=42):\n    \"\"\"\n    Аналог train_test_split, но с учётом мульти-классовой разметки.\n    \n    Аргументы:\n        df (pd.DataFrame): датафрейм, содержащий колонку 'parsed_labels'\n        label_to_idx (dict): словарь соответствия меток → индексы\n        test_size (float): доля от выборки в тест (валидацию)\n        random_state (int): seed для воспроизводимости\n\n    Возвращает:\n        train_df, val_df — сбалансированные подмножества\n    \"\"\"\n    # Создаём multi-hot матрицу\n    num_samples = len(df)\n    num_classes = len(label_to_idx)\n    y_multihot = np.zeros((num_samples, num_classes))\n\n    for i, label_list in enumerate(df[\"parsed_labels\"]):\n        for label in label_list:\n            if label in label_to_idx:\n                y_multihot[i, label_to_idx[label]] = 1\n\n    # Вычисляем нужное количество фолдов, чтобы получить нужный test_size\n    n_splits = int(1 / test_size)\n    if not 0 < test_size < 1:\n        raise ValueError(\"test_size должен быть между 0 и 1\")\n\n    # Разбиваем только один раз (fold==0)\n    mskf = MultilabelStratifiedKFold(n_splits=n_splits, shuffle=True, random_state=random_state)\n    train_idx, val_idx = next(mskf.split(df, y_multihot))\n\n    train_df = df.iloc[train_idx].reset_index(drop=True)\n    val_df = df.iloc[val_idx].reset_index(drop=True)\n\n    return train_df, val_df","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-20T10:45:08.797329Z","iopub.execute_input":"2025-04-20T10:45:08.797630Z","iopub.status.idle":"2025-04-20T10:45:08.821829Z","shell.execute_reply.started":"2025-04-20T10:45:08.797603Z","shell.execute_reply":"2025-04-20T10:45:08.820972Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# subset_df = metadata.sample(frac=0.1, random_state=42).reset_index(drop=True)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-20T10:45:08.822787Z","iopub.execute_input":"2025-04-20T10:45:08.823527Z","iopub.status.idle":"2025-04-20T10:45:08.826491Z","shell.execute_reply.started":"2025-04-20T10:45:08.823502Z","shell.execute_reply":"2025-04-20T10:45:08.825866Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_df, val_df = multilabel_stratified_train_test_split(metadata, label_to_idx, test_size=0.2)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-20T10:45:08.827174Z","iopub.execute_input":"2025-04-20T10:45:08.827508Z","iopub.status.idle":"2025-04-20T10:45:11.890772Z","shell.execute_reply.started":"2025-04-20T10:45:08.827489Z","shell.execute_reply":"2025-04-20T10:45:11.890190Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_filenames = set(train_df[\"filename\"])\nval_filenames = set(val_df[\"filename\"])\n\nintersection = train_filenames & val_filenames\nprint(f\"🔍 Пересечений по filename: {len(intersection)}\")\nif intersection:\n    print(\"Примеры совпадений:\", list(intersection)[:5])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-20T10:45:11.891488Z","iopub.execute_input":"2025-04-20T10:45:11.891706Z","iopub.status.idle":"2025-04-20T10:45:11.901136Z","shell.execute_reply.started":"2025-04-20T10:45:11.891689Z","shell.execute_reply":"2025-04-20T10:45:11.900354Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"cols_to_check = [\"collection\", \"author\", \"latitude\", \"longitude\"]\n\nfor col in cols_to_check:\n    if col in train_df.columns:\n        train_vals = set(train_df[col].dropna())\n        val_vals = set(val_df[col].dropna())\n        common = train_vals & val_vals\n        print(f\"🔍 {col}: {len(common)} общих значений\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-20T10:45:11.901901Z","iopub.execute_input":"2025-04-20T10:45:11.902173Z","iopub.status.idle":"2025-04-20T10:45:12.000979Z","shell.execute_reply.started":"2025-04-20T10:45:11.902148Z","shell.execute_reply":"2025-04-20T10:45:12.000306Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from collections import Counter\n\ndef get_label_counts(df):\n    c = Counter()\n    for labels in df[\"parsed_labels\"]:\n        c.update(labels)\n    return c\n\ntrain_counts = get_label_counts(train_df)\nval_counts = get_label_counts(val_df)\n\nprint(f\"Уникальных классов в train: {len(train_counts)}\")\nprint(f\"Уникальных классов в val: {len(val_counts)}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-20T10:45:12.001739Z","iopub.execute_input":"2025-04-20T10:45:12.001914Z","iopub.status.idle":"2025-04-20T10:45:12.024014Z","shell.execute_reply.started":"2025-04-20T10:45:12.001900Z","shell.execute_reply":"2025-04-20T10:45:12.023469Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_dataset = BirdCLEFDatasetOnTheFly(\n    df=train_df,\n    label_to_idx=label_to_idx,\n    audio_root=\"/kaggle/input/birdclef-2025/train_audio\",\n    random_crop=True\n)\n\nval_dataset = BirdCLEFDatasetOnTheFly(\n    df=val_df,\n    label_to_idx=label_to_idx,\n    audio_root=\"/kaggle/input/birdclef-2025/train_audio\",\n    random_crop=True\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-20T10:45:12.024810Z","iopub.execute_input":"2025-04-20T10:45:12.025066Z","iopub.status.idle":"2025-04-20T10:45:12.032420Z","shell.execute_reply.started":"2025-04-20T10:45:12.025044Z","shell.execute_reply":"2025-04-20T10:45:12.031767Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_loader = DataLoader(train_dataset, batch_size=8, shuffle=True,  num_workers=0)\nval_loader   = DataLoader(val_dataset,   batch_size=8, shuffle=False, num_workers=0)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-20T10:45:12.033140Z","iopub.execute_input":"2025-04-20T10:45:12.033465Z","iopub.status.idle":"2025-04-20T10:45:12.042183Z","shell.execute_reply.started":"2025-04-20T10:45:12.033443Z","shell.execute_reply":"2025-04-20T10:45:12.041610Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Model","metadata":{}},{"cell_type":"code","source":"import timm\nimport torch.nn as nn\n\nclass BirdCLEFNet(nn.Module):\n    def __init__(self, num_classes):\n        super().__init__()\n        self.backbone = timm.create_model(\"tf_efficientnetv2_s\", pretrained=True, in_chans=1)\n        self.backbone.global_pool = nn.Identity()  # убираем pooling\n        self.pooling = nn.AdaptiveAvgPool2d(1)\n        self.classifier = nn.Linear(self.backbone.num_features, num_classes)\n    \n    def forward(self, x):\n        x = self.backbone.forward_features(x)  # [B, C, H, W]\n        x = self.pooling(x).squeeze(-1).squeeze(-1)  # [B, C]\n        x = self.classifier(x)  # [B, num_classes]\n        return x","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-20T10:45:12.042972Z","iopub.execute_input":"2025-04-20T10:45:12.043369Z","iopub.status.idle":"2025-04-20T10:45:18.314806Z","shell.execute_reply.started":"2025-04-20T10:45:12.043346Z","shell.execute_reply":"2025-04-20T10:45:18.314034Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport torch.nn.functional as F\nfrom torch import optim\n\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\nmodel = BirdCLEFNet(num_classes=len(label_to_idx)).to(device)\noptimizer = optim.Adam(model.parameters(), lr=1e-4)\nloss_fn = nn.BCEWithLogitsLoss()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-20T10:45:18.315644Z","iopub.execute_input":"2025-04-20T10:45:18.315867Z","iopub.status.idle":"2025-04-20T10:45:19.985567Z","shell.execute_reply.started":"2025-04-20T10:45:18.315848Z","shell.execute_reply":"2025-04-20T10:45:19.984989Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"device","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-20T10:45:19.986302Z","iopub.execute_input":"2025-04-20T10:45:19.986550Z","iopub.status.idle":"2025-04-20T10:45:19.991194Z","shell.execute_reply.started":"2025-04-20T10:45:19.986532Z","shell.execute_reply":"2025-04-20T10:45:19.990552Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Train ","metadata":{}},{"cell_type":"code","source":"from tqdm import tqdm","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-20T10:45:19.991913Z","iopub.execute_input":"2025-04-20T10:45:19.992129Z","iopub.status.idle":"2025-04-20T10:45:20.002107Z","shell.execute_reply.started":"2025-04-20T10:45:19.992106Z","shell.execute_reply":"2025-04-20T10:45:20.001591Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import numpy as np\nfrom sklearn.metrics import roc_auc_score\n\ndef safe_macro_auc(y_true, y_pred):\n    \"\"\"Compute macro-averaged ROC AUC, skipping invalid classes.\"\"\"\n    scores = []\n    for i in range(y_true.shape[1]):\n        y_col = y_true[:, i]\n        p_col = y_pred[:, i]\n        if (y_col == 1).sum() > 0 and (y_col == 0).sum() > 0:\n            try:\n                score = roc_auc_score(y_col, p_col)\n                scores.append(score)\n            except:\n                continue\n    return np.mean(scores) if scores else float(\"nan\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-20T10:45:20.002846Z","iopub.execute_input":"2025-04-20T10:45:20.003033Z","iopub.status.idle":"2025-04-20T10:45:20.013916Z","shell.execute_reply.started":"2025-04-20T10:45:20.003020Z","shell.execute_reply":"2025-04-20T10:45:20.013223Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import csv\n\nLOG_PATH = \"/kaggle/working/train_log.csv\"\nCHECKPOINT_DIR = \"/kaggle/working/checkpoints\"\nos.makedirs(CHECKPOINT_DIR, exist_ok=True)\n\n# Записываем шапку для логов\nwith open(LOG_PATH, \"w\", newline=\"\") as f:\n    writer = csv.writer(f)\n    writer.writerow([\"epoch\", \"train_loss\", \"val_loss\", \"val_auc\"])\n\ndef train_one_epoch(model, dataloader, optimizer, loss_fn, device, epoch):\n    model.train()\n    running_loss = 0.0\n    for x, y in tqdm(dataloader, desc=f\"[Epoch {epoch}] Train\", leave=False):\n        x, y = x.to(device), y.to(device)\n        optimizer.zero_grad()\n        logits = model(x)\n        loss = loss_fn(logits, y)\n        loss.backward()\n        optimizer.step()\n        running_loss += loss.item()\n    return running_loss / len(dataloader)\n\ndef validate_one_epoch(model, dataloader, loss_fn, device, epoch):\n    model.eval()\n    running_loss = 0.0\n    all_targets = []\n    all_preds = []\n    with torch.no_grad():\n        for x, y in tqdm(dataloader, desc=f\"[Epoch {epoch}] Val\", leave=False):\n            x, y = x.to(device), y.to(device)\n            logits = model(x)\n            loss = loss_fn(logits, y)\n            probs = torch.sigmoid(logits)\n            all_preds.append(probs.cpu().numpy())\n            all_targets.append(y.cpu().numpy())\n            running_loss += loss.item()\n    y_true = np.vstack(all_targets)\n    y_pred = np.vstack(all_preds)\n    val_auc = safe_macro_auc(y_true, y_pred)\n    return running_loss / len(dataloader), val_auc","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-20T10:45:20.014718Z","iopub.execute_input":"2025-04-20T10:45:20.014935Z","iopub.status.idle":"2025-04-20T10:45:20.027657Z","shell.execute_reply.started":"2025-04-20T10:45:20.014914Z","shell.execute_reply":"2025-04-20T10:45:20.026984Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"NUM_EPOCHS = 10\nbest_auc = -1.0\n\nfor epoch in range(1, NUM_EPOCHS + 1):\n    train_loss = train_one_epoch(model, train_loader, optimizer, loss_fn, device, epoch)\n    val_loss, val_auc = validate_one_epoch(model, val_loader, loss_fn, device, epoch)\n\n    print(f\"[Epoch {epoch}] Train Loss: {train_loss:.4f} | Val Loss: {val_loss:.4f} | Val ROC AUC: {val_auc:.5f}\")\n\n    # Сохраняем в лог-файл\n    with open(LOG_PATH, \"a\", newline=\"\") as f:\n        writer = csv.writer(f)\n        writer.writerow([epoch, train_loss, val_loss, val_auc])\n\n    # Сохраняем модель, если улучшилась\n    if val_auc > best_auc:\n        best_auc = val_auc\n        model_filename = f\"baseline2_randomcrop_epoch{epoch}_auc{val_auc:.5f}.pth\"\n        torch.save(model.state_dict(), f\"{CHECKPOINT_DIR}/{model_filename}\")\n        print(f\"✅ New best AUC: {best_auc:.5f} — saved as {model_filename}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-20T10:45:20.028371Z","iopub.execute_input":"2025-04-20T10:45:20.028538Z","iopub.status.idle":"2025-04-20T10:45:43.180004Z","shell.execute_reply.started":"2025-04-20T10:45:20.028525Z","shell.execute_reply":"2025-04-20T10:45:43.178667Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}