{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.10.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":91844,"databundleVersionId":11361821,"sourceType":"competition"},{"sourceId":333392,"sourceType":"modelInstanceVersion","isSourceIdPinned":true,"modelInstanceId":279246,"modelId":300162}],"dockerImageVersionId":30918,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os\nimport gc\nimport warnings\nimport logging\nimport time\nimport math\nimport cv2\nfrom pathlib import Path\nimport numpy as np\nimport pandas as pd\nimport librosa\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nimport timm\nfrom tqdm.auto import tqdm\n\n# Suppress warnings and limit logging output\nwarnings.filterwarnings(\"ignore\")\nlogging.basicConfig(level=logging.ERROR)","metadata":{"trusted":true,"execution":{"iopub.status.idle":"2025-04-20T13:14:49.159349Z","shell.execute_reply.started":"2025-04-20T13:14:32.736408Z","shell.execute_reply":"2025-04-20T13:14:49.158243Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class CFG:\n    \"\"\"\n    推論パイプラインに必要なすべてのパスとパラメータを保持する設定クラス。\n    \"\"\"\n    # データパス\n    test_soundscapes = '/kaggle/input/birdclef-2025/test_soundscapes'  # テスト用の音響データのパス\n    submission_csv = '/kaggle/input/birdclef-2025/sample_submission.csv'  # 提出用CSVファイルのパス\n    taxonomy_csv = '/kaggle/input/birdclef-2025/taxonomy.csv'  # 種類情報のCSVファイルのパス\n    model_path = '/kaggle/input/regnrty008/pytorch/default/1'  # モデルのパス\n    \n    # 音声データのパラメータ\n    FS = 32000  # サンプリング周波数（32kHz）\n    WINDOW_SIZE = 5  # ウィンドウサイズ（秒単位）\n    \n    # メルスペクトログラムのパラメータ\n    N_FFT = 1024  # FFTサイズ\n    HOP_LENGTH = 256\n    N_MELS = 256\n    FMIN = 50  # 最小周波数（Hz） #20\n    FMAX = 14000  # 最大周波数（Hz）　#16000\n    TARGET_SHAPE = (256, 256)  # ターゲット画像の形状\n    \n    # モデル関連\n    model_name = 'regnety_008'  # 使用するモデル名\n    in_channels = 1  # 入力チャンネル数（モノラル音声）\n    device = 'cpu'  # 使用するデバイス（'cpu'または'cuda'）\n    \n    # 推論パラメータ\n    use_tta = False  # TTA（Test Time Augmentation）を使用するかどうか\n    tta_count = 3  # TTAの回数\n    threshold = 0.7  # クラスの予測確信度閾値\n    \n    # モデル選択に関する設定\n    use_specific_folds = False  # 特定のfoldを使用するかどうか\n    folds = [0, 1]  # use_specific_foldsがTrueの場合に使用するfoldのリスト\n    \n    # デバッグ用設定\n    debug = False  # デバッグモードのオンオフ\n    debug_count = 5  # デバッグモード時に使用するサンプル数\n\n    if debug:\n        test_soundscapes = '/kaggle/input/birdclef-2025/train_soundscapes'  # テスト用の音響データのパス","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-20T13:15:17.593718Z","iopub.execute_input":"2025-04-20T13:15:17.594061Z","iopub.status.idle":"2025-04-20T13:15:17.600594Z","shell.execute_reply.started":"2025-04-20T13:15:17.594034Z","shell.execute_reply":"2025-04-20T13:15:17.599368Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class BirdCLEF2025Pipeline:\n    \"\"\"\n    BirdCLEF-2025の推論タスクのためのパイプライン。\n\n    このクラスは、以下の一連の処理を整理します：\n      - タクソノミーデータのロード。\n      - トレーニングされたモデルのロードと準備。\n      - オーディオファイルをメルスペクトログラムに変換。\n      - 各オーディオセグメントの予測を実行。\n      - 提出ファイルの作成。\n      - 提出結果の後処理（予測の平滑化）。\n    \"\"\"\n\n    class BirdCLEFModel(nn.Module):\n        \"\"\"\n        BirdCLEF-2025のカスタムニューラルネットワークモデル（timmバックボーンを使用）。\n        \"\"\"\n        def __init__(self, cfg, num_classes):\n            \"\"\"\n            BirdCLEFModelの初期化。\n            \n            :param cfg: 設定パラメータ。\n            :param num_classes: 出力クラスの数。\n            \"\"\"\n            super().__init__()\n            self.cfg = cfg\n            # timmバックボーンを作成（指定されたパラメータを使用）\n            self.backbone = timm.create_model(\n                cfg.model_name,\n                pretrained=False,  \n                in_chans=cfg.in_channels,\n                drop_rate=0.0,    \n                drop_path_rate=0.0\n            )\n            # モデルのタイプに基づいて最終層を調整\n            if 'efficientnet' in cfg.model_name:\n                backbone_out = self.backbone.classifier.in_features\n                self.backbone.classifier = nn.Identity()\n            elif 'resnet' in cfg.model_name:\n                backbone_out = self.backbone.fc.in_features\n                self.backbone.fc = nn.Identity()\n            else:\n                backbone_out = self.backbone.get_classifier().in_features\n                self.backbone.reset_classifier(0, '')\n            \n            self.pooling = nn.AdaptiveAvgPool2d(1)  # グローバル平均プーリング\n            self.feat_dim = backbone_out\n            self.classifier = nn.Linear(backbone_out, num_classes)  # クラス分類層\n            \n        def forward(self, x):\n            \"\"\"\n            ネットワークの順伝播。\n            \n            :param x: 入力テンソル。\n            :return: 各クラスのロジット。\n            \"\"\"\n            features = self.backbone(x)  # バックボーンで特徴量を抽出\n            if isinstance(features, dict):\n                features = features['features']\n            # 特徴量が4次元であれば、グローバル平均プーリングを適用\n            if len(features.shape) == 4:\n                features = self.pooling(features)\n                features = features.view(features.size(0), -1)\n            logits = self.classifier(features)  # 最後に分類層を適用\n            return logits\n\n    def __init__(self, cfg):\n        \"\"\"\n        推論パイプラインの初期化。\n        \n        :param cfg: 設定オブジェクト（パスやパラメータを含む）。\n        \"\"\"\n        self.cfg = cfg\n        self.taxonomy_df = None\n        self.species_ids = []\n        self.models = []\n        self._load_taxonomy()  # タクソノミーデータのロード\n\n    def _load_taxonomy(self):\n        \"\"\"\n        タクソノミーデータをCSVからロードし、種識別子を抽出。\n        \"\"\"\n        print(\"タクソノミーデータをロードしています...\")\n        self.taxonomy_df = pd.read_csv(self.cfg.taxonomy_csv)\n        self.species_ids = self.taxonomy_df['primary_label'].tolist()  # 主ラベル（種識別子）の抽出\n        print(f\"クラス数: {len(self.species_ids)}\")\n\n    def audio2melspec(self, audio_data):\n        \"\"\"\n        生のオーディオデータを正規化したメルスペクトログラムに変換。\n        \n        :param audio_data: 1Dのnumpy配列としてのオーディオサンプル。\n        :return: 正規化されたメルスペクトログラム。\n        \"\"\"\n        if np.isnan(audio_data).any():\n            mean_signal = np.nanmean(audio_data)\n            audio_data = np.nan_to_num(audio_data, nan=mean_signal)\n        \n        mel_spec = librosa.feature.melspectrogram(\n            y=audio_data,\n            sr=self.cfg.FS,\n            n_fft=self.cfg.N_FFT,\n            hop_length=self.cfg.HOP_LENGTH,\n            n_mels=self.cfg.N_MELS,\n            fmin=self.cfg.FMIN,\n            fmax=self.cfg.FMAX,\n            power=2.0\n        )\n        mel_spec_db = librosa.power_to_db(mel_spec, ref=np.max)\n        mel_spec_norm = (mel_spec_db - mel_spec_db.min()) / (mel_spec_db.max() - mel_spec_db.min() + 1e-8)\n        return mel_spec_norm\n\n    def process_audio_segment(self, audio_data):\n        \"\"\"\n        オーディオセグメントを処理し、ターゲット形状のメルスペクトログラムを取得。\n        \n        :param audio_data: 1Dのnumpy配列としてのオーディオサンプル。\n        :return: 処理されたメルスペクトログラム（float32のnumpy配列）。\n        \"\"\"\n        # 必要なウィンドウサイズよりオーディオが短い場合はパディング\n        if len(audio_data) < self.cfg.FS * self.cfg.WINDOW_SIZE:\n            audio_data = np.pad(\n                audio_data,\n                (0, self.cfg.FS * self.cfg.WINDOW_SIZE - len(audio_data)),\n                mode='constant'\n            )\n        \n        mel_spec = self.audio2melspec(audio_data)  # メルスペクトログラムに変換\n        \n        # 必要であれば、ターゲット形状にリサイズ\n        if mel_spec.shape != self.cfg.TARGET_SHAPE:\n            mel_spec = cv2.resize(mel_spec, self.cfg.TARGET_SHAPE, interpolation=cv2.INTER_LINEAR)\n            \n        return mel_spec.astype(np.float32)\n\n    def find_model_files(self):\n        \"\"\"\n        指定されたモデルディレクトリからすべての.pthモデルファイルを検索。\n        \n        :return: モデルファイルのパスリスト。\n        \"\"\"\n        model_files = []\n        model_dir = Path(self.cfg.model_path)\n        for path in model_dir.glob('**/*.pth'):\n            model_files.append(str(path))\n        return model_files\n\n    def load_models(self):\n        \"\"\"\n        見つかったすべてのモデルファイルをロードし、アンサンブル推論の準備をする。\n        \n        :return: ロードされたPyTorchモデルのリスト。\n        \"\"\"\n        self.models = []\n        model_files = self.find_model_files()  # モデルファイルの検索\n        if not model_files:\n            print(f\"警告: {self.cfg.model_path}にモデルファイルが見つかりません!\")\n            return self.models\n\n        print(f\"合計{len(model_files)}個のモデルファイルが見つかりました。\")\n        \n        # 特定のフォールドを使用する場合は、モデルファイルをフィルタリング\n        if self.cfg.use_specific_folds:\n            filtered_files = []\n            for fold in self.cfg.folds:\n                fold_files = [f for f in model_files if f\"fold{fold}\" in f]\n                filtered_files.extend(fold_files)\n            model_files = filtered_files\n            print(f\"指定されたフォールド（{self.cfg.folds}）に対して{len(model_files)}個のモデルファイルを使用します。\")\n        \n        # 各モデルファイルをロード\n        for model_path in model_files:\n            try:\n                print(f\"モデルをロード中: {model_path}\")\n                checkpoint = torch.load(model_path, map_location=torch.device(self.cfg.device))\n                model = self.BirdCLEFModel(self.cfg, len(self.species_ids))\n                model.load_state_dict(checkpoint['model_state_dict'])\n                model = model.to(self.cfg.device)\n                model.eval()  # 推論モード\n                self.models.append(model)\n            except Exception as e:\n                print(f\"モデル{model_path}のロード中にエラーが発生しました: {e}\")\n        \n        return self.models\n\n    def apply_tta(self, spec, tta_idx):\n        \"\"\"\n        テスト時拡張（TTA）をメルスペクトログラムに適用。\n        \n        :param spec: 入力メルスペクトログラム。\n        :param tta_idx: 適用するTTAのインデックス。\n        :return: 拡張後のメルスペクトログラム。\n        \"\"\"\n        if tta_idx == 0:\n            # 拡張なし\n            return spec\n        elif tta_idx == 1:\n            # 時間シフト（水平フリップ）\n            return np.flip(spec, axis=1)\n        elif tta_idx == 2:\n            # 周波数シフト（垂直フリップ）\n            return np.flip(spec, axis=0)\n        else:\n            return spec\n\n    def predict_on_spectrogram(self, audio_path):\n        \"\"\"\n        単一のオーディオファイルを処理し、各5秒セグメントに対して種の存在を予測。\n        \n        :param audio_path: オーディオファイルのパス。\n        :return: 各セグメントに対する(row_id, predictions)のタプル。\n        \"\"\"\n        predictions = []\n        row_ids = []\n        soundscape_id = Path(audio_path).stem\n        \n        try:\n            print(f\"{soundscape_id}を処理中...\")\n            audio_data, _ = librosa.load(audio_path, sr=self.cfg.FS)\n            total_segments = int(len(audio_data) / (self.cfg.FS * self.cfg.WINDOW_SIZE))\n            \n            for segment_idx in range(total_segments):\n                start_sample = segment_idx * self.cfg.FS * self.cfg.WINDOW_SIZE\n                end_sample = start_sample + self.cfg.FS * self.cfg.WINDOW_SIZE\n                segment_audio = audio_data[start_sample:end_sample]\n                \n                end_time_sec = (segment_idx + 1) * self.cfg.WINDOW_SIZE\n                row_id = f\"{soundscape_id}_{end_time_sec}\"\n                row_ids.append(row_id)\n\n                # TTAが有効な場合\n                if self.cfg.use_tta:\n                    all_preds = []\n                    for tta_idx in range(self.cfg.tta_count):\n                        mel_spec = self.process_audio_segment(segment_audio)\n                        mel_spec = self.apply_tta(mel_spec, tta_idx)\n                        mel_spec_tensor = torch.tensor(mel_spec, dtype=torch.float32).unsqueeze(0).unsqueeze(0)\n                        mel_spec_tensor = mel_spec_tensor.to(self.cfg.device)\n\n                        if len(self.models) == 1:\n                            with torch.no_grad():\n                                outputs = self.models[0](mel_spec_tensor)\n                                probs = torch.sigmoid(outputs).cpu().numpy().squeeze()\n                                all_preds.append(probs)\n                        else:\n                            segment_preds = []\n                            for model in self.models:\n                                with torch.no_grad():\n                                    outputs = model(mel_spec_tensor)\n                                    probs = torch.sigmoid(outputs).cpu().numpy().squeeze()\n                                    segment_preds.append(probs)\n                            avg_preds = np.mean(segment_preds, axis=0)\n                            all_preds.append(avg_preds)\n                    final_preds = np.mean(all_preds, axis=0)\n                else:\n                    mel_spec = self.process_audio_segment(segment_audio)\n                    mel_spec_tensor = torch.tensor(mel_spec, dtype=torch.float32).unsqueeze(0).unsqueeze(0)\n                    mel_spec_tensor = mel_spec_tensor.to(self.cfg.device)\n                    \n                    if len(self.models) == 1:\n                        with torch.no_grad():\n                            outputs = self.models[0](mel_spec_tensor)\n                            final_preds = torch.sigmoid(outputs).cpu().numpy().squeeze()\n                    else:\n                        segment_preds = []\n                        for model in self.models:\n                            with torch.no_grad():\n                                outputs = model(mel_spec_tensor)\n                                probs = torch.sigmoid(outputs).cpu().numpy().squeeze()\n                                segment_preds.append(probs)\n                        final_preds = np.mean(segment_preds, axis=0)\n                \n                predictions.append(final_preds)\n        except Exception as e:\n            print(f\"{audio_path}の処理中にエラーが発生しました: {e}\")\n        \n        return row_ids, predictions\n\n    def run_inference(self):\n        \"\"\"\n        すべてのテスト音景オーディオファイルに対して推論を実行。\n        \n        :return: すべてのファイルから集計された(row_ids, predictions)のタプル。\n        \"\"\"\n        test_files = list(Path(self.cfg.test_soundscapes).glob('*.ogg'))  # テスト音景ファイルを取得\n        if self.cfg.debug:\n            print(f\"デバッグモードが有効です。{self.cfg.debug_count}個のファイルのみ使用\")\n            test_files = test_files[:self.cfg.debug_count]\n        print(f\"{len(test_files)}個のテスト音景ファイルが見つかりました\")\n\n        all_row_ids = []\n        all_predictions = []\n\n        for audio_path in tqdm(test_files):\n            row_ids, predictions = self.predict_on_spectrogram(str(audio_path))\n            all_row_ids.extend(row_ids)\n            all_predictions.extend(predictions)\n        \n        return all_row_ids, all_predictions\n\n    def create_submission(self, row_ids, predictions):\n        \"\"\"\n        予測結果に基づいて提出用のデータフレームを作成。\n        \n        :param row_ids: 各セグメントの識別子のリスト。\n        :param predictions: 予測結果のリスト。\n        :return: 提出用フォーマットに整形されたpandas DataFrame。\n        \"\"\"\n        print(\"提出用データフレームを作成中...\")\n        submission_dict = {'row_id': row_ids}\n        for i, species in enumerate(self.species_ids):\n            submission_dict[species] = [pred[i] for pred in predictions]\n\n        submission_df = pd.DataFrame(submission_dict)\n        submission_df.set_index('row_id', inplace=True)\n\n        sample_sub = pd.read_csv(self.cfg.submission_csv, index_col='row_id')\n        missing_cols = set(sample_sub.columns) - set(submission_df.columns)\n        if missing_cols:\n            print(f\"警告: 提出結果に{len(missing_cols)}種類の種が欠落しています\")\n            for col in missing_cols:\n                submission_df[col] = 0.0\n\n        submission_df = submission_df[sample_sub.columns]  # 提出形式に整える\n        submission_df = submission_df.reset_index()\n        \n        return submission_df\n\n    def smooth_submission(self, submission_path):\n        \"\"\"\n        提出CSVの予測結果を後処理し、時間的一貫性を保つように予測を平滑化。\n        \n        各音景（'row_id'の最後のアンダースコア前部分でグループ化）について、各行の予測を隣接する行と平均化します。\n        \n        :param submission_path: 提出CSVファイルのパス。\n        \"\"\"\n        print(\"提出結果の予測を平滑化しています...\")\n        sub = pd.read_csv(submission_path)\n        cols = sub.columns[1:]\n        # 'row_id'を基にグループを抽出\n        groups = sub['row_id'].str.rsplit('_', n=1).str[0].values\n        unique_groups = np.unique(groups)\n        \n        for group in unique_groups:\n            idx = np.where(groups == group)[0]\n            sub_group = sub.iloc[idx].copy()\n            predictions = sub_group[cols].values\n            new_predictions = predictions.copy()\n            \n            if predictions.shape[0] > 1:\n                # 隣接するセグメントとの予測を平均化して平滑化\n                new_predictions[0] = (predictions[0] * 0.8) + (predictions[1] * 0.2)\n                new_predictions[-1] = (predictions[-1] * 0.8) + (predictions[-2] * 0.2)\n                for i in range(1, predictions.shape[0]-1):\n                    new_predictions[i] = (predictions[i-1] * 0.2) + (predictions[i] * 0.6) + (predictions[i+1] * 0.2)\n            sub.iloc[idx, 1:] = new_predictions\n        \n        sub.to_csv(submission_path, index=False)\n        print(f\"平滑化された提出結果を{submission_path}に保存しました\")\n\n    def run(self):\n        \"\"\"\n        完全な推論パイプラインを実行するメインメソッド。\n    \n        このメソッドでは、以下の処理を行います：\n          - 事前学習されたモデルをロード。\n          - テスト音声ファイルを処理して予測を実行。\n          - 提出用のCSVを作成。\n          - 予測値にスムージングを適用。\n        \"\"\"\n        start_time = time.time()\n        print(\"BirdCLEF-2025 推論開始...\")\n        print(f\"TTA有効: {self.cfg.use_tta} (変動数: {self.cfg.tta_count if self.cfg.use_tta else 0})\")\n    \n        self.load_models()\n        if not self.models:\n            print(\"モデルが見つかりませんでした！モデルパスを確認してください。\")\n            return\n    \n        print(f\"使用モデル: {'単一モデル' if len(self.models) == 1 else f'{len(self.models)}モデルのアンサンブル'}\")\n        row_ids, predictions = self.run_inference()\n        submission_df = self.create_submission(row_ids, predictions)\n    \n        submission_path = 'submission.csv'\n        submission_df.to_csv(submission_path, index=False)\n        print(f\"初期提出ファイルが {submission_path} に保存されました\")\n    \n        # 提出結果にスムージングを適用\n        self.smooth_submission(submission_path)\n    \n        end_time = time.time()\n        print(f\"推論完了 (所要時間: {(end_time - start_time) / 60:.2f} 分)\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-20T13:15:18.076544Z","iopub.execute_input":"2025-04-20T13:15:18.076881Z","iopub.status.idle":"2025-04-20T13:15:18.116228Z","shell.execute_reply.started":"2025-04-20T13:15:18.076850Z","shell.execute_reply":"2025-04-20T13:15:18.115121Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"if __name__ == \"__main__\":\n    cfg = CFG()\n    print(f\"Using device: {cfg.device}\")\n    pipeline = BirdCLEF2025Pipeline(cfg)\n    pipeline.run()  # Use the correct method name here","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-20T13:15:22.889511Z","iopub.execute_input":"2025-04-20T13:15:22.889916Z","iopub.status.idle":"2025-04-20T13:15:28.058782Z","shell.execute_reply.started":"2025-04-20T13:15:22.889889Z","shell.execute_reply":"2025-04-20T13:15:28.057607Z"}},"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}]}