はじめに
以前、Onoma-to-Waveを実装した記事を書いたことがあった:
Onoma-to-Waveとはオノマトペ(文字列)を環境音(スペクトログラム)に変換するモデルである。RNNに基づくencoderとdecoderから構成されており、いわゆるSequence-to-Sequence(Seq2Seq)の構造を持つ。 それらをTransformerによって置き換えたモデルが、Onoma-to-Waveの著者らによって実はすでに提案されている。
岡本 悠希,井本 桂右,高道 慎之介,福森 隆寛,山下 洋一,"Transformerを用いたオノマトペからの環境音合成," 日本音響学会2021年秋季研究発表会,pp. 943-946,2021.
上記は国内学会における査読なしの予稿論文であり、2022年10月時点でTransformerに基づくモデルの提案自体は査読付き論文として出版されていない。ただし、著者らによる以下の論文では、本モデルを含む形で主観評価実験が実施されている。
著者による環境音合成のデモンストレーションは以下から試聴できる。
モデルの全容を把握するには以下のPDFファイルが役に立つだろう。
Transformer版Onoma-to-Waveの実装は2022年の本記事執筆時点では公開されていないため、自分で実装して環境音合成の再現実験を試みたというわけである。
事前準備
データセットは冒頭に示した記事と同様に、RWCP 実環境音声・音響データベース (Real World Computing Partnership-Sound Scene Database; RWCP-SSD) を利用した。
実装の概要
前回と同様にフォルダを分けている。
フォルダ名 | 説明 |
---|---|
unconditional |
音響イベントによる条件づけなし(オノマトペのみを入力) |
conditional |
音響イベントによる条件づけあり(オノマトペと音響イベントラベルを入力) |
それぞれのフォルダには以下の主要なスクリプトたちを用意した。
ファイル名 | 機能 |
---|---|
preprocess.py | 各種の前処理を実施するスクリプト |
training.py | モデルの訓練を実施するスクリプト |
inference.py | 訓練済のモデルを用いて環境音を合成するスクリプト(訓練に用いなかったテストデータを対象とした合成) |
synthesis.py | 訓練済のモデルを用いて環境音を合成するスクリプト |
preprocess.pyでは前回記事で紹介した前処理に加えて、今回は音響特徴量をオーディオデータから抽出して保存する処理を含んでいる。その理由については本記事の最後で述べているので、参考まで。
synthesis.pyではDataset, DataLoaderが使われず、またオノマトペを音素文字列として自由に与えることができる点に、inference.pyとの違いがある。例えば、オノマトペが"ビイイイイ"(/b i i i i i/)なら "b i i i i i" のように与えることができる、といった具合である。手元で色々とオノマトペを変えながら環境音合成を試す際にはsynthesis.pyのほうが便利である。
各種の設定は前回記事に引き続きyamlファイル(config.yaml)に記述し、それを読み込んで使う形とした。したがってコードの動かし方は、
- config.yamlの編集
- preprocess.py による前処理の実施
- training.pyによるモデル訓練の実施
- inference.py / synthesis.pyによる環境音合成の実施
となる。preprocess.pyは一度だけ動かせばよい。
モデルの条件付けに必要な音響イベントラベルの埋め込みにはいくつかの選択肢が考えられるが、本記事では以下の埋め込み方法で得られた結果を紹介する。
memory(エンコーダ出力)とイベントラベルを結合したのちに線形変換する
『RWCP 音声・音響データベースを用いた環境音・効果音合成の検討とオノマトペ拡張データセットの構築』では、 これ以外に「デコーダ入力とイベントラベルを結合し、線形変換する」ことでイベントラベルを埋め込む方法を併せて採用している。 しかしながら本記事に先立つ予備実験の結果から、memoryに埋め込むだけで環境音の合成にとって十分な効果をもたらすことを確認している。 ちなみに今回の実装ではどちらの埋め込み方法も選択できる(両方ON、両方OFF、どちらか片方のみONという切り替えが可能)。
環境音合成実験
実験条件
RWCP-SSDから以下の10種類の環境音を実験に採用した(カッコ内はデータベースにおける識別子)。
- 金属製のゴミ箱を叩いたときの音(trashbox)
- カップを叩いたときの音(cup1)
- ベルを鳴らしたときの音(bells5)
- 紙を破るときの音(tear)
- ドラムを叩いたときの音(drum)
- マラカスを振ったときの音(maracas)
- ホイッスルを吹いたときの音(whistle3)
- コーヒー豆をミルで挽いたときの音(coffmill)
- 電気シェーバーを動かしたときの音(shaver)
- 目覚まし時計の音(clock1)
各音響イベントにつき95個のオーディオファイルを訓練に用いた。さらに各オーディオファイルに付随するオノマトペを15個ランダムに選択して訓練に用いた。
記事が長くなるので、実験条件の詳細は折りたたみ形式で示す。ネットワークの設定など、どうしても気になるひとだけクリック。
実験条件を表示する
計算機環境を示す。
計算機環境 | バージョンなど |
---|---|
OS | Ubuntu 22.04 |
CPU | Intel i9-9900K |
GPU | RTX2070 |
Python | 3.10.6 |
PyTorch | 1.12 |
訓練の基本的な設定は以下の通りである。
項目 | 設定 |
---|---|
ミニバッチサイズ | 32 |
エポック数 | Seq2SeqTransformer は1500 Mel2linear は1000 |
学習率 | Seq2SeqTransformer は0.0003 Mel2linear は0.0001 |
勾配クリッピングのしきい値 | 1.0 |
なおTransformerの訓練には"Attention is All You Need"(アテンションしか勝たん)の論文で採用された学習率スケジューリングを採用した。warm-upのエポックは300とした。Mel2linear
はスケジューリングなし。
音響特徴量の設定は以下の通りである。
項目 | 設定 |
---|---|
音響特徴量 | 80次元のメルスペクトログラム |
FFTの窓長 | 2048 |
フレーム長 | 2048 |
フレームシフト | 512 |
続いてTransformerまわりの設定を示す。
設定項目 | 設定値 |
---|---|
埋め込み次元 | 512 |
Multi-head attentionのヘッド数 | 4 |
オプティマイザ | RAdam |
エンコーダの層数 | 3 |
デコーダの層数 | 3 |
Position-wise Feed-Forward Networkの次元数 | 1536 |
Activation function | ReLU |
Dropout率 | 0.1 |
Scaled Positional EncodingについてはDropout率を0.1に設定した。
Encoder Prenetの設定は以下の通りである。1次元のCNNを3層重ねている。
項目 | 設定 |
---|---|
埋め込み次元 | 512 |
1d-CNNのチャネル数 | 512 |
1d-CNNのカーネルサイズ | 3 |
1d-CNNの層数 | 3 |
活性化関数 | ReLU |
Dropout率 | 0.5 |
Decoder Prenetは以下の通りである。全結合層が2層からなる。また訓練時及び推論時で常にDropoutを有効にしている。 これにより、合成音の多様性を確保する狙いがある。
項目 | 設定 |
---|---|
埋め込み次元 | 512 |
層数 | 2 |
活性化関数 | ReLU |
Dropout率 | 0.5 |
Postnetの設定は以下の通りである。
項目 | 設定 |
---|---|
埋め込み次元 | 512 |
1d-CNNのチャネル数 | 512 |
1d-CNNのカーネルサイズ | 5 |
1d-CNNの層数 | 5 |
活性化関数 | tanh |
Dropout率 | 0.5 |
CBHGの設定は以下の通りである。
項目 | 設定 |
---|---|
畳み込みネットのバンク数 | 8 |
Highway Netの層数 | 4 |
プロジェクションの次元数 | 512 |
Dropout率 | 0.5 |
実験結果
音響イベントによる条件付けを行わない場合、すなわちオノマトペのみが入力の場合、異なるクラス(音響イベント)の音として合成されたり、他のクラスの音が冒頭に混じるなど、合成失敗となるケースが多く確認された。しかしながら条件付けを行うことで、クラスの食い違いのない合成音が得られた。
- bells5(オノマトペ: リンリーン 音素列: / r i N r i: N /)
条件付けなし | 条件付けあり |
---|---|
- clock1(オノマトペ: チリリリリ 音素列: / ch i r i r i r i r i /)
条件付けなし | 条件付けあり |
---|---|
- coffmill(オノマトペ: ガサガサ 音素列: / g a s a g a s a /)
条件付けなし | 条件付けあり |
---|---|
- cup1(オノマトペ: チィンッ 音素列: / ch i: N q /)
条件付けなし | 条件付けあり |
---|---|
- drum(オノマトペ: ポンッ 音素列: / p o N q /)
条件付けなし | 条件付けあり |
---|---|
- maracas(オノマトペ: シャカ 音素列: / sh a k a /)
条件付けなし | 条件付けあり |
---|---|
- shaver(オノマトペ: ビービー 音素列: / b i: b i: /)
条件付けなし | 条件付けあり |
---|---|
- tear(オノマトペ: スウィイイイイン 音素列: / s u w i i i i i N /)
条件付けなし | 条件付けあり |
---|---|
- trashbox(オノマトペ: ポーン 音素列: / p o: N /)
条件付けなし | 条件付けあり |
---|---|
- whistle3(オノマトペ: ピイッ 音素列: / p i i q /)
条件付けなし | 条件付けあり |
---|---|
続いて、共通のオノマトペを与えて、条件付けのクラスを色々と変えた場合の合成例を示す。ここでSeq2Seqとは前回Onoma-to-Waveによる合成法を指す。
- オノマトペ: ビイイイイイ (/ b i i i i i i /)
Seq2Seq | Transformer |
---|---|
"whistle3"で条件付け | "whistle3"で条件付け |
"shaver"で条件付け | "shaver"で条件付け |
"tear"で条件付け | "tear"で条件付け |
- オノマトペ: スャリスャリ (/ sh a r i sh a r i /)
Seq2Seq | Transformer |
---|---|
"maracas"で条件付け | "maracas"で条件付け |
"coffmill"で条件付け | "coffmill"で条件付け |
- オノマトペ: チィッ (/ ch i: q /)
Seq2Seq | Transformer |
---|---|
"cup1"で条件付け | "cup1"で条件付け |
"whislte3"で条件付け | "whislte3"で条件付け |
"shaver"で条件付け | "shaver"で条件付け |
- オノマトペ: ボンッ (/ b o N q /)
Seq2Seq | Transformer |
---|---|
"drum"で条件付け | "drum"で条件付け |
"transhbox"で条件付け | "trashbox"で条件付け |
- オノマトペ: リンリン (/ r i N r i N /)
Seq2Seq | Transformer |
---|---|
"bells5"で条件付け | "bells5"で条件付け |
"clock1"で条件付け | "clock1"で条件付け |
実装の舞台裏など
- モデルのEncoder Prenet、Decoder Prenet、Postnetの実装にあたり、Ryuichi Yamamoto氏が制作したttslearnを大いに参考にした。多少の修正を加えたが、ほぼそのままadaptした。ttslearnにかかるコピーライト表記はもちろん忘れずに行っている。
- Transformerの実装はPyTorch公式のモジュールを用いた。そのモジュールまわりで最初に戸惑うのは「マスク」の与え方である。今回はsource側の入力にオノマトペ(文字列)、target側の入力にメルスペクトログラム(スペクトル列)という構成であり、系列データに対するベーシックな定式化を踏襲した。このとき、source self-attentionのためのマスク(src_mask)は
None
、source-target attentionのためのマスク(memory_mask)もNone
でよい。つまりattentionの計算に関して特定のsource入力を除外するマスクは必要としない。target self-attentionのためのマスク(tgt_mask)はいわゆる先読みを防ぐ階段状のマスクを与えている。ほか、ミニバッチ内のオノマトペ長やフレーム長は保持しておき、パディングに応じたマスク(src_key_padding_mask, tgt_key_padding_mask, memory_key_padding_mask)を与えている。 - preprocess.pyに特徴量抽出処理を追加した理由:前回記事のOnoma-to-Waveの実装では、モデル訓練の際、Dataset構築時に音響特徴量の抽出処理を毎回やり直していた。しかしながら、訓練の度に同じ処理を繰り返すのは無駄が大きく、またバグの温床になるため改善が必要と考えた。そこで、抽出処理をDataset構築時に行うのでなく、preprocess.pyの中で一度だけ行う方式に変更した。以降の訓練では抽出済の特徴量を毎回ロードするだけでよい。ただ、毎回抽出し直すにせよ、事前に抽出してロードするにせよ、特徴量全体がメモリに乗ることを暗黙の前提としている(今回は130MBほど)。なお前処理に関する議論は以下のスライドを参考にするとよい(15枚目あたり)。
- 補助的に作成したモジュールたちは以下の通りである。
ファイル名 | 機能 |
---|---|
dataset.py | データセットのクラスOnomatopoeiaDataset を定義 |
mapping_dict.py | オノマトペ(英字)の列と数値列を相互に変換する辞書を与えるクラスMappingDict を定義 |
models.py | オノマトペから対数メルスペクトログラムに変換するSeq2SeqTransformer クラス、対数メルスペクトログラムから対数リニア(スケール)スペクトログラムに変換するMel2Linear クラスを定義 |
module.py | Seq2SeqTransformer クラスとMel2Linear クラスを定義するのに必要なモジュール群(クラス)を定義 |
scheduler.py | Seq2SeqTransformer クラスで利用可能な、学習率を調整するスケジューラーのクラスTransformerLR を定義 |
trainer.py | ミニバッチによるモデル訓練のループを回し、また訓練済みのモデルを用いて環境音を生成するクラスTrainer を定義 |
util.py | マスク作成や音響特徴量抽出など補助的な関数群を定義 |
Seq2SeqTransformer
というクラス名はPyTorchのチュートリアル由来である。せっかくなら今回の環境音合成に特化したクラス名が良いと考え、Onoma2WaveTransformer
やOnoma2Mel
といった代案を考えてみたが、どうもしっくりこなかった。前者はクラス名が長すぎる気がしており、後者は具体的な処理内容(オノマトペから対数メルスペクトログラムへの変換)が反映され悪くないが、Transformerを使っている感が薄れる気がした。Seq2SeqTransformer
クラスとMel2Linear
クラスは独立に訓練させるため、それぞれ専用のTrainerクラスを用意して別々のモジュール(スクリプト)にすることも実装当初に考えた。しかし、モジュールをむやみに増やすのはあまり美しくない設計と考え、両クラスの訓練と推論をひとつのTrainer
クラスに統合したのである。それぞれの訓練のON/OFFはconfigファイルで切り替える設計とした。TransformerLR
の実装は以下の記事で紹介したものである。実は本記事の布石であった。
おわりに
実装を通じて、Transformerの気持ちが少し分かった気がする。特にself-attentionやMulti-head attentionまわりはマスクの使い方を含めて勉強になった。やはり実装は理解への近道。