클립보드에 복사되었습니다
Post

BirdCLEF 2024

BirdCLEF 2024

BirdCLEF 2024 — Audio Signal Classification via Mel-Spectrogram

오디오 신호를 time-frequency representation으로 바꾸고, existing pretrained model을 붙여 분류 파이프라인을 직접 만든 프로젝트다. 지금 돌아보면, 내가 signal을 representation 관점으로 보기 시작한 첫 작업에 가까웠다. 이후 ECG, EEG 쪽으로 관심이 이어진 출발점이기도 하다.

Problem

Kaggle BirdCLEF 2024는 인도 웨스턴 가츠 산맥에서 수집된 조류 음성을 바탕으로 182종을 분류하는 대회였다. 입력은 OGG 파일이고, 제출은 5초 프레임 단위 예측이 필요했다.

처음엔 그냥 오디오 분류 대회라고 생각했는데, 실제로 해보니 모델 하나 잘 올리는 문제와는 좀 달랐다. 학습 환경과 제출 환경이 분리되어 있었기 때문이다.

  • 학습은 GPU 환경에서 가능
  • 제출은 Kaggle Notebook CPU-only
  • 인터넷 차단
  • 전체 런타임 2시간 제한

즉, 모델 성능만 보는 게 아니라 실제 제출 환경에서 끝까지 돌아가는 추론 파이프라인까지 같이 맞춰야 했다.

또 하나는 학습 단위와 제출 단위가 달랐다는 점이다.

  • 학습: 15초 파일 단위
  • 제출: 5초 프레임 단위

이 차이는 생각보다 컸다. 파일 전체 문맥을 보고 학습한 모델이 짧은 프레임에서도 그대로 잘 맞는지는 다른 문제였기 때문이다.

평가 지표는 macro-averaged ROC-AUC였고, 클래스 불균형도 있었다.


Data / Setting

  • 24,459개 OGG 파일
  • 182개 클래스
  • Xeno-canto 기반 데이터셋
  • Train 19,567 / Val 4,892
  • 80:20 random split

일부 종은 샘플 수가 꽤 적었다. 그래서 accuracy보다 class-wise discrimination 쪽을 더 의식하게 됐다.

학습은 15초 고정 길이 파일 기준으로 했고, 제출은 5초 프레임 기준이었다. 나중에 다시 보면 이 단위 차이가 결과 해석에도 계속 따라왔다.


Approach

전체 흐름은 단순했다.

text id="cxt8vy" OGG 로딩/검증 → 32kHz 리샘플링, 15초 crop/pad → Mel-Spectrogram 변환 → Z-score normalization, 3채널 복제 → existing pretrained model 입력 → 5초 sliding window 추론 → submission CSV 생성

오디오를 그대로 넣는 대신 Mel-Spectrogram으로 바꿔서 image-like input처럼 다뤘다. 이때 사용한 설정은 아래와 같았다.

  • n_fft = 2048
  • 입력 크기: 128 × 384
  • fmin = 20 Hz
  • fmax = 16 kHz

그 뒤 Z-score normalization을 하고, ImageNet pretrained model 입력 형식에 맞추기 위해 3채널로 복제했다.

지금은 너무 당연하게 느껴지지만, 당시에는 여기서 처음 크게 느낀 게 있었다. representation이 그냥 전처리 한 단계가 아니라 모델이 보는 입력 자체를 바꾼다는 점이다. Mel-Spectrogram 파라미터를 어떻게 잡느냐에 따라 모델이 학습하는 패턴이 달라졌다.

모델은 existing pretrained model backbone 위에 classification head를 붙이는 식으로 갔다. 가장 잘 됐던 건 EfficientNetV2-B2 (ImageNet pretrained) 였다.

head는 단순하게 붙였다.

  • Global Average Pooling
  • Dropout(0.5)
  • Dense(2048)
  • Softmax(182)

