鳥蛙コンペの振り返り
はじめに
2020年の12月から2月にかけてkaggleでRainforest Connection Species Audio Detectionというコンペが開催されており、最終的に5位を取ることができました。この記事ではコンペの概要や取り組みを記録しておこうと思います。
コンペとタスク
コンペ概要
今回のコンペは熱帯雨林で録音された音声から24種の匿名の鳴き声を検出・分類するというものでした。データはtrain/testともに音声ファイル(60s)1つにつき、複数種の鳴き声が存在するためmultilabelで学習・予測を行う必要がありました。
音声認識について
今回のタスクに似たコンペとしてDCASEという環境音認識のコンペがあり、こちらに音声認識の概要がわかりやすく書かれています。 engineering.linecorp.com
評価指標
今回の評価指標はLRAPという予測値のrankingに基づく指標にクラスごとのラベル数で重み付けされたLWLRAPが使用されていました。こちらは2019年に開催されたFreesound Audio Tagging 2019でも使用されており音声認識のタスクで使われる指標のようです。
評価指標の説明に関してはこちらのdiscussionがとても参考になりました。
鳥コンペとの違い
似たようなコンペとして2020年8~9月にkaggleでCornel Birdcall Identification(通称: 鳥コンペ)が行われていました。こちらも鳥の鳴き声を分類するタスクで今回紹介する鳥蛙コンペと非常に似た趣旨のコンペでした。 鳥コンペと異なる点としては以下のような項目が挙げられます。
鳥コンペ | 今回 | |
---|---|---|
評価指標 | micro averaged F1 score | LWLRAP |
ラベル付け | weak label | strong label |
クラス数 | 264 | 24 |
クラス名 | あり | なし(匿名) |
予測 | 5s単位で予測 | 60s単位で予測 |
その他 | nocall(鳥が鳴いていないこと)の予測が必要 | FPデータあり(後述) |
補足するとweak labelとは音声ファイル単位でラベルが付けられているもので、strong labelとは音声内のどこで鳴いているかという時間情報も加わったより詳細なラベルとなります。
課題(本コンペの特徴)
今回のコンペで個人的に重要なポイントと考えたのは以下の3点です。
1. train/testでアノテーション方法が異なる
本コンペではtrainとtestでアノテーション方法が異なることが知らされていました。 アノテーション方法について整理したdiscussionでまとめられていたのが以下の図です。
trainデータはrudimentary detection algorithumによって検知された音声の箇所を専門家によって正例か負例かに分類することでtrainデータのアノテーションを行っている一方で、testデータはアルゴリズムを介さずに専門家が音声とスペクトログラム画像を確認することでアノテーションがされています。この違いによりtrain/testでラベルの分布が大きく異なる可能性が考えられました。
2. missing labelが多い
課題1に関連して、こちらのdiscussionで紹介されているように今回のtrainデータには鳴き声があるがラベルがついていない, missing labelが存在していました。 下の図で赤色で示しているのが与えられているラベルですが、モデルで予測すると、青色で示すようにラベルがついているところ以外でも実際は鳴き声があることがわかります。(図は上記discussionより引用)
上記のように同じ種のラベルが欠損しているのであればそこまで問題ではありませんが、鳴いているのにラベル付けされていないクラスがある場合、学習に支障をきたすことが考えられます。 実際、trainデータのラベルを確認すると1132件の音声のうち複数の種のラベルが1つの音声についていたのはわずか27件のみでした。
コンペ終盤に出たdiscussionによるとtestデータは1つの音声に平均して4~5種の鳴き声が含まれているという示唆があったことから、trainデータはtestデータに比べて圧倒的にラベルの数が少ないことが課題であり、これに対処する必要がありました。
3. FPデータの扱い
今回は通常のラベルに加えてFalse Positive(FP)ラベルも与えられていました。これはどういうラベルなのかというと、課題1で説明したアルゴリズムによって鳴いている(Positive)と判定された種のうち、専門家によって「判定が正しい(Positive)」と判定されたものをTrue Positive(TP)データ、「判定が正しくない(False)」と判定されたものをFalse Positive(FP)データとして与えられていました。 TPデータはそのままラベルとして使うことができますが、FPデータは「種Aは鳴いていない」という情報しかないためそのまま使うのは難しいものでした。ただTPデータの音声は約1000件でFPデータの音声は約3000件あったのでFPデータをどのように使うかというのが一つのポイントであったと考えています。
解法
上記の課題を踏まえて私たちのアプローチを紹介しようと思います。 最も重要だと考えるポイントは以下の3つです。
- 3stageによる学習
- 訓練データに対するpseudo labelingによりmissing labelを補完
- custom lossにより曖昧なラベルはlossを計算しないようにする
今回のコンペでは後述するpseudo labelingとcustom lossによるoverfitを防ぐために3 stageで学習を行いました。以下私のモデルを例に説明していきます。
stage1
CV:0.81 LB:0.84
- EfficientNet-b2
- SED model (clipwiseでloss計算と予測を行う)
- 5fold StratifiedKFold
- 30 epoch
- LSEP Loss
- TPデータのみ使用
- 画像サイズ(height,width)=(244, 400)
- batchsize 16
- Adam
- learning_rate=1e-3
- CosineAnnealingLR(max_T=10)
- augmentationなし
- 10s単位で学習・予測
stage1では鳥コンペや本コンペで有効とされていたSound Event Detection(SED)モデルを使用しました。SEDについては鳥コンペのnotebookや本コンペのdiscussionがとても参考になります。
stage1では特別なことは行っていませんが1点だけ。多くの参加者がBCELossとFocalLossを用いていましたがここではLSEPLossを使用しました。 LSEP LossはFreesoundコンペの3rd solutionでも使用されていた損失関数で、今回のコンペのようなrankingに基づく評価指標の場合、BCELoss のような分類用の損失関数よりもより良い精度が出ると上記discussionで説明されていました。実際に使用したところBCELossと比べてLB scoreが0.01とかなりアップしました。
stage1の目的は後のstage2,3で使うpretrained modelを作ることです。コンペ前半はstage1のモデルを強化することに取り組んでいましたがなかなかスコアが伸びませんでした。
stage2
CV: 0.734 LB:0.896
- stage1とほとんど同じ構成
- stage1のモデルをpretrained modelとして使用
- 5 epoch
- FPデータをTPデータと同じ数だけsamplingして学習に使用
- FocalLossベースのカスタム損失関数
stage2の目的は2つあります。1つはstage1のmodelに追加学習する形で精度を向上させること、もう1つは精度が向上したstage2のモデルを利用してTP・FPデータに対して予測を行い、pseudo label(擬似ラベル)を作成することです。精度を向上させるためにstage2ではFPデータの追加とcustom lossを導入を行いました。この追加学習によりLB scoreが+0.05とかなり向上しました。
custom lossについて
本コンペの課題としてmissing labelがありました。missing labelがあるということはTPデータで0(負例)としてラベル付けされているクラスの中にも実際は1(正例)のクラスが含まれているということです。そこでラベルを以下の3つに分けることにしました。
1: TPラベル(鳴いている) 0: 曖昧なラベル(正例が混じっているかもしれない) -1: FPラベル(鳴いていない) 例) label = [0, 0, 1, 0,...., -1, -1]
この3つのラベルのうち0ラベルは曖昧なラベルとして扱い、loss計算時に省くようにlossを設計しました。 FPデータは間違われやすいけど専門家によって鳴いていないと判定された非常に有益な情報なのでそれを0ラベルと違うことがわかるよう-1としてラベル付けし、loss計算の時には0(鳴いていないもの)として計算しています。このようにFPデータを扱うことで本コンペの課題の1つであるFPデータの利用に対処しました。
これにより明確にラベル付けされているところだけ学習され、曖昧な箇所は学習されないようになりスコアの向上に寄与しました。ちなみにこの損失関数でstage1のように1から学習することも試しましたが学習効率が悪く、スコアも低下したことからstageを分けて5 epochだけ追加学習するアプローチを取りました。
実装は以下です。stage1で使ったLSEPLossは以下のような実装が困難だったのでFocalLossをベースにカスタムしました。BCELossよりFocalLossの方が効いたのでFocalLossを採用しました。
class FocalLoss(nn.Module): def __init__(self, gamma=2.0, alpha=1.0): super().__init__() self.posi_loss = nn.BCEWithLogitsLoss(reduction='none') self.nega_loss = nn.BCEWithLogitsLoss(reduction='none') self.zero_loss = nn.BCEWithLogitsLoss(reduction='none') self.gamma = gamma self.alpha = alpha # self.zero_smoothing = 0.45 def forward(self, input, target): # mask posi_mask = (target == 1).float() nega_mask = (target == -1).float() # (n_batch, n_class) zero_mask = (target == 0).float() # ambiguous label posi_y = torch.ones(input.shape).to('cuda') nega_y = torch.zeros(input.shape).to('cuda') zero_y = torch.full(input.shape, self.zero_smoothing_label).to('cuda') # use smoothing label posi_loss = self.posi_loss(input, posi_y) nega_loss = self.nega_loss(input, nega_y) zero_loss = self.zero_loss(input, zero_y) probas = input.sigmoid() focal_pw = (1. - probas)**self.gamma focal_nw = probas**self.gamma posi_loss = (posi_loss * posi_mask * focal_pw).sum() nega_loss = (nega_loss * nega_mask).sum() zero_loss = (zero_loss * zero_mask).sum() # stage2ではこれをlossに加えない return posi_loss, nega_loss, zero_loss
訓練データに対するpseudo labelingについて
上記の方法で学習したstage2モデルを使ってtrainデータ(TP/FP全て)に対して予測を行いpseudo labelを作成しました。目的はtrainデータに欠けているラベル(missing label)を補填するためです。 ラベル付けは以下のように行いました。
threshold = 0.5として 1: 0.5 <= 予測値 0: 予測値 < 0.5
これを新たなラベルとして追加しstage3で使用します。これによりmissing labelに対処しました。 0ラベルには正例のラベルが含まれている可能性もあることからあくまで曖昧なラベルとして扱います。
stage3
CV:0.954 / LB:0.950
- stage2とほとんど同じ構成
- original label+pseudo labelで学習
- stage1のモデルをpretrained modelとして使用
- 5epoch
- FPデータをTPデータと同じ数だけsamplingして学習
- Focallossベースのカスタム損失関数
- last layer mixup (+0.007)
- 曖昧なラベルに対してlabel smoothingをかける(0 -> 0.45としてlossを計算) (+0.009)
stage3が最終的なモデルになります。pseudo labelを新たなラベルとして加えることによって課題の1つであったmissing labelに対処しました。これによりLB scoreが0.04向上しました。またニューラルネットワークの最終層でmixupも有効でした。注意点としてstage2で使っているcustom lossは過学習に陥りやすかったのでstage3ではstage2のモデルではなく、stage1のモデルをpretrained modelとして学習しています。以上がモデルの全体像になります。
CV
今回は課題1で述べたようにtrainとtestでラベルの分布が大きく異なっておりvalidationが難しいコンペだったように思います。 私たちのチームでは以下の5指標を確認しながらサブミットを行っていました。
- pseudo labelありのLWLRAP
- pseudo labelなしのLWLRAP
- Recall
- Precision
- AUC
ただどの指標もLBと相関が十分にとれているとは言えませんでした。なのでPublic/Private LBに大きな差はないという仮定のもと、基本的にはtrust LBでモデルを改善していきました。 shake対策としては多様性の多いモデル(ViT, WaveNet, ResNet18)でpseudo labelと予測のアンサンブルを行いlabel及び予測がロバストになるようにしました。 結果としてはPublic/Privateで大きな差異はなく8th -> 5thにshakeupしてコンペ終了を迎えることができました。CVの良い方法についてはこれからdiscussionを読んで勉強したいと思います。
その他うまくいかなかったこと
- mean-teacher
- Conformer
- testデータに対するpseudo labeling
- 通常のmixup
- noise追加/除去
- TTA
- SAM optimizer
おわりに
今回最終的に初の金メダルを取ることできました。自分より格上の人とチームを組めたことで上位陣の戦い方や考え方を学ぶとてもいい機会になったと思います。一緒にチームを組んでくれた2人にはとても感謝しています。またコンペ序盤から有益な情報を共有してくださったaraiさんやshinmuraさんからも勉強させていただきました。これからはコンペの順位はもちろんのこと、コミニュティへの貢献も意識して取り組んでいきたいと感じたコンペでした。
参考
コンペのGitHubレポジトリはこちらです。 (branchごとに分けてやっていたのでごちゃごちゃしてしまった。) github.com