Pythonによる並列処理の書き方(joblib および concurrent.futures)

joblibとconcurrent.futuresを使った並列処理の実現方法についてそれぞれメモを残しておく。Pythonによる並列処理の関連記事はいくらでも見つかるのだが、個人的な備忘録ということで。

想定している状況:処理対象となるファイル数が多いので、直列で処理を動かすと時間がかかる。それを並列処理させることで、トータルでの実行時間の削減を目指す。例えば音声データ群(よくあるのは数百から数千ファイル)からの特徴量抽出を考えると、通常は音声ファイルごとに独立して抽出を行うので、並列処理に向いているというわけである。

サンプルコードの仕様:

  • 並列させたい処理: 関数 your_function にて定義
  • 処理対象となるファイル群: リスト変数 file_list で保持しておく

joblibを使う方法

joblibをpipでインストールしたのち、Parallelクラスおよびdelayedモジュールをインポートして使う。

from joblib import Parallel, delayed

def your_function(file_name):
    """並列処理実行
        
    Args:
         file_name (str): 並列処理の対象となるファイル
    """
    # 何らかの処理

Parallel(n_jobs=4)( # job数はお好みで
    delayed(your_function)(_file) for _file in file_list
)

delayedに関数を与えて、続いてその関数の引数を与える形である。

concurrent.futuresを使う方法

concurrent.futuresからProcessPoolExecutorクラスをインポートして使う。

from concurrent.futures import ProcessPoolExecutor

def your_function(file_name):
    """並列処理実行
        
    Args:
         file_name (str): 並列処理の対象となるファイル
    """
    # 何らかの処理

with ProcessPoolExecutor(max_workers=4) as executor: # max_workersがjob数に対応
    futures = [executor.submit(your_function, _file) for _file in file_list]
    for future in futures:
        future.result()

executor.submitの第1引数が並列処理対象の関数で、以降は関数の引数を与える形である。当然、関数の引数は複数個与えてもOK。

マルチスレッド用のThreadPoolExecutorもあるが、本記事では省略。

参考

github.com

tech.morikatron.ai

異常音検知関連の情報まとめ

異常音検知に関して、まとめてリストしておく。備忘録。

解説論文・スライド

スライド

論文

  • 深層学習を用いた異常検知技術(解説論文) Link
  • 環境音分析の研究動向(解説論文) Link
  • 環境音分析・異常音検知の研究動向(解説論文)Link
  • DCASE 2021のまとめ論文 Link
  • DCASE 2022のまとめ論文 Link

ソフトウェア

  • 異常検知/外れ値検知ライブラリ PyOD https://github.com/yzhao062/pyod
  • DCASE2020 Challenge Task 2 baseline system Link
  • DCASE2021 Challenge Task 2 baseline system (AutoEncoder) Link
  • DCASE2021 Challenge Task 2 baseline system (MobileNetV2) Link
  • DCASE2022 Challenge Task 2 baseline system (AutoEncoder) Link
  • DCASE2022 Challenge Task 2 baseline system (MobileNetV2) Link

データセット

書籍

コンペティション

  • DCASE2020 Challenge Task2 Link

  • DCASE2021 Challenge Task2 Link

  • DCASE2022 Challenge Task2 Link

微分可能な複素正弦波オシレータを用いて日本語5母音を近似させてみた

はじめに

SNSにて、しゃをみん氏が興味深い記事をツイートされていた。

上記の記事で紹介されている論文は、いわゆる複素正弦波オシレータが微分可能な形で定式化されることを示していた。 この「微分可能」なオシレータでは、元の波形を複素正弦波の重ね合わせで近似的に表現する。 そして各サンプル点において計算される波形の近似誤差に基づき、勾配降下法を利用してその誤差が小さくなるようにパラメータ全体を更新(訓練)できる。 ここでオシレータのパラメータとは、すなわち各複素正弦波の周波数、振幅、位相である。 特にPyTorchをはじめとする自動微分が得意な深層学習フレームワークの力を借りて訓練することで、 近似波形とそれにかかるパラメータたちを高速かつ容易に手に入れることができるというわけである。

そこで本記事では、日本語5母音に対して実際に近似を行ってみたので、その結果を紹介する。

実装

