Spherical Average (多点間の球面線形補間)について
こんにちは。GokRackこと極落にんじんです。
今回は、私が Beatrice VSTの2.0.0-beta.3のアップデートとして話者のモーフィング機能 (Voice Morphing Mode)を実装したときに用いた Spherical Average (多点間の球面線形補間) についてのお話です。
背景
私が Beatrice VST に興味を持ってソースコードを眺めたのは Ver.2.0.0-beta.2 の頃なのですが、その当時のVSTのソースコードを読み解く限り、 Beatrice は話者の特徴量を 256 次元の Embeddings で保持しており、話者切り替えは推論時に与える 256 次元のベクトルの値を複数切り替えることで実現されているということが読み取れました。
そこで、試しに VSTのソースコードに手を加えてそれら話者ごとの特徴量ベクトルの加重平均をとって推論させてみたところ、上手いこと話者の特徴が混ざったような変換結果が得られました。
その結果に気を良くして VST の UI などの改造も加えて prj_beatrice さんにプルリクを送ったところ、色々修正しつつも無事に採用されて 2.0.0-beta.3 としてリリースされたという流れになっています。
Spherical Weighted Average
私が最初に prj_beatrice さんにプルリクを送った時点では、話者モデルの加重平均手法としては単純な線形補間 (Lerp)を用いていたのですが、それに対して prj_beatrice さんから「球面線形補間(Slerp)も検討したい」というような反応を頂きました。
ただ、 Slerp は2つのベクトル同士の補間を行うことは出来ますが、Lerpと異なり3つ以上のベクトル同士の補間は出来ません。そこで多点間で使えるように Slerp を拡張した手法は無いだろうかと探してみたところ、Spherical Averages という論文を見つけました。
この論文中の Spherical Weighted Average を実装したところ、聴いてみた感じ変換結果も良くなった(ように感じられた)ため、最終的にこちらを採用しました。
Spherical Weighted Average は、単位超球面上のベクトル
アルゴリズム A1 は1次収束のアルゴリズムで、ざっくり説明すると
- 加重平均ベクトル
の初期値q として、Lerpによる計算結果のベクトルを単位超球面上まで伸ばしたものq_0 を用いる。q_0 = \frac{ \sum_i w_i p_i }{\| \sum_i w_i p_i \|} -
回目の反復計算において、t から見た各q_t の方向と距離が等しくなるような、p_i における超接平面上の点q_t を求める。l_{q_t}(p) -
の加重平均l_{q_t}(p) を計算し、その点に対応する元の単位超球面上の点\sum_i w_i l_{q_t}(p_i) で q の値を更新する。q_{t+1} = \exp_{q_t}\left(\sum_i w_i l_{q_t}\left(p_i\right)\right) -
が十分小さくなったら反復計算を終了する。小さくなかったら 2. に戻る。\|\sum_i w_i l_{q_t}(p_i) - q_t\|
といったものになっています。
なお、

