こちらの記事の続き
https://jsapachehtml.hatenablog.com/entry/2020/10/17/231526
今回はEnvNetを実装してみたのでメモ
EnvNet
生の波形をそのままCNNにかけて分類し、melspectrogramを使った場合よりよい精度が実現できるかを検証している。
公式の実装はchainerでこちら
https://github.com/mil-tokyo/bc_learning_sound
BC Learningという次年度に出した論文の実装と合わせて公開されているため名前が異なるがEnvNetを実装している。
なのだが、最初これを見つけられずに論文を見ながら実装したため異なっている点がけっこうあると思われるがあしからず。
データセット
ESC-50を利用した。以下のように前処理
- 16kHzにダウンサンプリング
- -1.0 ~ 1.0に正規化
- 1.5秒幅(segment)を一つの入力とする
- segmentは5秒の中からランダムで区間を抽出
- 無音の区間だった場合は再抽出
- 正規化後の値で最大値が0.2以下であれば無音と判定
class EnvNetDataset(BaseDataset): def __init__(self, config, csv_path, audio_dir, folderList): super(EnvNetDataset, self).__init__(config, csv_path, audio_dir, folderList) trans = transforms.Compose([ torchaudio.transforms.Resample(44100, 16000) ]) # 16kHzで1.5秒を1区間 self.segment_size = int(16000 * 1.5) self.sounds = [] for i, file in enumerate(self.filenames): path = os.path.join(self.audio_dir, file) sound = torchaudio.load(path, out = None, normalization = True) resampled = trans(sound[0].squeeze()) resampled /= torch.max(torch.abs(resampled)) self.sounds.append(resampled) def is_enough_amplitude(self, data): return 0.2 < torch.max(torch.abs(data)) def __getitem__(self, index): resampled = self.sounds[index] # 波形から1.5秒区間をランダムに抽出 # もっと効率的な方法はあると思うがこれで問題なかったためこのまま max_iter = 10000 for i in range(max_iter): start = random.randint(0, len(resampled) - self.segment_size) data = resampled[start : start + self.segment_size] # 無音の場合は除外 if self.is_enough_amplitude(data): break if i == max_iter - 1: self.config.logger.warning("valid section is not found: {}".format(path)) return data.unsqueeze(0), self.labels[index], index
学習ロジック
モデルのネットワークはこちらの通り(論文より引用)
論文の説明はかなり詳細に書いてあるため助かる
面白いのは、最初に1次元のCNNで処理して特徴フィルターを増やし、そのフィルター方向の次元を使って2次元のCNNを適用していること
- optimizerはモメンタム付きのSGD
- learning rateはスケジューリング
- 初期値は0.01、80, 100, 120 epochでそれぞれ0.1倍していく
- 重みの初期値はランダム
- ただし私の実装ではHeの初期化しちゃってます
- 畳み込み層にはbatchnorm、全結合層にはdropout(0.5)を適用
評価ロジック
logmelCNNと同じくprobability votingなので、logmelCNNの実装と同じものを使った。 https://github.com/y-kamiya/environmental-sound-classification/blob/0350460a89529afbbb9541c807cfdf531f19bb86/trainer.py#L112,L144
もちろんdatasetの処理は異なる。training用のデータセットとは違った処理必要だったので継承して別クラスに
class EnvNetEvalDataset(EnvNetDataset): def __init__(self, config, csv_path, audio_dir, folderList): super(EnvNetEvalDataset, self).__init__(config, csv_path, audio_dir, folderList) # 0.2秒ずつずらしながら波形を抽出する step_size = int(16000 * 0.2) self.sounds_segmented = [] self.labels_segmented = [] self.file_ids = [] for index, sound in enumerate(self.sounds): start = 0 # segmentの幅は学習時と変わらず1.5秒 clip = sound[start:(start+self.segment_size)] while clip.shape[0] == self.segment_size: if self.is_enough_amplitude(clip): self.sounds_segmented.append(clip.unsqueeze(0)) self.labels_segmented.append(self.labels[index]) self.file_ids.append(index) start += step_size clip = sound[start:(start+self.segment_size)] def __getitem__(self, index): return self.sounds_segmented[index], self.labels_segmented[index], self.file_ids[index] def __len__(self): return len(self.sounds_segmented)
実験
論文ではEnvNet単体の精度が64%くらいになっているため、それより少し低く出たことになる。
上図で精度が低い方はlearning rateを初期値(0.01)のまま固定したものであり、最初にlearning rateが1/10となる80 epoch以降に差が出ており、learing rateを下げることによって精度が向上しているのがわかる。
論文ではさらに、logmelCNNの学習済みモデルを併用することで71%程度の精度を達成している(ぞれぞれのモデルからの出力をprobability votingで合算?)ようだが、今回は生の波形を使ったモデルの実装をやってみるのが目的であるためそちらは試さない。
また論文の面白い点として、EnvNetの学習済みモデルに対してsin波を適用することで、pooling2の特徴フィルターがどの周波数帯に反応しているかを調査している。その結果を反応した周波数帯の値でsortするとlogmelのfeature mapと同様の形状となっており、生の波形から処理した結果としてlogmelと似たような特徴を学習したことがわかる。