kaggle TensorFlow Speech Recognition Challengeの上位者のアプローチを紹介する（後編）

## INTRODUCTION
前回に引き続き、kaggleのTensorflow Speech Recognition Challangeの上位者の
アプローチを紹介いたします。  
これは[この記事](https://qiita.com/daimonji-bucket/items/a20ffaa8056704b767e1)の続きです。　　
先にそちらをご覧ください。  

今回は
1. Network Architecture
2. optimizer
3. resampling
4. normalization / standarization
5. data augmenation
6. silenceクラスへの対応
7. 未知のunknonwへの対応
8. 軽量化・高速化の工夫
9. LBのデータのトレーニングへの利用

について触れます。


 [1st-link1]: https://www.kaggle.com/c/tensorflow-speech-recognition-challenge/discussion/47728  
 [1st-link2]: https://www.kaggle.com/c/tensorflow-speech-recognition-challenge/discussion/46945

 [2nd-link1]: https://www.kaggle.com/c/tensorflow-speech-recognition-challenge/discussion/47715

[3rd-link1]: https://www.kaggle.com/c/tensorflow-speech-recognition-challenge/discussion/47722  
[3rd-link2]: https://github.com/xiaozhouwang/tensorflow_speech_recognition_solution/blob/master/solution.pdf  

   [4th-link1]: https://www.kaggle.com/c/tensorflow-speech-recognition-challenge/discussion/47674  
   
   [4th-link2]: https://github.com/heyt0ny/Kaggle-TensorFlow-Speech-Recognition-Challenge-Solution

 [7th-link1]: https://www.kaggle.com/c/tensorflow-speech-recognition-challenge/discussion/47730
[10th-link1]: https://www.kaggle.com/c/tensorflow-speech-recognition-challenge/discussion/48546
[10th-link2]: https://github.com/tugstugi/pytorch-speech-commands
[10th-link3]: https://www.kaggle.com/c/tensorflow-speech-recognition-challenge/discussion/47660

## nerwork architecture
CNNベースのものがほとんどで、DNN以外のアプローチはほぼ見られません。 
2ndの人以外は画像に対しても利用されるオーソドックスなアーキテクチャーでした。  
なお、以下はすべてアンサンブル前のレベル１のモデルのarchitectureを示しています。 
またオリジナルから多少変更されている場合もあります。　　

1. Resnet ([1st CherKeng][1st-link2], [3rd Little Boat][3rd-link1], [4th P.Ostyakov][4th-link1], [10th tugstugi][10th-link2])
    - Little Boatは9 layer
    - tugstugiは32 layer

2. VGG ([1st CherKeng][1st-link2], [3rd LittleBoat][3rd-link1], [4th P.Ostyakov][4th-link1], [4th feels_g00d_man][4th-link2], [7th JihaoLiu][7th-link1], [10th tugstugi][10th-link2])
    - feels_g00d_man, tugstugiはbatchnormalizationを加えたVGG19を使ったようです。
    - Little Boatは終わりがGMPとGAPのVGG
    - JihaoLiuはVGG11  

3. RNN ([4th P.Ostyakov][4th-link1])
    - GRUかLSTMか不明  

4. Densenet(([4th feels_g00d_man][4th-link1], [3rd LittleBoat][3rd-link1], [10th tugstugi][10th-link2])
    - feels_g00d_manはDensenet201
    - LittleBoatはDensenet121
    - tugstugiはDenseNet 100, 190

5. Senet ([3rd Little Boat][3rd-link1])
    - Little BoatはSenet18 

6. wide resnet ([10th tugstugi][10th-link2])
    - tugstugiはwide resnet 28-10, ,28-10-dropout, 52-10

7. Resnext ([10th tugstugi][10th-link2])
    - tugstugiはResNext 29, 8x64

8. Dual Path Network ([10th tugstugi][10th-link2])
    - tugstugiはDPN 92

9. "carefully designed" 多層CNN  ([2th O'Malley][2nd-link1])

    おそらくアプローチを公開している中では、一番アーキテクチャーを工夫しており、
    **single model**でprivate LB 90.9と群を抜いていました。しかも**圧倒的にシンプル**です。　　
    **きちんと一層一層意味を考えて、オリジナルのnetworkを設計しています。** 
    （本当に狙い通りに働いているかは分かりません。）
　　以下に図示いたします。
   
    2D convolutionを使っていますが、最初のものを除き、 
    一度にfrequencyの畳み込みとtimeの畳み込みを同時に行わないようにしています。

 *注：こちらの表は自分がkerasで再実装し、その出力を改変したものです。*  

  **2018/02/11 fix:** 
  こちらは時間軸方向が16000となっていましたが、間違いです。 
  log mel filterbank energyに変換されていますので、 
  当然時間軸方向の次元も変わっています。 
  fftのwindowサイズとstepサイズが明記されていないため、 
  ここではtensorflowのspeech recognitionのtutorial内の 
  window size=30, step size=10に従っています。  
    
    _________________________________________________________________
    Layer (type)                 Output Shape              Param #   　O'Malleyの意図
    =================================================================
    input_1 (InputLayer)         (None, 1598, 120, 1)     0           1秒間16khz、120次元のlog mel filterbank energyの入力
    _________________________________________________________________
    conv2d_1 (Conv2D)            (None, 1598, 120, 64)    1344        ノイズ軽減
    (64(filter num), 
    (7, 3)(kernel size))
    _________________________________________________________________
    max_pooling2d_1 (MaxPooling2 (None, 1598, 40, 64)     0         　よく使われる40次元に周波数の次元を圧縮
    (1, 3)(pool size)
    _________________________________________________________________
    conv2d_2 (Conv2D)            (None, 1598, 40, 128)    57344       周波数帯域の局所的パターンを見つける
    (128, (1, 7))
    _________________________________________________________________
    max_pooling2d_2 (MaxPooling2 (None, 1598, 10, 128)    0           スピーカーによるバリエーションを許容
    (1, 4)
    _________________________________________________________________　
    conv2d_3 (Conv2D)            (None, 1598, 1, 256)     327680      残っている周波数帯域の違いを扱えるようにしつつ、次元を圧縮。
                                                                    　 phonemeのような特徴が残ったと考えている。
    (256, (1, 10), 
    padding="valid")
    _________________________________________________________________
    conv2d_4 (Conv2D)            (None, 1598, 1, 512)     917504      キーワードの中の時間的につながっているコンポーネントを探す
    (512, (7, 1)) 
    _________________________________________________________________
    global_max_pooling2d_1 (Glob (None, 512)               0         　すべてのコンポーネントを収集
    _________________________________________________________________
    dropout_1 (Dropout)          (None, 512)               0         　
    _________________________________________________________________
    dense_1 (Dense)              (None, 256)               131328    　なぜか性能が上がるためfully connected層を追加
    _________________________________________________________________
    dense_2 (Dense)              (None, 12)                3084      　クラスの予測を出力
    =================================================================
    Total params: 1,438,284
    Trainable params: 1,438,284
    Non-trainable params: 0
    _________________________________________________________________



## optimizer  
Adamを試みた人もいるようですが、結局はほとんどの人がSGDを使ったようです。

1. SGD ([4th feels_g00d_man][4th-link2], [10th tugstugi][10th-link2])　　
    - feels_g00d_manの場合
        - learning rate = 1e-3
        - momentum 0.9
        - weight decay 0.0001<br>
    - tugstugiの場合    
        - learning rate 0.01
        - momentum 0.9
        - weight decay 0.01
        
        learning scheduler
        - patience 5
        - gamma 0.1
        - max-epochs 70 

    どちらもプラトーになったら、学習率を減らしています。


## resampling  

1. balanced class sampling  ([1st][1st-link1])  
    トレーニング用のデータ内には他のクラスのサンプルに比べ、圧倒的にunknownクラスのものが多いです。  
    oversamplingやundersamplingを用いて、バッチに含まれるクラスごとのサンプル数をバランスがとれた状態にすると有効でした。  
    具体的にどちらを使ったのかは分かりませんが、  
同一クラス内のバリエーションが多いunknownのサンプル数をわざわざ減らすというのは考えにくいので、  
おそらくunknown以外をover-samplingをしたと推測されます。

## normalization / standarization
1. windowed peak volume standarization ([2th O'Malley][2nd-link1])  
    一つのサンプルを波形のまま20~50のチャンクに区切り、それぞれのチャンク内で音量をstandarizeします。  
    もし全体でstandarizeを行なうと、 単語の長さによって、平均が変わってしまうため、 区切ることで単語の長さの影響を和らげることができます。<br><br>
    - LB scoreの改善
        - 2% (O'Malleyの場合)

## data augmentation  
前述または後述の通り、実は何種類かのノイズ用ファイル(1 min)が訓練用データセットに含まれていました。 
順当に考えると、このノイズを他のファイルに合成してaugmentしたくなるところです。 
しかし自分が調べた範囲では、意外なことに上位者はこのアプローチは使わなかったようです。トラップですな・・・

1.  time stretching ([2th O'Malley](https://www.kaggle.com/c/tensorflow-speech-recognition-challenge/discussion/47715), [4th P.Ostyakov](https://www.kaggle.com/c/tensorflow-speech-recognition-challenge/discussion/47674))  
    ピッチをそのままにテンポを変えます。元のものより話すスピードをゆっくりにしたり、速くしたりします。<br><br>
    - LB scoreの改善
        - 1% (O'Malleyの場合)<br><br>
2. pitch shift ([4th P.Ostyakov](https://www.kaggle.com/c/tensorflow-speech-recognition-challenge/discussion/47674))  
    テンポをそのままにピッチ（音の高低）を変えます<br><br>
3. time shift ([4th P.Ostyakov](https://www.kaggle.com/c/tensorflow-speech-recognition-challenge/discussion/47674))<br>
    1秒間の音声の中で、単語が発声されているのは一部の部分だけです。  
    しかし、人によって発声のタイミングにずれがあります。  
    そこで発声している部分をずらすことで、対応できるようにします。<br><br>
4. random noise ([4th P.Ostyakov](https://www.kaggle.com/c/tensorflow-speech-recognition-challenge/discussion/47674)) 
    ピンクノイズやホワイトノイズといったランダムノイズを生成して、元の音声に合成します。ノイズの種類はわかりません。 
    ちなみに[Soxにはtype is one of sine, square, triangle, sawtooth, trapezium, exp, [white]noise, tpdfnoise, pinknoise,　brownnoise, pluckなどがあります。](http://linguistics.berkeley.edu/plab/guestwiki/index.php?title=Sox_in_phonetic_research#Add_noise_to_an_audio_file)<br><br>

5. VTLP ([2th O'Malley](https://www.kaggle.com/c/tensorflow-speech-recognition-challenge/discussion/47715))
    それぞれの周波数成分にスカラーのwarp factorをかけ、新しい周波数成分に変換します。 
    例えばwarp factorが1.1なら、100hzの周波数成分の強度が変換後には110hzの強度となります。  

    log-melと一緒に使う場合、mel filterbankに対してVTLPを適用し、そのfilterbankをspectrogramに適用します。

    詳しくは[VTLPの導入論文](https://pdfs.semanticscholar.org/3de0/616eb3cd4554fdf9fd65c9c82f2605a17413.pdf)または
    [ASRに適用した論文](https://arxiv.org/abs/1707.00722)を参照ください。
    - LB scoreの改善
        - 1% (O'Malleyの場合)<br><br>
6. mixup　([7th JihaoLiu][7th-link1], [10th tugstugi][10th-link2])
    今回かなり効果があったようです。 
    簡単に言うと、２つ以上のサンプルを線形に結合して人工的なサンプルを作ることで、 
    サンプル間の空間にデータを補完しようという手法です。
    言葉で説明するより、以下の式を見たほうが早いかと思います。  

    ```math
  
       \tilde{x} = λx_i +(1−λ)x_j \\
       \tilde{y} = λy_i +(1−λ)y_j
    ```
  $\tilde{x}$, $\tilde{y}$は新しく生成されたサンプルの特徴量とラベル、$\lambda$はbeta分布からランダムにサンプリングされたものです。
    JihaoLiuの場合、beta分布の$\alpha$, $\beta$はともに２に設定したそうです。
    (xi, yi), (xj, yx)は両方とも元のトレーニングデータから選ばれたサンプルの特徴量とラベルです。
    詳しくは[原著](https://arxiv.org/abs/1710.09412)または[このブログのポスト](http://www.inference.vc/mixup-data-dependent-data-augmentation/)をご覧ください。  
    - LB scoreの改善
        - 2.9%の改善 (JihaoLiuの場合)
        - 2.03%の改善 (tugstugiの場合)<br><br>

7. testtime-augmenation ([1st CherKeng][1st-link1])
    なんとtrainingデータではなく、**testデータをaugmentします。** 
    最終的な予測値はaugmentationしたものから得られた予測値を平均します。 
    意外なことに有効だったみたいです。  

    CherKengはaugmentationの種類として、  
    - time-shifting
    - 音量の増減
    - time stretching  
    
    を使ったそうです。  

## silenceクラスへの対応
1. power levelの小ささとsilence以外のクラスの事後確率をもとに判断 [4th P.Ostyakov](https://www.kaggle.com/c/tensorflow-speech-recognition-challenge/discussion/47674)  
    どのクラスにも判断しづらくかつspectrogramのpower levelが小さいものをsilenceとみなします。  
    - 手順
        1. silence以外の分類器を作成します。
        2. その分類器にLBデータのサンプルを入力するとクラスの事後確率が得られます。
        3. サンプルごとに最大のクラスの事後確率を取得します。
        4. 最大のクラスの事後確率に基づいて、昇順にLBデータのサンプルをソート
        5. 上から（つまり低い順に）K番目までsilenceの候補を取得。
        6. 候補のデータから得られたspectrogramのpower levelがLより小さければsilenceとみなします

        Ostyakovの場合、K=10000, L=1を使ったそうです。<br><br>
  - LB scoreの改善
      - 0.5% (Ostyakovの場合)  <br><br>
2. pseudo-labeling
    後述

## 未知のunknonwへの対応
1. "double words"(２つのunknownのサンプルを切ってつなげる) ([4th feels_g00d_man][4th-link1])
    ２つのunknonwのサンプルをそれぞれ振幅が最大のところで切って、つなげます。([コード][4th-link2])
    例えば"stop"と"dog"からは"stog"が作られます。
    実際に存在しない言葉でも有効なようです。  
    LB scoreの改善
    - 0.24 %<br><br>

2. pseudo-labeling
    後述

## 軽量化・高速化の工夫
1. model distillation([1st CherKeng][1st-link1], [7th JihaoLiu][7th-link1])  
    高性能だが大きなモデルの知識を、より小さいモデルに移します
    ([原著](https://arxiv.org/abs/1503.02531))  
    詳しくは調査中です

## LBのデータのトレーニングへの利用  
LBのデータのトレーニングへの利用してさらにスコアをあげる方法です。 
当然のことながら、手動でLBのデータをラベル付けすることは禁止されています。 

そこでモデルの出力したクラスの確率など何らかの基準に基づいて、
確実だと思われるラベルを自動でつけます。  
テストデータが未知のデータへの性能を測るためにあることを考えると、 
実務ではまずできません。 
（というかそれが許されるならちゃんとラベル付けしてtrainingデータに足しますよね）  
こういう点はコンペだなあと思います。
最近のkaggleのコンペだと割とよく行われるようです。 
さすがにここまでやるようになってくると、 
DeepAnalyticsみたいに、最終的な評価用のデータは別にしたほうがいいんじゃないですかね...？  

1. pseudo labeling ([1st Cherkeng][1st-link1], [3rd][3rd-link1])  
    性能が高いモデルが予測したラベルを正解ラベルとしてtrainingデータに加えます。  
    新しい情報を学習すること以上に、主にLBデータの分布を学ぶことに使われたようです。  
    加える方法はいくつかあります。<br><br>
    - 予測されたクラスの確率が閾値より高いものだけをすべてトレーニングに加える。
    - epochごとに20%~35%を選んでトレーニングデータと加える。 
    - トレーニングデータで訓練させた後、fine tuningをLBデータで行う。

    LB scoreの改善
    - 2% (Little Boatの場合)
  

## outro
以上が今回の上位者のアプローチの概要でした。  
アンサンブルの手法については、
特にこのコンペ特有というわけではなかったので、　　
触れませんでした。  

コンペの上位者のアプローチをちゃんと比較してまとめるのは初めてなのですが、 
思った以上に戦略がばらけたようです。  
また似ていても、結果に差がある参加者のアプローチと上位者のアプローチの 
微細な違いも気になる点です。  

## 余談
統計はありませんが、PyTorchを使っている参加者が目立ちました。 
Kerasを使用していた人達がPyTorchに乗り換える流れが来ているのでしょうか・・・？