DANet(Deep Attractor Network)の実装と解説


はじめに

音声を任意の数に分離するモデルであるDANet(Deep Attractor Network)の実装と解説をします。
DANetは論文"DEEP ATTRACTOR NETWORK FOR SINGLE-MICROPHONE SPEAKER SEPARATION"で提案されているモデルです。
このモデルを環境音データセットのESC-50を使って学習させ、その分離音性能をGNSDR、GSAR、GSIRで測定しました。

実装にはTensorflowとKerasを用いました。
実装の全コードはGithubにあります。
また、それらを実行して学習やGNSDR等の測定ができるGoogle Colabのファイルもあります(GithubのDANet.ipynbをGoogle Colabで開いたもの)。
公式のPytorchを用いた実装もGithubに公開されています。

概要

以下のように分けて説明します。
1. 実行したPythonのパージョンと使用したパッケージ
2. データの前処理
3. モデルの構成
4. 分離音性能の評価
5. パッケージとしての利用

1. 実行したPythonのバージョンと使用したパッケージ

  • Python 3.7.10
  • TensorFlow 2.4.1
    モデルの構築に利用
  • museval 0.4.0
    SDR、SIR、SARの計算に利用
  • SoundFile 0.10.3.post1
    音声のファイルへの書き込みに利用
  • pandas 1.1.5
    音声ラベルのcsvデータを扱う際に利用
  • numpy 1.19.5
    各種計算、配列処理等に利用
  • scipy 1.4.1
    ハニング窓の計算に利用
  • librosa 0.8.0
    音声のファイルからのロード、リサンプリング、短時間フーリエ変換等の音声処理に利用
  • matplotlib 3.2.2
    スペクトログラムや音声波形の描画に利用

2. データの前処理

論文での実験通りに行いました。
まず、入力音声を8000Hzでリサンプリングし、window lengthを32ms、hop sizeを8ms、窓関数をハニング窓の平方根とした短時間フーリエ変換を行いました。その後、絶対値を取ってから自然対数を取り、時間方向の次元数を100にしたものをモデルへの入力としました。

3. モデルの構成

画像の引用元:
Zhuo Chen, Yi Luo, Nima Mesgarani, "DEEP ATTRACTOR NETWORK FOR SINGLE-MICROPHONE SPEAKER SEPARATION," arXiv preprint arXiv:1611.08930v2, 2017
https://arxiv.org/pdf/1611.08930.pdf

まず、入力音声を双方向LSTMに入力し、その結果を結合することで周波数方向の次元数を600にします。
そして全結合層に入力し、周波数方向の次元数を129(モデル入力時の周波数方向の要素数)×20(embedding空間の次元数)にし、Reshapeすることで周波数方向とembedding空間方向に分離します。

その後、訓練時には下の式によりAttractorを計算します。
Vは入力音声のEmbedding結果であり、Yはideal mask(各時刻、各周波数において、混合音声の中で振幅が一番大きい音声を1、そうでない音声を0としたもの)を表します。

A_{c,k} = \frac{\sum_{f,t}V_{k,ft} \times Y_{c,ft}}{\sum_{f,t}Y_{c,ft}}

推論時にはVをkmeansクラスタリングし、そのときの中心点を用いることでAtractorを生成します。

また、AttractorとEmbedding結果を用いて下式のようにしてMaskを計算します。

M_{f,t,c} = Sigmoid(\sum_{k} A_{c,k} \times V_{ft,k})

混合音声の分離が難しいような条件のときはSigmoid関数の代わりにSoftmax関数を用いることもでき、今回の実装でもSoftmax関数を用いました。

M_{f,t,c} = Softmax(\sum_{k} A_{c,k} \times V_{f,t,k})

Maskを混合音声とかけることにより分離音声が生成され、モデルの出力となります。なお、推論時には混合音声として、短時間フーリエ変換した後に絶対値や対数を取っていない位相付きのスペクトログラムを用い、モデルの出力を逆短時間フーリエ変換することで音声波形に戻せるようにしました。

また、損失は下の式(正解音声とモデルの出力との差の二乗和)をf(周波数方向の次元数)×t(時間方向の次元数)で割ったものを用いました。

L = \sum_{f,t,c} \|S_{f,t,c} -  X_{f,t} \times M_{f,t,c}\|_2^2

4. 分離音性能の評価

参考: https://library.naist.jp/mylimedio/dllimedio/showpdf2.cgi/DLPDFR009675_P1-57

推定音声を$ \hat{s}(t) $とし、下のように正解音声$ s_{target}(t) $、非正解音声$ e_{interf}(t) $、ノイズ$ e_{artif}(t) $と分解しました。

\hat{s}(t) = s_{target}(t) + e_{interf}(t) + e_{artif}(t)

これらの値から、SDR、SIR、SARが下の式から計算されます。

