オノマトペ(擬音語)から環境音を合成するニューラルネットワーク(Onoma-to-Wave)をPyTorchで実装した

はじめに

下記の雑誌論文が最近出版された。

Yuki Okamoto, Keisuke Imoto, Shinnosuke Takamichi, Ryosuke Yamanishi, Takahiro Fukumori and Yoichi Yamashita, "Onoma-to-wave: Environmental Sound Synthesis from Onomatopoeic Words", APSIPA Transactions on Signal and Information Processing: Vol. 11: No. 1, e13, 2022.

www.nowpublishers.com

この論文では「オノマトペ」(擬音語)から環境音を合成する手法が提案されている。手法の核となる部分を一言で要約すれば、「オノマトペからスペクトログラムへのSeq2Seqに基づく系列変換モデル」である。

以下、著者らによるスライドに目を通したうえで本記事を読まれたい。

www.slideshare.net

2022年7月現在、著者らによる提案手法「Onoma-to-Wave」の実装は公開されていないが、ただ論文自体はこのように公開されているので、自分でOnoma-to-Waveを実装し、本当にオノマトペから環境音が合成可能なのか追実験してみよう、というわけである。

ちなみに論文著者らによるデモンストレーション用のページが用意されており、オノマトペから合成された環境音が試聴できる。果たしてこのクオリティの環境音合成が自分の実装で再現できるのかを本記事では検証する。

y-okamoto1221.github.io

事前準備

まず第一に、RWCP 実環境音声・音響データベース (Real World Computing Partnership-Sound Scene Database; RWCP-SSD) を入手すること。これがないと始まらない。以下のページを参考に。オンライン配布にも対応しており、アカデミア関係者ならば入手は容易である。

research.nii.ac.jp

www.nii.ac.jp

RWCP-SSDはVol1, Vol2, Vol3と分かれているが、実験ではVol1に同梱されている環境音データを用いる。続いて各環境音に対してオノマトペを付与したデータセットであるRWCP-SSD-Onomatopoeiaを入手する。

github.com

1つの環境音につき、15以上のオノマトペが付与されている。jpのほうがカナ表記されたオノマトペ、enのほうはカナを音素表記に変換したオノマトペである。ちなみにOnoma-to-Waveへの入力は音素表記のものを用いる。

ソースコード

以下のリポジトリに置いた。Enjoy!

実装の概要と動かし方

論文では音響イベントラベルによる条件付けのあり・なしで2通りのモデルが提案されている。そこで、

  • unconditional ... 音響イベントによる条件付けなし(オノマトペのみを入力)
  • conditional ... 音響イベントによる条件付けあり(オノマトペと音響イベントラベルを入力)

として実装を分けておいた。

それぞれのフォルダには以下のスクリプトたちがある。

  • preprocess.py ... 前処理を担当するスクリプトである。

    • raw_to_wav(): データベース内の環境音データはraw形式になっており、扱いやすさを考えてRIFFヘッダをくっつけてwav形式にする。
    • make_csv(): カナ表記のオノマトペおよび音素表記(英字)のオノマトペ、さらにオノマトペに対応するwavファイルのパス情報と音響イベント情報をcsvファイルにパッキングする。
    • make_symlink(): 訓練および推論に用いるwavファイルをシンボリックリンクの形で分けておく。
    • mapping_dict.make() & mapping_dict.save() : オノマトペの音素表現(文字列)と数値表現を相互に変換する辞書を作成&保存する。後述するMappingDictクラスのメソッドを利用している。
  • training.py ... モデルの訓練を実施するスクリプトである。

  • inference.py ... 訓練済のモデルを用いて環境音を合成するスクリプトである。

前処理として、上記のようにcsvファイルを中間的に作成しておく。実際のcsvファイルの中身の例を以下に示す。

ポンッ,p o N q,/path/to/your/dataset/nospeech/drysrc/a1/cherry1/16khz/005.wav,cherry1
トッ,t o q,/path/to/your/dataset/nospeech/drysrc/a1/cherry1/16khz/005.wav,cherry1
コンッ,k o N q,/path/to/your/dataset/nospeech/drysrc/a1/cherry1/16khz/005.wav,cherry1
トッ,t o q,/path/to/your/dataset/nospeech/drysrc/a1/cherry1/16khz/005.wav,cherry1
...

