1.任务概述
1.1竞赛目标
鸟类是生物多样性变化的极佳指标,因为它们流动性强,对栖息地的要求多种多样。因此,物种组合和鸟类数量的变化可以说明恢复项目的成败。然而,经常在大面积区域内开展传统的基于观察者的鸟类生物多样性调查不仅成本高昂,而且在后勤方面也极具挑战性。相比之下,被动声学监测(PAM)与基于机器学习的新型分析工具相结合,使保护工作者能够以更高的时间分辨率对更大的空间范围进行采样,并深入探索恢复干预与生物多样性之间的关系。
在本次竞赛中,您将利用机器学习技能,通过声音识别研究不足的印度鸟类物种。具体来说,你们将开发处理连续音频数据的计算解决方案,并通过其叫声识别物种。最优秀的作品将能够利用有限的训练数据训练出可靠的分类器。如果成功,您将帮助推进目前保护印度西高止山脉鸟类生物多样性的工作,包括由蒂鲁帕蒂(Tirupati)国际科学研究所V. V. Robin实验室领导的工作。
1.2背景
面对栖息地改变和气候变化带来的快速人为压力,我们需要利用最新的保护工具和技术来监测生物多样性。通过 Kaggle 竞赛,我们旨在从声音景观中对西高止山脉的鸟类进行自动检测和分类。
西高止山脉是全球生物多样性热点地区,山脉沿印度西南海岸延伸。这里有多种多样的生态系统,支持着非同寻常的生物多样性。这些生态系统包括高海拔森林-草原镶嵌区、热带干燥落叶林和湿润常绿雨林等。此外,该地区还居住着大量人口,他们的生活依赖于森林和该地区提供的自然资源。从鸟类的角度来看,该地区的鸟类种类繁多,其中有几种特有和濒危物种在其他地方都找不到。然而,这片山脉正在经历着剧烈的地貌和气候变化,对生物多样性造成了负面影响。因此,我们需要保护技术和工具来帮助我们快速评估和监测鸟类多样性。
本次 Kaggle 竞赛的总体目标包括
(1) 在声景数据中识别西高止山脉天空之境(sky-islands)的特有鸟类。
(2) 利用有限的训练数据检测/分类濒危鸟类物种(受保护物种)。
(3) 检测/分类了解甚少的夜间鸟类物种。
有了你们的创新,研究人员和保护工作者将更容易准确地调查鸟类种群趋势。因此,他们将能够评估威胁,并定期、更有效地调整保护行动。
本次竞赛由(按字母顺序排列)开姆尼斯理工大学、谷歌研究院、印度科学教育与研究院(IISER)提鲁帕提分院、康奈尔鸟类学实验室杨丽莎保护生物声学中心、LifeCLEF 和 Xeno-canto 共同组织。
1.3评估
本次竞赛的评估指标是macro-averaged ROC-AUC的一个版本,该版本忽视了没有正向标签的类。
P (Positive) 和 N(Negative) 代表模型的判断结果
T (True) 和 F(False) 评价模型的判断结果是否正确
FP: 假正例,模型的判断是正例 (P) ,实际上这是错误的(F),连起来就是假正例
FN:假负例,模型的判断是负例(N),实际上这是错误的(F),连起来就是假正例
TP:真正例, 模型的判断是正例(P),实际上它也是正例,预测正确(T),连起来就是真正例
TN:真负例,模型的判断是负例(N),实际上它也是负例,预测正确(T),就是真正例
Accuracy:准确率
准确率=预测正确的样本数/所有样本数,即预测正确的样本比例(包括预测正确的正样本和预测正确的负样本)。
Precision:查准率
用于衡量模型对某一类的预测有多准。
Recall:召回率(真正类率)
指的是某个类别的Recall。Recall表示某一类样本,预测正确的与所有Ground Truth的比例。
FPR:负正类率
代表分类器预测的正类中实际负实例占所有负实例的比例。FPR = 1 - TNR
TNR:真负类率
代表分类器预测的负类中负实例占所有负实例的比例,TNR=1-FPR
ROC 和 AUC
ROC空间将负正类率(FPR)定义为 X 轴,真负类率(TPR)定义为 Y 轴。
给定一个二元分类模型和它的阈值,就能从所有样本的(阳性/阴性)真实值和预测值计算出一个 (X=FPR, Y=TPR) 座标点。 在这条线的以上的点代表了一个好的分类结果(胜过随机分类),而在这条线以下的点代表了差的分类结果(劣于随机分类)。
AUC:ROC曲线下方的面积(英语:Area under the Curve of ROC ),其意义是:
- 因为是在1x1的方格里求面积,AUC必在0~1之间。
- 假设阈值以上是阳性,以下是阴性;
- 若随机抽取一个阳性样本和一个阴性样本,分类器正确判断阳性样本的值高于阴性样本之概率 ;
- 简单说:AUC值越大的分类器,正确率越高。
1.4提交格式及代码要求
对于每个 row_id,您应该预测特定鸟类物种出现的概率。每种鸟类有一列,因此您需要为每一行提供 182 项预测。每行涵盖 5 秒钟的音频窗口。
这是一项代码竞赛
必须通过笔记本提交代码。要在提交后激活 "提交 "按钮,必须满足以下条件:
CPU 笔记本运行时间 <= 120 分钟
GPU 笔记本提交无效。技术上可以提交,但只有 1 分钟的运行时间。
禁用互联网访问
允许自由公开外部数据,包括预训练模型
提交文件必须命名为 submission.csv
@misc{birdclef-2024, author = {HCL-Rantig, Holger Klinck, Maggie, Sohier Dane, Stefan Kahl, Tom Denton, Vijay Ramesh}, title = {BirdCLEF 2024}, publisher = {Kaggle}, year = {2024}, url = {https://kaggle.com/competitions/birdclef-2024} }
2.数据集概述
2.1数据集说明
您在本次竞赛中面临的挑战是识别在西高止山脉全球生物多样性热点地区录制的录音中哪些鸟在鸣叫。这对出于保护目的监测鸟类种群的科学家来说是一项重要任务。更准确的解决方案可以实现更全面的监测。
本次竞赛使用隐藏测试集。在对您提交的笔记本进行评分时,您的笔记本将提供实际测试数据。
2.2具体文件
train_audio/ 训练数据由 xenocanto.org 用户慷慨上传的单个鸟类叫声的简短录音组成。这些文件已酌情降低采样率至 32 kHz,以匹配测试集音频,并转换为 ogg 格式。训练数据应包含几乎所有相关文件;在 xenocanto.org 上查找更多文件不会有任何益处,并感谢您的合作以减轻其服务器的负担。
test_soundscapes/ 当您提交笔记本时,test_soundscapes 目录中将包含约 1100 条录音,用于评分。这些录音长度为 4 分钟,采用 ogg 音频格式。文件名是随机的,但一般格式为 soundscape_xxxxxx.ogg。加载所有测试音景大约需要 5 分钟。
unlabeled_soundscapes/ 与测试音频相同录音位置的未标记音频数据。
train_metadata.csv 为训练数据提供了大量元数据。最直接相关的字段有
primary_label - 鸟类物种代码。您可以通过将代码附加到 https://ebird.org/species/ 来查看鸟类代码的详细信息,如 https://ebird.org/species/amecro 表示美洲乌鸦。并非所有物种都有自己的页面;有些链接会失败。
latitude&longitude:记录地点的坐标。有些鸟类可能有当地的 "方言",因此您可能需要在训练数据中寻求地理多样性。
author - 提供录音的用户。
filename:相关音频文件的名称。
sample_submission.csv 有效的样本提交。
row_id: 预测的 soundscape_[soundscape_id]_[end_time]的标头。
[bird ID]: 有 182 个鸟类 ID 列。您需要预测每一行出现每种鸟类的概率。
eBird_Taxonomy_v2021.csv - 不同物种之间关系的数据。
3.音频深度学习
原作者:Ketan Doshi
链接:https://medium.com/search?q=Audio+Deep+Learning+Made+Simple
4.kaggle实战
4.1综述
这个任务在学完简单的音频深度学习以后就可以知道本身并不是特别困难,作为一名kaggle小白以及python初学者,我的目标就是完整地完成一个相对复杂的任务(代码能跑通就谢天谢地了),所以这里我只简要对数据集进行分析,并没有使用未标注样本参与训练,并将现有的方法进行组合。
在评估中有一项尤为值得注意,那就是最终的模型对新样本的运行时间需要在cpu下120min内完成,这其实相当严苛,所以对于模型的选取要慎重,我采用的是Efficientnet_b0模型,尝试了使用Efficientnet_b3,但是在我的代码框架下会超过时限。而b1和b2与b0性能相差不大,所以最后就选用b0模型了。
整个任务大体可以分为三步,首先是对数据进行分析,建立特征工程,然后是选择合适的模型训练,调整参数在验证集上获得相对满意的结果,最后在测试集上进行测试,并提交结果。
音频的处理方式其实有很多种,目前主流的方式是将其转换为频谱图然后直接套用图像分类的网络,非常简单粗暴。我也选择了这个路线。至于那种频谱还是有考量的,最常用的是梅尔频谱图,但是梅尔频谱图本质上根据人类听音习惯进行的特殊变换,而鸟类的发声区域要比人类的听音习惯更复杂一些。
此外不同地区的同种鸟类还存在方言的情况,这里我也稍微偷懒没去用鸟类栖息地图等信息进行聚类,只是单纯进行了数据增强,希望能提高训练出来模型的泛化能力。
4.2训练
首先导入需要的库
import os
import gc
import sys
import cv2
import math
import numpy as np
import pandas as pd
from glob import glob
from tqdm.notebook import tqdm
import matplotlib.pyplot as plt
from sklearn.model_selection import KFold
import librosa
from scipy import signal as sci_signal
import torch
from torch import nn
from torchvision.models import efficientnet
import albumentations as albu
import pytorch_lightning as pl
from torch.optim.lr_scheduler import CosineAnnealingWarmRestarts
from pytorch_lightning.callbacks import ModelCheckpoint, TQDMProgressBar
#引入竞赛的打分方式
sys.path.append('/kaggle/input/birdclef-roc-auc')
sys.path.append('/kaggle/usr/lib/kaggle_metric_utilities')
from metric import score
kaggle有很方便的一点就是有免费的notebook可以直接用来跑程序,而且实时交互也很方便,总的来说就像在服务器上部署的jupyter notebook一样。kaggle默认输入为/kaggle/input,输出为/kaggle/working。
input可以直接导入官方的所有数据集,比赛数据集,以及别人公开的数据集,同时也可以导入你自己写的其他notebook的输出。不过需要按右上角的Save Version才能把输出保存到notebook里,已经交互式运行完的输出需要quick save并且要在advanced settings里调整到保存当前数据。
#配置参数
class config:
#通用参数
SEED = 2024 #随机种子
DEVICE = 'cpu' #竞赛要求必须用cpu
MIXED_PRECISION = False #不使用混合精度训练,容易影响模型的泛化能力
OUTPUT_DIR = '/kaggle/working/' #输出文件夹
#数据参数
DATA_ROOT = '/kaggle/input/birdclef-2024' #原始数据目录
PREPROCESSED_DATA_ROOT = '/kaggle/input/birdclef24-spectrograms-via-cupy'
LOAD_DATA = True #使用预训练的数据,提高训练效率
FS = 32000 #采样率
N_FFT = 1095 #FFT点数
WIN_SIZE = 412 #频谱每段样本数量
WIN_LAP = 100 #频谱每段重叠样本数
MIN_FREQ = 40 #最小频率
MAX_FREQ = 15000 #最大频率
#模型参数
MODEL_TYPE = 'efficientnet_b0'
#数据集参数
BATCH_SIZE = 64
N_WORKERS = 4
#数据增强
USE_XYMASKING = True
#训练参数
FOLDS = 5 #k折参数
EPOCHS = 10 #最大迭代轮次
LR = 1e-3 #学习率
WEIGHT_DECAY = 1e-5 #优化器权重衰减
#其他参数
VISUALIZE = True # whether to visualize data and batch
print('fix seed')
pl.seed_everything(config.SEED, workers=True)
fix seed
2024
另外还可以白嫖以下算力。当然,有时间限制,┓( ´∀` )┏。
#创建标签
#将音频训练数据集的目录中的文件名与一个唯一的整数ID关联起来,创建两个字典
label_list = sorted(os.listdir(os.path.join(config.DATA_ROOT, 'train_audio')))
label_id_list = list(range(len(label_list)))
#用于从标签到ID的映射
label2id = dict(zip(label_list, label_id_list))
#用于从ID到标签的映射
id2label = dict(zip(label_id_list, label_list))
#使用gpu加速
os.environ["CUDA_VISIBLE_DEVICES"] = "0,1"
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
print('Using', torch.cuda.device_count(), 'GPU(s)')
Using 2 GPU(s)
4.2.1预处理
#读取训练数据的元数据
metadata_df = pd.read_csv(f'{config.DATA_ROOT}/train_metadata.csv')
metadata_df.head()
train_df = metadata_df[['primary_label', 'rating', 'filename']].copy()
#创建标签
train_df['target'] = train_df.primary_label.map(label2id)
#创建文件路径
train_df['filepath'] = config.DATA_ROOT + '/train_audio/' + train_df.filename
#创建新的样本编号
train_df['samplename'] = train_df.filename.map(lambda x: x.split('/')[0] + '-' + x.split('/')[-1].split('.')[0])
print(f'find {len(train_df)} samples')
train_df.head()
find 24459 samples
一共有这些样本
#音频转频谱图,使用cupy,有gpu加速时比scipy更快
def oog2spec_via_cupy(audio_data):
import cupy as cp
from cupyx.scipy import signal as cupy_signal
audio_data = cp.array(audio_data)
#处理Nan数据
mean_signal = cp.nanmean(audio_data)
audio_data = cp.nan_to_num(audio_data, nan=mean_signal) if cp.isnan(audio_data).mean() < 1 else cp.zeros_like(audio_data)
#频谱转换
frequencies, times, spec_data = cupy_signal.spectrogram(
audio_data,
fs=config.FS,
nfft=config.N_FFT,
nperseg=config.WIN_SIZE,
noverlap=config.WIN_LAP,
window='hann'
)
#滤波器频率范围
valid_freq = (frequencies >= config.MIN_FREQ) & (frequencies <= config.MAX_FREQ)
spec_data = spec_data[valid_freq, :]
#对频谱图数据应用对数变换,以增强频率成分的对比度,并添加一个很小的常数1e-20来避免对数运算中的负无穷问题
spec_data = cp.log10(spec_data + 1e-20)
#归一化
spec_data = spec_data - spec_data.min()
spec_data = spec_data / spec_data.max()
return spec_data.get()
虽然常用的是scipy来进行频谱图转换,但在kaggle讨论区看到可以使用cupy借助cuda更高效地完成数据转换,要知道原始数据集可是有足足24个G大小,单用cpu要好几个小时。
具体可以参考下面的链接。
BirdCLEF'24 | Speed up audio-to-spec. via CuPy | Kaggle
#频谱图数据加载
if config.LOAD_DATA:
print('load from file')
all_bird_data = np.load(f'{config.PREPROCESSED_DATA_ROOT}/spec_center_5sec_256_256.npy', allow_pickle=True).item()
else:
all_bird_data = dict()
for i, row_metadata in tqdm(train_df.iterrows()):
#加载音频文件
audio_data, _ = librosa.load(row_metadata.filepath, sr=config.FS)
#裁切
n_copy = math.ceil(5 * config.FS / len(audio_data))
if n_copy > 1: audio_data = np.concatenate([audio_data]*n_copy)
start_idx = int(len(audio_data) / 2 - 2.5 * config.FS)
end_idx = int(start_idx + 5.0 * config.FS)
input_audio = audio_data[start_idx:end_idx]
#转换成频谱图
input_spec = oog2spec_via_cupy(input_audio)
input_spec = cv2.resize(input_spec, (256, 256), interpolation=cv2.INTER_AREA)
all_bird_data[row_metadata.samplename] = input_spec.astype(np.float32)
#保存到输出
np.save(os.path.join(config.OUTPUT_DIR, f'spec_center_5sec_256_256.npy'), all_bird_data)
load from file
因为已经做好了转换,所以这里就直接导入转换好的数据。
#数据集构建
class BirdDataset(torch.utils.data.Dataset):
def __init__(
self,
metadata,
augmentation=None,
mode='train'
):
super().__init__()
#样本元数据
self.metadata = metadata
#数据增强参数
self.augmentation = augmentation
#指定数据集的存储模式,如 'train'(训练)、'val'(验证)或 'test'(测试)。默认为 'train'。
self.mode = mode
def __len__(self):
return len(self.metadata)
def __getitem__(self, index):
row_metadata = self.metadata.iloc[index]
#从全局变量 all_bird_data 中使用 samplename 作为键获取音频的频谱图数据
input_spec = all_bird_data[row_metadata.samplename]
#数据增强
if self.augmentation is not None:
input_spec = self.augmentation(image=input_spec)['image']
#从元数据中获取目标标签
target = row_metadata.target
return torch.tensor(input_spec, dtype=torch.float32), torch.tensor(target, dtype=torch.long)
#根据传入的类型 _type 返回不同的图像变换组合,用于数据增强
def get_transforms(_type):
if _type == 'train':
return albu.Compose([
albu.HorizontalFlip(0.5),
albu.XYMasking(
p=0.3,
num_masks_x=(1, 3),
num_masks_y=(1, 3),
mask_x_length=(1, 10),
mask_y_length=(1, 20),
) if config.USE_XYMASKING else albu.NoOp()
])
elif _type == 'valid':
return albu.Compose([])
#数据可视化函数定义
def show_batch(ds, row=3, col=3):
fig = plt.figure(figsize=(10, 10))
img_index = np.random.randint(0, len(ds)-1, row*col)
for i in range(len(img_index)):
img, label = ds[img_index[i]]
if isinstance(img, torch.Tensor):
img = img.detach().numpy()
ax = fig.add_subplot(row, col, i + 1, xticks=[], yticks=[])
ax.imshow(img, cmap='jet')
ax.set_title(f'ID: {img_index[i]}; Target: {label}')
#清理资源
plt.tight_layout()
plt.show()
然后我们来看一下数据增强的结果如何。
#数据可视化
dummy_dataset = BirdDataset(train_df, get_transforms('train'))
test_input, test_target = dummy_dataset[0]
print(test_input.detach().numpy().shape)
if config.VISUALIZE:
show_batch(dummy_dataset)
del dummy_dataset
gc.collect()
(256, 256)
18819
4.2.2模型
#模型
class EffNet(nn.Module):
#model_type:指定要使用的 EfficientNet 模型类型(例如 'efficientnet_b0');n_classes:分类任务中的类别数;pretrained:指定是否加载预训练的权重
def __init__(self, model_type, n_classes, pretrained=False):
super().__init__()
#根据 model_type 参数选择不同的 EfficientNet 模型版本
if model_type == 'efficientnet_b0':
if pretrained: weights = efficientnet.EfficientNet_B0_Weights.DEFAULT
else: weights = None
self.base_model = efficientnet.efficientnet_b0(weights=weights)
elif model_type == 'efficientnet_b1':
if pretrained: weights = efficientnet.EfficientNet_B1_Weights.DEFAULT
else: weights = None
self.base_model = efficientnet.efficientnet_b1(weights=weights)
elif model_type == 'efficientnet_b2':
if pretrained: weights = efficientnet.EfficientNet_B2_Weights.DEFAULT
else: weights = None
self.base_model = efficientnet.efficientnet_b2(weights=weights)
elif model_type == 'efficientnet_b3':
if pretrained: weights = efficientnet.EfficientNet_B3_Weights.DEFAULT
else: weights = None
self.base_model = efficientnet.efficientnet_b3(weights=weights)
else:
raise ValueError('model type not supported')
#修改 EfficientNet 模型中的分类器层,以适应 n_classes 类别的输出
self.base_model.classifier[1] = nn.Linear(self.base_model.classifier[1].in_features, n_classes, dtype=torch.float32)
#定义模型的前向传播函数
def forward(self, x):
x = x.unsqueeze(-1)
x = torch.cat([x, x, x], dim=3).permute(0, 3, 1, 2)
return self.base_model(x)
#模型测试
dummy_model = EffNet(config.MODEL_TYPE, n_classes=len(label_list))
dummy_input = torch.randn(2, 256, 256)
print(dummy_model(dummy_input).shape)
torch.Size([2, 182])
测试成功,符合预期。
#LightningModule 是 PyTorch Lightning 框架的核心,用于构建易于训练、验证和测试的模型
class BirdModel(pl.LightningModule):
def __init__(self):
super().__init__()
#主干网络
self.backbone = EffNet(config.MODEL_TYPE, n_classes=len(label_list))
#交叉熵损失函数
self.loss_fn = nn.CrossEntropyLoss()
#初始化一个列表,用于存储验证步骤的输出
self.validation_step_outputs = []
def forward(self, images):
return self.backbone(images)
#定义优化器和学习率调度器
def configure_optimizers(self):
#定义 Adam 优化器
model_optimizer = torch.optim.Adam(
filter(lambda p: p.requires_grad, self.parameters()),
lr=config.LR,
weight_decay=config.WEIGHT_DECAY
)
#定义余弦退火学习率调度器
lr_scheduler = CosineAnnealingWarmRestarts(
model_optimizer,
T_0=config.EPOCHS,
T_mult=1,
eta_min=1e-6,
last_epoch=-1
)
return {
'optimizer': model_optimizer,
'lr_scheduler': {
'scheduler': lr_scheduler,
'interval': 'epoch',
'monitor': 'val_loss',
'frequency': 1
}
}
#定义训练步骤
def training_step(self, batch, batch_idx):
#获取输入
image, target = batch
image = image.to(self.device)
target = target.to(self.device)
#前向传播
y_pred = self(image)
#计算损失
train_loss = self.loss_fn(y_pred, target)
#记录训练损失
self.log('train_loss', train_loss, True)
return train_loss
#定义验证步骤
def validation_step(self, batch, batch_idx):
#从验证批次中获取输入图像和目标标签
image, target = batch
image = image.to(self.device)
target = target.to(self.device)
#在不计算梯度的情况下进行前向传播
with torch.no_grad():
y_pred = self(image)
#将预测输出和目标存储在validation_step_outputs列表中
self.validation_step_outputs.append({"logits": y_pred, "targets": target})
#返回训练和验证数据的 DataLoader
def train_dataloader(self):
return self._train_dataloader
def validation_dataloader(self):
return self._validation_dataloader
#在每个验证周期结束时调用,用于处理整个验证集的结果
def on_validation_epoch_end(self):
#合并验证批次数据
outputs = self.validation_step_outputs
output_val = nn.Softmax(dim=1)(torch.cat([x['logits'] for x in outputs], dim=0)).cpu().detach()
target_val = torch.cat([x['targets'] for x in outputs], dim=0).cpu().detach()
#计算验证损失,使用存储的目标标签和模型的预测输出
val_loss = self.loss_fn(output_val, target_val)
#将目标标签转换为独热编码格式
target_val = torch.nn.functional.one_hot(target_val, len(label_list))
#评估指标
gt_df = pd.DataFrame(target_val.numpy().astype(np.float32), columns=label_list)
pred_df = pd.DataFrame(output_val.numpy().astype(np.float32), columns=label_list)
gt_df['id'] = [f'id_{i}' for i in range(len(gt_df))]
pred_df['id'] = [f'id_{i}' for i in range(len(pred_df))]
val_score = score(gt_df, pred_df, row_id_column_name='id')
#使用self.log记录验证损失和分数,以便在Lightning日志中跟踪
self.log("val_score", val_score, True)
#清除验证集输出
self.validation_step_outputs = list()
return {'val_loss': val_loss, 'val_score': val_score}
4.2.3预测
#使用给定的模型对 data_loader 中的数据进行预测,并且收集真实标签
def predict(data_loader, model):
model.to(config.DEVICE)
model.eval()
predictions = []
gts = []
for batch in tqdm(data_loader):
with torch.no_grad():
x, y = batch
x = x.cuda()
outputs = model(x)
outputs = nn.Softmax(dim=1)(outputs)
#收集预测结果和真实标签
predictions.append(outputs.detach().cpu())
gts.append(y.detach().cpu())
#合并预测结果和真实标签
predictions = torch.cat(predictions, dim=0).cpu().detach()
gts = torch.cat(gts, dim=0).cpu().detach()
#将真实标签转换为独热编码
gts = torch.nn.functional.one_hot(gts, len(label_list))
return predictions.numpy().astype(np.float32), gts.numpy().astype(np.float32)
#执行机器学习模型的训练和预测过程,用于 K 折交叉验证中的一个折(fold)
def run_training(fold_id, total_df):
#打印训练的相关信息,包括当前折的编号
print('================================================================')
print(f"==== Running training for fold {fold_id} ====")
#创建数据集和数据加载器
train_df = total_df[total_df['fold'] != fold_id].copy()
valid_df = total_df[total_df['fold'] == fold_id].copy()
print(f'Train Samples: {len(train_df)}')
print(f'Valid Samples: {len(valid_df)}')
train_ds = BirdDataset(train_df, get_transforms('train'), 'train')
val_ds = BirdDataset(valid_df, get_transforms('valid'), 'valid')
train_dl = torch.utils.data.DataLoader(
train_ds,
batch_size=config.BATCH_SIZE,
shuffle=True,
num_workers=config.N_WORKERS,
pin_memory=True,
persistent_workers=True
)
val_dl = torch.utils.data.DataLoader(
val_ds,
batch_size=config.BATCH_SIZE * 2,
shuffle=False,
num_workers=config.N_WORKERS,
pin_memory=True,
persistent_workers=True
)
#初始化模型
bird_model = BirdModel()
#创建一个 ModelCheckpoint 回调,用于保存最佳模型
checkpoint_callback = ModelCheckpoint(monitor='val_score',
dirpath=config.OUTPUT_DIR,
save_top_k=1,
save_last=False,
save_weights_only=True,
filename=f"fold_{fold_id}",
mode='max')
callbacks_to_use = [checkpoint_callback, TQDMProgressBar(refresh_rate=1)]
#初始化训练器
trainer = pl.Trainer(
max_epochs=config.EPOCHS,
val_check_interval=0.5,
callbacks=callbacks_to_use,
enable_model_summary=False,
accelerator="gpu",
deterministic=True,
precision='16-mixed' if config.MIXED_PRECISION else 32,
)
#训练模型
trainer.fit(bird_model, train_dataloaders=train_dl, val_dataloaders=val_dl)
#预测
best_model_path = checkpoint_callback.best_model_path
weights = torch.load(best_model_path)['state_dict']
bird_model.load_state_dict(weights)
preds, gts = predict(val_dl, bird_model)
#创建包含预测结果和真实标签的 DataFrame
pred_df = pd.DataFrame(preds, columns=label_list)
pred_df['id'] = np.arange(len(pred_df))
gt_df = pd.DataFrame(gts, columns=label_list)
gt_df['id'] = np.arange(len(gt_df))
#计算分数
val_score = score(gt_df, pred_df, row_id_column_name='id')
#保存结果
pred_cols = [f'pred_{t}' for t in label_list]
valid_df = pd.concat([valid_df.reset_index(), pd.DataFrame(np.zeros((len(valid_df), len(label_list)*2)).astype(np.float32), columns=label_list+pred_cols)], axis=1)
valid_df[label_list] = gts
valid_df[pred_cols] = preds
valid_df.to_csv(f"{config.OUTPUT_DIR}/pred_df_f{fold_id}.csv", index=False)
return preds, gts, val_score
4.2.4具体训练
#训练
torch.set_float32_matmul_precision('high')
#记录
fold_val_score_list = list()
oof_df = train_df.copy()
pred_cols = [f'pred_{t}' for t in label_list]
oof_df = pd.concat([oof_df, pd.DataFrame(np.zeros((len(oof_df), len(pred_cols)*2)).astype(np.float32), columns=label_list+pred_cols)], axis=1)
for f in range(config.FOLDS):
#获取当前折的验证集索引
val_idx = list(train_df[train_df['fold'] == f].index)
#调用 run_training 函数进行训练和验证,获取验证集的预测 val_preds、真实标签 val_gts 和验证分数 val_score
val_preds, val_gts, val_score = run_training(f, train_df)
#将当前折的验证集真实标签和预测结果更新到 oof_df,并记录当前折的验证分数
oof_df.loc[val_idx, label_list] = val_gts
oof_df.loc[val_idx, pred_cols] = val_preds
fold_val_score_list.append(val_score)
for idx, val_score in enumerate(fold_val_score_list):
print(f'Fold {idx} Val Score: {val_score:.5f}')
#计算整个 OOF 的评估分数
oof_gt_df = oof_df[['samplename'] + label_list].copy()
oof_pred_df = oof_df[['samplename'] + pred_cols].copy()
oof_pred_df.columns = ['samplename'] + label_list
oof_score = score(oof_gt_df, oof_pred_df, 'samplename')
print(f'OOF Score: {oof_score:.5f}')
#将包含训练过程中所有预测结果的 oof_df 保存为 CSV 文件
oof_df.to_csv(f"{config.OUTPUT_DIR}/oof_pred.csv", index=False)
执行过程中大概是这样。
最后得到checkpoints,和fold数相同的ckpt文件,以及各自对应的打分。
4.3提交
提交的方式相当有趣,需要你把模型参数导入,然后添加竞赛数据集,数据集中会有一个空文件夹,用来放置测试数据,你需要提交自己的notebook,然后kaggle就会在后台把测试数据存入竞赛数据集的空文件夹,然后执行你的notebook,最后会给你一个打分和排名,但不会给你测试数据集。
接下来看看具体执行文件,大体上和训练很像,但是只保留模型和预测部分(跳过几乎相同的配置部分)。
4.3.1预处理
#预处理
def oog2spec_via_scipy(audio_data):
#处理Nan数据
mean_signal = np.nanmean(audio_data)
audio_data = np.nan_to_num(audio_data, nan=mean_signal) if np.isnan(audio_data).mean() < 1 else np.zeros_like(audio_data)
#频谱转换
frequencies, times, spec_data = sci_signal.spectrogram(
audio_data,
fs=config.FS,
nfft=config.N_FFT,
nperseg=config.WIN_SIZE,
noverlap=config.WIN_LAP,
window='hann'
)
#滤波器频率范围
valid_freq = (frequencies >= config.MIN_FREQ) & (frequencies <= config.MAX_FREQ)
spec_data = spec_data[valid_freq, :]
#对频谱图数据应用对数变换,以增强频率成分的对比度,并添加一个很小的常数1e-20来避免对数运算中的负无穷问题
spec_data = np.log10(spec_data + 1e-20)
#归一化
spec_data = spec_data - spec_data.min()
spec_data = spec_data / spec_data.max()
return spec_data
#初始化
all_bird_data = dict()
#指定音频文件路径
if len(glob(f'{config.DATA_ROOT}/test_soundscapes/*.ogg')) > 0:
ogg_file_paths = glob(f'{config.DATA_ROOT}/test_soundscapes/*.ogg')
else:
ogg_file_paths = sorted(glob(f'{config.DATA_ROOT}/unlabeled_soundscapes/*.ogg'))[:10]
#批量将音频文件转换为频谱图
for i, file_path in tqdm(enumerate(ogg_file_paths)):
#使用正则表达式从文件路径中提取文件名
row_id = re.search(r'/([^/]+)\.ogg$', file_path).group(1) # filename
#加载音频文件
audio_data, _ = librosa.load(file_path, sr=config.FS)
#转换为频谱图
spec = oog2spec_via_scipy(audio_data)
#计算需要填充的列数,以确保频谱图的列数是512的倍数
pad = 512 - (spec.shape[1] % 512)
if pad > 0:
spec = np.pad(spec, ((0,0), (0,pad)))
#把频谱图重塑成256x256像素
spec = spec.reshape(512,-1,512).transpose([0, 2, 1])
spec = cv2.resize(spec, (256, 256), interpolation=cv2.INTER_AREA)
#每次迭代都从调整大小的频谱图中提取一个5秒的片段,循环48次,覆盖240s(竞赛提到的4min)
for j in range(48):
all_bird_data[f'{row_id}_{(j+1)*5}'] = spec[:, :, j]
因为测试数据仍旧是音频文件,所以还是需要将其转换为频谱图的形式。
#数据集构建
class BirdDataset(torch.utils.data.Dataset):
def __init__(
self,
bird_data,
augmentation=None,
):
super().__init__()
#存储传入的音频数据
self.bird_data = bird_data
#存储bird_data的键的列表,用于后续索引
self.keys_list = list(bird_data.keys())
#存储传入的数据增强对象
self.augmentation = augmentation
def __len__(self):
return len(self.bird_data)
def __getitem__(self, index):
#根据索引从bird_data中获取对应的音频数据
_spec = self.bird_data[self.keys_list[index]]
#如果提供了数据增强,将其应用于获取的音频数据。这里假设数据增强对象有一个接受image关键字参数的函数,并返回增强后的图像
if self.augmentation is not None:
_spec = self.augmentation(image=_spec)['image']
return torch.tensor(_spec, dtype=torch.float32)
#数据增强
def get_transforms(_type):
if _type == 'test':
return albu.Compose([])
#数据可视化函数定义
def show_batch(ds, row=2, col=2):
fig = plt.figure(figsize=(6, 6))
img_index = np.random.randint(0, len(ds)-1, row*col)
for i in range(len(img_index)):
img = ds[img_index[i]]
if isinstance(img, torch.Tensor):
img = img.detach().numpy()
ax = fig.add_subplot(2, 2, i + 1, xticks=[], yticks=[])
ax.imshow(img, cmap='jet')
ax.set_title(f'ID: {img_index[i]}')
plt.tight_layout()
plt.show()
#数据可视化
dummy_dataset = BirdDataset(all_bird_data, get_transforms('test'))
test_input = dummy_dataset[0]
print(test_input.detach().numpy().shape)
if config.VISUALIZE:
show_batch(dummy_dataset)
#清理资源
del dummy_dataset
gc.collect()
4.3.2模型
几乎同4.2.2,不过需要稍微改动,因为不需要这一步了。
#清除验证集输出
self.validation_step_outputs = list()
4.3.3预测和提交
#使用训练好的模型对给定的数据加载器 data_loader 中的数据进行预测
def predict(data_loader, model):
model.to(config.DEVICE)
model.eval()
pred = []
for batch in tqdm(data_loader):
#在预测时不计算梯度,从而减少内存消耗并加速计算
with torch.no_grad():
x = batch
outputs = model(x)
outputs = nn.Softmax(dim=1)(outputs)
pred.append(outputs.detach().cpu())
pred = torch.cat(pred, dim=0).cpu().detach()
return pred.numpy().astype(np.float32)
#读取检查点文件
ckpt_list = glob(f'{config.CKPT_ROOT}/*.ckpt')
print(f'find {len(ckpt_list)} ckpts in {config.CKPT_ROOT}.')
#主循环
predictions = []
for ckpt in ckpt_list:
#初始化模型
bird_model = BirdModel()
#加载检查点并确保权重被加载到CPU上
weights = torch.load(ckpt, map_location=torch.device('cpu'))['state_dict']
bird_model.load_state_dict(weights)
#创建测试数据集 test_dataset 和测试数据加载器 test_loader
test_dataset = BirdDataset(all_bird_data, get_transforms('test'))
test_loader = torch.utils.data.DataLoader(
test_dataset,
batch_size=config.BATCH_SIZE,
num_workers=config.N_WORKERS,
shuffle=False,
drop_last=False
)
predictions.append(predict(test_loader, bird_model))
#释放内存
gc.collect()
#计算所有预测结果的平均值
predictions = np.mean(predictions, axis=0)
#输出预测结果
sub_pred = pd.DataFrame(predictions, columns=label_list)
sub_id = pd.DataFrame({'row_id': list(all_bird_data.keys())})
sub = pd.concat([sub_id, sub_pred], axis=1)
sub.to_csv('submission.csv',index=False)
print(f'Submissionn shape: {sub.shape}')
sub.head(5)
5.结果展示
最后通过一点点调参也是从0.56,爬到0.6,再到0.63,最后终于来到0.65,到了170名,个人对于这次尝试还是很满意的(我是真的小白,很羡慕那些第一次尝试就能拿牌子的大神)。尤其是这段时间终于弄明白class是怎么写的了, 确实相当优雅简洁。kaggle的code和discussion氛围真的很棒,可以学到很多有用的东西。总的来说这次受益匪浅,以后有机会也会继续尝试的。下次就要争取不摸鱼,找队友,然后(一起摸鱼)争取拿个牌子啦。
6.后续(6.21更新)
啊这...竟然拿了个铜牌。
这完全出乎我个人的预料,让我深刻认识到kaggle数据集测试得到的成绩真的可能和最后结果差别很大。最后一段时间,我也没有再调参,估计是这次测试集和最终测试结果差别比较大。很多人过拟合了,让我钻了空子。也提醒自己,不要过于看重评分,而导致过拟合。