音声認識結果に基づき複数話者でテキスト音声合成する簡易音声変換アプリをPythonで書いた話

はじめに

PySimpleGUIの応用シリーズ。スクリプトの動作の様子は以下の通り。

本記事の趣旨は今回作成したスクリプトで実現された機能や利用したライブラリ群ほか、関連情報を補足的に説明することである。

作成したスクリプト

スクリプトへのURLを置く。

以下は機能の覚え書きなどである。

動作確認環境

必要なPythonライブラリはコマンドラインからインストールできる(はず)。

pip install numpy
pip install pysimplegui
pip install sounddevice
pip install soundfile
pip install speech_recognition
pip install ttslearn
pip install torch

PySimpleGUI

PySimpleGUIの関数群を用いて、ボタンやフレームを配置してみた。現時点では話者10人分のボタンを直接押すことで話者選択を行っているが、話者数が増えるとしんどいので(実は合成モデルは100人分用意されている)、プルダウンメニュー式の話者選択UIはそのうち作る予定である(やれたらやる)。

PySimpleGUI初学者向けのサンプルプログラムを公開している方がおり、私もこれで最初勉強した。とても参考になった。
PySimpleGUI 基礎解説 | K-TechLaboゼミ用学習ノート

PySimpleGUIの勉強にあたり、以下のWeb記事から概要を知るとよい。

基本的には各ボタンに「key」を紐付けることで、イベントループ内でボタン押下などのイベントを回収し、それぞれの処理を行う構造となる(「音声認識」や「話者変更」など)。

音声収録・音声再生機能

sounddeviceライブラリを用いることにした。

再生にはplay関数、録音にはrec関数を用いる。

再生の際には音割れ防止のための正規化をかけておいた(play_wav関数)。

def play_wav():
    """WAVを再生する"""
    if VARS["audio"] is None or len(VARS["audio"]) == 0:
        raise ValueError("Audio data must not be empty!")

    # 振幅の正規化
    audio = VARS["audio"] / np.abs(VARS["audio"]).max()
    audio = audio * (np.iinfo(np.int16).max / 2 - 1)
    audio = audio.astype(np.int16)

    sd.play(audio, SAMPLE_RATE)

    # 再生は非同期に行われるので、明示的にsleepさせる
    sd.sleep(int(1000 * len(VARS["audio"]) / SAMPLE_RATE))

音声収録の際にはプログレスバーをつけるようにした(listen関数)。sounddeviceのrec関数自体は非同期で動くので、プログレスバーの進行はthreadingを用いた並行処理によった。

def listen():
    """リッスンする関数"""

    # 音声録音の非同期実行
    VARS["audio"] = sd.rec(
        int(DURATION * SAMPLE_RATE), samplerate=SAMPLE_RATE, channels=N_CHANNEL
    )
    time.sleep(BUFFER)  # 少しだけ間を置く

    # 録音している間、プログレスバーを非同期表示
    record_thread = threading.Thread(target=progress_bar, daemon=True)
    record_thread.start()  # 終了すると自動でterminateする

https://gist.github.com/tam17aki/4f11c904832ce270b0fd4e11894bb1aa#file-pysimplegui_recog_tts-py-L204-L215
rec関数の戻り値は(収録サンプル数、チャネル)のshapeをもつarrayであり、モノラルであればチャネル数は1であるが2次元arrayである。

音声認識機能

本記事での音声認識とは、音声データをテキストデータに変換する技術を指す。

実装にはSpeechRecognitionライブラリを用いた。マイク入力で音声取得を行う場合は以前記事を書いたことがある。

今回はマイク入力した直後の音声データだけではなく、既存のwavファイルから音声データを読み込むことのできる仕様にしたいため、音声認識部のスクリプトにも多少の修正が加わっている。音声認識をかける前に、常に一時ファイルに保存したうえで、その一時ファイルをロードする形で音データを取得する。