各行は「オノマトペ(カナ)、オノマトペ(英字)、対応するwavへのパス、音響イベント名」の順でカラムが並ぶ。 上記の例では

  • オノマトペ(カナ): "ポンッ"
  • オノマトペ(英字): "p o N q"
  • wavへのパス: "/path/to/your/dataset/nospeech/drysrc/a1/cherry1/16khz/005.wav"
  • 音響イベント名: "cherry1"

となる。

以上の前処理はデータセットに含まれる全てのオノマトペ、全てのオーディオファイルが対象であり、実験に先駆けて一度だけ行えばよい。

そしてこれらを動かす前に、config.yamlを編集しておく必要がある。config.yamlには実験条件に関する設定が書かれているが、取り急ぎ動かす分には主にディレクトリのパスまわりを各自の環境に応じて編集すれば良い。実験で具体的に使う音響イベントはconfig.yamlの中で指定される。対象となる音響イベントを変えて実験するときには、yamlファイルを編集するだけでよい。

補助的に作成したモジュールの概要

training.py, inference.pyにおいてimportされる、補助モジュールについても簡単な説明をしておこう。

  • models.py ... 主にSeq2Seqモデルを定義している。Encoder, Decoder, Seq2Seqクラスがそれである。音響イベントラベルの埋め込み用にEventEmbeddingクラス、オノマトペの開始記号を埋め込むためのBosEmbeddingクラスも用意した。

  • dataset.py ... データセットを定義している。OnomatopoeiaDatasetクラスがそれである。データセットおよびデータローダをインスタンス化する関数や、collate_fnも定義している。

  • optimizer.py ... オプティマイザおよびスケジューラをインスタンス化する際のインタフェースを提供している。

  • mapping_dict.py ... オノマトペ(英字)の列と数値列を相互に変換する辞書を与えるクラスを定義している。MappingDictクラスがそれである。

  • trainer.py ... モデル訓練のループを回すためのクラスを定義している。 Trainerクラスがそれである。

  • generator.py ... 訓練済みのモデルを用いた推論により環境音を生成するためのクラスを定義している。Generatorクラスがそれである。

おまけ:訓練済みモデルのリリース

デモンストレーション用に訓練済みモデルもリポジトリ上で公開した。手軽(?)に合成を試すことのできるスクリプトも用意した。

  • demo.py ... 訓練済みモデルを用いた環境音合成用スクリプトである。inference.pyとは異なり、yamlファイル内で指定されたオノマトペと音響イベントに基づき手軽に合成できる。DataSetとDataLoaderを使わない分、実装もややシンプルになる。

  • pretrained_uncond.pt ... 音響イベントによる条件付けなしの訓練済みモデル

  • pretrained_cond.pt ... 音響イベントによる条件付けありの訓練済みモデル

まずはconfig.yamlを編集し、各自の環境に応じて訓練済みモデルへのパスを与えておく。続いて合成したいオノマトペ(音素表現)と、必要に応じて音響イベントもconfig.yaml内で与えておく。カナ表記でも合成可能にすることはTODOとしたい(実装の後回し)。ESPnetのようにGoogle Colab上で動くデモンストレーションも用意していない。本記事の読者ならば独自に用意できるだろう!

環境音合成実験

今回は論文を参考にして以下の環境音を実験に採用した(カッコ内はデータベースにおける識別子)。

  • 金属製のゴミ箱を叩いたときの音(trashbox)
  • カップを叩いたときの音(cup1)
  • ベルを鳴らしたときの音(bells5)
  • 紙を破るときの音(tear)
  • ドラムを叩いたときの音(drum)
  • マラカスを振ったときの音(maracas)
  • ホイッスルを吹いたときの音(whistle3)
  • コーヒー豆をミルで挽いたときの音(coffmill)
  • 電気シェーバーを動かしたときの音(shaver)
  • 目覚まし時計の音(clock1)

オーディオデータの特徴量抽出にかかる分析条件、訓練に用いるデータ量、ニューラルネットワークの構成など、訓練にかかる実験条件は原論文と揃えている。 エポック数、学習率は論文に記載がなかった。今回の実験ではエポック数を350とした。また学習率の初期値を0.0001として、 MultiStepLRによりエポック200, 250において学習率をそれぞれ0.5倍するようにした(0.0001 -> 0.5 * 0.0001 -> 0.5 * 0.5 * 0.0001)。

音響イベントによる条件付けなし