しゃをみん氏の記事で紹介されていたサンプルコードをもとに、簡単なクラスを定義して訓練を実施するスクリプトを書いた。

上記のスクリプトで読み込まれる、実験条件が書かれたconfigファイルは以下の通りである。

実験

実験条件

訓練データは下記サイトにて提供されている5母音のファイルを利用した(a_1.wav からo_1.wav)。 wsignal.sakura.ne.jp

音声の分析条件を示す。上記5母音は44100Hzだが、16000Hzにリサンプリングして利用した。

項目 設定
基本周波数 (Hz) 16000
フレーム長 1024
ホップ長 80

実際の訓練に使用したのは、パワーが最大となるフレーム(区間)の音声のみである。 上記の分析条件により、各母音ごとに1024点のデータ系列を使用した。

今回は128個の複素正弦波の和によって各母音を近似する。つまり128個の複素正弦波に対応するパラメータ(周波数、振幅、位相)を訓練対象とした。

なおオプティマイザはAdam、学習率は0.0001、エポック数は50,000とした。

計算機環境を示す。

計算機環境 バージョンなど
OS Ubuntu 22.04
CPU Intel i9-9900K
GPU RTX2070
Python 3.10.6
PyTorch 1.13

実験結果

各母音について、元の波形と近似波形をそれぞれ示す。

図1: /a/の波形

図2: /i/の波形

図3: /u/の波形

図4: /e/の波形

図5: /o/の波形

今回は128個の正弦波なので、ところどころで誤差は目立つが、概ね近似できているといえるだろう。

おわりに

複素正弦波オシレータ を使ってみた、という記事であった。まだまだ可能性を秘めていると思われるので、読者各位も色々とアイディアを試してみると良いだろう。

VOSKによるストリーミング音声認識の使い勝手を改善した話

はじめに

Python音声認識を実行するためのツールとして、最近はVOSKが少しずつ使われるようになってきた。その大きなメリットのひとつはオフラインで動作するという点である。 インストールも容易であり、様々なプラットフォーム上で動作するが、特にMacOSの場合、M1 にも対応したようである

VOSKのサンプルスクリプトは以下のリポジトリにて公開されている。

github.com

例えば "test_microphone.py"はマイク入力からのストリーミング音声認識を実行する。 これらはvoskが提供するモジュールをただ実行するだけのシンプルなスクリプト群である。 各自がアプリケーションに組み込むうえで必要となる、使い勝手の良いインターフェースは自分で仕様を考え、コードを書いて作らねばならない。 Python初心者にはハードルがやや高いものとなっている。

本記事の主旨は表題のように、VOSKのストリーミング音声認識を実行するうえで、単に公式のサンプルスクリプトを動かすだけの何も考えていない状態から脱却し、使い勝手を少し良くしてみようと試みたので、それを紹介するわけである。

改善のヒント

ところで、最近『音声対話システム 基礎から実装まで』(以降、『音声対話システム』)なる書籍が発売された。

www.ohmsha.co.jp

本書は「Python言語による音声対話システムの実装方法を詳しく説明」しているところがウリであり、以下のリポジトリにてソースコードが公開されているのが嬉しい。

github.com

このリポジトリでは音声認識モジュールについても一章を割いて説明している。 紹介されているサンプルスクリプトの不満な点は、

  • Google CloudのSpeech-to-TextのAPIを用いており、オンライン音声認識であること
  • 上記APIを使うまでの設定が手間であること(ユーザ登録やAPIキーの取得など)

が挙げられる。一方で、嬉しい点は

  • マイク入力のためのクラスが定義されており、ストリーミング音声認識がやりやすい
  • 音声認識APIのインタフェースのクラスが定義されている

が挙げられる。

そこで、上記サンプルスクリプトを参考にすれば、VOSKの使い勝手が良くなるのではと考えたのである。

ソースコードと動かし方

デモンストレーション用スクリプトのリンクを置く。後者は発話区間検出機能がついている。

これらを動かすにあたり、以下は必要である。

python3 -m pip install vosk
python3 -m pip install sounddevice

VOSKの音声認識モデルは公式サイトで配布されているものを用いる。

alphacephei.com

vosk-model-small-ja-0.22.tar.gzをダウンロード・解凍し、デモンストレーション用スクリプトと同じところに置く。その際、音声認識モデルのフォルダ名を"model"としておく。要するに、VOSKによる音声認識がすぐ動く状態にしておく。