ちょうどプログラムのこの辺りである(recog関数)。

    # 一旦ファイルに書き込む
    sf.write(
        file=OUTPUT_FILE,
        data=VARS["audio"],
        samplerate=SAMPLE_RATE,
        format="WAV",
        subtype="PCM_16",
    )

    r = sr.Recognizer()
    with sr.AudioFile(OUTPUT_FILE) as source:
        r.adjust_for_ambient_noise(source)
        audio = r.listen(source)  # 音声取得
        try:
            text = r.recognize_google(audio, language="ja-JP")
            VARS["window"]["-RECOG_TEXT-"].Update(text)
        except sr.UnknownValueError:
            VARS["window"]["-RECOG_TEXT-"].Update("認識に失敗しました")

音声合成機能・話者選択機能

本実装で扱う音声合成技術は深層学習に基づく「テキスト音声合成」である。システムにテキストデータを入力し、テキストに対応する合成音声データを出力として得る技術を指す。今回は入力テキストが音声認識結果により得られたものを想定している。

テキスト音声合成に関する機械学習モデルの発展も手伝って、高度な音声合成システムを比較的容易に、各自のローカル計算機環境下で構築・実現できるようになった。最近では『Pythonで学ぶ音声合成』という書籍も出版されており、機械学習・深層学習に基づく高度な音声合成技術をPythonプログラムとともに1冊で学ぶことができる。書籍全体を通しての難易度は専門性の高さから中級者向けであると言える。

今回の実装では、この書籍に付属のPythonライブラリ ttslearn で提供されている、音声合成のための事前学習済モデルを利用することにした。特にテキストから音響特徴量への変換モデルとしてTacotron、また音響特徴量から音声波形への変換モデル(ニューラルボコーダ)としてHiFi-GANに基づく事前学習済モデルを利用する。この事前学習済モデルの使い方は以下に示すページからJupyter notebookを見ることで確認できる。またGoogle Colaboratory上で実際にプログラムを動かし、合成音声を試聴することもできる。

上記リンクより、利用したのは"multspk_tacotron2_hifipwg_jvs24k"(JVSコーパスで訓練された複数話者Tacotoron with HiFi-GAN、サンプリング周波数24kHz) である。16kHzバージョンも利用できるが、多少音質が落ちる。

話者選択機能については、各話者に対応する「ボタン押下イベント」に話者IDを紐づけ、その話者IDを合成モジュールの引数に与えればよい。

def event_synth(event, values):
    """音声合成系のイベント処理"""
    if event == "-SYNTH-":
        text = VARS["window"]["-RECOG_TEXT-"].DisplayText

        # テキスト音声合成
        wav, sr = PWG_ENGINE.tts(text, spk_id=SPK_ID)  # SPK_IDが話者ID

        # 音割れ防止
        wav = (wav / np.abs(wav).max()) * (np.iinfo(np.int16).max / 2 - 1)

        # 再生
        sd.play(wav.astype(np.int16), sr)
        sd.sleep(int(1000 * len(wav) / sr))

関連事例

Seiren Voice

「誰でも100人の声に変えられる声変換システム」Seiren Voiceが発表された。

解説記事によれば、Seiren Voiceは収録された音声を「音素」(=言語情報のひとつ)と「音高」の成分にわけ、それを「学習済みの音声合成モデル」に渡して音声合成を行っている。モデルへの入力において、発話内容に対応する音素は「文字の情報」なので、話者に依存する成分(=声色)が含まれていないという点が重要である。音声合成モデル自体は、話者の数だけ事前に学習しておけばよい(実はこれが大変)。

収録済み音声を言語情報に変換し、それに基づいて音声波形を生成している点においては今回のアプリと関連している。違いの1つはモデルへの入力であり、Seiren Voiceは「音素」と「音高」に分離した状態となる。2つ目の違いは音声波形生成モデルであり、Seiren VoiceはWaveRNNを用いている。

VOICEVOX

今回利用したTactron + HiFi-GANのttslearn実装では、合成時に声の高さや話速のコントロールはできない。あくまで選択された話者について、テキストデータを渡して音声波形を手に入れるのみである。もしこれをコントロール可能となるよう望むならば、機械学習モデル自体に手を入れなければならない。

