🤗

(Style-)Bert-VITS2 JP-Extra (日本語特化版)について

2024/02/05に公開

宣伝

  • Style-Bert-VITS2のチュートリアル解説動画を作りました

https://www.youtube.com/watch?v=aTUSzgDl1iY

  • discordサーバー「AI声づくり研究会」によく出没しています

https://zenn.dev/p/aivoicelab

概要

2024-02-01、音声合成(TTS)の中国発オープンソースのBert-VITS2の日本語特化版のBert-VITS2 JP-Extra がリリースされ、私が作っているそれの改造版 Style-Bert-VITS2 でもJP-Extra版を2/3に使えるようになりました(しました)。

実際にどんな感じかは オンラインのデモ 上で試せるのでぜひお試しください。

これにより日本語の発音やアクセントやイントネーション等の自然性が上がり、クリアさや学習を回していったときのガタツキが大きく減る傾向があります。英語と中国語で音声合成したいという需要がなく日本語しか使わない場合はJP-Extra版を使うことを強くおすすめします。

本稿では、以前の
https://zenn.dev/litagin/articles/8c6edcf6b6fcd6
の記事で書いた2.1-2.3の構造と比べて、このJP-Extra版が何が違うのか、そしてStyle-Bert-VITS2ではそれをどうさらに改造しているのかについて述べることとします。

注意

機械学習や音声AIや日本語処理についてちゃんとした勉強をしたことがないので誤っている箇所が多く存在する可能性があります。

短くまとめ

他のバージョンに比べて、(Style-)Bert-VITS2 JP-Extraは、

  • これまでの本家バージョンにあった日本語の読み・アクセント取得部分のバグを修正(私のコントリビューションによるものです✌)
  • 事前学習モデルに用いられた日本語学習データ量が増えたらしい(日本語のみで800時間程度)
  • 日本語性能に全振りするため、中国語と英語に関するコンポーネントは削除
  • 2.2で用いられていたclapを使ったプロンプトによる声音制御の実装(ただし2.2と同じくあまり実用的ではなさそう)
  • 2.3で用いられていた新しいDiscriminator(WavLMを使ったもの)を使った自然性アップ
  • 2.3で音素間隔が不安定になっていたのでduration discriminatorを使わないように
  • Style-Bert-VITS2では、(2.1からStyle版への変更と同様)あまり機能してなさそうなclapを使った感情埋め込みを削除して、代わりに以前と同じ単なる全結合層でのスタイル埋め込みを追加
  • Style-Bert-VITS2ではさらにアクセントの制御も(ある程度は)指定できるように

以上によって、日本語の発音やアクセントがかなり自然になり、明瞭さも増しており、以前まであった「外国人が喋ってる日本語」のような印象がかなり改善されたものとなっております。

以下ではこれらの変更について、素人目線で少し書いていきます。

日本語学習データ量の増加

これについては公開されている情報はあまり無いので分かりませんが、Bert-VITS2 JP-Extraのリリースページによると:

3.We increased Japanese training data several times. Now up to ~800h single language.

とあるので、おそらく以前よりも学習に用いられる音声データ量が大幅に増加したものと思われます。データ量が重要な機械学習ではもしかしたらこれ(と下の日本語処理バグの修正)がなんだかんだモデル構造改良よりも自然性改善への寄与が大きい可能性があるかもしれません。

日本語処理について

モデル構造の本筋とは関係ないので、気になる人以外は飛ばしてください。

以前の記事でも、本家には日本語処理部分にバグがあり、Style-Bert-VITS2ではそれを修正していたということを述べました。今回の(Style-)Bert-VITS2 JP-Extraではその修正が取り入れられています。具体的にどのような処理が日本語テキストについて行われているかについてここでは述べます。

日本語TTSモデルでの日本語処理の概略