環境音を概ね期待通りに合成できた例を以下に示す。入力に使ったオノマトペはすべてテストデータである(訓練には用いていない)。

  • shaver (オノマトペ: ビイイイン 音素列: / b i i i i N /)
自然音 合成音


自然音 合成音


自然音 合成音


  • bells5(オノマトペ: チーンチーン 音素列: / c h i: N c h i: N /)
自然音 合成音


自然音 合成音


マラカスの合成音のクオリティは著者のデモンストレーションよりも良いような気がしているが、果たしてどうか。 またベルの音については音色はよく再現できているものの、音の「繰り返し」は合成できていない。

一方、論文でも指摘されている通り、オノマトペだけでは十分にスペクトログラムを条件づけできず、別の環境音が合成されてしまうケースが確認できた。コーヒーミルではそれが特に顕著だった。以下に当該オノマトペと合成音の例を示す。

  • coffmill
ガサガサ (/ g a s a g a s a /) →マラカスの音 ジビビビビッ (/ j i b i b i b i b i q /) → 目覚まし時計の音
チャラチャラ (/ ch a r a ch a r a /) →目覚まし時計の音


  • tear
スウィイイイイン (/ s u w i i i i i N /) →マラカスの音

音響イベントによる条件付けあり

合成がうまくいかなかった例を上で示したが、音響イベントによる条件付けをして合成した例を以下に示す。比較のため、「条件付けなし」の合成音も並べておく。条件付けにより音響イベントと合致した環境音が合成できていることが分かる。

  • coffmill(オノマトペ: ガサガサ 音素列: / g a s a g a s a /)
条件付けなし 条件付けあり


  • coffmill(オノマトペ: ジビビビビッ 音素列: / j i b i b i b i b i q /)
条件付けなし 条件付けあり


  • coffmill(オノマトペ: チャラチャラ 音素列: / ch a r a ch a r a /)
条件付けなし 条件付けあり


  • tear(オノマトペ: スウィイイイイン 音素列: / s u w i i i i i N /)
条件付けなし 条件付けあり


続いて、論文にもある例だが 、"ビイイイイイ (b i i i i i i)"を共通のオノマトペとして入力し、3つの音響イベント"whistle3", "shaver", "tear"によりそれぞれ条件付けして合成したときの例を以下に示す。

条件付けなし "whistle3"で条件付け
"shaver"で条件付け "tear"で条件付け

条件付けを行わないと、つねに"shaver"の音が合成されてしまう。条件付けを行うことで、たとえオノマトペは共通でも、音響イベントに対応して環境音が合成されていることが分かる。

ほか著者のデモンストレーションページを参考に、いくつかのオノマトペに加えて条件付けのあり/なしで変化する合成音を示しておく。

条件付けなし
"maracas"で条件付け "coffmill"で条件付け


  • チィッ (/ ch i: q /)
条件付けなし
"cup1"で条件付け "shaver"で条件付け
"whislte3"で条件付け "tear"で条件付け


  • ボンッ (/ b o N q /)
条件付けなし
"drum"で条件付け "trashbox"で条件付け


  • リンリン (/ r i N r i N /)
条件付けなし
"bells5"で条件付け "clock"で条件付け