特徴

『音声対話システム』のサンプルスクリプトではマイク入力を受け取るためにPyAudioを用いていたが、本実装ではマイク入力にsounddeviceを用いることにした。それによりPyAudio関連のモジュールがすっかりなくなっている。

上記デモンストレーションのmain関数を示そう(asr_streaming.py)。

def main(chunk_size=8000):
    """音声認識デモンストレーションを実行.

    Args:
       chunk_size (int): 音声データを受け取る単位(サンプル数)
    """
    SetLogLevel(-1)  # VOSK起動時のログ表示を抑制

    # 入力デバイス情報に基づき、サンプリング周波数の情報を取得
    input_device_info = sd.query_devices(kind="input")
    sample_rate = int(input_device_info["default_samplerate"])

    # マイク入力を初期化
    mic_stream = MicrophoneStream(sample_rate, chunk_size)

    # 音声認識器を構築
    recognizer = KaldiRecognizer(Model("model"), sample_rate)

    # マイク入力ストリームおよび音声認識器をまとめて保持
    VoskStreamingASR = namedtuple(
        "VoskStreamingASR", ["microphone_stream", "recognizer"]
    )
    vosk_asr = VoskStreamingASR(mic_stream, recognizer)

    print("<認識開始>")
    recog_result = get_asr_result(vosk_asr)
    print(f"認識結果: {recog_result}")
    print("<認識終了>")

VOSKの公式サンプルは入力され続ける音声を、無限ループのなかでひたすら文字起こしするものであり、プログラムを終了するにはCtrl-Cで強制終了させるしかない。これはとても使い勝手が悪い。一方で上記のmain関数ではget_asr_result関数を呼び出し、「1発話」分の音声認識を実行できる。関数の実行と1発話の音声認識が紐付くので、他のモジュールでも使いやすい(はずである)。実際、このような設計が『音声対話システム』でも用いられているから。

音声認識自体はVOSKに基づくので、『音声対話システム』のオリジナルに比べて、ソースコードがスッキリしたのもメリットになるだろう。

おわりに

書籍『音声対話システム』のサンプルスクリプトのおかげで、VOSKの音声認識を使いやすくすることができた。

著者の方々にはとても感謝している。

AIミュージックバトル!『弁財天』のスターターキットをPyTorchに移植してアドリブメロディの自動生成を試してみた

はじめに

最近、下記のイベントが開催されることがアナウンスされた。

benzaiten.studio.site

AIミュージックバトル!『弁財天』は「伴奏」から「アドリブメロディ」をAIで生成し「どれだけイケてるメロディか」を競うAI自動作曲コンテストです。』 とのことである。 本コンテストではTensorFlowベースのスターターキット(Google Colabで実行可能)が提供されており、自動作曲初心者でも無理なく始められるようになっている。

筆者はPyTorchユーザなので、スターターキットのPyTorch版を作成しておきたいと思ったわけである。自動作曲自体に興味があったということもある。

注意:本記事のPyTorch移植はGoogle Colab上での実行を想定していない。すなわち、Colab Notebookを提供するものではない。あくまでGPUを搭載した各自の卓上計算機(具体的にはUbuntu)で実行されることを想定した実装である。 とはいえ、Google Driveを適切にマウントすれば、実装をすぐに使えるようになるだろう。

12月3日追記

以下の記事にて、PyTorch版スターターキットのColab Notebookを提供するようにした。

tam5917.hatenablog.com

スターターキットについて

仕様を記述したドキュメントは主催者により用意されている。ぜひ一読されたい。

スターターキットが提供するモデルの概要

コンテストでは伴奏のMIDIファイルとコード進行を表すデータ(CSV形式)が主催者より与えられ、これが自動作曲システムへの入力となる。求められる出力は伴奏にメロディーを付与したMIDIファイルである。

