音響モデルから音素間の距離を求める

しばらく前に、「"工藤"を誤魔化すための単語」みたいなツイートをしたら、予想外に伸びた。

服部の「工藤の人」としての知名度ってすごいんやな。うれしい。

いろいろ指摘をもらったんだけど、その中でも「Juliusで提供されている音響モデル(たくさんの人の声を統計的に学習したもの)で音素間の距離を定義できるとPanPhonよりもよくなるかもしれない」っていうアドバイスをめっちゃ試したいと思ったので、やってみることにした。知らないことばかりだったので、やったことをメモする。

Juliusの音響モデル

dictation kitに入ってるhmmdefsって拡張子のやつが、モデルの定義ファイル。monophone用とtriphone用がある。

  • monophone用: "m", "n"みたいな音素一つ一つに対してモデルを定義したやつ

  • triphone用: "a-m+a", "a-n+a"みたいに3つ並んでる音素に対してモデルを定義したやつ

たぶんtriphoneの方がちゃんと音素環境を考慮してるからいいと思うし、アドバイスしてくれた方もtriphone勧めてくださってたけど、とりあえずわかりやすそうなmonophoneを使った。

HMM

「音響モデル」って聞いて、何となく音声ファイルみたいなのを想像してたんだけど、めちゃくちゃ見当外れだった。実際の音響モデルはHidden Markov Model (HMM)として定義されていた。HMMっていうのは、stateがある確率で変わっていって、変わるごとにある確率に基づいた何かを出力するってやつらしい。

音響モデルは後戻りしないので、HMMの中でもleft-to-rightのものとして分類される。確かに遷移確率見たら、リピートするか次に進むかの二択しかなかった。

hmmdefsの構造は、HTK Bookの7章読んだら何となくわかった。一つの音素に対して、

~h "音素"
<BEGINHMM>
    <NUMSTATES> stateの数
        <STATE> 2
            <NUMMIXES> mixtureの数
                <MIXTURE> 1 mixture1の重み
                    <MEAN> meanの数
                        meanが並んでる
                    <VARIANCE> varianceの数
                        varianceが並んでる
                    <GCONST>
                <MIXTURE> 2 mixture2の重み
                    (略)
        <STATE> 3
            (略)
        <STATE> 4
            (略)
    <TRANSP> stateの数
        stateの数×stateの数の行列
<ENDHMM>

って感じで定義されている。stateは5個、mixtureは16個、meanやvarianceは25個だった。

HMM同士の距離

どうやってHMM間の距離測るんやろって思っていろいろ読んだけど、mixtureを含んでいるためそんな単純に計算できない(たぶん)。Music Similarity Measuresによると、片方のHMMから出したサンプルがもう片方のHMMから出力される確率がどれくらいかを測って、それを二つのHMMの近さと見なしたらいいっぽい。どれくらいばらつきがあるかわからんけど、一つの組み合わせにつき1000回やって、その平均を取ることにした。

そのためには、HMMからサンプルを出す機能と、尤度を計算する機能が必要。hmmlearnがそういうことをやってくれるので、そのhmm.GMMHMMっていうモデルを使った。

GMM→GaussianMixture

GMMHMMのプロパティのgmms_には、scikit-learnのGMMっていうオブジェクトが入っている。それがもう古いらしくて、GMMの代わりに新しいGaussianMixtureを使えってwarningが出る。

github.com

hmmlearnのissueで話し合ってくれてはいるけど、途中で終わってる。なので自分で勝手にhmm.py内のGMMを全部GaussianMixtureに書き換えてしまった。それに伴って、sample()の引数とかscore()の戻り値の形式など変更があったところに対応できるようにちょっと直した。

GaussianMixtureのパラメータの設定

個々のMixtureの分のパラメータ(weights_とかmeans_)をGMMHMMから設定する方法がわからんかったので、gmms_に入ってるGaussianMixtureオブジェクトに対して直接_set_parameters()してしまった。そのときにprecisions_choleskyというものが欲しいみたいなんだけど、それが何なのか理解すらできなかったので、GaussianMixture._compute_precision_cholesky()を使って出してもらった。

調節とか

  • left-to-rightのモデルだから、最後のstateから行くとこがないので、遷移確率の行列の最後の行は全て0になる。でも、遷移確率の行ごとの合計が全て1にならないとscore()でエラーが出る。どうしたらいいかわからんけど、とりあえずリピートするところを1にした。

  • 遷移確率や分散に0があるとエラーになるので、めちゃくちゃ小さい値に置き換えた。

  • 普通のHMMは延々と続くので、sample()するときには何回繰り返すかを指定するんだけど、音響モデルはleft-to-rightだから最後のstateに辿り着いたら止めるようにしてほしい。なのでちょっと処理を書き換えた。

NotFittedError

パラメータをセットして、やっとsample()できる!と思ったら、NotFittedErrorとかいうものが出た。fitした後じゃないとsample出してくれへんらしい。

stackoverflow.com

↑初めにちっちゃいデータセットでっち上げてfitさせてからパラメータをセットし、その後sample()したらいいって回答がある(ちっちゃいって言っても、サンプルの数 >= Mixtureの数じゃないと無理だけど)。だから、適当にfitする→ちゃんとしたパラメータをセットする→sample()するっていう流れになった。

GMMHMMとGaussianMixtureでの引数名が指すもの

なんかcomponentとか微妙に意味が違うからややこしい。

GMMHMM GaussianMixture
the number of states n_components
the number of mixtures n_mix n_components
the number of features n_features

それぞれの音素がHMMとして定義されてて、そのHMMの中にstateがいくつかあって、それぞれのstateの出力確率がいくつかのmixtureから成ってて、それぞれのmixtureにfeatureが決められている。

log likelihoodはsampleの長さに影響される?

log likelihoodの行列(左がサンプルを出したもとのHMM、上がそのサンプルを出す尤度を測ったHMM)を可視化すると、以下のようになった。

f:id:YaaMaa:20171202193318p:plain

かろうじて対角線は見えてるけど、それより行ごとの違いが大きすぎる。左のHMMから取ったサンプルの長さの影響が大きいのかなと思って、x軸をサンプルの長さ、y軸をscore (= log likelihood)としたグラフを描いてみた。

f:id:YaaMaa:20171202032558p:plain

すごいあからさまに影響されてる。濃い青色のところは、行列では対角線上に当たる部分、つまり同じ音素同士を比べたときのスコア。これがだいたい横一直線になってもらわないといけないと思う。いろいろめちゃくちゃな方法で値を操作した結果、次のような行列になった。

f:id:YaaMaa:20171202200958p:plain

さっきに比べたらわりと納得できる感じになったと思う(適当)。

距離の可視化

ツイートに書いたのと同じ方法で、音素間の距離を可視化した。

f:id:YaaMaa:20171202201138p:plain

雑に分類してみると、

f:id:YaaMaa:20171202201245p:plain

何となく同じような属性を持つ音素同士で固まってるようなので満足!

あの意味わからん数字の羅列だったHMM定義ファイルの中身が、処理してみたらちゃんと人間の知覚に沿った感じになってるの、何かすごいな。

もうちょっとやりたいことがあった気がするけど、期末テストやってるうちに自分が何してたのか忘れてしまったので、終わる。