実装の舞台裏とかTIPS

  • オノマトペ(英字)は数値列に変換したうえでエンコーダに入力する。モデル入力前にone-hotベクトルの系列へとexplicitに変換しておく必要はない。エンコーダ内部でnn.Embeddingにより埋め込み表現が得られるという実装上の仕掛けがあり、その埋め込み表現はone-hotベクトルと重み行列との行列積を計算した結果に相当する。これがいわゆる「lookup table」とか「lookup embedding」などと呼ばれるテクニックである。とにかく必要なのはone-hotベクトルと一意に対応する数値表現である。オノマトペ(英字)ならばそれは各音素記号に通し番号を割り振ること、つまりone-hotベクトルの何次元目を1にするか、ということに対応する。音素記号に通し番号を割り振るため、変換用の辞書を用意するクラス(MappingDict)を実装した。ちなみに音響イベントラベルの埋め込みについては、エンコーダの最終フレームにおけるhidden stateと結合したうえでデコーダに入力する必要があるので、nn.Embeddingは使わずに事前にone-hotベクトル化しておく実装を採用した(nn.functional.one_hot 関数)。

  • 訓練時にはscheduled samplingを導入している。本来、seq2seqのモデル構造としては直前の時刻におけるデコーダ出力を、当該時刻のデコーダ入力に用いるのが正しい。しかし訓練初期におけるデコーダ出力はtargetからの乖離が大きく、それをさらに次時刻のデコーダ入力に用いれば訓練誤差がさらに拡大されるため、訓練誤差の収束には時間がかかる。このような訓練の困難さを緩和し、早期の誤差収束による効率化を図るため、直前時刻のtarget(教師データ)をデコーダ入力に用いる手法をteacher forcingと呼んでいる。しかしながら、teacher forcingを適用すると、デコーダの訓練時と推論時で入力データの分布が食い違ってしまうため、デコーダの予測精度がかえって低下してしまうおそれがある。これがいわゆるexposure biasと呼ばれる問題点である。そこでさらにexposure biasを緩和すべく、各時刻でtargetをデコーダ入力に利用するか否かを確率的に決定する手法が提案されており、これをscheduled samplingと呼んでいる。その率は論文中に0.6という記載があり、つまり60%の確率でtargetをデコーダ入力に使うことを意味する。実装では[0, 1)の範囲で乱数を発生し、0.6を下回ったらtargetをデコーダ入力にする、といった具合である。またデコーダが出力したテンソルを次時刻の入力に利用する際には、あらかじめ当該のテンソルdetach関数を適用し、計算グラフから切り離したうえで入力する必要があることも実装上は注意すべき点である。

  • 系列長の異なるデータをミニバッチにまとめて訓練を行うためには、系列に沿ったパディングを施して見かけの系列長を揃える必要がある(オノマトペおよびスペクトログラムそれぞれに対して)。筆者は以前、ミニバッチ構成前つまりDataSet作成時の前処理の一部として、パディングを全データに対して一斉に行っていた。しかしながらデータセット中の最大系列長にパディングを合わせることになるため、パディングの無駄が多くミニバッチ学習時の計算コストが不必要に大きくなる。つまり系列方向に計算グラフが不必要に伸びるため、forward / backwardの計算時間とメモリ消費量が増大してしまう。そこでパディングをDataSet作成時に行うのではなく、DataLoaderにおけるミニバッチ作成時に都度行うようにした。ミニバッチ内の最大系列長に合わせることでパディングの無駄が減り、訓練時間の削減と効率化が望める。そのために大いに役に立ったのがPyTorchにおけるcollate_fnの仕組みであり、本実装ではこれを自作して用いている。

  • スペクトログラムにパディングを施した箇所を損失関数の計算から除外するためにマスクをかけている。マスク作成のための関数はttslearnの実装を使った(ttslearn/util.py)。本記事の実装ではdataset.py内で借用した。同スクリプト内ではライセンス表記も配慮している(ttslearn作者のYamamoto氏のコピーライトを併記)。マスク作成時にはミニバッチを構成する各スペクトログラムのフレーム長のリストが必要なので、上記collate_fnの内部でリストを作成するようにした。

  • モデルの損失関数にL1 lossを適用することは効果的である。今回は音響特徴量が対数振幅スペクトログラムなので、L1 lossで十分に動くということだろう。初代TacotronでもL1 lossが使われていた実績があるため、その周辺で探せば損失関数に関連した議論が見つかるかもしれない。

  • デコーダに入力するスペクトログラムの先頭フレームには"<BOS>"という特別な記号を埋め込んでいる。その埋め込み表現を得るためのクラスがBosEmbeddingである。このクラスの初期化時にnn.Embeddingインスタンス化しているが、引数のpadding_idxに"<BOS>"に相当するものを指定する実装を採用した。ゆえに当該の埋め込みベクトル(テンソル型オブジェクト)における各要素は常に0からなる。要素が全て0からなるテンソルを手に入れるだけならば、クラスという大道具を持ち出す必要はないかもしれないが、コードがスッキリするメリットを優先した。常に0からなる埋め込みベクトルを得るクラスなので、訓練の対象にはならず、パラメータの保存対象にもならない。

  • 推論時、1フレームごとにデコーダを駆動してスペクトログラムを逐次生成しているが、その生成回数(駆動回数)は経験的に与える必要がある。generator.pyではtargetとなるオーディオデータのフレーム数を参照して決めている。今回のモデルにはTacotron2のようなstop tokenを計算する仕組みは組み込まれていないので、デコーダの駆動回数にヒューリスティックな上限値を設けておくのは一つの妥協案である。例えば入力オノマトペの音素系列長を定数倍した値により出力スペクトログラムの最大フレーム数を決定する、などの便法が考えられる。

  • RWCP-SSDの各オーディオデータには、少なからぬ「無音区間」(振幅がほぼゼロとなる区間)が環境音の前後に存在している。ゆえに訓練前にある程度除去しておくのが望ましい。「無音」を表現するオノマトペは存在しないため、除去しなければモデル訓練に好ましくない影響を及ぼすおそれがある。ちなみに無音区間の除去にはlibrosaのtrimメソッドを用いた。このメソッドでは無音判定のしきい値をdBで与えるのだが(top_dbという引数)、この値を小さく設定しすぎると環境音のアタックやリリースの部分まで削ってしまうので、気をつけなければならない(特にcupやbellは振幅の小さいリリース区間が長めなので注意)。

  • スペクトログラムは振幅を線形スケールから対数スケールに変換している。対数スケールへの変換にはlibrosaのamplitude_to_dbメソッドを用いた。そのうえで訓練前に事前に標準化を施した。実装ではscikit-learnのStandardScalerを用いた。デコーダ出力から波形に戻す前にはinverse_transformメソッドによりスペクトログラムのscaleを戻す必要があり、さらにlibrosaのdb_to_amplitudeメソッドを用いて線形スケールへと変換しておく。

  • スペクトログラムから波形に戻す際にはlibrosaのgriffinlimメソッドを用いた。

  • エポック数は300〜400程度あれば十分、つまり訓練誤差がほぼ収束するという感触を得ている。

  • 学習率は0.0001程度で十分な気がする。本記事の再現実験ではMultiStepLRにより学習率をスケジューリングした。

  • ミニバッチのサイズは論文にもあるとおり、5程度が適切のようだ。サイズを15や20に増やせば訓練時間は短縮されるが、推論時にモデルから合成される音がノイズのようになってしまうことがあった。

  • 実験条件の設定と管理にはHydraパッケージを利用した。yamlファイルには実験条件を構造化して書いておくことができるが、今回の実装では論文の追実験が目的なので当該のyamlを読み出す程度にしかHydraを活用しなかった。参考文献『Pythonで学ぶ音声合成』にはもっとカッコいい使い方が紹介されており、いつか試してみたい。