日本語TTSでは、与えられた日本語文章から、それを音素列に変換する処理が(今は大抵)走ります。この処理部分はよく g2p (Grapheme-to-Phoneme) と呼ばれています。学習時と推論時の両方に、与えられた文に対してg2pした音素列をモデルに入力として与えます(既存の多くのTTSではこの音素列のみを入力としていますが、Bert-VITS2系のすごいところは音素列に変換される前の日本語文章の情報もBERTを介して入れているところです)。

日本語音素処理等でおそらくデファクトスタンダードになっておりBert-VITS2系でも使われているpyopenjtalkではg2p関数が用意されており、たとえば

>>> import pyopenjtalk
>>> pyopenjtalk.g2p("おはよう!元気ですか?")
'o h a y o o pau g e N k i d e s U k a'

のように、入力文から音素リスト(を空白で繋げたもの)が帰ってきます。

pyopenjtalkのデフォルトg2pで不十分な点

pyopenjtalk.g2pは便利ですが、いくつかの点で不十分です。

  1. 単なる音素列のみが返ってきて、アクセントについての情報は何もない
  2. 文章中の句読点や「!?」記号等が全てポーズ音素扱いpauになり、例えば「私は……そう思う……。」と「私は!!!!そう思う!!!」が区別されない

アクセントを考慮したg2p

アクセントは日本語TTSでは大事な問題で、日本語の場合は他の英語や中国語等と違い、全単語について正しいアクセントというものが存在し、誤ったアクセントで喋られると違和感が大きくなるので、音素に加えてアクセントも付加情報として正しく学習させたいし、可能ならば手動修正可能な形でアクセント指定をして音声合成できることが望ましいです。

このアクセント情報をモデルにどう取り込むかというアプローチはいろいろあり、例えばTTS等いろんな音声関連の学習が可能であるESPNetの内部には、日本語のg2p関数として以下のようなものが用意されています。
https://github.com/espnet/espnet/blob/59733c2f1a962575667f6887e87fcdf04e06afc3/egs2/jvs/tts1/run.sh#L29-L49

これらアクセントの扱いを考慮したg2pについて、どれを用いればよいかという点はライブラリによっていろいろ違っているようで、どれがメジャーかは分かりませんが、例えば COEIROINK ではpyopenjtalk_prosodyが使用されています。このg2pは、アクセントの上昇箇所に[、下降箇所に]という記号を挿入し、これを音素記号の一部として扱っているものです。(他のアクセント付きg2pがどういうアクセントの読み方なのか自分には知識がなくて分かりません)

ただしこれらのg2p関数を使うと、上述2番目の、全ての文中の記号やその個数が区別されないという問題があります。我々には「私は……そう思う……。」は自信なさげに、「私は!!!!そう思う!!!」は堂々と怒鳴るように読んで欲しいという欲望がありますが、それが満たされません。

(Style-)Bert-VITS2 JP-Extra での日本語処理

そもそもBert-VITS2ではアクセント情報はtonesという名前で、音素列とは別にモデルに入力されます。これは音素列の各音素に対して数字0か1を割り当てるもので、日本語の場合は

おはよう!!!ございます?
→ (o: 0), (h: 1), (a: 1), (y: 1), (o: 1), (o: 1), (!, 0), (!, 0), (!, 0), (g: 0) (o: 0), (z: 1) (a: 1), (i: 1), (m: 1), (a: 1), (s: 0), (u: 0), (?, 0)

のように、低(0) と高(1) の2種類の数値を音素に対して割りあて、その数値列を音素列とは別にモデルに入力するという形式を取っています。さらに上の例のように文中の記号はそれ自体を音素として、個数含めてちゃんと区別するようになっています。

