スペクトログラムでオートエンコーダ
色々あって、スペクトログラムでオートエンコーダがしたい気持ちになったのでやった話です。基本的にはただのオートエンコーダで(よくあるMNISTとかを対象にしてるやつ)、データがスペクトログラムになってます。実装はここにおいてあります。
やりたいこと
スペクトログラムをオートエンコーダにかけて、再構築したい。もっというと音声データをスペクトログラムに変換してそれをオートエンコーダにかけた結果を音声データに戻したい。
ということで、やりたいことは以下の2つです。
- スペクトログラムから音声データに戻すプログラムを書く
- スペクトログラムを対象にしたオートエンコーダを組む
Griffin Lim法
これとかに詳しい。スペクトログラムは基本的に周波数成分のパワーを対象にするので、位相成分がわからなくなってしまい、そのまま元に戻すことができない。しかし、以下のようにして位相を推定することで元に戻すことができる...らしい。位相復元自体結構ちゃんとやられてるテーマみたいだけど、今回はちゃちゃっと試してみることがメインなので、とりあえずの実装をしてみる。
参考にしたpdfにのっていた方法は以下の通り。
適当な初期位相でスタートすると逆短時間フーリエ変換をしてからフーリエ変換をするだけでは元には戻らず、振幅を制約にして繰り返し処理を行うことで実際の位相に近づいていくらしい。この記述だけを真に受けた実装はこんな感じ。
A = librosa.db_to_amplitude(spect) theta = 0 X = A * np.cos(theta) + A * np.sin(theta) * 1j for i in range(100): x= librosa.istft(X, hop_length=160, win_length=400) X = librosa.stft(x, n_fft=512, hop_length=160, win_length=400) X = A * X / np.abs(X)
pythonで書いていて、音声のライブラリにはlibrosaを使用している。初期位相を乱暴に0にしちゃったけど多分あんまりよくない。
このプログラムを使って元に戻してみた結果はこんな感じ。このディレクトリのtest.wavが元々の音声で、test-reconstruction.wavが音声をスペクトログラムにした上でまた音声にしたものとなる。なんか再構成した音声はちょっとわんわんしてる気がするけど、だいたい元に戻っていると思う。なお、ここで使った音声や後でスペクトログラムにする音声は声優統計コーパスを使用している。
オートエンコーダ
入力データをエンコーダにかけて、それからデコーダにかけた結果と元の入力データの誤差が小さくなるように学習したら中間にある特徴量は入力データをいい感じに次元削減したデータになってるよね、的なアレ。真面目に論文を読んだとかではないからこの程度の理解です。
オートエンコーダの実装はこんな感じ。pytorchです。何層がいいのかとか、どれくらいの次元数にするのが適正なのかとか、いまいちわからず適当にやってしまったのは反省点です。
class AutoEncoder(nn.Module): def __init__(self): super(AutoEncoder, self).__init__() self.encoder = nn.Sequential( nn.Conv2d(1, 32, kernel_size=5, stride=4, padding=1), nn.ReLU(inplace=True), nn.Conv2d(32, 64, kernel_size=5, stride=4, padding=1), nn.ReLU(inplace=True), nn.Conv2d(64, 128, kernel_size=3, stride=2, padding=1), nn.ReLU(inplace=True), nn.Conv2d(128, 256, kernel_size=3, stride=2, padding=1), nn.ReLU(inplace=True), ) self.decoder = nn.Sequential( nn.ConvTranspose2d(256, 128, kernel_size=3, stride=2, padding=1, output_padding=1), nn.ReLU(inplace=True), nn.ConvTranspose2d(128, 64, kernel_size=3, stride=2, padding=1, output_padding=(1, 0)), nn.ReLU(inplace=True), nn.ConvTranspose2d(64, 32, kernel_size=5, stride=4, padding=1, output_padding=(1, 0)), nn.ReLU(inplace=True), nn.ConvTranspose2d(32, 1, kernel_size=5, stride=4, padding=1, output_padding=2), ) def forward(self, x): x = self.encoder(x) x = self.decoder(x) return x
学習データは声優統計コーパスの各データの冒頭3秒。データの量は少ないけどパラレルデータとしてちゃんと大きいのではないか。そこらへんあんまり詳しくないから適当言ってるけど。今回のオートエンコーダはパラレルデータである必要は特にないはずなので、もっと大きいデータでやるべきなんだと思うんだけど、家のネットワークが貧弱で大きいのダウンロードできなかった...ちゃんと大学に行ってやれということですよねわかります。というかデータを大きくしてやり直してみるのはやるつもりです。冒頭3秒を使ってるのはスペクトログラムのサイズを同じにするためにとりあえず一番小さいデータの秒数に合わせたから。
ここには書いていないけれど、スペクトログラムはnumpyの2次元配列をのnpy形式で保存したものを使っている。なのでnp.loadするだけで画像を読み込むときみたいに何かしらライブラリを使ったわけではない。
また、前処理について。今回は再構成したスペクトログラムから音声に戻したいというモチベーションだったので、スペクトログラムの元々のデータを入力値にしたかった(それ以外に方法が思いつかなかったのだけれどもし何かあったら知りたい)ので何も前処理を行なっていない。後述するけど結果が思わしくなかったのはここら辺がガバガバだからかもしれない。
結果
結果として以下の2つを提示します。
- 再構成したスペクトログラムの例
- 中間特徴量の可視化
再構成したスペクトログラムの例
微細な構造が死んでいるのがわかる。これが死んでいるのがネットワークのせいなのか、データの量の問題なのか、そもそもスペクトログラムを画像ライクに扱うことがオートエンコーダだとキツいのかはよくわからない。とりあえずデータの量を増やしてもっかいやりたい。
ちなみに音声を聞いたんだけど、かなりわんわんしてた。発話内容はきちんと聞き取れるけど誰が話しているのかを当てるのはちょっとキツいかなあ程度のわんわん感だった。というかはてなブログってサクッと音声を上げる方法ないんです?
中間特徴量の可視化
使用したデータセットが話者3人のパラレルデータだったので、中間特徴量でそれが可視化できてたら勝ちやろ!と思って可視化した。可視化対象は訓練データを含めたスペクトログラム(分類問題とかではないので、か学習とかは関係ないよね?)。中間特徴量は5120次元なのだけれど、これをt-SNEで2次元の散布図にした結果が以下の通り。
心の目で見ると0が真ん中に集まっているように見え...ませんね。今回の中間特徴量は話者性をはっきり表してくれる何かを提示しているとかそういうわけではなさそう。
まとめ
あんまりうまくいきませんでした!!!
とりあえずデータを大きくしてやり直してみたい。というかそもそも話者性を中間特徴量で表すなんて、元のスペクトログラムの画像見ても誰のスペクトログラムかわからないのに本当に行けるんでしょうかねえ。まあnumpy配列とかを学習データにしてもなんとか学習っぽいことはできますよ、という内容だったということで。