そのような声の高さや話速のコントロールまで含めた音声合成システムを提供しているのが、VOICEVOXである。


その合成エンジンのソースコードを紐解けば、上記の意味でコントロール可能な枠組みが実装されていることが分かる。
github.com
VOICEVOXのようなアクセント変更向けのGUIをPySimpleGUIで実現することは(おそらく)かなりの工数を要する。未来のスーパーマンに託したい(他力本願!)。

おわりに

今回はGUI構築にPySimpleGUIを用い、音声認識音声合成を直列につないだ非リアルタイム音声変換アプリをPythonで作成した。PySimpleGUIのおかげでGUI構築はかなり楽になったといえる。

Pythonで学ぶ音声合成』はオススメの1冊なので、気になった方はぜひ。

Yahoo天気予報をスクレイピングしてしゃべらせるPythonプログラム

表題の通り。

音声認識結果に基づく天気予報の結果を音声合成によりしゃべらせた、ということ。

必要なライブラリ

pipでインストール可能である。

pip3 install numpy
pip3 install pyopenjtalk
pip3 install speech_recognition
pip3 install urllib3
pip3 install beautifulsoup4

コード

Yahoo天気予報のスクレイピングは以下の記事を参考にした。

Google Homeリスペクトで音声認識の結果に「Ok Google」が含まれ、 かつ「天気」「天気予報」が含まれていた場合にスクレイピングおよび音声合成を発動する。

例えば「Ok Google 今日の名古屋市の天気は」と音声認識されたら、実際に名古屋の天気をしゃべるわけだ。

gist.github.com

参考

Pythonによるウェブスクレイピングについて、ばんくし氏の記事が参考になる。

vaaaaaanquish.hatenablog.com

Pythonの異常検知用パッケージPyODの形式に従って、ガウス混合モデル(GMM)に基づく異常検知を実装した

はじめに

2021年8月22日現在、Pythonの異常検知用パッケージであるPyODにガウス混合モデル(Gaussian Mixture Model; GMM)ベースの 異常検知が実装されていなかったので、それを実装したということである。

PyODのインストール

pipでインストール可能である。

pip3 install pyod

作成したクラス:GMM

すでに実装済のPyODのアルゴリズムを参考に、scikit-learnのGaussianMixtureクラスをラップする形で GMMのクラスを作った。PyODのBaseDetectorクラスを継承するだけなので簡単である。

【ここをクリックしてコードを表示する】 gist.github.com

GMMに基づく異常検知のデモンストレーション

簡単なデモンストレーションのnotebookを作成した。今回作成したGMMクラスをgmm.pyとして保存した場合のnotebookである。 2次元の特徴空間において3つのガウス分布から乱数を発生させてデータを作成している。うまく動いているように見える。 gist.github.com

おわりに

みんなPyOD使おうぜ!(2回目)

DAGMMに基づく教師なし異常検知をPyTorchで実装した

はじめに

教師なし異常検知の機械学習モデルの一つとして、"Deep Autoencoding Gaussian Mixture Model" (以降DAGMM)が知られている。 今回はこれを、異常検知/外れ値検知のためのPythonパッケージPyODの仕様に沿った形で、PyTorchにより実装したということである。

異常検知について

以下を参考にするとよい。

DAGMMについて

論文へのリンクを示す(ICLR 2018)。 openreview.net

解説スライドへのリンクを示す。

www.slideshare.net

流れは次の通り。

  1. オートエンコーダによる入力特徴量の次元圧縮

  2. 「圧縮された特徴量+α」の潜在特徴量からガウス混合モデル(Gaussian Mixture Model; GMM)の事後確率を推定

  3. 事後確率からGMMの統計量を計算(潜在特徴量を用いる)

  4. 統計量からエネルギー関数(=負の対数尤度に相当)を計算、異常検知のスコアとして利用