처음부터 “pretrain이 중요하다”는 문제의식을 명확하게 갖고 들어간 건 아니었다. 그때는 그냥 되는 모델을 찾는 쪽에 가까웠다. 그런데 결과를 놓고 다시 보니, 파라미터가 더 큰 모델보다 pretrain이 들어간 모델이 더 잘 됐다. 지금은 이 경험을 꽤 중요하게 본다. 실제 연구 환경에서는 무조건 큰 모델보다, 현재 데이터와 자원에 맞는 모델을 찾는 쪽이 더 중요하다고 보기 때문이다.

augmentation도 한 번 고민했었다. spectrogram이 이미지처럼 생겼으니 일반 이미지 augmentation을 그대로 써도 되지 않을까 생각했는데, 직접 보니 시간축은 단순한 spatial axis가 아니었다. 실제 순서 정보가 들어 있으니 flip이나 rotate는 의미를 망가뜨릴 가능성이 컸다.

그래서 일반 이미지 augmentation 대신 아래를 썼다.

  • Mixup (α = 0.4)
  • frequency masking
  • time masking

이 부분도 이후 바이오신호를 할 때 계속 남았다. 겉으로는 이미지처럼 보여도, 안쪽 구조는 signal 규칙을 따른다는 점이다.


Inference / Environment

이번 프로젝트에서 더 까다로웠던 건 학습보다 추론이었다. 제출 환경이 CPU-only였고, 인터넷도 막혀 있었고, 시간 제한도 있었다.

그래서 추론은 그냥 모델 저장 후 불러오는 수준이 아니라, Kaggle 제출 환경에 맞게 따로 만들었다.

  • 5초 프레임 sliding window 추론
  • tf.data pipeline 구성
  • AUTOTUNE + prefetch
  • 가중치는 Kaggle Dataset에 업로드
  • 제출 환경에서 model.load_weights()로 로드

이 경험 이후로는 학습 코드와 추론 코드를 같은 걸로 보지 않게 됐다. 둘은 실제로 병목도 다르고, 맞춰야 하는 제약도 다르다.


PyTorch → Keras/TensorFlow 이식

학습은 PyTorch/Colab에서 했고, 추론은 Keras/TensorFlow/Kaggle 환경에 맞췄다. 그래서 모델 구조를 직접 하나씩 대응시키며 옮겼다.

이건 단순 문법 변환은 아니었다.

  • layer 의미 확인
  • shape 변화 확인
  • preprocessing과 input 연결
  • weight loading 가능 여부 확인
  • CPU 추론 병목 확인

당시에는 생성형 AI 도움도 같이 받았지만, 그대로 복사해서 붙는 식은 아니었다. 결국 각 모듈이 뭘 하는지 이해한 뒤 맞춰야 했다. 지금은 새로운 코드베이스를 볼 때 먼저 의미 단위로 끊어보는 습관이 있는데, 이때 영향이 컸다고 본다.


Conformer 이식 시도

CNN backbone 말고 sequence 구조를 더 직접 다루는 모델도 써보고 싶어서 Conformer 이식도 시도했다. 다만 끝까지 완료하지는 못했다.

지금 보면 이유는 단순하다. 그때는 전체 파이프라인을 충분히 분해하지 않은 상태에서 상위 구조부터 붙이려 했고, 그래서 구현 난이도를 제대로 통제하지 못했다.

당시엔 실패로 끝났지만, 나중에는 오히려 작업 방식을 바꾸는 계기가 됐다. 지금은 새로운 프로젝트를 시작하면 먼저 baseline code를 의미 단위로 분해하고, 실험 환경부터 먼저 설계하는 쪽으로 들어간다.


실험 흐름

ResNet18으로 baseline이 도는지만 먼저 확인한 뒤, backbone과 pretrain 여부를 바꿔가며 총 12회 제출했다.

ModelPretrainedEpochPublic Score
ResNet18300.52
EfficientNetV2-B3100.58
EfficientNetV2-B2 + B3 Ensemble✅ / ❌100.61
EfficientNetV2-B2100.62

남는 관찰은 명확했다.

  • B3 (12M, no pretrain) < B2 (9.2M, pretrained)
  • 파라미터 수보다 pretrain 영향이 더 컸다
  • epoch을 20으로 늘렸을 때 B2는 소폭 하락, ensemble은 더 크게 하락
  • 많이 학습한다고 항상 좋아지는 건 아니었다

