{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":21669,"databundleVersionId":1692278,"sourceType":"competition"}],"dockerImageVersionId":31193,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# Импорты\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nimport numpy as np\nimport pandas as pd\nimport librosa\nimport os\nimport csv\nimport warnings\nimport gc\nfrom pathlib import Path\nfrom tqdm import tqdm\nfrom sklearn.model_selection import KFold\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import models\nfrom skimage.transform import resize\nfrom skimage import exposure\nimport copy\n\nwarnings.filterwarnings('ignore')\nprint('Библиотеки загружены')","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Конфигурация\nclass Config:\n    NUM_CLASSES = 24\n    SAMPLE_RATE = 48000\n    SEGMENT_LENGTH = 10\n    SEGMENT_SAMPLES = SEGMENT_LENGTH * SAMPLE_RATE\n    \n    IMG_HEIGHT = 224\n    IMG_WIDTH = 384\n    TOP_DB = 80\n    \n    NUM_FOLDS = 5\n    EPOCHS = 18\n    BATCH_SIZE = 6\n    LEARNING_RATE = 1.5e-4\n    WEIGHT_DECAY = 1e-4\n    LABEL_SMOOTHING = 0.1\n    MIXUP_ALPHA = 0.2\n    \n    DEVICE = 'cuda' if torch.cuda.is_available() else 'cpu'\n    RANDOM_SEED = 2024\n\ncfg = Config()\nprint(f'Устройство: {cfg.DEVICE}')\nprint(f'Сегменты: {cfg.SEGMENT_LENGTH} сек')\nprint(f'Изображения: {cfg.IMG_HEIGHT}×{cfg.IMG_WIDTH}')\nprint(f'Batch size: {cfg.BATCH_SIZE} (уменьшен для памяти)')\nprint(f'Фолдов: {cfg.NUM_FOLDS}, Эпох: {cfg.EPOCHS}')","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def audio_to_spectr(audio_segment, sr, f_min, f_max):\n    mel_spec = librosa.feature.melspectrogram(\n        y=audio_segment, \n        sr=sr, \n        fmin=f_min, \n        fmax=f_max\n    )\n    mel_db = librosa.power_to_db(mel_spec, top_db=cfg.TOP_DB)\n    return mel_db\n\n\ndef normalize_spectr(spec):\n    spec_resized = resize(spec, (cfg.IMG_HEIGHT, cfg.IMG_WIDTH))\n    \n    # Z-score нормализация\n    eps = 1e-6\n    mean = spec_resized.mean()\n    std = spec_resized.std()\n    spec_norm = (spec_resized - mean) / (std + eps)\n    \n    spec_min, spec_max = spec_norm.min(), spec_norm.max()\n    spec_scaled = 255 * (spec_norm - spec_min) / (spec_max - spec_min + eps)\n    spec_scaled = spec_scaled.astype(np.uint8)\n    \n    return spec_scaled\n\n\ndef apply_contrast(img, вероятность=0.5):\n    if np.random.rand() < вероятность:\n        return exposure.rescale_intensity(img)\n    return img\n\n\nprint('Функции обработки готовы')","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Загрузка и предобработка данных\n\nprint('Загрузка метаданных...')\ntrain_tp = pd.read_csv('/kaggle/input/rfcx-species-audio-detection/train_tp.csv')\n\nf_min_global = int(train_tp['f_min'].min() * 0.9)\nf_max_global = int(train_tp['f_max'].max() * 1.1)\n\nprint(f'Частотный диапазон: {f_min_global}-{f_max_global} Hz')\nprint(f'Записей: {len(train_tp)}, Видов: {cfg.NUM_CLASSES}')\n\n# Предобработка всех аудио\nprint('\\nПредобработка аудио...')\nаудио_кэш = {}\n\nfor idx, row in tqdm(train_tp.iterrows(), total=len(train_tp)):\n    recording_id = row['recording_id']\n    \n    # Загрузка аудио\n    audio_path = f'/kaggle/input/rfcx-species-audio-detection/train/{recording_id}.flac'\n    wav, sr = librosa.load(audio_path, sr=None)\n    \n    # Извлечение сегмента с центром на активности\n    t_min = int(row['t_min'] * sr)\n    t_max = int(row['t_max'] * sr)\n    центр = int((t_min + t_max) / 2)\n    \n    начало = max(0, центр - cfg.SEGMENT_SAMPLES // 2)\n    конец = min(len(wav), начало + cfg.SEGMENT_SAMPLES)\n    \n    if конец - начало < cfg.SEGMENT_SAMPLES:\n        начало = max(0, конец - cfg.SEGMENT_SAMPLES)\n    \n    segment = wav[начало:конец]\n    \n    # Преобразование в спектрограмму\n    mel_spec = audio_to_spectr(segment, sr, f_min_global, f_max_global)\n    img = normalize_spectr(mel_spec)\n    \n    аудио_кэш[recording_id] = img\n\nprint(f'✓ Предобработано {len(аудио_кэш)} записей')","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Dataset с Mixup аугментацией\n\nclass RFCXDataset(Dataset):\n    def __init__(self, recording_ids, labels, cache, is_train=True, use_mixup=True):\n        self.recording_ids = recording_ids\n        self.labels = labels\n        self.cache = cache\n        self.is_train = is_train\n        self.use_mixup = use_mixup and is_train\n    \n    def __len__(self):\n        return len(self.recording_ids)\n    \n    def __getitem__(self, idx):\n        rec_id = self.recording_ids[idx]\n        label = self.labels[idx]\n        \n        # Получаем изображение из кэша\n        img = self.cache[rec_id].copy()\n        \n        # Аугментации для тренировки\n        if self.is_train:\n            # Контраст\n            img = apply_contrast(img, вероятность=0.5)\n            \n            # Horizontal flip\n            if np.random.rand() < 0.5:\n                img = img[:, ::-1]\n            \n            # Vertical flip\n            if np.random.rand() < 0.3:\n                img = img[::-1, :]\n        \n        # Преобразование в 3 канала (RGB для ImageNet)\n        img = np.stack([img, img, img], axis=0)  # (3, H, W)\n        \n        return torch.FloatTensor(img), label\n\n\ndef mixup_data(x, y, alpha=0.2):\n    if alpha > 0:\n        lam = np.random.beta(alpha, alpha)\n    else:\n        lam = 1\n    \n    batch_size = x.size(0)\n    index = torch.randperm(batch_size).to(x.device)\n    \n    mixed_x = lam * x + (1 - lam) * x[index]\n    y_a, y_b = y, y[index]\n    \n    return mixed_x, y_a, y_b, lam\n\n\nprint('Dataset с Mixup готов')","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Модель: EfficientNet-B0 вместо ResNet50\n\ndef создать_модель():\n    # Используем pretrained EfficientNet-B0\n    model = models.efficientnet_b0(pretrained=True)\n    \n    # Заменяем классификатор\n    num_features = model.classifier[1].in_features\n    model.classifier = nn.Sequential(\n        nn.Dropout(p=0.3, inplace=True),\n        nn.Linear(num_features, cfg.NUM_CLASSES)\n    )\n    \n    return model.to(cfg.DEVICE)\n\n\n# Тестируем\ntest_model = создать_модель()\ntotal_params = sum(p.numel() for p in test_model.parameters())\nprint(f'EfficientNet-B0: {total_params:,} параметров')\ndel test_model","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Функция обучения с Mixup и Label Smoothing\n\ndef обучить_модель(model, train_loader, val_loader, fold_num):\n    # Loss с label smoothing\n    criterion = nn.CrossEntropyLoss(label_smoothing=cfg.LABEL_SMOOTHING)\n    \n    # Оптимизатор\n    optimizer = torch.optim.AdamW(\n        model.parameters(), \n        lr=cfg.LEARNING_RATE,\n        weight_decay=cfg.WEIGHT_DECAY\n    )\n    \n    # Cosine Annealing scheduler (вместо ReduceLROnPlateau)\n    scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(\n        optimizer, \n        T_max=cfg.EPOCHS,\n        eta_min=1e-6\n    )\n    \n    best_acc = 0.0\n    best_weights = None\n    \n    print(f'\\n=== Fold {fold_num} ===')\n    \n    for epoch in range(1, cfg.EPOCHS + 1):\n        # TRAIN\n        model.train()\n        train_losses = []\n        \n        for batch_idx, (images, labels) in enumerate(train_loader):\n            images = images.to(cfg.DEVICE)\n            labels = labels.to(cfg.DEVICE)\n            \n            # Mixup аугментация\n            if np.random.rand() < 0.5:  # 50% вероятность\n                images, labels_a, labels_b, lam = mixup_data(\n                    images, labels, alpha=cfg.MIXUP_ALPHA\n                )\n                \n                optimizer.zero_grad()\n                outputs = model(images)\n                loss = lam * criterion(outputs, labels_a) + \\\n                       (1 - lam) * criterion(outputs, labels_b)\n            else:\n                optimizer.zero_grad()\n                outputs = model(images)\n                loss = criterion(outputs, labels)\n            \n            loss.backward()\n            \n            # Gradient clipping для стабильности\n            torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)\n            \n            optimizer.step()\n            train_losses.append(loss.item())\n            \n            # Очистка памяти каждые 10 батчей\n            if batch_idx % 10 == 0 and cfg.DEVICE == 'cuda':\n                torch.cuda.empty_cache()\n        \n        # VALIDATION\n        model.eval()\n        val_losses = []\n        correct = 0\n        total = 0\n        \n        with torch.no_grad():\n            for images, labels in val_loader:\n                images = images.to(cfg.DEVICE)\n                labels = labels.to(cfg.DEVICE)\n                \n                outputs = model(images)\n                loss = criterion(outputs, labels)\n                \n                val_losses.append(loss.item())\n                \n                _, predicted = outputs.max(1)\n                total += labels.size(0)\n                correct += predicted.eq(labels).sum().item()\n        \n        val_acc = correct / total\n        \n        print(f'Epoch {epoch:2d}: '\n              f'train_loss={np.mean(train_losses):.4f}, '\n              f'val_loss={np.mean(val_losses):.4f}, '\n              f'val_acc={val_acc:.4f}')\n        \n        # Сохраняем лучшую модель\n        if val_acc > best_acc:\n            best_acc = val_acc\n            best_weights = copy.deepcopy(model.state_dict())\n        \n        scheduler.step()\n        \n        # Очистка памяти после эпохи\n        if cfg.DEVICE == 'cuda':\n            torch.cuda.empty_cache()\n    \n    # Загружаем лучшие веса\n    model.load_state_dict(best_weights)\n    print(f'Лучшая точность: {best_acc:.4f}')\n    \n    return model\n\n\nprint('Функция обучения готова')","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# K-Fold обучение\nimport gc\n\nrecording_ids = train_tp['recording_id'].values\nlabels = train_tp['species_id'].values\n\nkfold = KFold(\n    n_splits=cfg.NUM_FOLDS, \n    shuffle=True, \n    random_state=cfg.RANDOM_SEED\n)\n\ntrained_models = []\n\nfor fold_idx, (train_indices, val_indices) in enumerate(kfold.split(recording_ids)):\n    print(f'\\n{\"=\"*60}')\n    print(f'Fold {fold_idx}/{cfg.NUM_FOLDS}')\n    print(f'{\"=\"*60}')\n    \n    # Очистка памяти перед каждым фолдом\n    if cfg.DEVICE == 'cuda':\n        torch.cuda.empty_cache()\n    gc.collect()\n    \n    # Разделение данных\n    train_ids = recording_ids[train_indices]\n    train_labels = labels[train_indices]\n    val_ids = recording_ids[val_indices]\n    val_labels = labels[val_indices]\n    \n    # Datasets\n    train_dataset = RFCXDataset(\n        train_ids, train_labels, аудио_кэш, \n        is_train=True, use_mixup=True\n    )\n    val_dataset = RFCXDataset(\n        val_ids, val_labels, аудио_кэш, \n        is_train=False, use_mixup=False\n    )\n    \n    # DataLoaders\n    train_loader = DataLoader(\n        train_dataset, \n        batch_size=cfg.BATCH_SIZE, \n        shuffle=True, \n        drop_last=True,\n        num_workers=2,  # Параллельная загрузка\n        pin_memory=True if cfg.DEVICE == 'cuda' else False\n    )\n    val_loader = DataLoader(\n        val_dataset, \n        batch_size=cfg.BATCH_SIZE, \n        shuffle=False, \n        drop_last=False,\n        num_workers=2,\n        pin_memory=True if cfg.DEVICE == 'cuda' else False\n    )\n    \n    # Создание и обучение модели\n    model = создать_модель()\n    model = обучить_модель(model, train_loader, val_loader, fold_idx)\n    \n    # Сохранение\n    torch.save(model.state_dict(), f'model_fold_{fold_idx}.pt')\n    trained_models.append(model)\n    \n    # Очистка памяти\n    del train_dataset, val_dataset, train_loader, val_loader\n    if cfg.DEVICE == 'cuda':\n        torch.cuda.empty_cache()\n    gc.collect()\n\nprint('\\n' + '='*60)\nprint('Обучение всех фолдов завершено')\nprint('='*60)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Функции для инференса\n\ndef load_test_file(file_path):\n    wav, sr = librosa.load(file_path, sr=None)\n    \n    # Количество сегментов\n    num_segments = int(np.ceil(len(wav) / cfg.SEGMENT_SAMPLES))\n    \n    segments = []\n    for i in range(num_segments):\n        start = i * cfg.SEGMENT_SAMPLES\n        end = start + cfg.SEGMENT_SAMPLES\n        \n        if end > len(wav):\n            # Последний сегмент - берем с конца\n            segment = wav[-cfg.SEGMENT_SAMPLES:]\n        else:\n            segment = wav[start:end]\n        \n        # Преобразование в спектрограмму\n        mel_spec = audio_to_spectr(\n            segment, sr, f_min_global, f_max_global\n        )\n        img = normalize_spectr(mel_spec)\n        \n        # 3 канала\n        img_rgb = np.stack([img, img, img], axis=0)\n        segments.append(img_rgb)\n    \n    return np.array(segments)\n\n\ndef predict_file(file_name, models):\n    file_path = f'/kaggle/input/rfcx-species-audio-detection/test/{file_name}'\n    \n    # Загрузка сегментов\n    segments = load_test_file(file_path)\n    segments_tensor = torch.FloatTensor(segments).to(cfg.DEVICE)\n    \n    # Предсказания от каждой модели\n    ensemble_preds = []\n    \n    for model in models:\n        model.eval()\n        with torch.no_grad():\n            outputs = model(segments_tensor)\n            # Max агрегация по сегментам (как в оригинале)\n            max_pred = outputs.max(dim=0)[0]\n            ensemble_preds.append(max_pred.cpu())\n    \n    # Усреднение по моделям ансамбля\n    final_pred = torch.stack(ensemble_preds).mean(dim=0)\n    \n    # Формирование результата\n    file_id = file_name.split('.')[0]\n    result = [file_id] + final_pred.numpy().tolist()\n    \n    return result\n\n\nprint('Функции инференса готовы')","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Генерация submission\n\nprint('Генерация предсказаний...\\n')\n\ntest_files = os.listdir('/kaggle/input/rfcx-species-audio-detection/test/')\nprint(f'Тестовых файлов: {len(test_files)}')\n\nif cfg.DEVICE == 'cuda':\n    trained_models = [m.cuda() for m in trained_models]\n\n# Предсказания\npredictions = []\nfor test_file in tqdm(test_files):\n    pred = predict_file(test_file, trained_models)\n    predictions.append(pred)\n\n# Сохранение submission\nwith open('submission.csv', 'w', newline='') as f:\n    writer = csv.writer(f)\n    \n    # Header\n    header = ['recording_id'] + [f's{i}' for i in range(cfg.NUM_CLASSES)]\n    writer.writerow(header)\n    \n    # Predictions\n    for pred in predictions:\n        writer.writerow(pred)\n\nprint('\\n' + '='*60)\nprint('Все процессы завершены')\nprint('='*60)\nprint('\\nSubmission сохранен: submission.csv')","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}