これを受けてスターターキットでは、サンプルのMIDIファイルおよびコード進行データ(CSV形式)が提供されている。これらを前処理し、以下のようにメロディ生成モデルへの入出力としている。

  • モデルの入力:メロディをone-hot エンコーディング(ノート番号を表現)、コード(chord)をmany-hot エンコーディング、休符の有無を0/1で表したうえで、それらを結合したベクトル(61次元)系列
  • モデルの出力:同じくone-hot エンコーディングされたメロディ + 休符からなるベクトル(49次元)系列
  • モデルの構造:LSTMを用いて入力系列を固定次元の隠れ状態ベクトルへとエンコードし、そして再びLSTMを用いて隠れ状態ベクトルから出力系列をデコードする。それら2つのRNNの間に、変分自己符号化器(Variational AutoEncoder; VAE)が挟まっている形である。

LSTMエンコーダ:(メロディ+コード系列)→LSTM → 隠れ状態ベクトル h

VAEエンコーダ:隠れ状態ベクトル h → 潜在変数zの平均μ, 分散σ

VAEデコーダ: 潜在変数のサンプリング z' 〜 N(μ, σ) → 隠れ状態ベクトル h'

LSTMデコーダ:隠れ状態ベクトル h' →LSTM→(メロディー系列)

のようにデータが流れる。スターターキットの説明・実装ではLSTM部分を含めてVAEのエンコーダ / デコーダとなっているが、上記のようにモジュールごとに分けて解釈するのが個人的には理解がスムーズだった。上記VAEにはサンプリングの手続きが入るため、メロディ生成にランダム性を持たせることが可能となる。つまり生成のたびに毎回異なるメロディが得られる。ちなみに、隠れ状態ベクトル hとは最終フレーム(時刻)におけるLSTMの出力ベクトルを指す。

実装

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

github.com

必要なパッケージ

Ubuntuで動かす場合、FluidSynthに関するサウンドフォントが必要なので入れておく。

apt-get install fluidsynth

MacOSではFluidSynthに関するパッケージがbrewから提供されているので、参考まで。

brew install fluid-synth

ほか、pipでパッケージを入れておく(順番は適当)。

python -m pip install torch
python -m pip install midi2audio
python -m pip install music21
python -m pip install hydra-core
python -m pip install joblib
python -m pip install progressbar2
python -m pip install matplotlib
python -m pip install numpy
python -m pip install scipy

実装の概要

主要なファイルと機能について示しておく。

ファイル名 機能
config.yaml 各種設定を記述したYAML形式のファイル
preprocess.py 前処理を実施するスクリプト
training.py モデルの訓練を実施するスクリプト
synthesis.py 訓練済のモデルを用いてメロディーを合成するスクリプト

コードの動かし方は、

  1. config.yamlの編集
  2. preprocess.py による前処理の実施
  3. training.pyによるモデル訓練の実施
  4. synthesis.pyによるメロディー合成の実施

となる。preprocess.pyは一度だけ動かせばよい。 なお前処理にはモデル訓練に用いるMusicXML群のダウンロードや、それらからの特徴量抽出、またスターターキットが提供する伴奏データ・コード進行データのダウンロードが含まれる。

synthesis.pyには合成結果のMIDIファイルへの書き出し、Wavファイルへのポート、またメロディのピアノロール画像の作成・保存が含まれる。

メロディ生成実験

実装に基づいてスクリプトを動かしてみた。

実験条件

訓練データは下記サイトにて提供されている50個のMusicXMLファイルから準備した。 homepages.loria.fr

計算機環境を示す。

計算機環境 バージョンなど
OS Ubuntu 22.04
CPU Intel i9-9900K
GPU RTX2070
Python 3.10.6
PyTorch 1.12

訓練の基本的な設定は以下の通りである。

項目 設定
ミニバッチサイズ 32
エポック数 3000
学習率 0.0003

スターターキットでは学習率が0.0005だったが、これはやや大きかったので、本記事の実験では学習率をやや下げたうえで、MultistepLRにより学習率をスケジューリングした。エポック数は大きめに取ったが、これでも訓練に要する時間はトータルで1時間を下回った。

その他、ニューラルネットワークの細かい設定(ユニット数など)は上記リポジトリのconfig.yamlを見よ。

訓練が終わったのち、synthesis.pyを用いてメロディを生成する前に、サウンドフォントファイルへのパスもconfig.yamlで指定しておくことは注意を要する。本実験(Ubuntu環境)では以下のパスであった:

sound_font: "/usr/share/sounds/sf2/FluidR3_GM.sf2"

実験結果

「伴奏のみ」と「伴奏+メロディ」音源を併せて示す。

