#音声処理#データ可視化#話者埋め込み#PCA

アンカー分布を可視化して「声の地図」を作る


声質変換のアンカーを70件も集めると、今度は逆の悩みが出てきます。「この声のライブラリ、ちゃんと満遍なく揃っているのか?」。低い男性声ばかり増えていないか、高く明るい女性声に空白はないか、似た声ばかり重複していないか——。

1つ1つのアンカーの品質は、別記事で書いた選定フィルタや品質監査で担保できます。でも「群として」偏っているかどうかは、個々を見ていても分かりません。必要なのは俯瞰、つまり 声のライブラリ全体を1枚の地図にすることです。

この記事は、声質変換アプリのアンカー群を2つの図で可視化して、カバレッジと偏りを目で確かめられるようにした話です。モデルは不要で、numpy と matplotlib だけで描けます。

何を可視化したいのか:2つの空間

声のアンカーには、性質の異なる2つの「空間」があります。

  1. 意味軸の空間 — このアプリはユーザーが声を「年齢感」「性別」「声の高さ」「体格」「ハスキー度」「明瞭度」「温かさ」「粗さ」の8軸(0〜100%)で調整できます。各アンカーがこの8軸上のどこにいるか。これは 設計空間のカバレッジを表します。
  2. 話者埋め込みの空間 — campplus の192次元ベクトル。声質そのものの分布で、似た声は近くに、違う声は遠くに配置されます。これは 声質空間の広がりを表します。

前者は「ユーザーが操作できる軸で、選べる幅が足りているか」、後者は「純粋な声質として、どこが密でどこが疎か」を教えてくれます。両方を1枚に並べて描くことにしました。

(A) 意味軸のカバレッジ:ストリッププロット

まず8軸それぞれについて、全アンカーの値を横一列に散らします。軸ごとに1本のストリップを描き、点が縦に重ならないよう微小なジッターを加え、平均を赤い縦線で重ねます。

for i, key in enumerate(axes_keys):
    vals = np.array([sv[n][i] for n in names])
    y = np.full_like(vals, i, dtype=float) + (rng.random(len(vals)) - 0.5) * 0.5
    axA.scatter(vals, y, s=18, alpha=0.55, color="#3a76b4")
    axA.scatter(vals.mean(), i, s=90, marker="|", color="crimson", zorder=3)  # 平均

この図の読み方はシンプルです。点が 0〜100% に散らばっている軸はカバレッジが良く、片側に固まっている軸は空白がある。たとえば「性別」で点が両端まで伸びていれば男性的〜女性的まで選べるということ。逆に平均が大きく偏っていたり、片側に点が無ければ、その方向のアンカーが足りていないと一目で分かります。

「まだ○○な声が足りない」という感覚を、軸上の空白として具体的に見せられるのがこの図の価値です。

(B) 声質空間の散布図:PCAで2次元に落とす

もう1枚は、192次元の話者埋め込みを2次元に圧縮した散布図です。特別なライブラリは使わず、平均を引いてから SVD をかける素朴な PCA で第1・第2主成分を取ります。

X = np.asarray(bank.embeddings, float)
Xc = X - X.mean(axis=0)
U, S, Vt = np.linalg.svd(Xc, full_matrices=False)
Z = Xc @ Vt[:2].T                               # 先頭2主成分に射影
ev = (S[:2] ** 2) / (S ** 2).sum() * 100        # 各主成分の寄与率[%]

各点(アンカー)には話者名を添え、性別軸の値で色付け(青=男性的 / 赤=女性的)します。埋め込みという抽象的な空間に、人間が解釈できる意味の軸を1本重ねるわけです。

gi = list(axes_keys).index("gender")
gcol = np.array([sv[n][gi] for n in names])
axB.scatter(Z[:, 0], Z[:, 1], c=gcol, cmap="coolwarm", s=40, ...)

この散布図では、密集した塊(似た声が重複している領域)と、ぽっかり空いた領域(声質の空白)が見えます。色付けのおかげで、埋め込み空間の主軸が性別とどう対応しているかも読み取れる。軸ラベルには寄与率(PC1 (xx%))を出しているので、この2次元でどれだけ元の分散を説明できているかも添えられます。

実装上の地味な工夫

派手さはありませんが、可視化を「使える」ものにするための細部があります。

  • 日本語フォントの明示的な選択。matplotlib は放っておくと日本語が豆腐(□)になります。Hiragino Sans など候補を順に試し、見つかったものを使う関数を先頭に置きました。話者名やラベルが読めなければ地図の意味が半減します。
  • ラベルは対応表から引くanchor_sources.json があれば spk24 を話者名で表示し、無ければ spk 名にフォールバックします。別記事のラベリングがここで効いてきて、番号ではなく名前で分布を語れます。
  • モデル非依存。この可視化は埋め込みとスライダー値さえあれば描けるので、重い Seed-VC のロードが不要です。numpy と matplotlib だけ。CI や手元でさっと回せる軽さは、可視化を習慣化するうえで地味に効きます。

図を「意思決定」につなげる

可視化はゴールではなく、次の一手を決めるための道具です。この2枚の地図から、実際にこういう判断ができます。

  • (A)で片側に空白のある軸 → その方向の声(例: もっと年配の声、もっとハスキーな声)を優先的に集める
  • (B)で密集した塊 → 似た声が重複しているので、これ以上そこは増やさない
  • (B)で疎な領域 → 声質のカバレッジの穴。ここを埋める素材を探す

「なんとなく足りない気がする」を「この軸のこの範囲が空いている」に変換できると、素材集めが当てずっぽうでなくなります。声のライブラリを設計対象として扱えるようになる、というのが可視化の一番の効用でした。

まとめ

  • アンカー群を 2つの空間で可視化した: (A)8つの意味軸のカバレッジ、(B)話者埋め込み192次元のPCA散布図
  • (A)ストリッププロットで各軸の散らばりと平均を見て、選べる幅の空白を発見する
  • (B)は平均除去+SVDの素朴なPCAで2次元化し、性別軸で色付け。密集(重複)と空白(穴)が見える
  • 日本語フォント指定、対応表からのラベル復元、モデル非依存の軽さが「使える可視化」の条件
  • 図の目的は俯瞰ではなく意思決定。空白のある軸・疎な領域を、次に集める声の指針にする
← 記事一覧へ