{"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":"none","dataSources":[{"sourceId":91844,"databundleVersionId":11361821,"sourceType":"competition"}],"dockerImageVersionId":31012,"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","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nfrom pathlib import Path\nimport pandas as pd\nimport numpy as np\nimport librosa\nfrom tqdm import tqdm\nimport tensorflow as tf\nfrom tensorflow.keras.models import Sequential\nfrom tensorflow.keras.layers import *\nfrom tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint\nfrom sklearn.preprocessing import LabelEncoder\n\n# ==================== 配置部分 ====================\nDATA_DIR = '/kaggle/input/birdclef-2025/'\nTRAIN_AUDIO_DIR = os.path.join(DATA_DIR, 'train_audio')\nTEST_SOUNDSCAPES_DIR = os.path.join(DATA_DIR, 'test_soundscapes')\nTRAIN_METADATA = os.path.join(DATA_DIR, 'train.csv')\nTAXONOMY = os.path.join(DATA_DIR, 'taxonomy.csv')\nSUBMISSION_PATH = '/kaggle/working/submission.csv'\n\n# ==================== 特征提取部分 ====================\ndef extract_features(audio, sr=32000, n_mels=128, n_mfcc=20):\n    \"\"\"提取音频特征（Mel频谱图+MFCC+Chromagram）\"\"\"\n    if len(audio) < sr * 5:\n        audio = np.pad(\n            audio,\n            (0, max(0, sr * 5 - len(audio))),\n            mode='constant'\n        )\n    else:\n        audio = audio[:sr * 5]\n\n    mel_spec = librosa.feature.melspectrogram(\n        y=audio, sr=sr, n_mels=n_mels,\n        fmin=20, fmax=16000,\n        n_fft=2048, hop_length=512\n    )\n    mel_spec_db = librosa.power_to_db(mel_spec, ref=np.max)\n\n    mfcc = librosa.feature.mfcc(S=mel_spec_db, n_mfcc=n_mfcc)\n    mfcc_delta = librosa.feature.delta(mfcc)\n    mfcc_delta2 = librosa.feature.delta(mfcc, order=2)\n\n    chroma = librosa.feature.chroma_stft(\n        y=audio, sr=sr,\n        n_fft=2048, hop_length=512\n    )\n\n    features = np.concatenate([\n        np.mean(mel_spec_db, axis=1),\n        np.std(mel_spec_db, axis=1),\n        np.mean(mfcc, axis=1),\n        np.std(mfcc, axis=1),\n        np.mean(mfcc_delta, axis=1),\n        np.mean(mfcc_delta2, axis=1),\n        np.mean(chroma, axis=1)\n    ])\n    return (features - features.min()) / (features.max() - features.min() + 1e-8)\n\n# ==================== 模型定义部分 ====================\ndef create_model(input_shape, num_classes):\n    \"\"\"创建CNN+BiLSTM混合模型\"\"\"\n    model = Sequential([\n        InputLayer(input_shape=input_shape),\n        Conv2D(32, (3, 1), activation='relu', padding='valid'),\n        BatchNormalization(),\n        MaxPooling2D((2, 1)),\n        Dropout(0.3),\n\n        Conv2D(64, (3, 1), activation='relu', padding='valid'),\n        BatchNormalization(),\n        MaxPooling2D((2, 1)),\n        Dropout(0.3),\n\n        Conv2D(128, (3, 1), activation='relu', padding='valid'),\n        BatchNormalization(),\n        MaxPooling2D((2, 1)),\n        Dropout(0.3),\n\n        Reshape((-1, 128)),\n        Bidirectional(LSTM(64, return_sequences=True)),\n        Bidirectional(LSTM(64)),\n        Dense(256, activation='relu'),\n        Dropout(0.5),\n        Dense(num_classes, activation='softmax')\n    ])\n    model.compile(\n        optimizer=tf.keras.optimizers.Adam(learning_rate=0.001),\n        loss='sparse_categorical_crossentropy',\n        metrics=['accuracy']\n    )\n    return model\n\n# ==================== 数据准备部分 ====================\ndef load_and_prepare_data():\n    \"\"\"加载并准备训练数据\"\"\"\n    metadata = pd.read_csv(TRAIN_METADATA)\n    taxonomy = pd.read_csv(TAXONOMY)\n    data = metadata.merge(taxonomy, on='primary_label', how='left')\n\n    data['filepath'] = data['filename'].apply(\n        lambda f: os.path.join(TRAIN_AUDIO_DIR, f)\n    )\n    if 'rating' in data.columns:\n        data = data[data['rating'] >= 3.0]\n    return data\n\n# ==================== 训练部分 ====================\ndef train_model(train_data, classes):\n    \"\"\"训练音频分类模型\"\"\"\n    le = LabelEncoder()\n    y = le.fit_transform(train_data['primary_label'])\n\n    X = []\n    print(\"正在提取特征...\")\n    for filepath in tqdm(train_data['filepath']):\n        audio, sr = librosa.load(filepath, sr=32000)\n        features = extract_features(audio, sr)\n        X.append(features)\n\n    X = np.array(X)\n    X = X.reshape((X.shape[0], X.shape[1], 1, 1))\n\n    model = create_model(input_shape=(X.shape[1], X.shape[2], X.shape[3]),\n                         num_classes=len(classes))\n    model.fit(\n        X, y,\n        batch_size=32,\n        epochs=50,\n        validation_split=0.2,\n        callbacks=[\n            EarlyStopping(patience=5, restore_best_weights=True),\n            # 修复了 ModelCheckpoint 路径名和变量拼写问题\nModelCheckpoint('best_model.weights.h5', save_best_only=True, save_weights_only=True)\n# TEST_SOUNDSCAPESDIR 改为 TEST_SOUNDSCAPES_DIR\n\n        ],\n        verbose=1\n    )\n    return model, le\n\n# ==================== 预测部分 ====================\ndef predict_audio_segment(model, audio_segment, sr=32000):\n    \"\"\"预测单个5秒音频片段\"\"\"\n    features = extract_features(audio_segment, sr)\n    x = features.reshape((1, features.shape[0], 1, 1))\n    return model.predict(x, verbose=0)[0]\n\n\ndef process_test_file(file_path, model, classes, sr=32000):\n    \"\"\"处理1分钟测试音频文件\"\"\"\n    try:\n        audio, _ = librosa.load(file_path, sr=sr)\n        segments = [audio[i*sr*5:(i+1)*sr*5] for i in range(12)]\n        return np.array([predict_audio_segment(model, seg, sr) for seg in segments])\n    except Exception as e:\n        print(f\"处理文件 {file_path} 出错: {str(e)}\")\n        return None\n\n# ==================== 主流程 ====================\ndef main():\n    print(\"加载数据...\")\n    train_data = load_and_prepare_data()\n    classes = train_data['primary_label'].unique().tolist()\n\n    print(\"\\n训练模型...\")\n    model, label_encoder = train_model(train_data, classes)\n\n    print(\"\\n生成提交文件...\")\n    sample_sub = pd.read_csv(os.path.join(DATA_DIR, 'sample_submission.csv'))\n    required_species = sample_sub.columns[1:].tolist()\n\n    submission = pd.DataFrame(columns=['row_id'] + required_species)\n    for file in tqdm(list(Path(TEST_SOUNDSCAPES_DIR).glob('*.ogg')), desc=\"处理测试音景\"):\n        soundscape_id = file.stem\n        preds = process_test_file(str(file), model, classes)\n        if preds is not None:\n            for i, pred in enumerate(preds):\n                row_data = {'row_id': f\"{soundscape_id}_{(i+1)*5}\"}\n                for species, prob in zip(classes, pred):\n                    if species in required_species:\n                        row_data[species] = prob\n                submission = submission.append(row_data, ignore_index=True)\n    \n    submission = submission.reindex(columns=['row_id'] + required_species, fill_value=0.0)\n    submission.to_csv(SUBMISSION_PATH, index=False)\n    print(f\"\\n提交文件已保存: {SUBMISSION_PATH}\")\n    print(submission.head())\n\nif __name__ == \"__main__\":\n    main()\n","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}