おわりに

本記事ではOnoma-to-WaveのPyTorch実装を紹介した。オノマトペからの環境音合成には無事に成功したので安心した。個人的にはlookup embeddingによる埋め込み表現の取得、collate_fn活用によるデータローダ作成、パディングの実施、損失関数計算時のマスク作成が特に勉強になった。

今後はKanaWaveのようにGUIをつけたうえで環境音合成を実施してみたい。PySimpleGUIなどGUIに特化したモジュールを用いたスタンドアロンなアプリや、クライアントサーバーモデルに基づくウェブアプリケーションの実装も考えられる。GUIプログラムへの組み込みを容易にするためにはOnoma-to-Waveのモジュール化もしておきたいが(pyonoma?)、かなりのリファクタリングが必要だろう。

合成可能な音のバリエーションを増やすことも今後の課題だろう。単純に合成可能な音響イベントの数を増やすならば、RWCP-SSDのオーディオデータ全体を用いてモデルを訓練すればよいが、オリジナル論文のネットワークアーキテクチャでどこまで合成音のクオリティがキープできるかは未知数である。アーキテクチャの吟味も必要になってくるだろう。もし仮にモデル訓練が成功裏に終われば、pre-trainedモデルとして公開し、上記のpyonomaモジュールからロードして使うことも視野に入ってくる。合成音のバリエーションの増やし方はいくつか考えられ、検討の余地は大いに残されている。

しかしながら現状はどれも工数が足りていない。そのうち、やれたらやる。

最後に、Onoma-to-Waveの論文著者各位に深い感謝の意を表したい。

参考文献

Seq2Seqの勉強に『ゼロから作るDeep Learning ❷ ――自然言語処理編』は大いに役立った。

www.oreilly.co.jp

『深層学習による自然言語処理』も参考になる。

www.kspub.co.jp

PyTorchによるSeq2Seqの実装は以下のリポジトリが参考になった。

github.com

その他、実装に関しては『Pythonで学ぶ音声合成』も大いに役立った。この本は熟読すべき。

r9y9.github.io

collate_fnの自作には以下の記事が参考になった。

qiita.com