{"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":11075449,"sourceType":"datasetVersion","datasetId":6902504}],"dockerImageVersionId":30918,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"## Fork from BirdCLEF 2025: EDA + DEEP LEARNING ANALYSIS\n\nhttps://www.kaggle.com/code/amritarajput54/birdclef-2025-eda-deep-learning-analysis","metadata":{}},{"cell_type":"markdown","source":"https://www.kaggle.com/code/docxian/birdclef-2025-eda-geography\n\nhttps://www.kaggle.com/code/xiaoazuzong/lb-0-778-efficientnet-b0-pytorch-inference/notebook\n","metadata":{}},{"cell_type":"code","source":"# 导入操作系统相关的模块，用于处理文件和目录\nimport os\n# 导入垃圾回收模块，用于手动管理内存\nimport gc\n# 导入警告模块，用于控制警告信息的显示\nimport warnings\n# 导入日志模块，用于记录程序运行时的信息\nimport logging\n# 导入时间模块，用于处理时间相关的操作\nimport time\n# 导入数学模块，提供数学函数\nimport math\n# 导入OpenCV库，用于图像处理\nimport cv2\n# 导入Path模块，用于处理文件路径\nfrom pathlib import Path\n\n# 导入NumPy库，用于数值计算\nimport numpy as np\n# 导入Pandas库，用于数据处理和分析\nimport pandas as pd\n# 导入librosa库，用于音频处理\nimport librosa\n# 导入PyTorch库，用于深度学习\nimport torch\n# 导入PyTorch的神经网络模块\nimport torch.nn as nn\n# 导入PyTorch的函数模块，包含常用的函数操作\nimport torch.nn.functional as F\n# 导入timm库，提供预训练的深度学习模型\nimport timm\n# 导入tqdm库，用于显示进度条\nfrom tqdm.auto import tqdm\n\n# 再次导入NumPy和Pandas库（重复导入，无实际作用）\nimport numpy as np\nimport pandas as pd\n\n# 导入matplotlib.pyplot模块，用于绘图\nimport matplotlib.pyplot as plt\n# 导入seaborn库，用于数据可视化\nimport seaborn as sns\n# 导入folium库，用于地理数据可视化\nimport folium\n\n# 再次导入librosa库（重复导入，无实际作用）\nimport librosa\n# 导入librosa.display模块，用于音频可视化\nimport librosa.display\n# 导入IPython.display模块，用于在Jupyter Notebook中播放音频\nfrom IPython.display import Audio\n\n# 忽略所有警告信息\nwarnings.filterwarnings(\"ignore\")\n# 配置日志记录级别为ERROR，只记录错误信息\nlogging.basicConfig(level=logging.ERROR)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-20T04:46:10.415487Z","iopub.execute_input":"2025-03-20T04:46:10.416093Z","iopub.status.idle":"2025-03-20T04:46:10.425550Z","shell.execute_reply.started":"2025-03-20T04:46:10.416033Z","shell.execute_reply":"2025-03-20T04:46:10.423562Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 定义一个名为CFG的配置类，用于存储和管理模型训练和推理的参数\nclass CFG:\n    # 测试音频文件的路径\n    test_soundscapes = '/kaggle/input/birdclef-2025/test_soundscapes'\n    # 提交文件的路径（样本提交文件）\n    submission_csv = '/kaggle/input/birdclef-2025/sample_submission.csv'\n    # 鸟类分类学文件的路径\n    taxonomy_csv = '/kaggle/input/birdclef-2025/taxonomy.csv'\n    # 预训练模型文件的路径\n    model_path = '/kaggle/input/birdclef-2025-efficientnet-b0'  \n    \n    # 音频处理参数\n    FS = 32000  # 采样率（32kHz）\n    WINDOW_SIZE = 5  # 窗口大小（秒）\n    \n    # Mel频谱图参数\n    N_FFT = 1024  # FFT窗口大小\n    HOP_LENGTH = 512  # 帧移大小\n    N_MELS = 128  # Mel频带数量\n    FMIN = 50  # 最小频率\n    FMAX = 14000  # 最大频率\n    TARGET_SHAPE = (256, 256)  # 目标图像尺寸\n    \n    # 模型相关参数\n    model_name = 'efficientnet_b0'  # 使用的模型名称\n    in_channels = 1  # 输入通道数（灰度图像为1）\n    device = 'cpu'  # 使用的设备（CPU或GPU）\n    \n    # 推理参数\n    batch_size = 16  # 批量大小\n    use_tta = False  # 是否使用测试时数据增强（Test Time Augmentation）\n    tta_count = 3  # TTA的次数\n    threshold = 0.5  # 分类阈值\n    \n    # 模型折叠相关参数\n    use_specific_folds = False  # 是否使用特定的模型折叠（False表示使用所有找到的模型）\n    folds = [0, 1]  # 使用的模型折叠列表（仅在use_specific_folds为True时生效）\n    \n    # 调试参数\n    debug = False  # 是否启用调试模式\n    debug_count = 3  # 调试模式下处理的样本数量\n\n# 创建CFG类的实例，用于访问配置参数\ncfg = CFG()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-20T04:46:10.428058Z","iopub.execute_input":"2025-03-20T04:46:10.428523Z","iopub.status.idle":"2025-03-20T04:46:10.449115Z","shell.execute_reply.started":"2025-03-20T04:46:10.428485Z","shell.execute_reply":"2025-03-20T04:46:10.447542Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 读取训练数据文件\ndf = pd.read_csv('../input/birdclef-2025/train.csv')\n\n# 读取分类学文件\ndf_taxo = pd.read_csv('../input/birdclef-2025/taxonomy.csv')\n\n# 将训练数据与分类学数据合并，基于'primary_label'列进行左连接\n# 添加分类学数据中的'inat_taxon_id'和'class_name'列到训练数据中\ndf = pd.merge(left=df, right=df_taxo[['primary_label', 'inat_taxon_id', 'class_name']], how='left', on='primary_label')\n\n# 打印合并后的数据的前几行，用于检查数据是否正确合并\nprint(df.head())\n\n# 打印数据的基本信息，包括列名、数据类型和非空值数量\nprint(df.info())\n\n# 统计并打印'collection'列中各值的频次，用于了解数据分布\nprint(df.collection.value_counts())\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-20T04:46:10.451517Z","iopub.execute_input":"2025-03-20T04:46:10.452001Z","iopub.status.idle":"2025-03-20T04:46:10.672189Z","shell.execute_reply.started":"2025-03-20T04:46:10.451931Z","shell.execute_reply":"2025-03-20T04:46:10.670655Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 创建一个简单的散点图，显示数据集中鸟类记录的地理位置分布\nplt.figure(figsize=(12, 6))  # 设置图像的大小为12x6英寸\n\n# 使用Seaborn绘制散点图，x轴为经度（longitude），y轴为纬度（latitude）\n# 数据来自df，点的颜色设置为深蓝色（darkblue）\nsns.scatterplot(data=df, x='longitude', y='latitude', color='darkblue')\n\nplt.grid()  # 添加网格线，便于观察数据分布\nplt.show()  # 显示图像\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-20T04:46:10.673478Z","iopub.execute_input":"2025-03-20T04:46:10.673779Z","iopub.status.idle":"2025-03-20T04:46:10.967881Z","shell.execute_reply.started":"2025-03-20T04:46:10.673751Z","shell.execute_reply":"2025-03-20T04:46:10.966774Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 加载音频文件\nfilename = 'XC112602.ogg'  # 音频文件名\ny, sr = librosa.load('../input/birdclef-2025/train_audio/banana/' + filename)  # 使用librosa加载音频文件，y为音频数据，sr为采样率\n\n# 播放音频\nAudio(y, rate=sr)  # 使用IPython.display.Audio播放音频\n\n# 绘制音频波形图\nplt.figure(figsize=(14, 5))  # 设置图像大小为14x5英寸\nplt.plot(y, color='darkblue')  # 绘制音频波形，颜色为深蓝色\nplt.grid()  # 添加网格线\nplt.show()  # 显示图像\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-20T04:46:10.969143Z","iopub.execute_input":"2025-03-20T04:46:10.969459Z","iopub.status.idle":"2025-03-20T04:46:11.558476Z","shell.execute_reply.started":"2025-03-20T04:46:10.969430Z","shell.execute_reply":"2025-03-20T04:46:11.557362Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 播放音频\nAudio(y, rate=sr)  # 使用IPython.display.Audio播放音频\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-20T04:46:11.559584Z","iopub.execute_input":"2025-03-20T04:46:11.559918Z","iopub.status.idle":"2025-03-20T04:46:11.609504Z","shell.execute_reply.started":"2025-03-20T04:46:11.559886Z","shell.execute_reply":"2025-03-20T04:46:11.607834Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 示例音频路径\n# audio_path = \"../input/birdclef-2025/train_audio/XC12345.ogg\"  # 替换为实际的音频文件路径\n\n# 加载音频文件并获取其持续时间\n# y, sr = librosa.load(audio_path, sr=None)  # 加载音频，y为音频数据，sr为采样率\n# duration = librosa.get_duration(y=y, sr=sr)  # 计算音频的持续时间\n# print(f\"Duration: {duration:.2f} seconds\")  # 打印音频持续时间，保留两位小数\n\n# 绘制音频波形图\n# plt.figure(figsize=(10, 4))  # 设置图像大小为10x4英寸\n# librosa.display.waveshow(y, sr=sr)  # 绘制音频波形\n# plt.title('Waveform of Sample Audio')  # 设置图像标题\n# plt.xlabel('Time (s)')  # 设置x轴标签\n# plt.ylabel('Amplitude')  # 设置y轴标签\n# plt.show()  # 显示图像\n\n# 定义绘制梅尔频谱图的函数\n# def plot_spectrogram(audio_path):\n#     y, sr = librosa.load(audio_path, sr=None)  # 加载音频\n#     S = librosa.feature.melspectrogram(y=y, sr=sr, n_mels=128)  # 计算梅尔频谱图\n#     S_dB = librosa.power_to_db(S, ref=np.max)  # 将频谱图转换为分贝单位\n\n#     plt.figure(figsize=(10, 4))  # 设置图像大小为10x4英寸\n#     librosa.display.specshow(S_dB, sr=sr, x_axis='time', y_axis='mel')  # 显示梅尔频谱图\n#     plt.colorbar(format='%+2.0f dB')  # 添加颜色条\n#     plt.title('Mel Spectrogram')  # 设置图像标题\n#     plt.show()  # 显示图像\n\n# 示例：绘制指定音频的梅尔频谱图\n# plot_spectrogram(audio_path)  # 调用函数绘制频谱图\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-20T04:46:11.610674Z","iopub.execute_input":"2025-03-20T04:46:11.610995Z","iopub.status.idle":"2025-03-20T04:46:11.615649Z","shell.execute_reply.started":"2025-03-20T04:46:11.610967Z","shell.execute_reply":"2025-03-20T04:46:11.614146Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 导入librosa库，用于音频处理\nimport librosa\n\n# 定义正确的音频文件路径\naudio_path = \"../input/birdclef-2025/train_audio/greani1/XC132190.ogg\"\n\n# 加载音频文件\ny, sr = librosa.load(audio_path, sr=None)\n# 打印音频加载成功信息及音频持续时间\nprint(f\"Audio Loaded! ✅ Duration: {librosa.get_duration(y=y, sr=sr):.2f} seconds\")\n\n# 导入glob库，用于文件路径匹配\nimport glob\n\n# 获取所有子目录中的OGG文件\nall_audio_files = glob.glob(\"../input/birdclef-2025/train_audio/**/*.ogg\", recursive=True)\n\n# 加载并分析前3个音频文件\nfor audio_path in all_audio_files[:3]:\n    y, sr = librosa.load(audio_path, sr=None)\n    duration = librosa.get_duration(y=y, sr=sr)\n    print(f\"Loaded: {audio_path.split('/')[-1]} | Duration: {duration:.2f} seconds\")\n\n# 导入librosa.display和matplotlib.pyplot库，用于音频可视化\nimport librosa.display\nimport matplotlib.pyplot as plt\n\n# 绘制音频波形图\nplt.figure(figsize=(10, 4))\nlibrosa.display.waveshow(y, sr=sr)\nplt.title('Waveform of Sample Audio')\nplt.xlabel('Time (s)')\nplt.ylabel('Amplitude')\nplt.show()\n\n# 绘制梅尔频谱图\nS = librosa.feature.melspectrogram(y=y, sr=sr, n_mels=128)\nS_dB = librosa.power_to_db(S, ref=np.max)\n\nplt.figure(figsize=(10, 4))\nlibrosa.display.specshow(S_dB, sr=sr, x_axis='time', y_axis='mel')\nplt.colorbar(format='%+2.0f dB')\nplt.title('Mel Spectrogram')\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-20T04:46:11.618971Z","iopub.execute_input":"2025-03-20T04:46:11.619345Z","iopub.status.idle":"2025-03-20T04:46:30.752354Z","shell.execute_reply.started":"2025-03-20T04:46:11.619313Z","shell.execute_reply":"2025-03-20T04:46:30.751134Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 定义一个函数，用于从音频文件中提取特征\ndef extract_features(audio_path, max_pad_len=128):\n    # 加载音频文件，采样率为32 kHz\n    y, sr = librosa.load(audio_path, sr=32000)\n    # 计算梅尔频谱图\n    S = librosa.feature.melspectrogram(y=y, sr=sr, n_mels=128)\n    # 将梅尔频谱图转换为分贝单位\n    S_db = librosa.power_to_db(S, ref=np.max)\n\n    # 对频谱图进行填充或截断，使其长度固定为max_pad_len\n    if S_db.shape[1] < max_pad_len:  # 如果频谱图长度小于max_pad_len\n        pad_width = max_pad_len - S_db.shape[1]  # 计算需要填充的宽度\n        S_db = np.pad(S_db, ((0, 0), (0, pad_width)), mode='constant')  # 在右侧填充0\n    else:  # 如果频谱图长度大于max_pad_len\n        S_db = S_db[:, :max_pad_len]  # 截断右侧多余的部分\n\n    return S_db  # 返回处理后的频谱图\n\n# 示例：提取音频文件的特征\naudio_path = \"../input/birdclef-2025/train_audio/greani1/XC132190.ogg\"  # 音频文件路径\nfeatures = extract_features(audio_path)  # 调用函数提取特征\nprint(f\"Extracted Features Shape: {features.shape}\")  # 打印提取的特征的形状\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-20T04:46:30.754428Z","iopub.execute_input":"2025-03-20T04:46:30.754777Z","iopub.status.idle":"2025-03-20T04:46:30.839697Z","shell.execute_reply.started":"2025-03-20T04:46:30.754745Z","shell.execute_reply":"2025-03-20T04:46:30.838222Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 读取训练数据文件\ntrain_df = pd.read_csv('../input/birdclef-2025/train.csv')\n\n# 读取分类学文件\ntaxonomy_df = pd.read_csv('../input/birdclef-2025/taxonomy.csv')\n\n# 将训练数据与分类学数据合并，基于'primary_label'列进行左连接\n# 添加分类学数据中的'class_name'列到训练数据中\ntrain_df = pd.merge(train_df, taxonomy_df[['primary_label', 'class_name']], how='left', on='primary_label')\n\n# 打印合并后的数据的前几行，用于检查数据是否正确合并\nprint(train_df.head())\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-20T04:46:30.841005Z","iopub.execute_input":"2025-03-20T04:46:30.841472Z","iopub.status.idle":"2025-03-20T04:46:30.988095Z","shell.execute_reply.started":"2025-03-20T04:46:30.841416Z","shell.execute_reply":"2025-03-20T04:46:30.987036Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 导入必要的库\nimport os\nimport numpy as np\nimport pandas as pd\nimport librosa\nimport cv2\nfrom sklearn.model_selection import train_test_split\nfrom tensorflow.keras.utils import to_categorical\n\n# 定义函数：对频谱图进行填充或截断，使其固定大小为128x128\ndef pad_or_truncate(S, max_len=128):\n    \"\"\"Pad or truncate the spectrogram to a fixed size of 128x128.\"\"\"\n    if S.shape[1] < max_len:  # 如果频谱图长度小于max_len\n        # 在右侧填充0\n        pad_width = max_len - S.shape[1]\n        S = np.pad(S, ((0, 0), (0, pad_width)), mode='constant')\n    else:  # 如果频谱图长度大于max_len\n        # 截断右侧多余的部分\n        S = S[:, :max_len]\n\n    return S\n\n# 定义函数：从音频文件中提取特征，并进行填充或截断\ndef extract_features(audio_path):\n    try:\n        # 加载音频文件\n        y, sr = librosa.load(audio_path, sr=None)\n        # 计算梅尔频谱图\n        S = librosa.feature.melspectrogram(y=y, sr=sr, n_mels=128)\n        # 将梅尔频谱图转换为分贝单位\n        S_dB = librosa.power_to_db(S, ref=np.max)\n\n        # 对频谱图进行填充或截断，使其固定大小为128x128\n        S_fixed = pad_or_truncate(S_dB, max_len=128)\n        \n        return S_fixed\n    except Exception as e:\n        # 如果处理失败，打印错误信息\n        print(f\"❌ Error processing {audio_path}: {e}\")\n        return None\n\n# 从训练数据中随机抽取100个样本（为了快速测试）\nsample_df = train_df.sample(100, random_state=42)  # 使用42作为随机种子以确保可重复性\nX, y = [], []  # 初始化特征和标签列表\n\n# 遍历样本数据，提取特征\nfor i, row in sample_df.iterrows():\n    # 构造音频文件路径\n    audio_file = f\"../input/birdclef-2025/train_audio/{row['filename']}\"\n    if os.path.exists(audio_file):  # 检查文件是否存在\n        # 提取特征\n        feature = extract_features(audio_file)\n        \n        # 检查特征是否有效且形状为128x128\n        if feature is not None and feature.shape == (128, 128):\n            X.append(feature)\n            y.append(row['primary_label'])\n        else:\n            # 如果特征无效，跳过并打印警告\n            print(f\"⚠️ Skipping {row['filename']} due to invalid feature shape.\")\n\n# 检查是否有有效的特征和标签\nif len(X) == 0 or len(y) == 0:\n    raise ValueError(\"❌ No valid audio files were processed. Check file paths and feature extraction!\")\n\n# 将特征和标签转换为NumPy数组\nX = np.array(X)\n# 将特征重塑为适合CNN输入的形状（样本数, 高度, 宽度, 通道数）\nX = X.reshape(X.shape[0], 128, 128, 1)\n# 将文本标签编码为整数，并转换为one-hot编码\ny_encoded, y_labels = pd.factorize(y)\ny = to_categorical(y_encoded)\n\n# 将数据集拆分为训练集和验证集\nX_train, X_val, y_train, y_val = train_test_split(X, y, test_size=0.2, random_state=42)\n\n# 打印训练集和验证集的形状\nprint(f\"✅ Training data shape: {X_train.shape}, Validation data shape: {X_val.shape}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-20T04:46:30.989291Z","iopub.execute_input":"2025-03-20T04:46:30.989628Z","iopub.status.idle":"2025-03-20T04:46:42.098437Z","shell.execute_reply.started":"2025-03-20T04:46:30.989583Z","shell.execute_reply":"2025-03-20T04:46:42.096455Z"},"_kg_hide-output":true,"_kg_hide-input":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":" ","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 打印成功处理的音频文件数量\nprint(f\"✅ Files successfully processed: {len(X)}\")\n\n# 打印数据集中唯一的类别数量\nprint(f\"✅ Unique classes: {len(np.unique(y_encoded))}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-20T04:46:42.101201Z","iopub.execute_input":"2025-03-20T04:46:42.101724Z","iopub.status.idle":"2025-03-20T04:46:42.113394Z","shell.execute_reply.started":"2025-03-20T04:46:42.101682Z","shell.execute_reply":"2025-03-20T04:46:42.111483Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 导入必要的Keras模块\nfrom tensorflow.keras.models import Sequential\nfrom tensorflow.keras.layers import (\n    Conv2D, MaxPooling2D, Flatten, Dense, Dropout, BatchNormalization\n)\nfrom tensorflow.keras.optimizers import Adam\n\n# 定义优化的CNN模型\ndef build_cnn_model(input_shape=(128, 128, 1), num_classes=100):\n    model = Sequential()\n\n    # 卷积块1\n    model.add(Conv2D(16, (3, 3), activation=\"relu\", input_shape=input_shape, padding=\"same\"))\n    model.add(MaxPooling2D((2, 2)))\n\n    # 卷积块2\n    model.add(Conv2D(32, (3, 3), activation=\"relu\", padding=\"same\"))\n    model.add(MaxPooling2D((2, 2)))\n\n    # 卷积块3\n    model.add(Conv2D(64, (3, 3), activation=\"relu\", padding=\"same\"))\n    model.add(MaxPooling2D((2, 2)))\n\n    # 展平层和全连接层\n    model.add(Flatten())\n    model.add(Dense(128, activation=\"relu\"))\n    model.add(Dropout(0.3))  # 降低Dropout率以保留更多有用特征\n\n    # 输出层\n    model.add(Dense(num_classes, activation=\"softmax\"))\n\n    # 使用较低的学习率编译模型\n    optimizer = Adam(learning_rate=1e-4)  # 较低的学习率以稳定训练\n    model.compile(\n        optimizer=optimizer,\n        loss=\"categorical_crossentropy\",\n        metrics=[\"accuracy\"]\n    )\n\n    return model\n\n# 构建并编译改进后的模型\nmodel = build_cnn_model(input_shape=(128, 128, 1), num_classes=len(y_labels))\n\n# 打印模型摘要\nmodel.summary()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-20T04:46:42.115630Z","iopub.execute_input":"2025-03-20T04:46:42.116055Z","iopub.status.idle":"2025-03-20T04:46:42.290477Z","shell.execute_reply.started":"2025-03-20T04:46:42.116018Z","shell.execute_reply":"2025-03-20T04:46:42.289270Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 导入必要的库\nimport numpy as np\nimport tensorflow as tf\nfrom tensorflow.keras.models import Sequential\nfrom tensorflow.keras.layers import (Conv2D, MaxPooling2D, Flatten, Dense, Dropout, BatchNormalization)\nfrom tensorflow.keras.optimizers import Adam\n\n# 📚 创建虚拟数据集（替换为真实数据）\nX_train = np.random.rand(300, 64, 64, 3)  # 300个64x64的3通道图像\ny_train = np.random.randint(0, 5, 300)  # 300个标签，范围为0到4\nX_val = np.random.rand(50, 64, 64, 3)  # 50个64x64的3通道图像\ny_val = np.random.randint(0, 5, 50)  # 50个标签，范围为0到4\n\n# 🧠 将标签转换为One-Hot编码\ny_train_onehot = tf.keras.utils.to_categorical(y_train, 5)  # 5个类别\ny_val_onehot = tf.keras.utils.to_categorical(y_val, 5)\n\n# 🎨 构建模型\nmodel = Sequential()\n\n# 🔥 卷积层1\nmodel.add(Conv2D(64, (3, 3), activation='relu', input_shape=(64, 64, 3)))  # 64个3x3的卷积核\nmodel.add(BatchNormalization())  # 批量归一化\nmodel.add(MaxPooling2D(pool_size=(2, 2)))  # 2x2的最大池化\n\n# 🔥 卷积层2\nmodel.add(Conv2D(128, (3, 3), activation='relu'))  # 128个3x3的卷积核\nmodel.add(BatchNormalization())\nmodel.add(MaxPooling2D(pool_size=(2, 2)))\n\n# 🔥 卷积层3\nmodel.add(Conv2D(256, (3, 3), activation='relu'))  # 256个3x3的卷积核\nmodel.add(BatchNormalization())\nmodel.add(MaxPooling2D(pool_size=(2, 2)))\n\n# 🧠 展平层和全连接层\nmodel.add(Flatten())  # 展平多维输入\nmodel.add(Dense(512, activation='relu'))  # 512个神经元的全连接层\nmodel.add(Dropout(0.5))  # 50%的Dropout\nmodel.add(Dense(5, activation='softmax'))  # 输出层，5个类别\n\n# ⚡️ 编译模型\nmodel.compile(\n    optimizer='adam',  # 使用Adam优化器\n    loss='categorical_crossentropy',  # 多分类交叉熵损失函数\n    metrics=['accuracy']  # 评估指标为准确率\n)\n\n# 🏋️‍♂️ 训练模型\nhistory = model.fit(\n    X_train / 255.0, y_train_onehot,  # 训练数据（归一化）和标签\n    validation_data=(X_val / 255.0, y_val_onehot),  # 验证数据（归一化）和标签\n    epochs=5,  # 训练5个epoch\n    batch_size=32,  # 批量大小为32\n    callbacks=[\n        tf.keras.callbacks.ReduceLROnPlateau(monitor='val_loss', factor=0.5, patience=2, min_lr=1e-6),  # 动态调整学习率\n        tf.keras.callbacks.EarlyStopping(monitor='val_loss', patience=4, restore_best_weights=True)  # 早停策略\n    ]\n)\n\n# 📈 评估模型\nval_loss, val_acc = model.evaluate(X_val / 255.0, y_val_onehot)  # 在验证集上评估\nprint(f\"✅ Final Validation Accuracy: {val_acc:.4f}\")  # 打印最终的验证准确率\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-20T04:46:42.291770Z","iopub.execute_input":"2025-03-20T04:46:42.292290Z","iopub.status.idle":"2025-03-20T04:47:08.442706Z","shell.execute_reply.started":"2025-03-20T04:46:42.292240Z","shell.execute_reply":"2025-03-20T04:47:08.441277Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# print(y_train[:5])  # If this looks like [0, 1, 2, 3, 4] — use sparse_categorical_crossentropy\n# print(y_train_onehot[:5])  # If this looks like one-hot — use categorical_crossentropy\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-20T04:47:08.444026Z","iopub.execute_input":"2025-03-20T04:47:08.444470Z","iopub.status.idle":"2025-03-20T04:47:08.449246Z","shell.execute_reply.started":"2025-03-20T04:47:08.444427Z","shell.execute_reply":"2025-03-20T04:47:08.447813Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# import numpy as np\n# unique, counts = np.unique(y_train, return_counts=True)\n# print(dict(zip(unique, counts)))\n# print(X_val.shape, y_val.shape)\n# print(np.argmax(y_val[:5], axis=1))  # Check first few labels\n# preds = model.predict(X_val / 255.0)\n# print(np.argmax(preds[:5], axis=1))  # Check predictions\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-20T04:47:08.450476Z","iopub.execute_input":"2025-03-20T04:47:08.450870Z","iopub.status.idle":"2025-03-20T04:47:08.479820Z","shell.execute_reply.started":"2025-03-20T04:47:08.450828Z","shell.execute_reply":"2025-03-20T04:47:08.478117Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# from tensorflow.keras.applications import MobileNetV2\n# from tensorflow.keras.layers import GlobalAveragePooling2D, Dense, Dropout\n# from tensorflow.keras.models import Model\n# from tensorflow.keras.optimizers import Adam\n# from tensorflow.keras.callbacks import ReduceLROnPlateau, EarlyStopping\n\n# # 📸 Load MobileNetV2 with pre-trained ImageNet weights, excluding the top layer\n# base_model = MobileNetV2(weights='imagenet', include_top=False, input_shape=(64, 64, 3))\n\n# # 🛠️ Add custom classification head\n# x = base_model.output\n# x = GlobalAveragePooling2D()(x)\n# x = Dense(512, activation='relu')(x)\n# x = Dropout(0.5)(x)  # Add dropout to reduce overfitting\n# predictions = Dense(5, activation='softmax')(x)\n\n# # 🧠 Create final model\n# model = Model(inputs=base_model.input, outputs=predictions)\n\n# # 🧊 Freeze base model layers initially\n# for layer in base_model.layers:\n#     layer.trainable = False\n\n# # 🚀 Compile the model with a small learning rate for initial training\n# model.compile(\n#     optimizer=Adam(learning_rate=1e-4),\n#     loss='categorical_crossentropy',\n#     metrics=['accuracy']\n# )\n\n# # 📉 Callbacks for better training\n# reduce_lr = ReduceLROnPlateau(monitor='val_loss', factor=0.2, patience=3, min_lr=1e-6)\n# early_stop = EarlyStopping(monitor='val_loss', patience=5, restore_best_weights=True)\n\n# # 🏋️‍♂️ Train the model (initial training with frozen base layers)\n# history = model.fit(\n#     X_train / 255.0, y_train_onehot,\n#     validation_data=(X_val / 255.0, y_val_onehot),\n#     epochs=10,\n#     batch_size=32,\n#     callbacks=[reduce_lr, early_stop]\n# )\n\n# # 🔓 Unfreeze some of the top layers in base_model for fine-tuning\n# for layer in base_model.layers[-20:]:\n#     layer.trainable = True\n\n# # 🆙 Recompile with a smaller learning rate for fine-tuning\n# model.compile(\n#     optimizer=Adam(learning_rate=1e-5),  # Smaller LR for fine-tuning\n#     loss='categorical_crossentropy',\n#     metrics=['accuracy']\n# )\n\n# # 🎯 Fine-tuning the model with more epochs\n# fine_tune_history = model.fit(\n#     X_train / 255.0, y_train_onehot,\n#     validation_data=(X_val / 255.0, y_val_onehot),\n#     epochs=10,\n#     batch_size=32,\n#     callbacks=[reduce_lr, early_stop]\n# )\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-20T04:47:08.481360Z","iopub.execute_input":"2025-03-20T04:47:08.481784Z","iopub.status.idle":"2025-03-20T04:47:08.505620Z","shell.execute_reply.started":"2025-03-20T04:47:08.481746Z","shell.execute_reply":"2025-03-20T04:47:08.504016Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 读取分类学CSV文件\ntaxonomy_df = pd.read_csv(cfg.taxonomy_csv)\n\n# 获取所有物种的primary_label（唯一标识）并转换为列表\nspecies_ids = taxonomy_df['primary_label'].tolist()\n\n# 计算类别的数量（即物种的总数）\nnum_classes = len(species_ids)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-20T04:47:08.507217Z","iopub.execute_input":"2025-03-20T04:47:08.507600Z","iopub.status.idle":"2025-03-20T04:47:08.539714Z","shell.execute_reply.started":"2025-03-20T04:47:08.507568Z","shell.execute_reply":"2025-03-20T04:47:08.538402Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 导入PyTorch的神经网络模块\nimport torch.nn as nn\n\n# 定义BirdCLEF模型类\nclass BirdCLEFModel(nn.Module):\n    def __init__(self, cfg, num_classes):\n        super().__init__()  # 调用父类的初始化方法\n        self.cfg = cfg  # 保存配置对象\n        \n        # 使用timm库创建预训练模型（不加载预训练权重）\n        self.backbone = timm.create_model(\n            cfg.model_name,  # 模型名称（如'efficientnet_b0'或'resnet18'）\n            pretrained=False,  # 不使用预训练权重\n            in_chans=cfg.in_channels,  # 输入通道数\n            drop_rate=0.2,  # Dropout率\n            drop_path_rate=0.2  # DropPath率\n        )\n        \n        # 根据模型名称获取backbone的输出特征维度\n        if 'efficientnet' in cfg.model_name:\n            backbone_out = self.backbone.classifier.in_features  # EfficientNet的特征维度\n            self.backbone.classifier = nn.Identity()  # 移除分类器\n        elif 'resnet' in cfg.model_name:\n            backbone_out = self.backbone.fc.in_features  # ResNet的特征维度\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        # 定义自适应平均池化层\n        self.pooling = nn.AdaptiveAvgPool2d(1)\n        self.feat_dim = backbone_out  # 保存特征维度\n        # 定义分类器（全连接层）\n        self.classifier = nn.Linear(backbone_out, num_classes)\n        \n    def forward(self, x):\n        # 提取特征\n        features = self.backbone(x)\n        \n        # 如果输出是字典，提取'features'键的值\n        if isinstance(features, dict):\n            features = features['features']\n            \n        # 如果特征维度为4（如[batch_size, channels, height, width]），进行池化和展平\n        if len(features.shape) == 4:\n            features = self.pooling(features)\n            features = features.view(features.size(0), -1)\n        \n        # 通过分类器生成logits\n        logits = self.classifier(features)\n        return logits\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-20T04:47:08.541464Z","iopub.execute_input":"2025-03-20T04:47:08.541896Z","iopub.status.idle":"2025-03-20T04:47:08.553011Z","shell.execute_reply.started":"2025-03-20T04:47:08.541852Z","shell.execute_reply":"2025-03-20T04:47:08.551848Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import numpy as np\nimport librosa\nimport cv2\n\ndef audio2melspec(audio_data, cfg):\n    \"\"\"将音频数据转换为梅尔频谱图\"\"\"\n    # 检查音频数据中是否存在NaN值，如果存在则用均值填充\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    '''\n    # 计算梅尔频谱图\n    mel_spec = librosa.feature.melspectrogram(\n        y=audio_data,  # 音频数据\n        sr=cfg.FS,  # 采样率\n        n_fft=2048,       # 增加FFT窗口大小\n        hop_length=512,   # 增加帧移\n        n_mels=128,       # 增加梅尔频带数\n        fmin=50,          # 调整最低频率\n        fmax=8000         # 调整最高频率\n    )\n    '''\n\n    \n    # 计算梅尔频谱图\n    mel_spec = librosa.feature.melspectrogram(\n        y=audio_data,  # 音频数据\n        sr=cfg.FS,  # 采样率\n        n_fft=cfg.N_FFT,  # FFT窗口大小\n        hop_length=cfg.HOP_LENGTH,  # 帧移\n        n_mels=cfg.N_MELS,  # 梅尔频带数\n        fmin=cfg.FMIN,  # 最低频率\n        fmax=cfg.FMAX,  # 最高频率\n        power=2.0  # 功率谱的指数\n    )\n\n    \n    # 将梅尔频谱图转换为分贝单位\n    mel_spec_db = librosa.power_to_db(mel_spec, ref=np.max)\n    # 对梅尔频谱图进行归一化\n    mel_spec_norm = (mel_spec_db - mel_spec_db.min()) / (mel_spec_db.max() - mel_spec_db.min() + 1e-8)\n    \n    return mel_spec_norm\n\ndef process_audio_segment(audio_data, cfg):\n    \"\"\"处理音频片段以获取梅尔频谱图\"\"\"\n    # 如果音频数据长度小于目标长度，则用0填充\n    if len(audio_data) < cfg.FS * cfg.WINDOW_SIZE:\n        audio_data = np.pad(audio_data, \n                          (0, cfg.FS * cfg.WINDOW_SIZE - len(audio_data)), \n                          mode='constant')\n    # 添加随机时间拉伸\n    if cfg.use_augmentation:\n        stretch_factor = np.random.uniform(0.8, 1.2)\n        audio_data = librosa.effects.time_stretch(audio_data, rate=stretch_factor)\n    \n    # 将音频数据转换为梅尔频谱图\n    mel_spec = audio2melspec(audio_data, cfg)\n    \n    # 如果需要，调整梅尔频谱图的尺寸为目标形状\n    if mel_spec.shape != cfg.TARGET_SHAPE:\n        mel_spec = cv2.resize(mel_spec, cfg.TARGET_SHAPE, interpolation=cv2.INTER_LINEAR)\n        \n    return mel_spec.astype(np.float32)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-20T04:47:08.554254Z","iopub.execute_input":"2025-03-20T04:47:08.554672Z","iopub.status.idle":"2025-03-20T04:47:08.580774Z","shell.execute_reply.started":"2025-03-20T04:47:08.554635Z","shell.execute_reply":"2025-03-20T04:47:08.579334Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from pathlib import Path\nimport torch\nimport librosa\nimport numpy as np\n\ndef find_model_files(cfg):\n    \"\"\"\n    在指定的模型目录中查找所有.pth模型文件\n    \"\"\"\n    model_files = []\n    \n    model_dir = Path(cfg.model_path)\n    \n    # 递归查找所有.pth文件\n    for path in model_dir.glob('**/*.pth'):\n        model_files.append(str(path))\n    \n    return model_files\n\ndef load_models(cfg, num_classes):\n    \"\"\"\n    加载所有找到的模型文件并准备用于集成\n    \"\"\"\n    models = []\n    \n    # 查找模型文件\n    model_files = find_model_files(cfg)\n    \n    # 如果没有找到模型文件，打印警告并返回空列表\n    if not model_files:\n        print(f\"Warning: No model files found under {cfg.model_path}!\")\n        return models\n    \n    print(f\"Found a total of {len(model_files)} model files.\")\n    \n    # 如果指定了特定的folds，过滤模型文件\n    if cfg.use_specific_folds:\n        filtered_files = []\n        for fold in 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\"Using {len(model_files)} model files for the specified folds ({cfg.folds}).\")\n    \n    # 加载每个模型文件\n    for model_path in model_files:\n        try:\n            print(f\"Loading model: {model_path}\")\n            # 加载模型检查点\n            checkpoint = torch.load(model_path, map_location=torch.device(cfg.device))\n            \n            # 初始化模型并加载权重\n            model = BirdCLEFModel(cfg, num_classes)\n            model.load_state_dict(checkpoint['model_state_dict'])\n            model = model.to(cfg.device)\n            model.eval()  # 设置为评估模式\n            \n            models.append(model)\n        except Exception as e:\n            print(f\"Error loading model {model_path}: {e}\")\n    \n    return models\n\ndef predict_on_spectrogram(audio_path, models, cfg, species_ids):\n    \"\"\"\n    处理单个音频文件并预测每个5秒片段的物种存在情况\n    \"\"\"\n    predictions = []\n    row_ids = []\n    soundscape_id = Path(audio_path).stem  # 获取音频文件名（不含扩展名）\n    \n    try:\n        print(f\"Processing {soundscape_id}\")\n        # 加载音频数据\n        audio_data, _ = librosa.load(audio_path, sr=cfg.FS)\n        \n        # 计算总片段数\n        total_segments = int(len(audio_data) / (cfg.FS * cfg.WINDOW_SIZE))\n        \n        # 遍历每个片段\n        for segment_idx in range(total_segments):\n            start_sample = segment_idx * cfg.FS * cfg.WINDOW_SIZE\n            end_sample = start_sample + cfg.FS * cfg.WINDOW_SIZE\n            segment_audio = audio_data[start_sample:end_sample]\n            \n            # 生成片段ID\n            end_time_sec = (segment_idx + 1) * cfg.WINDOW_SIZE\n            row_id = f\"{soundscape_id}_{end_time_sec}\"\n            row_ids.append(row_id)\n\n            # 如果启用TTA（测试时增强）\n            if cfg.use_tta:\n                all_preds = []\n                \n                # 对每个TTA索引进行预测\n                for tta_idx in range(cfg.tta_count):\n                    mel_spec = process_audio_segment(segment_audio, cfg)\n                    mel_spec = apply_tta(mel_spec, tta_idx)\n\n                    # 将梅尔频谱图转换为张量并移动到指定设备\n                    mel_spec = torch.tensor(mel_spec, dtype=torch.float32).unsqueeze(0).unsqueeze(0)\n                    mel_spec = mel_spec.to(cfg.device)\n\n                    # 如果只有一个模型，直接预测\n                    if len(models) == 1:\n                        with torch.no_grad():\n                            outputs = models[0](mel_spec)\n                            probs = torch.sigmoid(outputs).cpu().numpy().squeeze()\n                            all_preds.append(probs)\n                    else:\n                        # 如果有多个模型，对每个模型进行预测并取平均\n                        segment_preds = []\n                        for model in models:\n                            with torch.no_grad():\n                                outputs = model(mel_spec)\n                                probs = torch.sigmoid(outputs).cpu().numpy().squeeze()\n                                segment_preds.append(probs)\n                        \n                        avg_preds = np.mean(segment_preds, axis=0)\n                        all_preds.append(avg_preds)\n\n                # 对所有TTA结果取平均\n                final_preds = np.mean(all_preds, axis=0)\n            else:\n                # 如果不启用TTA，直接处理音频片段\n                mel_spec = process_audio_segment(segment_audio, cfg)\n                \n                # 将梅尔频谱图转换为张量并移动到指定设备\n                mel_spec = torch.tensor(mel_spec, dtype=torch.float32).unsqueeze(0).unsqueeze(0)\n                mel_spec = mel_spec.to(cfg.device)\n                \n                # 如果只有一个模型，直接预测\n                if len(models) == 1:\n                    with torch.no_grad():\n                        outputs = models[0](mel_spec)\n                        final_preds = torch.sigmoid(outputs).cpu().numpy().squeeze()\n                else:\n                    # 如果有多个模型，对每个模型进行预测并取平均\n                    segment_preds = []\n                    for model in models:\n                        with torch.no_grad():\n                            outputs = model(mel_spec)\n                            probs = torch.sigmoid(outputs).cpu().numpy().squeeze()\n                            segment_preds.append(probs)\n\n                    final_preds = np.mean(segment_preds, axis=0)\n                    \n            # 保存预测结果\n            predictions.append(final_preds)\n            \n    except Exception as e:\n        print(f\"Error processing {audio_path}: {e}\")\n    \n    return row_ids, predictions\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-20T04:47:08.582112Z","iopub.execute_input":"2025-03-20T04:47:08.582432Z","iopub.status.idle":"2025-03-20T04:47:08.605811Z","shell.execute_reply.started":"2025-03-20T04:47:08.582404Z","shell.execute_reply":"2025-03-20T04:47:08.604476Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import numpy as np\nimport pandas as pd\nfrom pathlib import Path\nfrom tqdm import tqdm\n\ndef apply_tta(spec, tta_idx):\n    \"\"\"\n    应用测试时增强（Test-Time Augmentation, TTA）到频谱图上\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        # 默认返回原始频谱图\n        return spec\n\ndef run_inference(cfg, models, species_ids):\n    \"\"\"\n    对所有测试音频文件运行推理\n    \"\"\"\n    # 获取所有测试音频文件\n    test_files = list(Path(cfg.test_soundscapes).glob('*.ogg'))\n    \n    # 如果启用调试模式，只使用部分文件\n    if cfg.debug:\n        print(f\"Debug mode enabled, using only {cfg.debug_count} files\")\n        test_files = test_files[:cfg.debug_count]\n    \n    print(f\"Found {len(test_files)} test soundscapes\")\n\n    all_row_ids = []\n    all_predictions = []\n\n    # 遍历所有测试文件并进行推理\n    for audio_path in tqdm(test_files):\n        row_ids, predictions = predict_on_spectrogram(str(audio_path), models, cfg, species_ids)\n        all_row_ids.extend(row_ids)\n        all_predictions.extend(predictions)\n    \n    return all_row_ids, all_predictions\n\ndef create_submission(row_ids, predictions, species_ids, cfg):\n    \"\"\"\n    创建提交文件的数据框\n    \"\"\"\n    print(\"Creating submission dataframe...\")\n\n    # 初始化提交字典，包含row_id和每个物种的预测值\n    submission_dict = {'row_id': row_ids}\n    \n    # 将每个物种的预测值添加到字典中\n    for i, species in enumerate(species_ids):\n        submission_dict[species] = [pred[i] for pred in predictions]\n\n    # 将字典转换为数据框\n    submission_df = pd.DataFrame(submission_dict)\n\n    # 将row_id设置为索引\n    submission_df.set_index('row_id', inplace=True)\n\n    # 读取示例提交文件\n    sample_sub = pd.read_csv(cfg.submission_csv, index_col='row_id')\n\n    # 检查是否有缺失的物种列\n    missing_cols = set(sample_sub.columns) - set(submission_df.columns)\n    if missing_cols:\n        print(f\"Warning: Missing {len(missing_cols)} species columns in submission\")\n        # 为缺失的物种列填充0.0\n        for col in missing_cols:\n            submission_df[col] = 0.0\n\n    # 确保提交文件的列顺序与示例文件一致\n    submission_df = submission_df[sample_sub.columns]\n\n    # 重置索引，将row_id恢复为列\n    submission_df = submission_df.reset_index()\n    \n    return submission_df\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-20T04:47:08.609509Z","iopub.execute_input":"2025-03-20T04:47:08.610158Z","iopub.status.idle":"2025-03-20T04:47:08.633662Z","shell.execute_reply.started":"2025-03-20T04:47:08.610103Z","shell.execute_reply":"2025-03-20T04:47:08.632401Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import time\n\ndef main():\n    \"\"\"\n    主函数：执行BirdCLEF-2025推理流程\n    \"\"\"\n    # 记录开始时间\n    start_time = time.time()\n    print(\"Starting BirdCLEF-2025 inference...\")\n    \n    # 打印TTA（测试时增强）配置\n    print(f\"TTA enabled: {cfg.use_tta} (variations: {cfg.tta_count if cfg.use_tta else 0})\")\n\n    # 加载模型\n    models = load_models(cfg, num_classes)\n    \n    # 如果没有加载到模型，打印错误信息并退出\n    if not models:\n        print(\"No models found! Please check model paths.\")\n        return\n    \n    # 打印模型使用情况（单个模型或集成模型）\n    print(f\"Model usage: {'Single model' if len(models) == 1 else f'Ensemble of {len(models)} models'}\")\n\n    # 运行推理，获取片段ID和预测结果\n    row_ids, predictions = run_inference(cfg, models, species_ids)\n\n    # 创建提交文件的数据框\n    submission_df = create_submission(row_ids, predictions, species_ids, cfg)\n\n    # 保存提交文件\n    submission_path = 'submission.csv'\n    submission_df.to_csv(submission_path, index=False)\n    print(f\"Submission saved to {submission_path}\")\n    \n    # 记录结束时间并打印总耗时\n    end_time = time.time()\n    print(f\"Inference completed in {(end_time - start_time)/60:.2f} minutes\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-20T04:47:08.635196Z","iopub.execute_input":"2025-03-20T04:47:08.635587Z","iopub.status.idle":"2025-03-20T04:47:08.660678Z","shell.execute_reply.started":"2025-03-20T04:47:08.635545Z","shell.execute_reply":"2025-03-20T04:47:08.659345Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n\nif __name__ == \"__main__\":\n    main()\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-20T04:47:08.661860Z","iopub.execute_input":"2025-03-20T04:47:08.662234Z","iopub.status.idle":"2025-03-20T04:47:09.875410Z","shell.execute_reply.started":"2025-03-20T04:47:08.662202Z","shell.execute_reply":"2025-03-20T04:47:09.874167Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}