エネルギー関数の値が大きいほど、異常度が大きいということになる(負の対数尤度が大きい、つまり対数尤度が小さいから発生頻度も小さい)

準備

PyODはpipでインストール可能である。

pip3 install pyod

ほか、torch, sklearn, numpy, tqdmのインストールを済ませておく。

実装したクラス:DAGMM

DAGMMについて、先人によるPyTorch実装はいくつか見つかる。

しかしながら、次に挙げる点が不満だった。

  • エネルギー関数の計算時にPyTorchのinverse関数やpinv関数を用いている

  • 平方根や対数計算時にフロア値を適用していないため、計算が不安定になる(おそれがある)

そこで今回の実装ではPyODの仕様に従いつつ、上記の点も改善するようにした。 ちなみにPyLintを適用してコードをチェックしてある(スコア9.92; 満点は10.0)。

【DAGMMクラスのコードを表示するにはここをクリック】 gist.github.com

直接のURLはこちらから。

念のため、クラス引数の説明を載せておく。

引数 説明
comp_neurons compression network(オートエンコーダ)のニューロン
comp_activation compression networkの活性化関数
estim_neurons estimation network(事後確率推定)のニューロン
estim_activation estimation networkの活性化関数
lambda_energy 損失関数におけるエネルギー関数にかかる重み
comp_activation 損失関数における「共分散行列の逆数和」にかかる重み
epochs 学習のエポック数
batch_size ミニバッチサイズ
weight_decay 各層の重みに課す正則化の強さ
validation_size バリデーションデータの比率
batch_norm バッチノルムの有無
dropout_rate ドロップアウトの比率
verbose ログメッセージの段階; 0は非表示、1はプログレスバー
2は各エポックごとに損失関数の値を1行ずつ表示
contamination 外れ値の(想定)比率

ほか、注意点としてはoptimizerがAdam固定である。

異常検知デモンストレーションその1

Toshihiro NAKAE 氏による、DAGMMのデモンストレーション用notebookがある。

NAKAE氏による実装はTensorFlow 1系によるが、データ作成方法などを参考にして、 今回のPyTorch実装(dagmm.pyとして保存し利用)をローカルで動かしたものを示す。

gist.github.com

NAKAE氏とほぼ同様の結果が得られたことがわかる。

異常検知デモンストレーションその2

DAGMMの論文にはKDDCup 10% データセットによる異常検知の実験結果が載っている。 この実験結果を再現する試みである。ちなみにこちらも先と同様Toshihiro NAKAE 氏による、 DAGMMのデモンストレーション用notebookがある。

上記のnotebookを参考にしてPyTorch実装をローカルで動かしたものを以下に示す。

gist.github.com

論文に掲載された結果よりも幾分良いスコアが得られたが、 乱数の引きが良かったのだろう。

おわりに

PyODフォーマットで実装するシリーズも4つ目となった。他にも色々と実装する予定である(公開するとは言ってない)。

DAGMM論文に掲載された実験結果が再現できて安心した。ソースコードの可読性やスタイルにはまだ改善の余地はある。

余談

DAGMMクラスのattributeの数を7個以下に制限するために(さもなくばPyLintに怒られる!)、 今回はPythonNamedTupleを利用した。具体的にはクラスのinitメソッドに与えた引数を「学習条件」や「ネットワーク定義」のくくりで束ねることで、むやみにattributeを増やすことを回避することに成功した。

DAGMMにより得られる低次元特徴表現(estimation networkの入力)の可視化機能の実装も今回は見送った。モデル内部の挙動を観察・把握し、各モデルの異常検知性能をさらに深く考察する際にはとても有用な機能であるが、どちらかというと研究用途でありクラスが提供する異常検知機能とは切り分けて考えることにした。本記事の読者ならば、DAGMMクラスに独自のメソッドを追加し、低次元表現を可視化することは容易に実現できるだろう。

OC-NNに基づく教師なし異常検知をPyTorchで実装した

はじめに