元論文より、
アルゴリズム A2 は A1 の 3. の部分を
- ヘッセ行列
を用いてH として値を更新する。q_{t+1} = \exp_{q_t}( q_t + H^{-1}(\sum_i w_i l_{q_t}(p_i)-q_t))
というふうに変更することにより、収束速度を二次収束に早めたものになっています。
ただ、A2ではヘッセ行列の算出に回転行列が登場するのですが、元論文で扱っている2次元や3次元の場合はともかく、今回のような M 次元の空間での回転行列がちょっと難しそうだったこと、また行列積演算や逆行列演算が必要となるがその実装はちょっと面倒だし1反復あたりの演算量が大きくなることからリアルタイム演算には向かないだろうと考え、2.0.0-beta.3 時点でのBeatrice の話者マージでは A1 を実装して使用しました。
アルゴリズム高速化検討
2.0.0-beta.3 時点ではこのアルゴリズム A1 でも問題なさそうだったのですが、2.0.0-rc.0からは話者特徴量を大幅に増やすとのことで、ちょっと計算量的に厳しくなるような気がしてきました。
なので、もっと速く求めるアルゴリズムは無いかと考えてみました。
このアルゴリズム A1 を眺めてみると毎回の反復で
なんとかこの行ったり来たりを無くせないでしょうか。
そこで大元の最適化問題に立ち返ってみます。
単位超球面上
と表すことが出来ます。
この問題は q が単位超球面上にある (
この目的関数
と意外と簡単に計算できそうな形をしています。
そこで Riemann 多様体上の最適化問題用に修正した最急降下法やL-BFGS法を python で実装し、計算時間と計算結果を比較したところ、下記のような結果が得られました。
-
計算条件
- ベクトル数
N = 256 - 次元数
M = 256 - 重み
: 0~1 の一様乱数によって計算したあと、w_i となるように正規化\sum_i w_i = 1 - ベクトル
: 平均 0 標準偏差 1 の正規分布乱数によって計算したあと、p_i となるように正規化\|p_i\| = 1 - 異なる重みとベクトルについて 1000 回計算し、その平均所要時間を算出
- 計算には 32bit float を使用(Beatrice VST で用いている話者特徴量が 32bit float なので)
- そのため、
程度の演算誤差が見込まれる。10^{-7}
- そのため、
- ベクトル数
-
計算環境
- OS : Windows 11Pro 24h2
- CPU : Ryzen 5600X
- Mem : DDR4-2933 32GB
- Python 3.12.6, numpy 2.2.0
-
結果
- アルゴリズム A1
- 平均反復回数 : 89.16 回
- 平均所要時間 : 7.72 ms
- 1反復あたりの平均所要時間 : 0.0866 ms
- 最急降下法
- 平均反復回数 : 44.94 回
- 平均所要時間 : 1.89 ms
- 1反復あたりの平均所要時間 : 0.0421 ms
- A1 の結果と比較しての q と v の最大誤差 : 1.788139e-07
- L-BFGS法
- 平均反復回数 : 5.06 回
- 平均所要時間 : 0.43 ms
- 1反復あたりの平均所要時間 : 0.0859 ms
- A1 の結果と比較しての q と v の最大誤差 : 3.129244e-07
- アルゴリズム A1
なお、実装した python スクリプトは下記場所に置いております。
最急降下法、L-BFGS法ともに、元アルゴリズム A1 に比べて圧倒的に高速化出来てます。
得られた値の誤差についても 32bit float の演算誤差と同程度に収まっているため、解の精度にも問題はなさそうですね。
最急降下法では A1 に比べて超接平面への射影などが減ることから1反復あたりの所要時間が減るだろうなとは予想していたのですが、反復回数それ自体が半分くらいに減るのは嬉しい誤算でした。
L-BFGS法については、最急降下法より1反復あたりの所要時間は増加するだろうとは思っていましたが、増えても A1 と同程度であることからそのまま A1 を L-BFGS法で置き換えてしまっても問題なさそうに感じます。
Appendix.
Spherical Weighted Average の話者特徴量のモーフィングへの適用方法
Spherical Weighted Average は複数の単位ベクトル同士の間で球面上の加重平均を求める手法である一方、Beatrice の話者特徴量は必ずしも単位ベクトルではありません。
そこで 2.0.0-beta.3 の話者モーフィング機能では、
- Beatrice の話者特徴量ベクトル
と同じ方向の単位ベクトルp_i^{\prime} p_i = p_i^{\prime} / \|p_i^{\prime}\|
と話者ごとの重み係数 (w_i を満たす)を用いて Spherical Weighted Average によって加重平均ベクトル\sum_i w_i = 1 を求めるq -
となるような重み係数\sum_i v_i p_i = q を求めるv_i -
を計算してモーフィング後の話者特徴として使用する\sum_i v_i p_i^{\prime}
と言った流れで話者特徴量ベクトルをモーフィングしています。[1]
この
まず、先述の単位超球面の点 p を点q における超接空間に射影する関数
また、論文よりうまく収束した場合は
このことから
両辺を
として求められます。[4]
2点間の場合の Spherical Average と Slerp の一致確認
念の為、Spherical Weighted Average がちゃんと Slerp の多点間拡張に相当するのかどうかを確認するために、2点間の Spherical Weighted Average と Slerp の比較を考えます。
2点の場合、重み係数は
また、Slerp の場合、
この
となるのですが、これは Slerpの定義式に一致するため、
このとき、
となることから
よって、2点間の Spherical Weighted Average は Slerp と同じ解に収束するため、両者は同じものであると扱ってしまっても良さそうです。
他に誰か実装してないの?
ふと、元論文以外で他に誰か実装してないかなと気になって少し探してみましたが、いつくか見つかりました。
流石に高速化まではしてなくて、どれも大体アルゴリズム A1 の実装してるだけではあるようですが。
見た感じアルゴリズムA1っぽいですかね?
geoopt というライブラリは知りませんでしたが、色々便利そうですね…。[5]
割と最近の記事で、neural networks 向けということで今回のケースと同じモチベーション由来でやってるみたいですね。
こちらも元論文のアルゴリズム A1 を実装しているみたいです。
mergekitにも、multi-slerpという名前で収録されているように見受けられます。
こちらは実装スクリプトを見ると、どうやらアルゴリズムA1の反復計算を1回だけ計算してそれで止めているように見えますね…。
スクリプト中に含まれるreference_urlのリンクが消えちゃってたりしてて詳細はわかりませんが、まぁLLMのモデルマージという目的を考えると、収束するまで回さなくても1反復だけで精度的には十分という話なのかも知れません。[6]
-
Slerp を用いたモデルマージでも、ベクトル
を単位ベクトル化したベクトルp' を用いて求めた重み係数p をもとのに適用して加重平均をとっていることに相当するため、このような流れで大丈夫だろうと判断しました。 ↩︎v -
実際に計算するときは
関数(\mathrm{sinc} )が使えるならそっちを使って計算したほうが良いです。\mathrm{sinc}\theta = \frac{\sin\theta}{\theta} 近傍での計算精度の担保が結構面倒なので。 ↩︎\theta=0 -
厳密にはここで
であることを確認する必要がある気がしますが、加重平均を取るベクトルの数 N がベクトルの次元数 M 以下の場合は多分大丈夫じゃないかと思います。 ↩︎\sum_n\frac{w_n \theta_n \cos \theta_n}{\sin\theta_n} \neq 0 -
ただし、この値を用いることができるのは平均を取る点の数 N がベクトルの次元数 M 以下の場合に限られます。
N > M の場合は本質的に劣決定問題となり、 を満たす\sum_i v_i p_i = q が一意に定まらず無数に存在し、v_i の値が\sum_n\frac{w_n \theta_n \cos \theta_n}{\sin\theta_n} に近くなったりするため上記の式で計算される0 が異常に大きな値になったりしてしまいます。v_i
その場合は、 を並べた行列p_i を用いて、P の最小ノルム解Pv = q を採用する必要がありそうな気がします。逆行列演算が出てくるんで正直VSTに組み込むのはめんどくさいんですが…。 ↩︎v = P^T(PP^T)^{-1} q -
今回は最終的に VST 向けに C++ で実装するための原理確認という目的もあって自力実装を頑張ったという面もあるので、知ってても多分使わなかった気がしますが。 ↩︎
-
Beatriceでも収束するまで回す必要は無いんじゃないか説は大いにあり得ると思っていて、計算量の増加具合では最急降下法を1反復で止めたりL-BFGS法を2反復ぐらいで止めるのもありかなーとは思ってます。最悪、
をそのまま使うのもアリ。 ↩︎q_0
Discussion