伴奏のみ(主催者から提供されたMIDI 伴奏+メロディ

メロディ生成結果を図1に示す(縦軸と横軸にラベルが書かれていないので、減点なのだが)。ピアノロールもどきであり、横軸は時間を表す認識でよい。縦軸の下に向かうほど音が高くなっている点は注意を要する。一番下は休符要素を表す。

図1: 生成されたメロディ

自動生成メロディもくり返し聞くとジワジワ来るものはある。これはこれでアリではないかと(評価が難しい!コンテスト実施の必要性を実感)。

実装の舞台裏やTIPS、TensorFlow版との違いなど

  • training.pyは結局オレオレ実装になってしまった 。main()に全てをベタ書きでもよいとは思ったが、ひとつひとつの関数をコンパクト(行数少なめ)にしたかった。
  • スターターキットにおいて、VAEの全結合層(隠れ状態ベクトル h, h'とμ, σの間; input-latent)は特に活性化関数を与えていなかった。一方、本記事の実装では、エンコーダとデコーダに全結合層がそれぞれ一つ余分に増える形となった(input-hidden-latent)。そこで、input-hidden間とhidden-latent間に活性化関数としてReLU関数をそれぞれ1つ入れてみた。完全移植でない所以でもある(これぐらいの変更は別に構わないだろう!気になるひとは取り除けばよい)。
  • デコーダの入力は、VAEから生成された隠れ状態ベクトル h'を全フレーム(時刻)に渡って複製することで与えている。この点も改良のしどころであり、例えば、直前時刻のデコーダ出力を当該時刻のデコーダ入力として与えるよう変更するのは容易である(いわゆるseq2seqの定式化)。
  • VAEにバッチ正規化は入れていない(入れると訓練誤差が減らない)。本記事の読者が色々と試行錯誤する際は、バッチ正規化やDropoutをはじめとする各種の正則化テクニックを組み込んでみるのも良いだろう。
  • スターターキットではグローバル変数が定義され、それぞれの関数の中に埋め込まれている。今回のPyTorch実装ではそれらグローバル変数たちをソースコードから全て追い出し、YAML形式の設定ファイル(config.yaml)に書くことにした。
  • 今回は組み込み関数のgetattrとhydraパッケージの力を借りて、YAMLに基づいてオプティマイザや学習率スケジューラを構築できるように実装した。ソースコードには具体的なオプティマイザのクラス名、例えば"Adam"や"RAdam"は現れてこないので、汎用性が増す利点がある。その際、ttslearnの実装を大いに参考にした。
  • PyTorchではクロスエントロピー損失関数(CrossEntropy)への与え方がTensorFlowと異なる。予測データはsoftmaxを取る直前のいわゆるlogitsの形で与える必要があり、さらにターゲットラベルはone-hotエンコードしておく必要はない。この仕様のため、スターターキットが提供する一部の関数に仕様の変更が生じた(calc_xy()など)。
  • 主催者より提供された伴奏MIDIをmidi2audioによりwavへ出力すると、曲後に約1分間の無音が挿入される現象を確認している。実体のfluidsynthコマンド単体でも同様の現象が確認された。別のMIDIファイルで試してみると、そのような無音挿入は生じなかった。よって少なくとも伴奏MIDIに特有の現象であるが、原因は不明である。
  • 補助的に作成したモジュールたちは以下の通りである:
ファイル名 機能
dataset.py データセットのクラスBenzaitenDatasetを定義
factory.py オプティマイザ、スケジューラ、損失関数クラスの各インタフェースを定義
model.py メロディを生成するためのクラスSeq2SeqMelodyComposerを定義
util.py スターターキットで提供される関数群を定義

おわりに

スターターキットをPyTorchに移植してみて、確かに色々と改良を図れる箇所がいくつもあることを確認した。コンテストへのエントリー自体はさておき、自動作曲AIでしばらく遊んでみることにする。

最後に、AIミュージックバトル!『弁財天』の関係者各位に深く感謝したい。スターターキットの提供は英断だったと思う。コンテストの成功を祈る。

Let's ad-lib!

Transformerを用いてオノマトペから環境音を合成する手法をPyTorchで実装した(Transformer版 Onoma-to-Wave)

はじめに

以前、Onoma-to-Waveを実装した記事を書いたことがあった:

tam5917.hatenablog.com

Onoma-to-Waveとはオノマトペ(文字列)を環境音(スペクトログラム)に変換するモデルである。RNNに基づくencoderとdecoderから構成されており、いわゆるSequence-to-Sequence(Seq2Seq)の構造を持つ。 それらをTransformerによって置き換えたモデルが、Onoma-to-Waveの著者らによって実はすでに提案されている。

岡本 悠希,井本 桂右,高道 慎之介,福森 隆寛,山下 洋一,"Transformerを用いたオノマトペからの環境音合成," 日本音響学会2021年秋季研究発表会,pp. 943-946,2021.

上記は国内学会における査読なしの予稿論文であり、2022年10月時点でTransformerに基づくモデルの提案自体は査読付き論文として出版されていない。ただし、著者らによる以下の論文では、本モデルを含む形で主観評価実験が実施されている。

arxiv.org

著者による環境音合成のデモンストレーションは以下から試聴できる。

y-okamoto1221.github.io

モデルの全容を把握するには以下のPDFファイルが役に立つだろう。

Transformer版Onoma-to-Waveの実装は2022年の本記事執筆時点では公開されていないため、自分で実装して環境音合成の再現実験を試みたというわけである。

事前準備

データセットは冒頭に示した記事と同様に、RWCP 実環境音声・音響データベース (Real World Computing Partnership-Sound Scene Database; RWCP-SSD) を利用した。

research.nii.ac.jp

実装の概要

ソースコードは以下のリポジトリに置いた。Enjoy!

github.com

前回と同様にフォルダを分けている。

フォルダ名 説明
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)に記述し、それを読み込んで使う形とした。したがってコードの動かし方は、

  1. config.yamlの編集
  2. preprocess.py による前処理の実施
  3. training.pyによるモデル訓練の実施
  4. 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 /)