教師なし異常検知の機械学習モデルの一つとして、"One-Class Neural Network" (以降OC-NN)が知られている。 今回はこれを、異常検知/外れ値検知のためのPythonパッケージPyODの仕様に沿った形で、PyTorchにより実装したということである。

異常検知について

以下の記事を読むのが良いだろう。 qiita.com

深層学習を用いた異常検知技術について

以下の解説論文を読むのが良い。OC-NNについての解説もある。 www.jstage.jst.go.jp

One-Class Neural Network(OC-NN)について

OC-NNはDeep SVDDと同時期に提案された教師なし異常検知モデルであり、OC-SVMを深層学習を用いて拡張したものとして位置づけられる。 OC-SVMの損失関数のカーネル計算および重みとの内積計算をニューラルネットワークで置き換えている。

論文は以下から読むことができる。ver 1 (KDD2018)とver 2で掲載されている実験が異なっていたりするので、両方に目を通すのがよい。特に後者にはDeep SVDDとの比較実験が載っている。 arxiv.org

先述の解説論文に加えて、解説スライドおよび解説記事へのリンクを以下に置く。

www.slideshare.net www.smartbowwow.com

作成したクラス:OneClassNN

OC-NNの実装は探すとTensorflowやPyTorchのものが見つかるが、自分には少々使いづらいものだったので、改めて実装を試みた。 オートエンコーダによる重みの事前学習も可能にしてあるが、あまり使う機会はないかもしれない。 ほか実装にはまだ怪しい箇所があるかもしれない。

【コードを表示する】 gist.github.com

注意点

前回記事のDeep SVDDと同様に、ニューラルネット部分は全結合層のみからなっている。 画像データを対象にする場合には畳み込み層やプーリング層へと実装を修正する必要がある。

デモンストレーション

PyODから提供されている各種アルゴリズムのexample用スクリプトを参考に、簡単なデモンストレーションのnotebookを作成した。 今回作成したOneClassNNクラスをocnn.pyとして保存した場合のnotebookである。 データの生成および評価にはPyODから提供されている関数generate_dataおよびevaluate_printをそれぞれ用いた。

300次元の疑似データ量をいくつか変えて試してみた。疑似データが簡単すぎるせいか、今回は検出精度がかなり高く出た。 gist.github.com

おわりに

前回記事のDeep SVDDの実装ができたので、それを参考にすれば今回のOC-NNの実装は比較的容易だった。

とはいえ実際に実装すると理解が進む(それはそう)。例えば損失関数の計算について、データで平均を計算している箇所はtorch.mean()として「ミニバッチ内の平均」として翻訳できる。またmax(0, 某)の箇所はtorch.max(torch.zeros_like(某), 某)として明示的に計算するか、もしくはミニバッチのsample-wiseにrelu関数(torch.nn.functional.relu)を通す、という翻訳ができるなど。

先に述べたとおり、OC-NNの上記論文にはDeep SVDDとの比較実験も載っている。 Deep SVDDを上回る性能を示すケースも見られたが、下回ることもあり、「匹敵する」モデルと言えるだろう。

OC-NNはDeep SVDD同様に特徴量空間に写像した圧縮表現に基づいており、元のデータの「構造」に関する情報は多少なり落ちている。 それゆえ、圧縮表現を得るためのencoderの相対的な重要性が増しているが、OC-NNは外付けネットワークからのencoder出力を使っている。 論文の図(ver1はFigure 2, ver2はFigure 1)ではencoder部分はfreezeと書いてある。そして本文を読むと、ver1では"frozen (not trained)"と書いてあり、ver2では"not frozen (but trained)"と書いてあったりする(どっちやねん!)。前者は事前学習したオートエンコーダのencoder重みを与えて固定しており、後者はOC-NNのfeed-forward部分の重み更新時に、encoder部分の重みまで一緒に更新するということを意味する。

たとえdeep系を使うにせよ良質な特徴抽出(と潜在表現獲得) is all you needな感はあるが、いずれにせよもう少しend-to-end志向にしたいものだ。