이때부터 이미 “큰 모델이 더 좋다”는 생각은 실제 문제에서는 자주 틀릴 수 있겠다고 느끼기 시작했다.


My Role

이 프로젝트에서 맡은 역할은 아래와 같다.

  • OGG 입력부터 Mel-Spectrogram 생성, 모델 입력 연결까지 end-to-end pipeline 구성
  • existing pretrained model 기반 분류 구조 설계
  • 5초 프레임 단위 추론 및 submission pipeline 구현
  • PyTorch(학습) → Keras/TensorFlow(추론) 프레임워크 이식
  • Kaggle CPU-only / 인터넷 차단 / 2시간 런타임 제약에 맞춘 inference pipeline 구성
  • Conformer 이식 시도
  • 보고서 작성

Result / Takeaway

  • Private ROC-AUC: 0.5880
  • Ranking: 643위 / 974팀
  • 최고 성능: EfficientNetV2-B2 (ImageNet pretrained, epoch 10)

이 프로젝트를 하고 남은 건 대략 이런 쪽이었다.

하나는 pretrain이 param count보다 더 중요할 수 있다는 점이다. 큰 모델보다, 현재 task와 representation 위에서 안정적으로 작동하는 pretrained initialization이 더 중요했다.

또 하나는 train 단위와 inference 단위 차이는 별도 문제라는 점이다. 15초 파일 단위로 학습했다고 해서 5초 프레임 단위 추론에서도 그대로 잘 맞는 건 아니었다.

마지막으로, feature pipeline도 모델만큼 중요하다는 걸 직접 느꼈다. Mel-Spectrogram 파라미터(n_fft, hop_length, fmin, fmax)는 단순 설정값이 아니라 입력 representation 품질 자체를 바꾼다. 도메인 이해 없이 feature를 자동화하는 데는 한계가 있었다.


EEG / ECG로 옮겨보면

오디오에서 사용한

raw signal → time-frequency representation → pretrained model

이 구조는 EEG/ECG에도 자연스럽게 이어진다. 다만 그대로 복사할 수는 없고, 도메인에 맞게 바뀌는 부분이 있다.

  • 주파수 범위 설정
  • 전처리 방식 예: band-pass filter, notch filter, OC/CO filter
  • segment 길이
  • class imbalance 처리

나중에 여러 프로젝트를 하면서 느낀 점은, spectrogram 변환 자체도 꽤 큰 비용이라는 점이었다. 비용이 중요하면 raw signal을 그대로 활용하는 방식이 더 맞을 수 있다.

결국 여기서 자연스럽게 이어진 질문은 이런 쪽이었다.

  • 오디오에서 본 pretrain > model size 경향이 biosignal에서도 성립하는가
  • ImageNet pretrain보다 domain-specific pretrain이 더 유효한가
  • representation을 명시적으로 만들 것인가, raw signal에서 바로 학습할 것인가

이 질문들이 이후 ECG, EEG 프로젝트로 이어졌다.


반성할 점

돌아보면, 이 프로젝트는 출발점으로는 중요했지만 아쉬움도 분명히 있었다.

첫째, 모델을 빨리 붙이고 결과를 보는 데 집중해서 실험 구조를 충분히 분해하지 못했다. Conformer 이식이 끝까지 가지 못한 것도 이와 이어진다. 지금은 프로젝트를 시작하면 baseline code를 먼저 잘게 나누고, 실험 환경부터 먼저 잡는다.

둘째, 추론 환경 제약은 잘 넘겼지만, 학습 단위와 제출 단위 차이를 더 초반부터 강하게 의식했으면 좋았을 것 같다. 지금은 실제 inference scenario를 먼저 놓고 split과 evaluation을 설계하려고 한다.

셋째, 지금 다시 보면 이 프로젝트는 단순한 오디오 분류 경험이라기보다, 내가 signal을 representation 문제로 보기 시작한 첫 사례였다. 그때는 이걸 명확하게 언어화하지는 못했지만, 지금 정리해보면 여기서부터 ECG/EEG로 확장이 시작됐다.

This post is licensed under CC BY 4.0 by the author.