まだタイトルない

アウトプット用です

【Kaggle挑戦記】鳥コンペ2(BirdCLEF 2021)銅メダル振り返り【#8】

f:id:teyoblog:20210605153916p:plain:w500

こんにちは

先日終了した鳥コンペ2に参加してきました。

結果はpublic41位からprivate66位で目標としていた銀メダル以上を取ることはできませんでした。

本コンペではTakamichi Todaさんと、shinmura0さんのチームに途中から仲間にしてもらいチームでの取り組みとなりました。この記事では自分がやってたCNN部分の取組内容を書いていきます。

自分がCNNで進めていた理由は過去の鳥コンペと、鳥蛙コンペでの上位solutionで使われており、十分戦えると判断したためです。SEDについて全然わからないのでもっと積極的に教えてもらいに行けばよかったと後悔している・・・。

コンペ概要

与えられた音声データの中で、指定された鳥のどの種類の鳥が鳴いているのか、どれも鳴いていないか(=nocall)を判別するマルチラベル分類

似たようなコンペがkaggleで過去にあったため今回は鳥コンペ2といえる。

前回のコンペとの違い

  • 対象となる鳥の種類が増えた。
    • 識別する鳥の種類が264種類から397種類に増えました
  • 音声ファイルのメタ情報が増えた。
    • いつどこで録音されたものかの情報が増えました。
    • 例えば日本で録音されたデータには日本にいない鳥の鳴き声は入り得ないので、後処理でご識別を削除という対応が取れます。
  • テストデータでCVを算出できるようになった。

過去コンペ

データ

traindata

  • train_short_audio
    • 397種の鳥の音源を合計62874ファイル
    • フォーマットはogg
    • サンプリングレートは(たしか)事前に32000にサンプリングし直し済み
    • 音源中で鳴いてるわけではない
  • train_soundscapes
    • testdata(test_soundscapes)とほとんど同じデータ
    • 録音場所や、時期、機材がほとんど一緒とされている

oggファイルについて

oggファイルのまま学習すると(特にkaggle環境では)ファイルの読み込みがボトルネックになって非常に時間がかかる状態でした。wavに変換したり、画像に変換済みのデータセットが公開されていたのでこれを使うなどの対応がありました。

音源全体でその鳥が鳴いてるわけではない

課題点として、音源データを通してすべての時間帯で鳥が鳴いているわけではないし、どこで鳴いてるかのメタ情報がない事が挙げられます。secondary labelsも与えられてますがこちらはさらに頻度は少ないと考えられます。下の図のようなイメージです。

f:id:teyoblog:20210605154542p:plain

testdata

10分の音源があり、5秒おきに397種のうち鳴いてる鳥があればそのラベル(複数可),鳴いていなければnocallをつけます。

f:id:teyoblog:20210605154552p:plain

