Pythonによる日本語チャットボットの構築 in 2022 〜ChatterBotインストール編〜

はじめに

Pythonでチャットボットを作りたくなった。

ChatterBotが便利に使えそうだということで、インストールログを残しておく。

動作環境

  • Python3.9.13
  • macOS BigSur 11.6

インストール

まず仮想環境を準備する。

python3 -m venv chatbot

chatbotディレクトリができるので、移動する。その後、binディレクトリにactivateコマンドがあるので、sourceで仮想環境を起動する。

cd chatbot
source bin/activate

以下の手順を実行する。

git clone  https://github.com/feignbird/ChatterBot-spacy_fixed
pip3 install ./ChatterBot-spacy_fixed
pip3 install chatterbot-corpus
pip3 uninstall pyYAML
pip3 install pyYAML==5.3.1
python -m spacy download en_core_web_sm

pip3 install chatterbot-corpus実行時、chatterbot-corpusが要求するpyyamlのバージョンが低く、一時的にpyyaml(実行時はver 5.3.1)がアンインストールされ、ver 3.13がインストールされる。以下はその際のメッセージ抜粋。

Installing collected packages: PyYAML, chatterbot-corpus
  Attempting uninstall: PyYAML
    Found existing installation: PyYAML 5.3.1
    Uninstalling PyYAML-5.3.1:
      Successfully uninstalled PyYAML-5.3.1
  Running setup.py install for PyYAML ... done
ERROR: pip's dependency resolver does not currently take into account all the packages that are installed. This behaviour is the source of the following dependency conflicts.
chatterbot 1.1.0a7 requires pyyaml<5.4,>=5.3, but you have pyyaml 3.13 which is incompatible.
Successfully installed PyYAML-3.13 chatterbot-corpus-1.2.0

しかしながら、これはchatterbot-corpusのメンテがほぼされていないためであって、pyyamlは新しくしてもOKなのである。 そこで古いpyyamlをアンインストールし、改めて新しいものを入れるわけである。

以上のコマンド群が走れば、あとは日本語用のコーパス(会話テンプレート)をセットすれば完了する。

chatterbot-corpusのリポジトリをzipでダウンロードする。 github.com

ダウンロードされたファイルはchatterbot-corpus-master.zipである。 これを適当なところで解凍する。そして chatterbot_corpus-master/chatterbot_corpus/data/から、japaneseディレクトリをコピーする。 その後、venvの仮想環境フォルダ(chatbot)以下の chatbot/lib/python3.9/site-packages/chatterbot_corpus/data/ にコピーする。

テスト

from chatterbot import ChatBot
from chatterbot.trainers import ChatterBotCorpusTrainer

chatbot = ChatBot('test')
trainer = ChatterBotCorpusTrainer(chatbot)
trainer.train("chatterbot.corpus.japanese")
response = chatbot.get_response("こんにちは")
print(response)

これを適当な名前で保存する(test_chat.pyとか)。

そして実行する。

python3 test_chat.py

すると以下の実行結果が得られる。