このようなg2p関数をどうやって実装したらよいか、おそらく日本語TTS界隈の凄い人たちなら簡単に実装できると思うのですが、自分にはよく分からなかったので以下のような手法を取りました。

  1. まず上の、ESPNetにあるpyopenjtalk_prosodyを用いて、上昇記号・下降記号を含めた音素列を取得する(ただし「!」や「…」等の記号の情報は消える
  2. それを使って、まず記号が完全に消えた音素とアクセント高低のペアのリストを作る
  3. 一方でこれらの処理とは別に、記号を含んだ音素列(アクセント情報無し)を作る
  4. その2つの結果を融合させて求めるものを作る

単純にpyopenjtalkを単体で一回で使って上の操作を実現できないかとも思うのですが、以下の困難があります。

  • pyoepnjtalk(OpenJTalk)では、アクセントを取得する際は、おそらく必ずテキストからフルコンテキストラベルを取得する (pyopenjtalk.extract_fullcontext) ことが必須だが、そのフルコンテキストラベルの情報の時点で文中の記号の個数や種類についての情報は消えてしまう(よってフルコンテキストラベルからパースすることでは原理的に上の結果を得ることはできない)

一方で、上述3番目については、pyopenjtalk.run_frontendという関数が、次のように記号の種類や個数を保持したままpronに読みを取得してくれます:

>>> pyopenjtalk.run_frontend("おはよう!!!ございます?")
[
    {'string': 'おはよう', 'pos': '感動詞', 'pos_group1': '*', 'pos_group2': '*', 'pos_group3': '*', 'ctype': '*', 'cform': '*', 'orig': 'おは よう', 'read': 'オハヨウ', 'pron': 'オハヨー', 'acc': 0, 'mora_size': 4, 'chain_rule': 'C2', 'chain_flag': -1},
    {'string': '!', 'pos': '記 号', 'pos_group1': '一般', 'pos_group2': '*', 'pos_group3': '*', 'ctype': '*', 'cform': '*', 'orig': '!', 'read': '、', 'pron': '、', 'acc': 0, 'mora_size': 0, 'chain_rule': '*', 'chain_flag': 0},
    {'string': '!', 'pos': '記号', 'pos_group1': '一般', 'pos_group2': '*', 'pos_group3': '*', 'ctype': '*', 'cform': '*', 'orig': '!', 'read': '、', 'pron': '、', 'acc': 0, 'mora_size': 0, 'chain_rule': '*', 'chain_flag': 0},
    {'string': '!', 'pos': '記号', 'pos_group1': '一般', 'pos_group2': '*', 'pos_group3': '*', 'ctype': '*', 'cform': '*', 'orig': '!', 'read': '、', 'pron': '、', 'acc': 0, 'mora_size': 0, 'chain_rule': '*', 'chain_flag': 0},
    {'string': 'ござい', 'pos': '動詞', 'pos_group1': ' 自立', 'pos_group2': '*', 'pos_group3': '*', 'ctype': '五段・ラ行特殊', 'cform': '連用形', 'orig': 'ござる', 'read': 'ゴザイ', 'pron': 'ゴザイ', 'acc': 4, 'mora_size': 3, 'chain_rule': '*', 'chain_flag': 0},
    {'string': 'ます', 'pos': '助動詞', 'pos_group1': '*', 'pos_group2': '*', 'pos_group3': '*', 'ctype': '特殊・マス', 'cform': '基本形', 'orig': 'ます', 'read': 'マス', 'pron': 'マス', 'acc': 1, 'mora_size': 2, 'chain_rule': '動詞%F4@1/助詞%F2@1', 'chain_flag': 1},
    {'string': '?', 'pos': '記号', 'pos_group1': '一般', 'pos_group2': '*', 'pos_group3': '*', 'ctype': '*', 'cform': '*', 'orig': '?', 'read': '?', 'pron': '?', 'acc': 0, 'mora_size': 0, 'chain_rule': '*', 'chain_flag': 0}
] 

よってこのpron部分の値を音素列に手動で変換すれば、上述の3が実現できます。

あとは頑張ってこの2つを合わせる処理を書けば、求めるg2pの完成です。

詳しくは(自分用にも)コメントをいっぱい書いた
https://github.com/litagin02/Style-Bert-VITS2/blob/master/text/japanese.py
のファイルをご参照ください。

また、「もっとこうすると楽ができるよ」等の意見も募集しています。

以前存在していたバグ

以前のBert-VITS2では、上の方法を取ってはおらず、次のバグがありました。

  1. pyopenjtalk.run_frontendの結果の読みを音素リストに変換するときに、カタカナ読みをさらにpyopenjtalk.g2pしてしまっていたため、そこでおかしいことが起こっていた
  2. アクセントの取得方法が不正確で、単語の切れ目のところで情報がリセットされ、例えば「私は思う」のアクセントが「ワ➚タシワ オ➚モ➘ウ」が正しいところで「ワ➚タシ➘ワ オ➚モ➘ウ」になっていた(「私」と「は」部分が分けて処理されていたことでの弊害)

1については、例えばpyopenjtalk.g2pで「車両」の読み「シャリョオ」をさらにg2pかけると、

>>> pyopenjtalk.g2p("シャリョオ")
'sh a r i y o o'

で「シャリヨオ」になっている等がおかしさの原因でした。その後のアクセント処理部分はこの結果に影響されて、「車両」や「思う」等の単語があるとその後のアクセントが全て「0」になってしまっているバグがありました。

(このpyopenjtalk.g2pの挙動が意図したものなのかバグなのか自分にはわからないです)

モデルの構造の変更点

「モデルへの入力に音素列だけではなくBERTから取得した意味情報のようなものも入力することで、文章内容に応じた読み上げをしてくれる」という大枠は何も変わっていません。が、いくつかJP-Extra版では既存の2.1-2.3モデルとは変わっているところがあるので、それについて述べます。

前提となる大枠

https://zenn.dev/litagin/articles/8c6edcf6b6fcd6#共通する部分

ここをご参照ください。大枠を再掲すると、(Style)-Bert-VITS2 の音声生成(generator)部分は以下のコンポーネントからなります。

  • TextEncoder(テキストを受け取り、いろんな情報を返す)
  • DurationPredictor(音素間隔(ある文字(正確には音素)の発音の長さ)を返す)
  • StochasticDurationPredictor(DurationPredictorにランダム性をつけたやつ?)
  • Flow(声音、とくに声の高さの情報を持つ)
  • Decoder(いろんな情報から最終的に合成して声を出力、ここも声音情報を持つ)

さらに、学習の仕組みとしてはGANを使っているため、学習時にはGenerator(音声の生成)とDiscriminator(生成音声が本物か偽物か見分けるやつ)が訓練されます。以下が具体的に実際の学習で保存されるファイルに対応するコンポーネントです。

  • Generator: 上の構造をしていて、実際にテキストから音声を生成するもの。G_1000.pthファイルに保存されるやつ。推論にはこれのみ必要。
  • MultiPeriodDiscriminator: これが何か解説できる知識はありません。D_1000.pthファイルに保存されるやつ。メインのdiscriminatorで、HiFi-GANとやらとかで使われているらしいです。
  • DurationDiscriminator: 音素間隔(上のDurationPredictor等の出力)に対するdiscriminatorらしい。DUR_1000.pthファイルに保存されていたやつ。
  • WavLMDiscriminator: 後で詳しくは述べますが、WD_1000.pthファイルに保存されるもので、WavLMという音声のSSLモデルを使ったdiscriminatorのようです。

本家JP-Extra版での変更

本家JP-Extraは、ざっくりいうと、だいたいはBert-VITS2のver 2.3構造で、それにCLAPでのプロンプトによる出力制御を試みている2.2を付け加えたものです。

構造上重要そうな点は以下となります。

  1. ver 2.3で導入されたWavLMDiscriminatorの使用
  2. DurationDiscriminatorの削除
  3. gin_channelsというパラメータを2.3と同じく(2.1, 2.2の256から)512へ引き上げ
  4. CLAPを使ったプロンプトによる声音制御

1. WavLMDiscriminator

解説できるほどの知識も能力も勉強もしていないので、把握している点のみ書きます。

機械学習界隈ではSSL (Self-Supervised Learning) モデルという、ラベル無し大量データで学習されたモデルが有用のようです。これは、下流で他のタスクを実行する際の上流の基盤モデルとして使われるみたいです。

音界隈では以下が有名かもしれません:

この一番下のWavLMの、microsoft/wavlm-base-plus がBert-VIT2の2.3から導入され、JP-Extra版でも使用されています。

というわけで、自分には分かりませんが、このWavLMというSSLモデルをdiscriminatorに使うことで、さらにdiscriminatorの質を上げて音声の質を上げている、という感じだと思います。2.3や(Style-)Bert-VITS2 JP-Extraで新たにWD_*のようなファイルが事前学習モデルやら学習途中やらで見かけるようになりましたが、これがこれです。

2. DurationDiscriminatorの削除

(Style-)Bert-VITS2では以前からDurationDiscriminatorを導入していましたが、2.3以降(WavLMDiscriminatorとの相性?)から、音素間隔が少し不安定になる(間延びした感じになったり学習で安定しない)というものがありました。

よってJP-Extra版ではそれを踏まえて、このコンポーネントを使わないようにしています(よってDUR_0の名前の事前学習モデルは必要なくなった)。

その結果として、若干早口になるかなーという印象が少しあるかもしれませんが、全体的には音素間隔が落ち着いた感じになっていると思います。またこのDurationDiscriminatorの有無は設定で簡単に変えられるので、もしかしたら入れてみて実験すると何か分かったりするのかもしれません。

3. gin_channelsの増加

これも感想しか言えません。これは本家の2.3で行われた変更です。モデルの各種隠れ層の次元を一括で決めるgin_channelsというパラメータがあるのですが、これが256から512になりました。

以前のBert-VITS2 2.3についての感想では、ここを上げることでちょっと学習しきれないという微妙さが増えたんじゃないかという感想を抱いていましたが、JP-Extra版での成功を考えると、結果として(事前学習モデルに使われたデータ数の増加があるのかもしれませんが)次元を増やすのは良い変更だったんじゃないでしょうか(といっても256で実験したことはないので何とも言えないです)。

4. CLAP

これは本家Bert-VITS2のver 2.2で導入されたもの(2.3では消えたもの)です。CLAPという音声とテキストのペアを学習させたモデルを用いて、テキストプロンプト(Happy等)で出力音声を制御しようという試みです。

具体的には、CLAPモデル(音声・テキストそれぞれから特徴量ベクトルが取り出せる)についての埋め込みをTextEncoder部分に追加しています。

正直なところ、事前学習モデルレベルではこれは効果があったのかもしれませんが、そこからファインチューンして使う実用上では、その効果はほぼ消えてしまい、プロンプトでの音声制御はほぼできない、という体感です。

Style-Bert-VITS2 JP-Extraでの変更

以上の踏まえて、Style-Bert-VITS2ではJP-Extra版の構造を少し変更して使えるようにしています(旧バージョンでもこれまで通り音声合成や学習はできます)。

具体的には本家JP-Extraに以下の変更をしています(これは実質、本家ver 2.1 → Style-Bert-VITS2の変更と同じです)。

  1. CLAP部分を削除
  2. スタイルによる声音制御のため、この音声からの特徴量の埋め込みに関する全結合層をTextEncoder部分に追加(音声合成時はこのベクトルを与えることでスタイル・声音を制御)
  3. アクセントを手動で制御する仕組みの実装。これによって誤ったアクセントになってしまっている単語のアクセントを修正でき、万能ではありませんが自然性が増すことがあります。これは単に上述のg2p部分に対してアクセント列をWebUI上で手動で指定できるようにしたものです。

また相変わらずsafetensors形式にしたり使いやすくしたりみたいな改造はしています。

まとめ

みんなStyle-Bert-VITS2使ってね!データセット作りのツールも同梱しており、またインストールにPythonの環境構築等も不要で、グラボさえあれば簡単に学習できるよ!自分だけのオリジナル音声合成モデルを作ろう!

https://github.com/litagin02/Style-Bert-VITS2

Discussion