SDR = 10 \log_{10} \frac{\|s_{target}(t)\|^2}{\|e_{interf}(t) + e_{artif}(t)\|^2}\\
SIR = 10 \log_{10} \frac{\|s_{target}(t)\|^2}{\|e_{interf}(t)\|^2}\\
SAR = 10 \log_{10} \frac{\|s_{target}(t) + e_{interf}(t)\|^2}{\|e_{artif}(t)\|^2}

今回はmusevalというパッケージを用いてこれを計算しました。

また、この計算における推定音声の部分を混合音声に変えてSDRに対応するものを計算し、SDRからその値を引いたものがNSDRとなります。
NSDR、SIR、SARの各平均をとったものがGNSDR、GSIR、GSARとなり、これが論文で使用されている分離音性能の評価の指標となります(数値が大きいほど性能が高い)。

5. パッケージとしての利用

Githubに載せた実装をパッケージとして利用することで、ESC-50データセットの内2種類の音声を混ぜ、その分離を学習させることが簡単にできます。

実装したモデル自体は他のデータを用いたり3種類以上の音声を分離したりすることも可能ですが、データの前処理や訓練データ、テストデータの生成に関するモジュールは上の場合にしか対応していません。

  1. パッケージのインストール、インポート

    $pip install git+https://github.com/KMASAHIRO/DANet
    
    import DANet
    
  2. ESC-50データセットの用意

    $wget https://github.com/karoldvl/ESC-50/archive/master.zip
    $unzip master.zip
    
  3. データの前処理(リサンプリング、短時間フーリエ変換等)

    # 短時間フーリエ変換後の音声をFouriers、音声の種類を表す名前をsound_namesに格納
    Fouriers, sound_names = DANet.preprocess.preprocess(labelpath="ESC-50-master/meta/esc50.csv", audiopath="ESC-50-master/audio/")
    
  4. モデルを構築

    model = DANet.models.create_model()
    
  5. モデルの学習

    # 学習データを生成するジェネレータをメソッドとして含むクラスのインスタンスを作成
    Generator = DANet.generating_data.generator(Fouriers, sound_names)
    # batch sizeを指定
    batch_size=25
    # batch sizeからsteps数(1 epochにジェネレータを呼び出す回数)を計算
    steps = Generator.get_steps(batch_size)
    #学習(下の場合、混合させる音は風の音とカエルの鳴き声、epoch数は20)
    model.fit(x=Generator.generator_train('wind','frog',batch_size), steps_per_epoch=steps, epochs=20, initial_epoch=0)
    
  6. テストデータによる推論

    # テストデータを生成する関数をメソッドとして含むクラスのインスタンスを生成
    create_test_data = all_test_data(Fouriers,sound_names)
    # テストデータ、混合させる前のテストデータ(後の評価に利用)を生成
    test_data, before_data = create_test_data.generate_data('wind','frog')
    # 推論
    result = model.prediction(test_data)
    
  7. 音声波形への復元

    # 分離前の音声(before)、混合した音声(mixed)、分離後の音声(after)を生成
    before, mixed, after = DANet.evaluation.return_to_sound(result, before_data)
    
  8. 分離音性能の評価

    # 一つずつの推論結果に対してNSDR、SIR、SARを計算
    NSDR_list, SIR_list, SAR_list = DANet.evaluation.evaluation(before, mixed, after)
    # GNSDR、GSIR、GSARを計算
    GNSDR, GSIR, GSAR = DANet.evaluation.final_eval(NSDR_list, SIR_list, SAR_list)
    
  • モデルの重みを保存
filepath = "DANet_weights_wind_frog.h5"
model.save_weights(filepath, save_format='h5')
  • モデルの重みをロード
model = DANet.models.create_model()
filepath = "DANet_weights_wind_frog.h5"
model.loading(filepath)
  • 復元した音声を保存する

モデルへの1回の入力に対応する分離前の音声、混合した音声、分離後の音声を保存します。

# numは保存するテストデータのindexを示す。
num = 0
DANet.evaluation.save_sound(num, before, mixed, after)
  • 音声波形のグラフを保存する

モデルへの1回の入力に対応する分離前の音声波形、混合した音声波形、分離後の音声波形を保存します。

# numは保存するテストデータのindexを示す。
num = 0
DANet.evaluation.save_sound_fig(num, before, mixed, after)

おわりに

音声を任意の数に分離するモデルであるDANet(Deep Attractor Network)の実装と解説をしました。
混合させる音の組み合わせを様々に変えて学習させた結果もGoogle Colabに載せましたが、GSARは一部論文の結果を上回ったものがありましたが、GNSDR、GSIRについては論文の結果を超えるものはなく半分以下の値になることが多かったです。原因の1つ目としてはデータ量の違い、2つ目としては論文で分離している人の声と今回使用した環境音の性質の違いが考えられます。

今回作成したパッケージを使い、ぜひ様々な音声の分離をしてみてください。
不明な点、実装コードに関するアドバイス、パッケージとして利用する際の問題などありましたらコメントお願いします。

(この記事は研究室インターンで取り組みました:https://kojima-r.github.io/kojima/)