日本語x-vectorから感情成分を分離するニューラルネットワークを構築してみた −感情分類に敵対的な損失関数の導入−

はじめに

本記事は前回記事の続編に相当する.

前回記事では声優統計コーパスの3話者・3感情の音声データに対してx-vector抽出器を適用し,UMAPで可視化を試みた. この可視化の実験を通じて,感情成分が分離できていない傾向が見られた.すなわち,本来は話者3クラスにも関わらず,疑似的な9クラス(= 3話者 × 3感情) が存在するように見える,というものである(x-vector抽出器の学習データを考えてみれば,それはそうなのだが).せっかくx-vectorが手元にあるのだから,感情成分を分離/除去するフィルタの役割を果たす手法を実装してみたいと考えた.本記事はその実装の詳細と簡単な検証実験に関する報告である.

感情成分を分離するニューラルネットワーク

先行研究と論文

今回の実装にあたり下記の論文を参考にした.本論文では,音響特徴量(ベクトル系列)に含まれる話者成分とテキスト情報を表す成分を分離するよう,損失関数に制約が加えられたニューラルネットワークによってnon-parallelな音声変換を実現している.この「損失関数とネットワーク構造を工夫することで特徴量に含まれる特定の成分を分離する」というコンセプトが, 今回のx-vectorを用いた感情成分の分離にも使えるはずだ,という個人的な直感があった.

Jing-Xuan Zhang, Zhen-Hua Ling, and Li-Rong Dai. 2020. Non-Parallel Sequence-to-Sequence Voice Conversion With Disentangled Linguistic and Speaker Representations. IEEE/ACM Trans. Audio, Speech and Lang. Proc. 28 (2020), 540–552. https://doi.org/10.1109/TASLP.2019.2960721

ネットワーク構造,損失関数およびモデル訓練アルゴリズム

ネットワーク構造の概略を図1に示す.

図1: ネットワーク構造

ねらい

仮にx-vectorに話者分類器をくっつけてsoftmax層の直前のベクトル( logitではない)を取り出せば,感情成分は話者ラベルに丸めこまれる形となって,見かけ上で感情成分は消えてしまう.しかし,それでは面白くない.話者分類器を手元のx-vector群に関してファインチューニングすると,x-vectorに埋め込まれていることが期待される多様な発話スタイルを含む話者特徴が消えてしまうおそれがある.逆に感情分類器のみをくっつけると,今度はもとのx-vectorを忘れてしまう.つまり感情分類器は入力がx-vectorいかんに関わらず構築できるため,話者特徴が消えてしまい,本来の目的から遠ざかる. そこでまず,話者成分と感情成分をそれぞれ独立に抽出するencoderを用意する(speaker encoderとemotion encoder).各encoderで抽出された各成分をdecoderに通し,もとのx-vectorを復元する制約を付与する.これによりencoder出力とx-vectorとの間の暗黙的な相互情報量の最大化を期待している.emotion encoder側には感情分類器(線形層 + softmax)をくっつけることで,感情成分の抽出を促進するねらいがある. 一方,speaker encoder側にも,補助の感情分類器(auxiliary classifier)をくっつける.ただしこのclassifierが強くなりすぎるとspeaker encoderの出力に感情成分が混在してしまうので,auxiliary classifierの感情分類に敵対的な損失関数(adversarial loss)を導入し,感情成分の分離・除去を促進する.つまり,もし仮に話者成分と感情成分が混在したx-vectorから感情成分がすっかり除去されれば,auxiliary classifierの予測はすべて等確率(3感情の分類ならばすべて1/3)になるはずだという直感のもと,敵対的損失関数により感情の予測分布が一様分布へと近づくようにclassifierおよびencoderを成長させる.

損失関数たち