データの差

  • レーニングデータと公開されてないテストデータとの間にはドメインシフトがある
    • train_short_audioはxeno cantoに登録されているもの
    • soundscapesは実際の鳥の声(うまく言えない
    • 録音機材機材などが違ってくる
  • soundscpapeではnocallというラベルが存在する
  • soundscapesでは複数のラベルをつけることもある

取組内容

パイプラインは大まかに下記の流れです。

  1. データの準備(音声から5秒間分切り抜いてくる)
  2. Data augmentation(waveform transform)
  3. logmelspec(画像)に変換
  4. Data augmentation(spec augmentation)
  5. Training
  6. postprocess

1. データの準備

traindataの箇所でも触れましたが、音源からランダムで5秒間切り取ってきてこれはスズメの鳴き声です!とlabelつけして扱うと、環境音しか入ってないけどスズメとして学習がすすみます。そのため、如何にprimaly labelの鳥の声が入ってる部分を切り取るかというのはまず取り組まなければいけないことと判断しました。

これは鳥1でも同じですので、過去コンペSolutionを見てといくつか対応策を候補に上げました。

  • ハンドラベリングする
    • 一番強そう
  • out-of-foldで正解予測確率が高い部分をクリップ
  • RMSトリミングを使って鳥の鳴き声を含むクリップを取得
  • 最初の5秒か、最後の5秒に鳥が含まれていると仮定する
  • 5秒でなくもっと長くとってくる
    • train時の画像が時間方向に長くなるが画像内の特徴を学習するから問題ない選択肢と判断していましたが認識が違えば指摘ください(ちなみにこれは試しましたがうまくいきませんでした。)

今回はチームメンバーが一部ハンドラベリングしてくれていたデータが有ったことと、最後5秒は人の声とかが入ってる場合があるとのことで、基本最初の5秒+ハンドラベリングされてるファイルならそこが入るように切り抜くという方法を取りました。

効果としてはLB0.04くらい上がりました。

また、鳥1と違って今回は音声データ(1dデータ)をGPUに乗せてから、torchlibrosaを使ってスペクトラム変換、ログメル変換、spec augment を行いました。

2.Data augmentation

  • NoiseInjection
  • PinkNoise
  • RandomVolume
  • 環境音(ESC50)
  • modify mixup

modify mixupは鳥1の3rd place solutionで紹介されていた手法で、同一バッチ内で組み合わせて、ラベルはunionを取るものです。

環境音に関してはあまり改善が見られなかったのですが、最終サブでは使用されました。
SSWの録音してる場所の近くには空港がある等様々な自然音の情報もありましたので、入れて学習することは理にかなって入ると思います。

3.logmelspec(画像)に変換

torchlibrosaを使ってもModel内で音声データ⇛スペクトログラム⇛ログメルスペクトログラムへと変換していきます。パラメータは下記

  • sample_rate = 32000
  • n_mels = 128
  • fmin = 20
  • fmax = 16000
  • n_fft = 2048
  • hop_length = n_fft//4

ここからチャンネル数を3chにします(imagenetでのpretrained weightが3chのため) 今回は鳥1の4-th place solutionを参考に delta,deltadeltaを使うことにしました。

他にも単純にrepeatするだけであったり、repeatしてからimagenetに合わせた標準化をしたり、こちらのスライドでは(melspec,pcen,melspec**1.5)というのも紹介されています。

4.Data Augmentation

こちらでもtorchlibrosaを使ってSpecAugmentationを行います。前回のコンペでも聞いていたため最初から使っていました。

結局解決してないというか考えるのをやめたのですが、モデル内で音声からログメルまで変換したり、SpecAugmentationをした場合、逆伝播はどうなってるんでしょうね・・・

5.training

StratifiedKFoldで5-foldに分割し、学習。

val loss best よりもTrainSoundScapeに対するスコアが高いとき方がpublicLBがよく、またTrainsoundscapeと LBは比較的相関が取れてたのでよかったです。val loss bestに比べて0.01-0.02ほどスコアが良くなったと思います。

f:id:teyoblog:20210605154644p:plain

推移を見てみると、lossに比べてTrainsoundscapeに対する指標が暴れてるので、これをいい感じにできる学習を目指すのがいいのかなと思いましたが結局できませんでした。

6.postprocess

  • 前回コンペのソリューションからpredictionの平滑化(前後の時間のpredictionを使う)を採用したところスコアは向上
  • 鳥2では収録場所がアメリカ(カリフォルニア、ニューヨーク)、コスタリカ、コロンビアト決まっていて、ファイル毎にどこでいつ収録されたのかもわかるため、その時期そこにいない鳥という情報を使った後処理も実施
  • 閾値最適化
    • 今回のソリューションで一番好きな部分です。こういうのを思いついて実装できるようになっていきたい。

下記gitから引用

閾値の決め方は

  • TSにおいて、model_predictを取得
  • このとき、labelがnocallではなく、「各鳥でnocall」の部分を取得する
  • このnocall_predictは、各鳥で結構な時間(2時間以上)集まるので信頼性がある
  • 次に、各鳥でnocall_predictの分布を指数分布にあてはめ、閾値を決める
  • 閾値は0.2 + 99.9%(指数分布)が標準だが、モデルによって調整する

f:id:teyoblog:20210605154654p:plain

from scipy.stats import expon

def get_threshold(score, label): #score(2400,397)  #label(2400,397) is 1 or 0
    optim_thresh = np.zeros(397)
    
    for i in range(397):
        target_score = score[:, i]
        target_label = label[:, i]
        nocall_score = target_score[target_label==0]

        # 指数分布にフィッティング
        fit_parameter = expon.fit(nocall_score)
        frozen_expon = expon.freeze(*fit_parameter)

        # 0.2 + 99.9%で閾値を決める
        optim_thresh[i] = frozen_expon.ppf(0.999) + 0.2
            
    return optim_thresh # shape(397)

他にも提出したもう一つのサブでは重み付きvotingなどでスコアが上がっていました。

この後処理に関しては私はほとんど携われていないので反省の限りです。

試したけど駄目だったもの

  • high contrastにする
  • GaussianNoise
  • 学習時の音声データの長さを長くする
  • labelsmooting
  • 最後に2層drop out
  • site別にfinetuning
    • そのsiteにいない鳥をnocallとして入れる
  • efficientnet
  • ViT系
  • melspecを224まであげる

結果/感想

結果

冒頭に書いたとおりShake downして銅メダルに終わりました。ちょっとpublicにover fitしていたかもしれませんが、結果を見るとTrainsoundscape,public,privateすべて連動している感じだったので、過剰なoverfitではなかったのかもしれません。上位solutionも出揃ってきたので、引き続き勉強して次のコンペに挑みたいと思います。

感想

今回は既存のチームに入れて貰う形でチームをくんで2-3週間取り組ませてもらいました。改めて二人にはくんでいただいて感謝です。個人的にはチームをくんだ結果メンバーに甘えてだらけてしまうこともなく、ソロでやってるよりも真剣にできたのではないかと思います。コンペ中に気になったことを雑談できるだけでもスッキリしますし、今回の結果は私のモデルがベースになってるものの一人では到達できなかった地点なのでチームは学びにも結果にもいいものがあると改めて感じました。

取り組みのペース?として実験数や、アイデアを実装して実験する速度の速さを目の当たりにしたのでもっと技術的なレベルアップもしていきたいと強く感じました。

今回はcolacproを活用するということで、kaggleでコーディングしてローカルとcolabでノートブックを走らせるということをしました。そこで困ったこととして、例えば Dataset のコードを更新したときに手元で枝分かれしたコードたちからForkするときにDatasetのコードが過去のものになっていたりと、コードの管理がうまくできませんでした。また、colabproではバッチ数をローカルより大きくできるのですが、したときの学習率が悪かったのかうまく学習が進まないことが多発してしまい苦労しました。

solution

最後に、CV/public/privateベストになったsolutionを図にしてみました。

f:id:teyoblog:20210605154704p:plain

Links