条件付けなし 条件付けあり


条件付けなし 条件付けあり


条件付けなし 条件付けあり


条件付けなし 条件付けあり


条件付けなし 条件付けあり


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


条件付けなし 条件付けあり


条件付けなし 条件付けあり


続いて、共通のオノマトペを与えて、条件付けのクラスを色々と変えた場合の合成例を示す。ここでSeq2Seqとは前回Onoma-to-Waveによる合成法を指す。

Seq2Seq Transformer
"whistle3"で条件付け "whistle3"で条件付け
"shaver"で条件付け "shaver"で条件付け
"tear"で条件付け "tear"で条件付け


Seq2Seq Transformer
"maracas"で条件付け "maracas"で条件付け
"coffmill"で条件付け "coffmill"で条件付け


Seq2Seq Transformer
"cup1"で条件付け "cup1"で条件付け
"whislte3"で条件付け "whislte3"で条件付け
"shaver"で条件付け "shaver"で条件付け


Seq2Seq Transformer
"drum"で条件付け "drum"で条件付け
"transhbox"で条件付け "trashbox"で条件付け


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)はNonesource-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枚目あたり)。

speakerdeck.com

  • 補助的に作成したモジュールたちは以下の通りである。
ファイル名 機能
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のチュートリアル由来である。せっかくなら今回の環境音合成に特化したクラス名が良いと考え、Onoma2WaveTransformerOnoma2Melといった代案を考えてみたが、どうもしっくりこなかった。前者はクラス名が長すぎる気がしており、後者は具体的な処理内容(オノマトペから対数メルスペクトログラムへの変換)が反映され悪くないが、Transformerを使っている感が薄れる気がした。

  • Seq2SeqTransformerクラスとMel2Linearクラスは独立に訓練させるため、それぞれ専用のTrainerクラスを用意して別々のモジュール(スクリプト)にすることも実装当初に考えた。しかし、モジュールをむやみに増やすのはあまり美しくない設計と考え、両クラスの訓練と推論をひとつのTrainerクラスに統合したのである。それぞれの訓練のON/OFFはconfigファイルで切り替える設計とした。

  • TransformerLRの実装は以下の記事で紹介したものである。実は本記事の布石であった。

tam5917.hatenablog.com

おわりに

実装を通じて、Transformerの気持ちが少し分かった気がする。特にself-attentionやMulti-head attentionまわりはマスクの使い方を含めて勉強になった。やはり実装は理解への近道。

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