図1より,以下の損失関数を考える:

  • $\mathcal{L}_{rec}$ : encoder-decoderによる再構成損失(平均二乗誤差)
  • $\mathcal{L}_{enc}$ : emotion encoder側の感情分類に関する損失(交差エントロピー
  • $\mathcal{L}_{cls}$ : speaker encoder側の感情分類に関する損失(auxiliary classifierの交差エントロピー
  • $\mathcal{L}_{adv}$ : speaker encoder側の感情分類に関する敵対的損失

$\mathcal{L}_{adv}$の計算には選択の余地がある:

(1) 一つは auxiliary classifierに関する感情クラスの予測分布を$\hat{\mathbf{p}}$とし,一様分布を表すベクトルを$\mathbf{u}$とするとき,$\hat{\mathbf{p}}$と$\mathbf{u}$の平均二乗誤差を損失に設定する方法である.その場合の損失は次式で書くことができる:

$$ \mathcal{L}_{adv} = \frac{1}{N} \sum_{i = 1}^{N} ||\hat{\mathbf{p}}^{(i)} - \mathbf{u}||_{2}^{2} $$

ただし$N$はミニバッチのサイズであり,一様分布を$\mathbf{u} = [1/C, 1/C, \ldots, 1/C]$というベクトルで表現している.$C$は感情クラスの数を表し,今回は$C = 3$である.したがって$\mathbf{u} = [1/3, 1/3, 1/3]$である.$\hat{\mathbf{p}}$の肩に書いてある添字はミニバッチ内の$i$番目のサンプルに関して計算した予測であることを明示する.平均二乗誤差の最小化により,予測分布を一様分布に近づける,すなわちどの感情クラスも均等に分類を誤るようauxiliary classifierに制約を課す.

(2) もう一つの選択は$\mathcal{L}_{cls}$の符号を反転したものを$\mathcal{L}_{adv}$に設定する方法である.すなわち$\mathcal{L}_{adv} = -\mathcal{L}_{cls}$とする.これはauxiliary classifierが相反する損失関数を持つという意味で明解である.後述する実装ではこれら敵対的損失をオプションで切り替えられるようにした.

損失関数に基づく訓練アルゴリズム

ネットワーク全体のうち,auxiliary classifier以外の部分を"main"と称する.

  1. x-vectorをネットワークに流すことで$\mathcal{L}_{rec}$ , $\mathcal{L}_{enc}$, $\mathcal{L}_{cls}$, $\mathcal{L}_{adv}$を計算する.

  2. "main"の損失$\mathcal{L}_{\mathrm{main}} = \mathcal{L}_{rec} + \mathcal{L}_{enc} + w_{adv} \mathcal{L}_{adv}$を計算し,この損失関数に基づいて勾配を流しネットワーク重みを更新する.その間,auxiliary classifierのネットワークパラメタは固定(freeze)する.ここで$w_{adv}$は$\mathcal{L}_{adv}$の影響を調整する重み係数である.

  3. auxiliary classifierのパラメタの固定を解除し,逆に"main"のパラメタを固定する.$w_{cls} \mathcal{L}_{cls}$に基づいて勾配を流し,auxiliary classifier のパラメタを更新する.その後,"main"のパラメタの固定を解除する.ここで$w_{cls}$は$\mathcal{L}_{cls}$の影響を調整する重み係数である.

  4. 指定回数だけ1. から 3. を繰り返す.

実装

以下のリポジトリに置いた.Enjoy!

github.com

動かし方

  1. config.yaml を各自の環境に合わせて修正する.root_dirの修正のみで十分と思われる.

  2. $ python preprocess.py: 前処理を実行する. 具体的には,声優統計コーパスのダウンロード(および解凍),x-vector抽出のための事前学習済モデルのダウンロード,および声優統計コーパスの音声群からx-vectorを抽出して保存する処理からなる.

  3. $ python training.py: モデルの訓練を実行する.

  4. $ python inference.py: 訓練済みのモデルにx-vectorを通すことで,感情成分が除去されたx-vectorを手に入れ,保存する.

  5. $ python plot_umap_all.py: UMAPによりx-vectorを可視化する.

実験

実験条件

前回記事と同様に,声優統計コーパスの音声サンプルを利用している.敵対的損失関数は感情分類の予測分布と一様分布の間の平均二乗誤差とした. そのほか詳細は折りたたみ形式で示す。ネットワークの次元数など、どうしても気になるひとだけクリック。

実験条件を表示する

計算機環境を示す.

計算機環境 バージョンなど
OS Ubuntu 22.04
CPU Intel i9-9900K
GPU RTX2070
Python 3.10.6
PyTorch 2.0.1

訓練の基本的な設定は以下の通りである.

項目 設定
ミニバッチサイズ 32
エポック数 1500
学習率 0.001からスタートし,500エポックおきに0.6倍

ニューラルネットワーク全体を通じて,すべて全結合層からなる.speaker encoderにおける各layerの次元数は 入力側から順に(512)-(256)-(256)とした.emotion encoderも同様の次元数である.emotion encoderに直接くっつけるclassifierはlogit計算のための線形層が1つ挟まっている.auxiliary classifierはencoder側からsoftmax層に向かって(256)-(256)-(256)-(256)-(3) である.decoderの次元数はencoder側から出力方向に向かって(256+256)-(512)-(512)-(512)-(512)である.次元数を256+256と表現しているのはspeakerとemotionのencoder出力(それぞれ256次元)を結合しているためである. これら全結合層にはバッチ正規化が組み込まれており,また活性化関数にはReLUを採用した.

以上の設定はconfig.yamlに書いてある.

実験結果

図2に可視化結果を示す.話者tsuchiyaのごく一部のサンプルが話者fujitouのクラスタに紛れ込んでいるが,概ね話者ごとに3クラスタに分かれており,感情成分の分離はある程度成功したのではないかと考えられる.話者uemuraや話者fujitouは各感情のサンプルがよく重なっており美しいが,話者tsuchiyaはangryクラスが依然として独立する傾向にある.実際の音声サンプルをさらに詳細に調べることで,このような傾向の違いが生じた原因が見えてくる可能性はある.その際,発話長や抑揚, ボリュームなど,個々の発話から計算できる特徴を統計的な分析にかけることが有用であると考えられる(例:平均発話長,基本周波数の平均および分散).

図2: 感情成分を分離したx-vectorのUMAPによる可視化

TIPS

敵対的損失関数にかかる重み係数の調整には少し気を使うが,ある程度の範囲で係数を変えても同様の結果は得られそうである(係数の値の変化にはあまり敏感でないかも?).またモデル訓練のエポック数も多くしすぎるのは避けたほうがよい.実験結果はエポック数1500だったが,3000回では各話者のx-vectorが連続的につながるような可視化結果を得ている(図3).ほどほどのエポック数で止めておくのが良さそうである.各ネットワークのユニット数(各レイヤーの次元数) や層数も調整の余地はあるだろうが,あまり次元数を絞りすぎるとencoderやdecoder,auxiliary classifierが弱くなり,その場合も期待通りの分離結果は得られない.

図3: エポック数3000のときの結果

おわりに

本記事では日本語x-vectorから感情成分を分離するニューラルネットの実装を試みた. 感情成分を分離するために,感情分類器に敵対的な損失関数を導入した. UMAPによる可視化実験の結果,前回記事におけるx-vectorの疑似9クラス(= 3話者 × 3感情)が概ね話者ごとに独立した3つのクラスタを作るようになり,本記事の試みはある程度成功したといえるだろう.

感情成分が分離されたx-vectorを何らかの後段のタスクに使ってみるのも一興だろう.今回は幸いにも話者ラベル・感情ラベルが利用可能なので,各種の距離学習の導入も考えられる.そのほか,教師なし(= ラベル情報なし)で分離する手法への発展も考えられるだろう.今後の課題としたい.

参考文献

冒頭で示した論文の公式実装.実装面で大いに参考になった. github.com