Training ai.yml: [####################] 100%
Training botprofile.yml: [####################] 100%
Training computers.yml: [####################] 100%
Training conversations.yml: [####################] 100%
Training emotion.yml: [####################] 100%
Training food.yml: [####################] 100%
Training gossip.yml: [####################] 100%
Training greetings.yml: [####################] 100%
Training health.yml: [####################] 100%
Training history.yml: [####################] 100%
Training humor.yml: [####################] 100%
Training literature.yml: [####################] 100%
Training money.yml: [####################] 100%
Training movies.yml: [####################] 100%
Training politics.yml: [####################] 100%
Training psychology.yml: [####################] 100%
Training science.yml: [####################] 100%
Training sports.yml: [####################] 100%
Training trivia.yml: [####################] 100%
お元気ですか?

最後の「お元気ですか?」の部分はランダムに変わる(「こんにちは」に変わったりなど)。その返答の候補たちはgreetings.yamlに書いてある。つまりこれらのyamlファイルを調整することで返答は作り込める。

さらに進んで

上記のテストスクリプトは実行のたびにtraining(訓練)が走るので、非効率である。しかしながらchatbotを「保存」するということはできない。厳密には保存という概念ではなく、データベースの「更新」なのである。これがChatterBotの仕様である。

テストスクリプトを実行した後、db.sqlite3というファイルが保存されていることに気づく。これが先ほど訓練したchatbotのデータベースである。ファイル名はデフォルトの状態であり、例えば以下のようにすれば保存ファイルを変更できる。

from chatterbot import ChatBot
from chatterbot.trainers import ChatterBotCorpusTrainer
 
chatbot = ChatBot(
    'test',
    database_uri='sqlite:///database.db'
)

trainer = ChatterBotCorpusTrainer(chatbot)
trainer.train("chatterbot.corpus.japanese")

データベース名はdatabase.dbである。このデータベースを再度読み込んでchatbotを構築する際には、

from chatterbot import ChatBot
 
chatbot = ChatBot(
    'test',
    database_uri='sqlite:///database.db',
    # read_only=True  # 読み込み専用、上書きしないときにはこれを指定する
)

response = chatbot.get_response("こんにちは")
print(response)

を使う。データベースを更新済なので、再度の訓練は不要である。当然、新たにyamlファイルを更新すれば、訓練してデータベースを更新する必要はある。

ChatterBotにはまだいくつか訓練するためのクラスが存在しており、必ずしもyamlの編集・更新が必要というわけではないらしい。

おわりに

ひとまずインストールは完了した。

ChatterBotを「賢く」するには、何らかの方法でデータベースを更新する必要がある(自動・半自動・手動)。 それはまた記事を改めて書くことにする。

参考

インストール全般はこちらのスレッドを参考にした。 github.com

言語モデルを導入して賢くしていくには、以下の記事が参考になろう。 zerofromlight.com

Transformerの学習率を調整するSchedulerクラスをPyTorchで書いた

はじめに

Attention is All You Needという論文で「warmup & ステップ数の逆平方根で学習率を減衰」させる学習率スケジューリングが提案されたが、そのようなスケジューリングを手軽に行うスケジューラを書いたということである。

ソースコード

from torch.optim.lr_scheduler import _LRScheduler

class TransformerLR(_LRScheduler):
    """TransformerLR class for adjustment of learning rate.

    The scheduling is based on the method proposed in 'Attention is All You Need'.
    """

    def __init__(self, optimizer, warmup_epochs=1000, last_epoch=-1, verbose=False):
        """Initialize class."""
        self.warmup_epochs = warmup_epochs
        self.normalize = self.warmup_epochs**0.5
        super().__init__(optimizer, last_epoch, verbose)

    def get_lr(self):
        """Return adjusted learning rate."""
        step = self.last_epoch + 1
        scale = self.normalize * min(step**-0.5, step * self.warmup_epochs**-1.5)
        return [base_lr * scale for base_lr in self.base_lrs]

学習率のスケジューラをオリジナルで書きたい場合は、_LRSchedulerを継承し、get_lr関数を自作するのが良い。PyTorch本家がそのような実装となっている。

github.com

実装において、min(step**-0.5, step * self.warmup_epochs**-1.5)の部分が学習率調整の本質である。なおself.normalizeは値の範囲を0から1に収めるための正規化定数の役割を果たす。 これらの積によりscaleが計算され、それをoptimizerの構築時に指定した学習率に掛けることで学習率が更新される。

注意すべきは、上記の実装はepoch単位で学習率を調整することが前提になっているという点である (step = self.last_epoch + 1というあたり)。

学習の初期段階では0に近い係数(scale)が学習率に掛けられるが、徐々に増えていき、warmup_epochsまでエポック数が達すると係数は1で最大となる。 その後は係数が1より小さくなるため、学習率が減衰されていく。

参考文献

arxiv.org

オノマトペ(擬音語)から環境音を合成するニューラルネットワーク(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

プログレスバーを表示するためのPythonライブラリ progressbar2を使ってみた

はじめに

Yoshipon氏の以下のツイートからprogressbar2の存在を知ったので、使ってみたということ。

インストール

pipでインストール可能。

progressbar2 · PyPI

テスト

まずは、こんなスクリプトを書いてみた。

# -*- coding: utf-8 -*-

from time import sleep

from progressbar import progressbar as prg

for i in prg(range(10), prefix="A demonstration", suffix="\n"):
    sleep(0.1)
    print(i)

ターミナル出力はこんな感じ(アニメーションを用意したい!)。

A demonstration  0% (0 of 10) |                                                                           | Elapsed Time: 0:00:00 ETA:  --:--:--
0
A demonstration 10% (1 of 10) |#######                                                                    | Elapsed Time: 0:00:00 ETA:   0:00:00
1
A demonstration 20% (2 of 10) |###############                                                            | Elapsed Time: 0:00:00 ETA:   0:00:00
2
A demonstration 30% (3 of 10) |######################                                                     | Elapsed Time: 0:00:00 ETA:   0:00:00
3
A demonstration 40% (4 of 10) |##############################                                             | Elapsed Time: 0:00:00 ETA:   0:00:00
4
A demonstration 50% (5 of 10) |#####################################                                      | Elapsed Time: 0:00:00 ETA:   0:00:00
5
A demonstration 60% (6 of 10) |#############################################                              | Elapsed Time: 0:00:00 ETA:   0:00:00
6
A demonstration 70% (7 of 10) |####################################################                       | Elapsed Time: 0:00:00 ETA:   0:00:00
7
A demonstration 80% (8 of 10) |############################################################               | Elapsed Time: 0:00:00 ETA:   0:00:00
8
A demonstration 90% (9 of 10) |###################################################################        | Elapsed Time: 0:00:00 ETA:   0:00:00
9
A demonstration100% (10 of 10) |##########################################################################| Elapsed Time: 0:00:01 Time:  0:00:01

シェルのリダイレクトを活用してテキストファイルにログとして残す場合はちょっと注意が必要である。 例えば、上記のスクリプトをdemo.pyとして保存して実行するならば、

python3 demo.py > log.txt 2>&1

と行った具合に標準出力と標準エラー出力を束ねるわけである。progressbarは標準エラー出力に向いているので。

redirect_stdout=Falseのとき(デフォルト)、log.txtの中身は

A demonstration  0% (0 of 10) |          | Elapsed Time: 0:00:00 ETA:  --:--:--
A demonstration 10% (1 of 10) |#         | Elapsed Time: 0:00:00 ETA:   0:00:00
A demonstration 20% (2 of 10) |##        | Elapsed Time: 0:00:00 ETA:   0:00:00
A demonstration 30% (3 of 10) |###       | Elapsed Time: 0:00:00 ETA:   0:00:00
A demonstration 40% (4 of 10) |####      | Elapsed Time: 0:00:00 ETA:   0:00:00
A demonstration 50% (5 of 10) |#####     | Elapsed Time: 0:00:00 ETA:   0:00:00
A demonstration 60% (6 of 10) |######    | Elapsed Time: 0:00:00 ETA:   0:00:00
A demonstration 70% (7 of 10) |#######   | Elapsed Time: 0:00:00 ETA:   0:00:00
A demonstration 80% (8 of 10) |########  | Elapsed Time: 0:00:00 ETA:   0:00:00
A demonstration 90% (9 of 10) |######### | Elapsed Time: 0:00:00 ETA:   0:00:00
A demonstration100% (10 of 10) |#########| Elapsed Time: 0:00:01 Time:  0:00:01
0
1
2
3
4
5
6
7
8
9

となる(標準エラー出力と標準出力が分離しているものをリダイレクトで束ねたので、こういう見かけ)。

redirect_stdout=Trueのときは、スクリプトはこうなる(デフォルトではFalseなので明示的にTrueにする)。

# -*- coding: utf-8 -*-

from time import sleep

from progressbar import progressbar as prg

for i in prg(range(10), prefix="A demonstration", suffix="\n", redirect_stdout=True):
    sleep(0.1)
    print(i)

その際、log.txtの中身は

A demonstration  0% (0 of 10) |                                                                           | Elapsed Time: 0:00:00 ETA:  --:--:--
0                                                                                                                                                
A demonstration  0% (0 of 10) |                                                                           | Elapsed Time: 0:00:00 ETA:  --:--:--
1                                                                                                                                                
A demonstration 10% (1 of 10) |#######                                                                    | Elapsed Time: 0:00:00 ETA:   0:00:01
2                                                                                                                                                
A demonstration 20% (2 of 10) |###############                                                            | Elapsed Time: 0:00:00 ETA:   0:00:01
3                                                                                                                                                
A demonstration 30% (3 of 10) |######################                                                     | Elapsed Time: 0:00:00 ETA:   0:00:00
4                                                                                                                                                
A demonstration 40% (4 of 10) |##############################                                             | Elapsed Time: 0:00:00 ETA:   0:00:00
5                                                                                                                                                
A demonstration 50% (5 of 10) |#####################################                                      | Elapsed Time: 0:00:00 ETA:   0:00:00
6                                                                                                                                                
A demonstration 60% (6 of 10) |#############################################                              | Elapsed Time: 0:00:00 ETA:   0:00:00
7                                                                                                                                                
A demonstration 70% (7 of 10) |####################################################                       | Elapsed Time: 0:00:00 ETA:   0:00:00
8                                                                                                                                                
A demonstration 80% (8 of 10) |############################################################               | Elapsed Time: 0:00:00 ETA:   0:00:00
9                                                                                                                                                
A demonstration 90% (9 of 10) |###################################################################        | Elapsed Time: 0:00:01 ETA:   0:00:00
A demonstration100% (10 of 10) |##########################################################################| Elapsed Time: 0:00:01 Time:  0:00:01

となっており、 冒頭に示したターミナル上での実行と見かけは一緒になる(それはそう)。

深層学習系のスクリプトを回す時、エポック毎のlossを表示させながらprogressbarも出したい時、便利ではなかろうか。tqdmだとテキストファイルに出力したときに表示が崩れるので。

以上。

Hydraのtutorialに従って書いたらpylintに怒られたので修正した話

最近Hydraを使い始めた。tutorialに従えばひとまず使えるようにはなる。 例えば以下のyamlファイルをconfig.yamlとして保存したとする(ニューラルネット的なconfig)。

model:
  hidden_dim: 128
  latent_dim: 32
  output_dim: 256
training:
  epoch: 100
  batch_size: 16

次にconfig.yamlと同じディレクトリに、以下のexample.pyを保存しておくとする。

# -*- coding: utf-8 -*-
"""Hydra example.

Copyright (C) 2022 by ballforest

MIT licence.
"""

from omegaconf import DictConfig

import hydra


# config_pathはyamlファイルの場所(ディレクトリ)
# config_nameはyamlファイル名から拡張子を抜いたもの("config.yaml" なら "config")
@hydra.main(version_base=None, config_path=".", config_name="config")
def main(cfg: DictConfig):
    """Perform examples."""
    print(cfg.model.hidden_dim)
    print(cfg["model"]["latent_dim"])


if __name__ == "__main__":
    main()

そして実行する。

python3 example.py

実行結果は

128
32

といった具合で、yamlの中身を引っ張ってこれる。

このexample.pyをpylintにかけたところ、以下の警告文で怒られた。

example.py:21:4: E1120: No value for argument 'cfg' in function call (no-value-for-parameter)

つまりmain関数の呼び出しに際して、引数がないので怒られているというわけだ。pylintがhydraのデコレータを解釈できないので、それはそうなのだが、修正方法はあるのか?と。

tutorialを引き続き調べると、initialize と composeを使えば良いことがわかった。

"""Hydra example.

Copyright (C) 2022 by ballforest

MIT licence.
"""

from omegaconf import DictConfig

from hydra import compose, initialize


def main(cfg: DictConfig):
    """Perform examples."""
    print(cfg.model.hidden_dim)
    print(cfg["model"]["latent_dim"])


if __name__ == "__main__":
    with initialize(version_base=None, config_path="."):
        config = compose(config_name="config")
        main(cfg=config)

このコードならばpylintから怒られずに済む(引数を明示的に与えているので)。

以上、備忘録であった。

wav2pixがpython2系で実装されていたので、python3系で動くように修正した

wav2pixというGANの派生モデルがある。

要するに、音声から顔画像を生成するgeneratorを持つGANである。

図1: wav2pixのネットワーク構成

qiita.com

wav2pixの実装はgithub上に見つかるのだが、いかんせんpython2系で実装されておりイマイチだったので、 これをpython3系でも動くよう、取り急ぎfixしたということ。

github.com

動作確認環境はpython3.6.9 on Ubuntu 18.04 LTSである。スクリプト全体はblackによりフォーマット済である。

実際にwav2pixを手元で動かして得られた生成画像はこんな感じ。

図2: wav2pixによる生成結果
上記の記事で紹介されているものと同様の画像であり、悪くない結果と言えるだろう。

ソースコードをpylintでチェックするとまだスコアが低いので、コードの修正はさらに必要である。

オンラインOCSVMに基づく外れ値検知をPythonで実装し、訓練時間削減の効果をベンチマークデータで検証した

はじめに

scikit-learn 1.0の新機能として、OC-SVMのオンライン版が紹介されている。 scikit-learn.org

オンライン化のご利益は訓練時間の大幅な削減である。 そこで本記事では、「結局オンライン化でどれくらい速くなったの?」という疑問に答えるべく、オンライン版OCSVMを使いやすい形となるようPyODのフォーマットで実装し、外れ値検知のベンチマークデータセットの上で性能を評価した。

準備

pip3 install pyod
pip3 install scikit-learn -U  # 最新版1.0系を入れるという意味

作成したクラス

SGD_OCSVMクラスを作成した。以下がソースコードである。

カーネル近似のためのNyström methodを実装したNystroemクラスと、SGDOneClassSVMクラスをそれぞれインスタンス化して実装している。

Nyström methodによりグラム行列を低ランク近似&feature mapを手に入れる。feature mapによりデータを(一般には)高次元空間に写像した後に、OC-SVMの目的関数が最小となるべく、パラメタをSGDで更新する。

つまり内部的には

  • 訓練データに対するNystroemのfit & transform
  • transform後のデータに対して、SGDOneClassSVMのfit

ということであり、sklearn的なパイプラインである。

参考までにそれらクラスの公式ドキュメントを示しておく。

今回は2クラスの引数をそのまま受け継ぐ形でSGD_OCSVMクラスを実装しているので、引数の数が多いのはイケてないが、やむを得ない。

使い方の例

上記のPythonファイルを、例えばsgd_ocsvm.pyとして保存する。使い勝手はPyODに合わせるようにした。

# PyODのOCSVM(バッチ型)の訓練
from pyod.models.ocsvm import OCSVM
clf = OCSVM(gamma="auto")  # デフォルトはRBFカーネル
clf.fit(X)

# SGDによるオンライン型のOCSVMの訓練
from sgd_ocsvm import SGD_OCSVM
clf = SGD_OCSVM(gamma="auto")  # デフォルトはRBFカーネル
clf.fit(X)

実験

PyODのベンチマークセット15種類に対して、バッチ型とオンライン型それぞれでモデルを訓練し、その所要時間および異常検知精度を比較する。 データセットは以下のサイトから入手できる。

これらデータセットの特性を簡単にまとめておく。

データセット サンプル数 特徴量次元 外れ値比率
arrhythmia 452 274 14.6%
cardio 1831 21 9.61%
glass 214 9 4.21%
ionosphere 351 33 35.9%
letter 1600 32 6.25%
mnist 7603 100 9.21%
musk 3062 166 3.17%
optdigits 5216 64 2.88%
pendigits 6870 16 2.27%
pima 768 8 34.90%
satellite 6435 36 31.6%
satimage-2 5803 36 1.22%
vertebral 240 6 12.5%
vowels 1456 12 3.43%
wbc 378 30 5.56%

実験に使用したノートブックを示す。なお訓練時のデータには外れ値は含まれている(訓練データとテストデータを6:4で分割)。

gist.github.com

結果

ノートブックより得られるグラフを図1に示す。左から、「訓練時間」「ROC」「Precision @ n」である。OCSVMはバッチ型、SGD-OCSVMはオンライン型である。

図1: 性能比較(バッチ型とオンライン型; 15データセットの平均)

上図より、訓練時間はデータセット平均でも劇的に削減できていることが分かる。なおかつ、異常検知精度は据え置きであり、今回はカーネル近似の影響をほぼ受けていない結果となった。

各データセットごとの訓練時間を示したのが図2である。

図2: 各データセットの訓練時間

上図より、サンプル数の多いmnistやoptdigits、pendigits、satellite、satimage-2で時間削減の効果が大きいことが読み取れる。特にmnistに関しては99%の時間削減となった (1- 0.053 / 6.1873 = 0.99) 。それ以外のデータセットについてはサンプル数が少なく、時間削減の効果は小さい。

参考までに、各データセットごとのROCスコアとPrecision @ nスコアを図3と図4にそれぞれ示す。ROCはほぼ同じグラフである。Precision @ nがオンライン化でやや劣るケースが見られるのは近似の影響であろう。

図3: 各データセットROCスコア

図4: 各データセットのPrecision @ n スコア

おわりに

今回の要点:

  • SGDを用いた最適化に基づくOC-SVMを、PyODフォーマットで利用可能となるように実装。カーネル近似手法の一つであるNyström methodの組み合わせ(パイプライン)。
  • 15ベンチマーク用データセットを用いて、訓練時間削減の効果を検証。サンプル数が多い場合に時間削減の効果が大きいことを確認。

みんなPyOD使おうぜ!