楽曲から特定の楽器のみを抜き出す 後編
この記事は前編の続きです。 まだ前編を読んでない方はそちらを先に読んでください。
3. 2.で得た基底行列を用いて、かえるの歌のスペクトログラムに対してNMFを適用
今度は、かえるの歌のスペクトログラムに対してNMFを適用します。 ただ、2.で得たを使いたいので、分解の方法を先ほどとは少し変えます。
かえるの歌のスペクトログラムをとした時、これを以下のように分解します。
→2.で得たトランペットの教師音の基底行列
→に対応するアクティベーション行列
→トランペット以外の音の基底行列
→に対応するアクティベーション行列
は2.で求めたものをそのまま使うので、ここではととを変化させながらをに近づけます。 を上手く分解できれば、はトランペットのスペクトログラム、はトランペット以外の音(つまりピアノ)のスペクトログラムとなります。
じゃあ、さっきと同じように目的関数をと設定して……と行きたいところですが1つ問題があります。 このまま分解しようとすると、との間に何も制約が無いので上手くいかない可能性があります。
上手くいかない例を1つ挙げると、を分解した時に(は零行列)となってしまう可能性があります。 そうなると、もになってしまうので、トランペットの音を抜き出したら結果が無音なんていう変な結果になってしまいます。
それを防ぐために、先ほどのNMFの目的関数を少し変えて以下のようにします。
という項が増えました。 は直感的には、とがどれくらい似ているかを表します。 とが似ているほど値が大きくなり、違う行列であるほど値が小さくなるという感じです。
を分解した結果がとなったと仮定します。 この時、はに分解され、の値はとても小さくなります。 しかし、その場合、はの周波数パターンを含む音源なので、はと似た値を含む行列になるはずです。 なので、の値がとても大きくなってしまい、結果としての値が大きくなります。 以上より、の項によってとなるような分解は避けられます。
ちなみに、は罰則項の重みを表すパラメータです。
目的関数に罰則項が増えたので、更新式も変更する必要があります。
ととの更新式は以下のようになります。
導出はβ-divergence規範による罰則条件付き教師あり非負値行列因子分解を用いた目的楽器音抽出を見てください。
ただし、
コードはこんな感じです。
さっきのNMFの更新式がちょっと難しくなっただけなので大きな違いは無いです。
def PSNMF(Y, F, l, num_iter, mu): #D = Y -(FG + HU) + mu * norm(F.T, H) M, N = Y.shape k = F.shape[1] G = np.random.rand(k, N) U = np.random.rand(l, N) H = np.random.rand(M, l) error = [] eps = np.spacing(1) H = normalize(H) for i in tqdm(range(num_iter)): error.append(euclid_norm(Y, np.dot(F, G) + np.dot(H, U))) H *= np.dot(Y, U.T) / (np.dot(np.dot(F, G) + np.dot(H, U), U.T) + 2 * mu * np.dot(F, np.dot(F.T, H)) + eps) H = normalize(H) U *= np.dot(H.T, Y) / (np.dot(H.T, np.dot(F, G) + np.dot(H, U)) + eps) G *= np.dot(F.T, Y) / (np.dot(F.T, np.dot(F, G) + np.dot(H, U)) + eps) return (G, H, U, error) def normalize(X): N = X.shape[1] for n in range(N): X[:, n] /= sum(X[:, n]) return X
途中でH
を正規化してるのは、参考にしたスライドにそう書いてあったからです。
ぶっちゃけ入れるべき理由はまだよく分かってないです。
4. 3.の結果に逆フーリエ変換をして分離した音源を得る
2.と3.で得られたとをかけたがトランペット音のみのスペクトログラムです。 スペクトログラムは周波数の情報しかないので、これを逆フーリエ変換して音の波形に戻します。 要するに、1.でやった作業の逆をやればいいだけです。
def istft(x, win, step): N, M = x.shape l = (M - 1) * step + N X = np.zeros(l, dtype = "float64") X_count = np.zeros(l, dtype = "int") for m in xrange(M): start = m * step X[start:start+N] += np.fft.ifft(x[:, m]).real / win X_count[start:start+N] += 1 X /= X_count return X
結果
さっそく、上のやり方で抜き出した結果を見てみます。
実際に抜き出した音を聴く前に、まずはNMFで分解したスペクトログラムを確認してみます。 つまり、手法の中のとですね。
左がもとのかえるの歌、真ん中が(トランペット)、右が(ピアノ)のスペクトログラムです。 この図からだと上手くいってるかどうか分かり辛いですね。
ただ、最初の方の時刻(0~2000のあたり)を見ると人の目でもわかる違いがあります。 もとの音源ではトランペットとピアノのスタートが1小節ずれています。 なので、最初の方はピアノの音は鳴っていません。 それを踏まえて先ほどの図を見ると、右側のピアノのスペクトログラムの最初の方の時刻は周波数の強度が小さいです。 これはNMFによってトランペットとピアノが分離てきているからだと期待できます。
では、実際に抜き出したトランペットの音を聴いてみましょう。
うーん、微妙……
一応、もとの音よりはピアノの音が小さくなってますけど、だいぶピアノの音が残ってますね。 もうちょっと上手くいくと(勝手に)思ってたので、残念です。
ただ、分離に使っている情報はトランペットのドレミファソラシドの音だけです。 なので、その情報だけでここまでできると考えると結構すごいとも言えそうです。
ちなみに、ピアノの方の音はこんな感じです。
所々ノイズがひどい部分がありますが、トランペットよりは上手く抜き出せてる感じがします。
まとめ
音声処理に関するプログラミングをしたことが無かったので、興味を持ってこういうものを作ってみました。 結果は微妙でしたが、音声処理に関する知識が少しついたので良かったです。
あと、NMFも前から名前は聞いていましたが自分で使うのは初めてでした。 導出は大変ですが、実装は簡単で良いですね。
参考文献・サイト
β-divergence規範による罰則条件付き教師あり非負値行列因子分解を用いた目的楽器音抽出
直交化及び距離最大化則条件を用いた教師あり非負値行列因子分解による音楽信号分離(SlideShare)