Style-Bert-VITS2の差分マージで遊ぶ
はじめに
オープンソースな日本語音声合成 (TTS) であるStyle-Bert-VITS2の2024-06-16のver 2.6.0で、マージ機能に差分マージやヌルモデルマージが追加されました。
この記事は、これらのマージでできることをいろいろ紹介して、みんな実験して共有してみてね、という記事です。
注意
- 日本語特化版 (JP-Extra版) とそうでないモデル同士のマージはできません。
- このページで共有している結果やモデルは全てJP-Extra版のものです。
差分マージとは?
通常のマージについて
SBV2では、今まで以下の点で2つのモデルをマージすることができました:
- 声質(誰が喋っているか)
- 声の高さ
- 話し方・感情表現
- 話すリズム・テンポ
具体的には、2つのモデルを A
, B
とすると、スカラー weight
について、
new_model = (1 - weight) * A + weight * B
を、上記4つの要素が入っていると思われるモデルの重みについて計算してやって新しいモデルを作ることをやっていました。式から、weight = 0
でA
が、weight = 1
でB
が、その間では両方を混ぜたようなものができます。
これによって、例えば「A
の声のまま、喋り方だけB
に」「声はA
とB
の中間、話し方はA
だけど声の高さだけB
に寄せる」みたいなモデルを作れるわけです。
このマージ機能等含め、Style-Bert-VITS2については、リポジトリREADMEや、前に投稿した動画 をご参照ください(ただし動画からバージョンをいくつか上げており、使い方や画面等は少し現在のバージョンと異なっている箇所があるのでご注意ください。)
差分マージ
2.6で追加された差分マージは、画像生成の文脈でAdd differenceのマージと呼ばれていたものに着想を得ていて、3つのモデル A
, B
, C
とスカラー weight
から次のように新しいモデルを作ります。
new_model = A + weight * (B - C)
これはつまり元のモデルA
に、B - C
的な何かを足すということをやっています。
この差分マージのような考え方や有効性や歴史がどのような経緯かは分かりませんが、例えば B
と C
が「似たようなモデルなんだけど B
はC
に加えてある追加要素が学習されているモデル」であるようなときに差分マージが有効であることが、音声AI以外いくつかの分野で経験的に知られているようです。
画層生成AIの場合
あまり詳しくはありませんが、画像生成AIでのStable Diffusionのマージ沼の文脈で、例えば次の3つのモデルがある状況を考えます。
-
A
: 強化したいモデル -
B
: あるA
とは別の絵柄で、NSFWな絵も出るモデル -
C
:B
と同じ絵柄なんだけど、NSFWな絵が出ないモデル
この場合、差分の B - C
は NSFWな絵を出す能力を抽出したベクトルのようなものだとみなすことができ、上述のように A
に B - C
を足してやると、Aの絵柄のままNSFWな絵に強いモデルができる、ということがあるようです。
LLMの場合
また最近では、LLMの文脈でChat Vectorというものがあるらしいです。この場合は、次の3つのモデルを考えます。
-
A
: 強化したい言語モデル -
B
: 下のモデルC
にチャット機能が強くなるよう学習させたモデル -
C
: あるモデル
この場合、差分の B - C
はチャットができるような能力ベクトルのようなものだみなすことができ、上述のように A
に B - C
を足してやると、なぜか A
のチャット機能が強化される、というもののようです。
音声の差分マージで何ができるか?
差分マージのアイデア自体は単純に重みを足し引きするだけなので、SBV2含む音声モデルにも簡単に実装することができます。
しかし実際それがどのようなことに使えるのかについては、まだ手探りでよい活用法をみなさんに教えてもらいたいという事情はありますが、この記事ではいくつかの差分マージの使い方を紹介し、実際に適応に使えるヌルモデルを共有します。
まず、(他に使い方があるかもしれませんが)状況を簡単にするため、今回の記事では以下のような種類の差分マージを考えることとします。
-
A
: あるSBV2モデルで、これをもとにいろいろと改造したいモデル -
B
:A
とは別のある話者のデータで学習されたモデル -
C
:B
と同じ話者のデータで学習されたモデル
ここで B
と C
は同じ話者のデータで学習されたモデルですが、話し方等、何らかの意味で音声データの質に差があるものとします。そうすると B - C
は C
を基準としたときの B
の特徴を表すベクトルだとみなすことができ、それを A
に足すことで、うまいことその差分の要素のみを A
に付加したモデルを作ることができるのではないかと考えられます。
マージ元となるモデル
以下では具体的にこれらを適応してみたパターンをやってみます。またここではもととなるモデル A
として、kokuren さんが提供している 凛音エルちゃんのモデルを使ってみることとします。
サンプル音声は以下のとおりです(川端康成「雪国」冒頭)。
ささやきにしてみる
次に、このエルちゃんのモデルをささやき声で話すモデルにしてみることを考えます。
この場合、例えば次のように差分マージを取れば実現できそうだと考えられます:
-
A
: 凛音エルちゃん -
B
: ある話者のささやき声コーパスで学習されたモデル -
C
:B
と同じ話者の通常の発話のコーパスで学習されたモデル
実際、そのような B
と C
は、通常音声とささやき音声の両方をコーパスが公開されているデータがいくつかありますので、そこから学習させて作ることができます。
(例えば東北ずん子・ずんだもんプロジェクトのマルチモーダルデータベースでの各種話者のコーパスやあみたろ様のITAコーパスなど、他にも探せばいろいろあります。)
すると B - C
がちょうど「通常の発話を基準としたときの、ささやき音声の特徴を表すベクトル」とみなすことができるので、差分マージでエルちゃんをささやきにできるのではと予想できます。
実際にやってみた結果が以下のようになります。
ここで差分マージ時の weight
は「声質」「声の高さ」「話し方」「話すリズム・テンポ」の4点で指定でき、前半は重みを少し少なめ、後半は重めにしています。また4つの数値のバランスをいろいろ変えると微妙にニュアンスが変わるので試してみるとよいかもしれません(実際に試すのに使えるヌルモデルは後半で公開していますが、自分で簡単に作ることもできます)。
また、上の差分マージでのささやきは無声ささやきでしたので、有声ささやきを B
とすることで、完全なささやきではない「ヒソヒソ声」のモデルも、以下のように差分マージで作ることができます。
元となる最初の動画と比べると、エルちゃんのかわいい声はそのままで、ヒソヒソ感がきちんと出ているのが分かると思います。
棒読みにしてみる
次は、以下のように B
と C
を定めます。
-
B
:C
のデータセットを機械的に抑揚をゼロにした、音の上げ下げがゼロの音声データで学習させたモデル -
C
: あるデータセットで学習させたモデル
つまり、ある音声データセットがあったとして、それで学習させたモデルを C
とし、一方で C
の音声データを機械的に抑揚無しにして作ったデータを B
としています。すると B - C
は抑揚の無さを表すベクトルだと思えそうなので、差分マージにより エルちゃんの声の抑揚を皆無にすることが可能となります。
次の動画で具体的に聞いてもらうのが早いと思います。
見事に音の高低が消えた棒読みすぎる音声になっています。もちろんパラメータを中間にすれば、棒読み度合いを調整できます。
ここで、どのように機械的に抑揚をゼロにするかについては技術的なので、興味ある方は以下のトグルを参照して、自分でもお試しください。
抑揚を消す方法
実は抑揚や音高制御機能はSBV2にあるので、そのコードを流用できます。具体的には pyworld をそのまま使ってやっているだけです。つまり、声のf0を計算し、平均をとって、その平均を元にf0曲線をいじって抑揚が変換できますが、その抑揚 intonation_scale
を 0
にすれば、棒読みになります。詳しい仕組みは自分でpyworldを調べてググってください。
import numpy as np
import pyworld
from numpy.typing import NDArray
from typing import Any
def adjust_voice(
fs: int,
wave: NDArray[Any],
pitch_scale: float = 1.0,
intonation_scale: float = 1.0,
) -> tuple[int, NDArray[Any]]:
wave = wave.astype(np.double)
f0, t = pyworld.harvest(wave, fs)
sp = pyworld.cheaptrick(wave, f0, t, fs)
ap = pyworld.d4c(wave, f0, t, fs)
non_zero_f0 = [f for f in f0 if f != 0]
f0_mean = sum(non_zero_f0) / len(non_zero_f0)
for i, f in enumerate(f0):
if f == 0:
continue
f0[i] = pitch_scale * f0_mean + intonation_scale * (f - f0_mean)
wave = pyworld.synthesize(f0, sp, ap, fs)
return fs, wave
この関数を使って、例えば次のようにデータセットを一気に変換すればよいです。
from pathlib import Path
import soundfile as sf
def export_adjusted_voice(
input_audio: Path, output_audio: Path, intonation_scale: float = 0.0
) -> None:
audio, sr = librosa.load(input_audio, sr=None)
sr, flattened = adjust_voice(
sr, audio, pitch_scale=1.0, intonation_scale=intonation_scale
)
sf.write(output_audio, flattened, sr)
抑揚を大げさにしてみる
次は、上の棒読み化差分マージでの B
と C
の役割と入れ替えてみることを考えます。つまり、以下の差分マージ A + weight * (B - C)
を考えます
-
B
: あるデータセットで学習させたモデル -
C
:B
のデータセットの抑揚を機械的にゼロにしたデータセットで学習させたモデル
すると B - C
は抑揚をつけるということを表す差分のようなものだと考えられ、通常のモデル A
に差分マージすることで A
の抑揚を強調することができるのではと予想されます。(またこの差分マージは、上述の棒読み声差分を負の重みで適応してやったもの、と思うことができます。(画像生成AIでflat LoRAをマイナス適応すると描き込みが大きくなるアレを思い出しますね))
これを実際にエルちゃんでやってみると次のようになりました。最初の動画と聴き比べると、確かに抑揚が大きく強調されて発声されているのが分かります。
上の棒読みの場合もですが、「声の高さ」と「話し方」の2つのパラメータのみが今回の差分マージでは重要なようで、上の動画を聞いていただければ「話し方」のみを差分マージすると自然に抑揚が大きく、「声の高さ」もさらに上げると抑揚はより大きくなるが少し不自然に、というふうになるようです。
ヌルモデルマージ
差分マージの場合は、変更したいモデル A
に加えて2つのモデルが必要でした。が、その差分のみをあらかじめ取り出しておいて、それを後から A
に加えられるようにしたものがヌルモデルマージと呼んでいるものです。
これは、次の式によりマージされます。
new_model = A + weight * B
ここで B
として通常のモデルを使ってしまうと声がぶっ壊れるモデルができますが、SBV2の加算和機能によりあらかじめ B - C
というモデル(ヌルモデル)を保存することができ、それを上記の B
として使ってやれば、実質的に差分マージ new_model = A + weight * (B - C)
を実現することができます。
このように差分を保存しておくことで、それを後から好きにマージして味付けすることができます。またこのヌルモデルはそれ単独では何の音声合成もできず、またその学習に使われた話者についての情報を持っていません(なので共有が気軽にできると思います)。
今回の上の実験で使ったモデルの差分の重みは下で共有しているので、ヌルモデルマージにてお試しください:
日本語特化版 (JP-Extra)のモデルにしか使えません。
また、差分マージの他の面白い使い方や結果があれば、ぜひ共有ください(ヌルモデルの共有もお待ちしております)。
Discussion